fix(upload-queue): rehydrate the persisted queue app-wide, not only on /upload
loadQueue() had exactly one call site in the entire frontend: the /upload route's onMount. So after a reload, an iOS tab discard or a PWA relaunch, staged photos sat in IndexedDB while the badge read 0 and nothing sent them — unless the guest happened to navigate back to /upload, which they have no reason to do, having already been shown a success. The photo never leaves the phone and the guest is never told. The root cause was narrower than "loadQueue isn't called enough". requeueRetriable() read IndexedDB but only .map()'d over whatever the in-memory store already held, so it could reset statuses and never ADD an entry — and processQueue reads only that store. That is why the `online` listener and the SSE resume hooks, which both call it, could not recover a cold start either. It now REBUILDS the store from IndexedDB, which makes all three resume paths work. Rebuilding needs one guard: entryToQueueItem downgrades `uploading` to `pending` with progress 0, and this runs on every `online` event and every SSE reconnect, so a blind rebuild would visibly reset the progress bar of a request still on the wire. In-flight items are carried over by id. Hydration is module-level, SSR-guarded and idempotent, re-armed via onSetAuth/onClearAuth because login is a client-side goto() — no module re-import, no onMount re-run — so a hydration that no-oped for lack of a token gets a second chance. Module level rather than a layout onMount because +layout.svelte already imports this module on every entry point, it matches the file's own bindOnline()/bindSse() pattern, and a store owning its own persistence keeps the layout free of a concern it cannot test. auth.ts does not import this module, so no cycle. The burst-queue e2e test no longer navigates to /upload after its reload — it now asserts the resume happens wherever the reload lands, which is the actual regression guard. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -199,9 +199,12 @@ test.describe('Upload — client queue under a burst', () => {
|
||||
// a closed tab / killed PWA. The remaining pending items live only in
|
||||
// IndexedDB now.
|
||||
await page.reload();
|
||||
// The queue only resumes where loadQueue() runs — the /upload route's
|
||||
// onMount. Navigating there is the "reopen the composer" recovery path.
|
||||
await page.goto('/upload');
|
||||
// Deliberately NOT navigating to /upload. Rehydration is now module-level and
|
||||
// auth-gated (upload-queue.ts `hydrateQueue`), so the queue resumes wherever the
|
||||
// reload lands. This assertion is the regression guard for the defect it replaced:
|
||||
// `loadQueue()` used to have a single call site in the whole app — the /upload
|
||||
// route's onMount — so a guest who reloaded anywhere else saw a 0 badge and their
|
||||
// staged photos never left the phone, having already been shown a success.
|
||||
|
||||
// (4) RESUME: every file ends up server-side without re-staging anything.
|
||||
// `>=` not `===`: the only imperfection possible is a DUPLICATE (an upload
|
||||
|
||||
Reference in New Issue
Block a user