a LOT sync fixes...

This commit is contained in:
olcxja 2026-10-01 17:12:53 +02:00
commit 806e173e43
14 changed files with 1073 additions and 240 deletions

View file

@ -1144,7 +1144,9 @@ const LAN_CATEGORIES = [
{ id: 'apps-portable', label: 'lansync.appsPortable' },
{ id: 'apps-prefix', label: 'lansync.appsPrefix' },
{ id: 'wines', label: 'lansync.wines' },
{ id: 'prefixes', label: 'lansync.prefixes' }
{ id: 'prefixes', label: 'lansync.prefixes' },
// opt-in: without it a sync never touches the other machine's defaults
{ id: 'defaults', label: 'lansync.defaults' }
]
// Byte sizes for the progress popups. Decimal units, the way a file manager
@ -1158,19 +1160,45 @@ function formatBytes(bytes) {
return `${(value / 1000 / 1000 / 1000).toFixed(2)} GB`
}
// "Games: 3 · Prefixes: 1 · Wine versions: 2" - only the categories this
// sync actually carries are listed.
function lanStatsLine(items) {
if (!items) return ''
// "Apps: 2/5 · Prefixes: 1/2 · Wine versions: 0/1" - how much of each category
// is done out of how much the sync carries, so the line says what is still
// coming instead of only listing what has already been walked over. A peer
// that does not announce its totals falls back to the plain count.
function lanStatsLine(items, itemsDone, totals) {
const rows = [
[items.apps, 'lansync.statsApps'],
[items.prefixes, 'lansync.statsPrefixes'],
[items.wines, 'lansync.statsWines']
['apps', 'lansync.statsApps'],
['prefixes', 'lansync.statsPrefixes'],
['wines', 'lansync.statsWines']
]
return rows
.filter(([count]) => count > 0)
.map(([count, key]) => t(key, count))
.join(' · ')
const out = []
for (const [kind, key] of rows) {
const listed = Number(items && items[kind]) || 0
const total = Number(totals && totals[kind]) || 0
const known = itemsDone && Number.isFinite(Number(itemsDone[kind]))
const done = known ? Number(itemsDone[kind]) : listed
if (total > 0) out.push(t(key, `${Math.min(done, total)}/${total}`))
else if (listed > 0) out.push(t(key, listed))
}
return out.join(' · ')
}
// The bar follows BYTES, not file counts: a sync spends its time on the big
// files, and a file-count bar sits at 99% while the last gigabyte crawls in.
// It also follows the PHASE it is in (barBytes/barTotalBytes): a phase starts
// from an empty bar instead of inheriting a full one from the phase before, so
// hashing never looks finished while it has just begun.
function renderProgressBar(bar, bytes, totalBytes) {
const sent = Math.max(0, Number(bytes) || 0)
const total = Math.max(0, Number(totalBytes) || 0)
bar.max = Math.max(total, 1)
bar.value = Math.min(sent, bar.max)
}
// Older peers only send the overall byte totals - then those drive the bar.
function barValues(info) {
const bytes = info.barBytes == null ? info.bytes : info.barBytes
const total = info.barTotalBytes == null ? info.totalBytes : info.barTotalBytes
return [bytes, total]
}
// "340 MB / 1.2 GB" once the total is known (the sending side learns it as it
@ -1296,8 +1324,10 @@ async function runLanSync(device, code, categories, force) {
<p class="subtitle" id="ls-run-count"></p>
<p class="subtitle" id="ls-run-stats"></p>
<p class="subtitle" id="ls-run-bytes"></p>
<p class="subtitle sync-warning" id="ls-run-warning"></p>
`, { locked: true })
modal.querySelector('#ls-run-title').textContent = t('lansync.progress', device.name)
modal.querySelector('#ls-run-warning').textContent = t('lansync.keepOpen')
const bar = modal.querySelector('progress')
const phase = modal.querySelector('#ls-run-phase')
const count = modal.querySelector('#ls-run-count')
@ -1307,17 +1337,14 @@ async function runLanSync(device, code, categories, force) {
state.lanPushing = true
window.plwl.onLanSyncProgress(data => {
if (!active) return
const key = 'lansync.phase.' + data.phase
phase.textContent = t(key, data.label || '')
if (data.total > 0) {
bar.max = data.total
bar.value = data.done
count.textContent = t('lansync.progressFiles', data.done, data.total)
} else {
bar.removeAttribute('value')
count.textContent = ''
}
stats.textContent = lanStatsLine(data.items)
// Every line keeps its value while the sync moves between phases: hashing
// reports its own file counter, copying its own, and the bar follows the
// bytes the whole way through
phase.textContent = t('lansync.phase.' + data.phase, data.label || '').trim()
renderProgressBar(bar, ...barValues(data))
if (data.total > 0) count.textContent = t('lansync.progressFiles', data.done, data.total)
else count.textContent = ''
stats.textContent = lanStatsLine(data.items, data.itemsDone, data.totals)
bytes.textContent = lanBytesLine(data.bytes, data.totalBytes)
})
let res = null
@ -1353,9 +1380,19 @@ async function runLanSync(device, code, categories, force) {
// library changes under the user's hands with no explanation. Same popup, but
// driven by the events the receiving server emits, and equally not closable
// by a click.
const RECEIVE_WATCHDOG_MS = 5 * 60 * 1000
// The sending machine does its hashing before anything is written here, which
// can be minutes of complete silence - so the popup switches to "waiting"
// instead of closing, and only a really long silence drops it. Events after
// that reopen it (the sync is still running, the user just cannot see it).
const RECEIVE_WATCHDOG_MS = 15 * 60 * 1000
const RECEIVE_STALL_MS = 6 * 1000
// phases whose wording differs on this side (everything else is neutral:
// "Comparing X", "Updating the library", "Finishing up")
const RECV_PHASE_KEYS = { copy: 'lansync.recvPhase.copy' }
let receiving = false
let receiveClosed = false
let receiveWatchdog = null
let receiveStall = null
let receiveCloseTimer = null
function receiveModalParts() {
@ -1378,32 +1415,47 @@ function endReceiving(message) {
clearTimeout(receiveWatchdog)
receiveWatchdog = null
}
if (receiveStall) {
clearTimeout(receiveStall)
receiveStall = null
}
if (receiveCloseTimer) {
clearTimeout(receiveCloseTimer)
receiveCloseTimer = null
}
const wasReceiving = receiving
receiving = false
receiveClosed = true
if (!wasReceiving) return
if (message) toast(message, 'error')
closeModal()
}
// A sender that crashed halfway through must not leave this window locked: the
// popup closes itself if no event arrives for a while (the server drops the
// session after the same idle time).
// popup closes itself if nothing at all arrives for a long time (the server
// drops the session after the same idle time). While it waits, the phase line
// says so rather than the popup disappearing mid-transfer.
function armReceiveWatchdog() {
if (receiveWatchdog) clearTimeout(receiveWatchdog)
receiveWatchdog = setTimeout(() => endReceiving(t('lansync.recvIdle')), RECEIVE_WATCHDOG_MS)
if (receiveStall) clearTimeout(receiveStall)
receiveStall = setTimeout(() => {
receiveStall = null
const parts = receiveModalParts()
if (parts) parts.phase.textContent = t('lansync.recvStalled')
}, RECEIVE_STALL_MS)
}
function onLanReceiving(info) {
if (!info || !info.stage) return
if (state.lanPushing) return // our own push owns the (single) popup
if (info.stage === 'start') receiveClosed = false
if (!receiving) {
// open the popup only for the start of a sync, and only when the user is
// not in the middle of another dialog (there is one modal at a time)
if (info.stage !== 'start') return
// The popup also opens mid-sync: the sender may have been hashing for
// minutes when this window starts listening, and a popup that the safety
// timeout closed has to come back for the rest of the transfer. A sync
// that already ended stays closed.
if (receiveClosed && info.stage !== 'start') return
if (qs('#modal-overlay').style.display !== 'none') return
receiving = true
const modal = showModal(`
@ -1413,8 +1465,10 @@ function onLanReceiving(info) {
<p class="subtitle" id="ls-recv-count"></p>
<p class="subtitle" id="ls-recv-stats"></p>
<p class="subtitle" id="ls-recv-bytes"></p>
<p class="subtitle sync-warning" id="ls-recv-warning"></p>
`, { locked: true })
modal.querySelector('#ls-recv-title').textContent = t('lansync.receiving', info.from || info.deviceId || '')
modal.querySelector('#ls-recv-warning').textContent = t('lansync.keepOpen')
armReceiveWatchdog()
return renderReceiving(info)
}
@ -1425,17 +1479,14 @@ function onLanReceiving(info) {
function renderReceiving(info) {
const parts = receiveModalParts()
if (!parts) return
const phaseKey = 'lansync.phase.' + info.stage
// The same phases read from this side's point of view: a file arriving here
// is being received, not sent.
const phaseKey = RECV_PHASE_KEYS[info.stage] || 'lansync.phase.' + info.stage
parts.phase.textContent = (info.stage === 'start' ? t('lansync.recvWaiting') : t(phaseKey, info.label || '')).trim()
if (info.total > 0) {
parts.bar.max = info.total
parts.bar.value = info.done
parts.count.textContent = t('lansync.progressFiles', info.done, info.total)
} else {
parts.bar.removeAttribute('value')
parts.count.textContent = ''
}
parts.stats.textContent = lanStatsLine(info.items)
renderProgressBar(parts.bar, ...barValues(info))
if (info.total > 0) parts.count.textContent = t('lansync.progressFiles', info.done, info.total)
else parts.count.textContent = ''
parts.stats.textContent = lanStatsLine(info.items, info.itemsDone, info.totals)
parts.bytes.textContent = lanBytesLine(info.bytes, info.totalBytes)
if (info.stage === 'done') {
// let the totals be read, then get out of the way (the library reloads

View file

@ -191,6 +191,10 @@ content {
}
.subtitle { color: var(--darker-text-color); font-size: 0.9rem; margin-top: 0.2rem; }
/* "Do not close the app while this runs" - the one line on a sync popup the
user has to notice, so it stands out from the subtitles around it. */
.sync-warning { color: var(--big-red); margin-top: 0.6rem; }
.button {
height: var(--button-height);
padding: 0 1rem;