Closing the app (especially during an active upload or on a hard kill) and reopening sometimes left already-uploaded files sitting in the queue as if still pending. Root cause is three layers stacked: 1. Persist-starvation: persistQueueStateSoon() reset a 10s debounce on every progress event, so during an upload the on-disk queue snapshot was never rewritten and stayed frozen at the pre-upload state (all jobs "preview"). 2. The beforeunload sync flush only covers a clean close; a hard kill / crash leaves that stale snapshot on disk. 3. Startup auto-dedup only dropped jobs with status "done". The completed files were stored as "preview" in the stale snapshot, so they survived and reappeared. Fix (mechanism-independent — holds whether the stale snapshot came from starvation, a mid-upload close race, or a hard kill): FIX A (core, durable): the upload log is the source of truth. Each persisted snapshot is now stamped with savedAt; on restart any restored job whose newest matching log entry is timestamped at/after floor(savedAt) is dropped regardless of status — it provably completed after the snapshot, so a "preview" row for it is a ghost. lib/queue-dedup.js gains an additive 3rd savedAt param; without savedAt or without log timestamps it behaves exactly as before (the 5 canary tests stay green, so intentional re-uploads of older files still survive). FIX B: new lib/throttle-timer.js with a max-wait. During uploads the snapshot is now written at most ~20s into a continuous progress burst instead of never; idle stays a pure debounce. The fallback shim honors max-wait too, so a missing library can never silently reintroduce the starvation. FIX C: the synchronous close-write retries renameSync on EBUSY/EPERM/EACCES and uses a pid-unique tmp (de-conflicts it from config-store._atomicWrite's fixed .tmp). A startup sweep reclaims orphaned <config>.<pid>.tmp files left by a hard kill between write and rename. lib/upload-log.js extracts formatUploadLogLine + parseUploadLogLine from main.js so the real writer -> reader -> gate seam is unit-tested (a future log-format or epoch-basis change can no longer pass green while breaking the fix). Verified: 334/334 tests green (incl. throttle fake-clock starvation/maxWait, ts-gate multi-hoster partial-completion, and the real-format seam tests), ESLint clean, smoke-boot identical to baseline, and an adversarial multi-agent review (15 findings, 14 refuted, 1 low — the tmp orphan, now swept) on the diff. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
32 lines
1.2 KiB
JavaScript
32 lines
1.2 KiB
JavaScript
(function (root) {
|
|
'use strict';
|
|
|
|
function _pad(n) { return String(n).padStart(2, '0'); }
|
|
|
|
function formatUploadLogLine(date, hoster, link, fileName) {
|
|
const d = date instanceof Date ? date : new Date();
|
|
const dateStr = `${d.getFullYear()}-${_pad(d.getMonth() + 1)}-${_pad(d.getDate())} ` +
|
|
`${_pad(d.getHours())}:${_pad(d.getMinutes())}:${_pad(d.getSeconds())}`;
|
|
return `${dateStr}|${hoster}|${link}||${fileName}|\n`;
|
|
}
|
|
|
|
function parseUploadLogLine(line) {
|
|
if (typeof line !== 'string') return null;
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith('#')) return null;
|
|
const parts = trimmed.split('|');
|
|
if (parts.length < 5) return null;
|
|
const hoster = (parts[1] || '').trim();
|
|
const fileName = (parts[4] || '').trim();
|
|
if (!hoster || !fileName) return null;
|
|
const tsStr = (parts[0] || '').trim();
|
|
const tsParsed = tsStr ? Date.parse(tsStr.replace(' ', 'T')) : NaN;
|
|
const ts = isNaN(tsParsed) ? undefined : tsParsed;
|
|
return { hoster, fileName, ts };
|
|
}
|
|
|
|
const api = { formatUploadLogLine, parseUploadLogLine };
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
else if (root) root.UploadLog = api;
|
|
})(typeof window !== 'undefined' ? window : this);
|