feat(plugin-history-sync): support preventDefault via history reconciliation (FEP-2001)#719
feat(plugin-history-sync): support preventDefault via history reconciliation (FEP-2001)#719ENvironmentSet wants to merge 5 commits into
Conversation
…sts (FEP-2001 stage 1) Remove the relay loadRef activity.context test because Relay integration is outside the plugin-history-sync public contract. Remove the fallbackActivity single-call plugin-instance test because it manually invokes overrideInitialEvents once and asserts the same call count. Delete the orphan Relay GraphQL fixtures and remove Relay/GraphQL-only dev dependencies, config, PnP, cache, and lockfile entries.
…and FEP-2001 target specs (stage 2) The 8 it.failing specs are the acceptance criteria for the stage 3 reconciliation implementation. They should be converted to normal it specs once reconciliation is implemented and the target behavior is satisfied.
…ation to support preventDefault (FEP-2001) Replace the imperative delta-and-flag paradigm (queueing history.back() in onBefore* hooks, counting pushFlag, muting popstates with silentFlag) with a desired/actual reconciliation engine: the desired entry list is computed from the current stack (entered activities x live steps), the actual browser history is tracked by a partial per-session model (entryIndex coordinates, optimistic unknown entries preserved as restoration targets), and a serial reconciler converges the browser onto the stack with go/pushState/replaceState — one operation per iteration, recomputing both sides each time. popstate-driven navigations (back/forward/go) are now translated into the formal action path (actions.pop/stepPop/push/stepPush), so every plugin's onBefore* hooks — including preventDefault — participate in browser-initiated navigation. When a navigation is prevented the stack stays unchanged and the unconditional post-popstate reconcile pass restores the browser to it automatically. Hooks no longer perform history operations, making re-entrant dispatches safe (intermediate states collapse into the next reconcile pass), and pushFlag/silentFlag are removed entirely (self-induced popstates are identified by expected entryIndex instead). Unfails the 8 stage-2 acceptance specs (it.failing -> it) covering blocker-interop back/forward/step navigation, proceed replay, rapid back convergence, re-entrant navigation inside onBlocked, and go(n) jumps; adds 3 regression tests from review probes (stale forward-branch truncation after back+push and after replace-shrink, in-place replace preserving ancestors). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
commit: |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
stackflow-docs | 56a7872 | Commit Preview URL | Jun 11 2026, 03:57 AM |
Deploying stackflow-demo with
|
| Latest commit: |
56a7872
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://d017cf46.stackflow-demo.pages.dev |
| Branch Preview URL: | https://feature-fep-2001.stackflow-demo.pages.dev |
…journal lifecycle specs (FEP-2001 follow-up) ① Obs-1(관찰-only 엔트리 보호) 양 모드 스펙과 cross-reload 멀티 점프 체인 충실도 스펙은 it.failing 4건으로 후속 구현의 수용 기준. ② harness에 sessionStorage shim(fault/부재/접근 throw)과 reloadHarness 추가.
… restoration fidelity (FEP-2001 follow-up) - Restrict rewrite/truncation eligibility to entries this session itself wrote, via an explicit provenance on known entries: journal-restored and observed-only entries stay protected restoration targets even after being visited, resolving Obs-1 (blocked back across a reload boundary no longer rewrites a previous session's step entry) in both journal and no-journal fallback modes. - Introduce HistoryEntryJournal (sessionStorage, flatted, internal): boot seeds the model with validated previous-session entry knowledge (stack restoration unchanged), and backward/forward multi-entry jumps replay the journaled chain historically (original event ids/dates) — including slot materialization for the forward region so the core's slot-order-derived active activity stays aligned with navigation order — restoring stack fidelity for cross-reload go(±n). - Journal failures (quota, privacy modes, corrupt or mismatching persisted data) are expected: they degrade to the optimistic per-entry behavior with a one-time diagnostic and never block reconciliation. - Unflag the four cross-reload acceptance specs (Obs-1 journal/fallback, backward and forward multi-entry jump fidelity) — now passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolves FEP-2001
Problem
plugin-history-synccould not compose withpreventDefault:Popped/StepPoppeddirectly viadispatchEvent, so no plugin'sonBeforePop(e.g.plugin-blocker) ever ran for browser-initiated navigation.history.back()ticks were queued inonBeforePop/onBeforeStepPop/onBeforeReplacebefore knowing whether another plugin would prevent the event, leaving the URL and the stack permanently out of sync.pushFlagleaked on prevented forward navigations — the counter was incremented beforepush(), and never decremented when the push was prevented, silently swallowing history sync for subsequent pushes.preventDefault().Two deeper constraints shaped the solution: the core runs post-effect hooks synchronously (nested dispatches re-enter mid-chain, so hook-time queueing can never guarantee operation order), and the
Poppedreducer truncates steps on directexit-donetransitions (so effect-payload-based entry counting is unreliable).Solution: reconciliation
The imperative delta-and-flag engine is replaced with a reconciliation engine that treats browser history as a render target:
BrowserHistoryEntryModel) tracks the real browser history with absoluteentryIndexcoordinates persisted in history state. Unknown entries (previous sessions) are preserved optimistically as restoration targets; entries below the app's anchor are never touched.HistoryReconciler) converges actual onto desired through a mutex queue, one operation per iteration, recomputing both sides each time. Self-induced popstates are identified by expectation matching (replacingsilentFlag); stale forward branches recorded this session are truncated via push semantics.actions.pop()/stepPop()/push()/stepPush(), so every plugin'sonBefore*hooks andpreventDefaultwork for browser navigation. A prevented navigation leaves the stack unchanged and the reconciler restores the browser to the stack's position automatically.onBeforePush/onBeforeReplaceonly fillactivityContext.path.pushFlagandsilentFlagare gone.Expected conditions (prevented navigation, unknown entries, out-of-app cursor, paused stack) are handled as values; unexpected desync (go timeout, convergence cap, out-of-range op) raises
HistorySyncDesyncErrorwith diagnostics, one resync attempt, and an explicit give-up — nothing is silently swallowed.Follow-up: per-tab entry journal for cross-reload restoration fidelity
Two residual limitations surfaced during review were resolved in the same PR:
session-write/journal/observation), and rewrite/truncation eligibility is restricted to entries this session itself wrote. Observed-only and journal-restored entries remain protected restoration targets even after being visited — fixed in both journal and no-journal fallback modes.history.go(±n)and long-press history jumps over entries recorded before a reload used to restore only the landing entry's snapshot, losing the intermediate stack chain. A new internalHistoryEntryJournal(sessionStorage, per tab, same flatted serialization as history state) records this app's entries; on boot it is validated against the current entry and seeds the model — stack boot restoration is unchanged (no UX/loader change). Backward jumps into journal-known territory replay the known ancestor chain as historical events (user-visible exits still go through formal, preventablepop()dispatches), and forward jumps replay known intermediates, so back/forward chains stay intact across reloads.Journal failures (storage unavailable,
SecurityErroron access, quota, corrupted/foreign payload, version mismatch) are expected conditions: one-shot diagnostics, graceful fallback to the previous optimistic behavior, fully isolated from the reconcile path.Commits (5 stages)
2c1253c029200573it.failingencoding the target behavior.380ed94cit.failingspecs flipped to green, plus 3 regression tests reproducing reviewer probes (stale forward branch truncation, in-place replace root preservation, replace-shrink truncation).6750b448it.failingspecs + 5 green specs (prevent combinations, dual instance, storage fallback); harness gained a sessionStorage shim (fault/absence/access-throw) and reload simulation.56a78723it.failingspecs flipped to green.Verification
@stackflow/plugin-history-sync: 8 suites / 78 tests green (repeated runs, no flakes) — including all 27 pre-existing engine specs unchanged@stackflow/plugin-blocker: 37 tests green (proceed-replay and notification-order contracts intact)tsc --noEmit, biome, build greenKnown limitations (documented, non-blocking)
🤖 Generated with Claude Code