fix(drafts): drop the visibilitychange flush — debouncer keeps running on hidden tabs

Tab switching just hides the page; the JS context survives and the
debouncer's `setTimeout` keeps counting down. When it fires, the runner
POSTs normally and the server's response updates `lastSync`. There's
nothing left for a visibilitychange-driven flush to do that the
ordinary pipeline doesn't already handle, and adding one only creates
extra POSTs to reason about.

`pagehide` remains the single trigger for the keepalive flush — that's
the case where the JS context is actually being torn down and the
runner's pending fetch would otherwise be killed mid-flight.
This commit is contained in:
Diego Imbert
2026-06-08 13:13:49 +02:00
parent 289c0d6010
commit 32ca1c251e
+8 -34
View File
@@ -205,32 +205,6 @@ async function postSave(opts: UserDraftDbSyncerSaveOpts): Promise<void> {
}
}
/**
* Tab/app switch flush — the document is hidden but the page is still
* alive. Route every pending edit through the normal runner pipeline so
* the server's response can land and `setLastSync` can bump the local
* baseline. Without that, the next foreground edit attaches the stale
* `last_sync` we sent before the flush, the server's `created_at` is
* now ahead, and the user gets a spurious conflict modal for their own
* background-tab write.
*
* `debouncer.cancel(key)` first so a still-pending keystroke debounce
* can't fire a second runner POST with the same stale `last_sync`
* after the flush submission — that would also self-conflict.
*
* Doesn't clear `pendingSaveOpts`: `postSave` deletes the entry on
* success (gated on `pendingSaveOpts.get(key) === opts`), and a later
* `pagehide` keepalive needs to see anything that's still in flight or
* that the user edited after this flush.
*/
function flushOnVisibilityHidden(): void {
if (pendingSaveOpts.size === 0) return
for (const [key, opts] of pendingSaveOpts) {
debouncer.cancel(key)
runner.submit(key, () => postSave(opts))
}
}
/**
* True-unload flush — `pagehide` is the browser's commitment that the
* document is going away. Use `keepalive: true` so the network stack
@@ -291,14 +265,14 @@ function flushOnPageHide(): void {
}
if (typeof document !== 'undefined') {
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') flushOnVisibilityHidden()
})
// `pagehide` is the only signal we trust to mean "the document is
// truly going away" — `visibilitychange → hidden` is too broad
// (fires on every tab/app switch with the page surviving), which is
// why we route THAT through the normal runner above and reserve
// keepalive for here.
// `pagehide` is the only signal that means "the document is truly
// going away". We deliberately do NOT listen for
// `visibilitychange → hidden`: it fires on every tab/app switch with
// the page surviving, and on a surviving page the debouncer's
// pending `setTimeout` keeps running in the background, the runner
// POST eventually fires, and the server's response updates
// `lastSync` — no flush needed, no spurious conflict modal on the
// user's own background-tab write.
window.addEventListener('pagehide', flushOnPageHide)
}