fix: tour -- close the save-bypass at its root, not by enumerating callers

Round 3's review found the enumeration approach (patchStore()'s blocked
list) was still incomplete, and worse, live rather than dormant: keyboard
shortcuts (Delete/Backspace/S) bypass the tour's click-blocking backdrop
entirely, since CSS pointer-events does nothing to a window-level keydown
listener. timeline.js's shortcut handler calls removeSelectedScenes(),
removeAudioTrack(), removeSelectedOverlay(), and splitScene() -- none of
them in the blocked list -- and every one ends in scheduleSave(), whose
setTimeout calls the closure-scoped commit() directly, reaching a real,
unmocked PUT /funpack/api/projects/<id>. tour-s1 is pre-selected from the
moment the tour boots, so pressing Delete at any point immediately
triggers this -- no click needed, no specific step required.

The blocked-list pattern (three rounds of fixes: parsed.scenes crash,
syncFromPreview, saveClipToMediaBin) was closing this one caller at a
time and kept finding another. Fixed at the root instead: every mutating
path in store.js funnels through the single closure-scoped commit()
function before it can reach the network, so that's the one place a
tour-mode guard covers every current AND future caller by construction,
without needing tour.js to know the name of anything that might call it.

Verified live: dispatched a real Delete keydown during the tour with
tour-s1 selected (the default boot state) -- the scene still gets removed
locally (harmless, it's fake demo data), but zero requests to
/api/projects appear in the network log across the full debounce window
after the fix, versus a real PUT before it. Confirmed the guard is a
no-op outside tour mode (window.__FUNPACK_TOUR__ is false there) and the
full 16-step tour sequence still completes with zero thrown errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-09-15 22:41:39 +03:00
co-authored by Claude Sonnet 5
parent da04063a77
commit 61a4817460
+11
View File
@@ -969,6 +969,17 @@
}
async function commit(opts = {}) {
// Every mutating path in this module funnels through here before it can
// reach a real network write -- scheduleSave()'s own setTimeout calls
// this closure-scoped function directly, so patching Store.commit (or
// any individual caller) from outside cannot stop it. The tour's own
// welcome copy promises "nothing here touches your real files," and
// enumerating every caller in tour.js's blocklist misses ones reachable
// outside a click (keyboard shortcuts bypass the tour's click-blocking
// backdrop entirely, since CSS pointer-events does nothing to keydown
// listeners) -- so the guard belongs here, once, covering every path by
// construction instead of by an ever-growing enumeration.
if (window.__FUNPACK_TOUR__) return;
if (!state.project) return;
if (_commitPromise) { _commitQueued = true; return _commitPromise; }
const skipPreviewRefresh = !!opts.skipPreviewRefresh;