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 7 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 | 1fb2a08 | Commit Preview URL | Jun 11 2026, 12:10 PM |
Deploying stackflow-demo with
|
| Latest commit: |
1fb2a08
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://043ee4fd.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>
…elay loadRef context tests Restore the two tests removed in 2c1253c at maintainer request: fallbackActivity single-call per plugin instance and relay loadRef activity context loading. Also restore the relay fixture/dependency support needed by the context test. Verified that both restored tests pass against the reconciliation engine from 380ed94 and 56a7872.
439bc4c to
ad07863
Compare
Summarize the FEP-2001 work for continuation by other agents, in four documents under .agents/worklogs/fep-2001/: problem recognition and definition (01), direction setting with rejected approaches and decision principles (02), solution design for the reconciliation engine and the entry journal (03), and implementation context with the full decision logs D1-D10/FD1-FD10, core behavior facts, commit map, review history, and known limitations (04). 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. Every known entry carries a provenance (session-write/journal/observation); 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 written 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.Cross-reload restoration fidelity (
HistoryEntryJournal)A per-tab journal (sessionStorage, same flatted serialization as history state, internal module) records the entries this app wrote, so knowledge survives reloads:
session-writeentries. Journal-restored and observed-only entries remain protected restoration targets even after being visited — a blocked back across a reload boundary no longer rewrites a previous session's step entry (back-granularity preserved). The protection also holds in no-journal fallback mode (storage unavailable/invalid), where observed-only entries are simply never rewritten.go(±n), history long-press) across reload boundaries reconstruct the intermediate chain: backward landings replay the known ancestor chain historically (each user-visible pop still goes through the formal, preventable pipeline) and materialize forward-region slots so core slot ordering matches navigation order; forward jumps replay known intermediate entries.Scope guarantees:
@stackflow/coreunchanged; public plugin API unchanged; new modules are internal (not exported);HistoryQueueContextcontract preserved; SSR /defaultHistorystaged setup and post-reload boot UX preserved.Commits
2c1253c0ad078638at maintainer request).29200573it.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.failing(Obs-1 both modes, cross-reload chain fidelity) + prevent/dual-instance/storage-fault specs.56a78723it.failingspecs flipped to green.ad078638Verification
@stackflow/plugin-history-sync: 8 suites / 80 tests green (repeated runs, no flakes) — including all 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