Multi-Hoster-Upload/lib/orphan-tmp.js
Administrator 6b0c515a9b fix(queue): close two lost-work / sticky-ghost edge cases found by intensive testing
Follow-up hardening on the v3.3.80 queue-persistence fix after a deeper adversarial
sweep + reviewer pass over the WHOLE subsystem (not just the diff). Two real defects:

1. Basename-collision lost work (regression introduced by v3.3.80 FIX A).
   restoreQueueStateFromConfig collapses jobs on the FULL path while the ts-gate keys
   on basename|hoster. Two genuinely different files with the same basename queued to
   the same hoster from different folders therefore share a gate key: if one was logged
   after savedAt, the ts-rule dropped BOTH — silently losing the still-pending one.
   Pre-FIX-A only 'done' jobs were dropped, so a pending file was never at risk.
   Fix: an ambiguity guard in partitionRestoredJobsByLog — the ts-rule is suppressed
   when a basename|hoster key maps to more than one distinct file path (the log records
   only basenames, so it can't say which physical file completed). The done-in-log rule
   is unchanged. Fails safe: worst case a visible ghost survives, never silent data loss.

2. selectedFiles re-materialization with removeFromQueueOnDone=ON (second mechanism,
   independent of the stale snapshot). When that setting is on, a completed job is
   stripped from queueJobs but its path stays in selectedFiles (syncSelectedFilesFromQueue
   only runs at batch-done, never on a mid-upload close). On restart the ts-gate operates
   on queueJobs and never sees it, then the startup updateUploadView -> buildQueuePreview
   re-creates it as a preview ghost AFTER the gate ran, and it re-persists with a fresh
   savedAt — sticky. Fix: completedSelectionKeys() seeds _completedUploadKeys (the set
   buildQueuePreview already consults) from the log at startup, keyed on full path, with
   the same ambiguity guard. Log-based so it survives a hard kill, consistent with FIX A.

Also extracts the orphan-tmp sweep decision into lib/orphan-tmp.js (was untested inline
code in main.js; behavior-preserving) and adds executable coverage for the paths that
were previously only argued from logic: orphan-tmp sweep, config-store pendingQueue+savedAt
round-trip, an end-to-end scenario in the exact user-reported shape (300 queued / ~200
finished mid-session), and a 3000+500-iteration property fuzz of the gate invariant
including the lost-work guarantee. 359/359 green, ESLint clean, smoke-boot unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 06:51:48 +02:00

30 lines
1.0 KiB
JavaScript

(function (root) {
'use strict';
function selectOrphanTmps(fileNames, opts) {
const o = opts || {};
const baseName = String(o.baseName || '');
const currentPid = o.currentPid;
const isAlive = typeof o.isAlive === 'function' ? o.isAlive : () => false;
const out = [];
if (!baseName || !Array.isArray(fileNames)) return out;
const prefix = baseName + '.';
const suffix = '.tmp';
for (const file of fileNames) {
if (typeof file !== 'string') continue;
if (!file.startsWith(prefix) || !file.endsWith(suffix)) continue;
const mid = file.slice(prefix.length, file.length - suffix.length);
if (!/^\d+$/.test(mid)) continue;
const pid = Number(mid);
if (pid === currentPid) continue;
if (isAlive(pid)) continue;
out.push(file);
}
return out;
}
const api = { selectOrphanTmps };
if (typeof module !== 'undefined' && module.exports) module.exports = api;
else if (root) root.OrphanTmp = api;
})(typeof window !== 'undefined' ? window : this);