Skip to content

feat(navigation): promote OperationToken to the eligibility authority (#1790 PR 5)#2009

Draft
NathanDrake2406 wants to merge 2 commits into
cloudflare:mainfrom
NathanDrake2406:feat/promote-operation-token
Draft

feat(navigation): promote OperationToken to the eligibility authority (#1790 PR 5)#2009
NathanDrake2406 wants to merge 2 commits into
cloudflare:mainfrom
NathanDrake2406:feat/promote-operation-token

Conversation

@NathanDrake2406

Copy link
Copy Markdown
Contributor

Stacked on #2008 (the V0-suffix rename). This draft currently shows that rename commit as well; once #2008 merges, this branch rebases onto main and the diff becomes logic-only. Review the second commit (feat(navigation): promote OperationToken…) for the actual change. Cross-fork PRs can't target a fork branch as base, hence the stack note rather than a base of #2008.

What this changes

Promotes OperationToken from a passively-carried struct into the single proof-of-eligibility authority for App Router client navigation. The token now verifies whether a navigation result may enter commit approval or cache reuse; ApprovedVisibleCommit remains the separate proof-of-mutation object. This is PR 5 of the navigation architecture work tracked in #1790.

A new server/operation-token.ts module owns the token type and verifyOperationToken — a pure verifier over four dimensions (active-navigation, visible-commit, graph-version, cache-variant) returning a branded VerifiedOperationToken on success.

Why

The token threaded through every NavigationDecision but only its lane field was ever read. The authority it was meant to own lived inline elsewhere:

  • The commit-staleness gate (resolvePendingNavigationCommitDispositionDecision) was a raw boolean — startedNavigationId !== activeNavigationId || startedVisibleCommitVersion !== visibleCommitVersion — evaluated before the token was even built.
  • Cache reuse was gated separately, in its own universe.
  • The token carried operationId (a per-render counter) but no navigation id, so it literally could not answer "does this result belong to the active navigation?"

So traversal, refresh, intercepted routes, redirects, and cache reuse did not share one authority model — exactly the split-brain #1790 calls out.

Approach

  • navigationId on the token (sourced from startedNavigationId) supplies the missing active-navigation fact.
  • Commit gate routed through verifyOperationTokenForCommit, inline boolean deleted in the same change. The staleOperation skip + trace are byte-identical. planPendingRootBoundaryFlightResponse now requires the VerifiedOperationToken brand, making verify-before-plan a compile-time guarantee rather than a control-flow convention.
  • Cache reuse shares the authority: planFlightResponseArrived gates an accepted cache-entry reuse decision through verifyOperationTokenForCacheReuse, hard-navigating (cacheReuseTokenRejected) when the proof's graph version no longer matches the installed route graph.
  • Absence is not permission: a checked dimension tolerates an absent authority fact (low-context paths keep working), but a required one fails closed, and require implies evaluation. This is the fail-closed contract segment-cache / BFCache writes need in PR 6/7.

Scope boundaries (non-goals)

  • Behavior-preserving today. The cache-variant dimension ships in the verifier (unit-tested) but its live data feed is PR 7 (segment cache variant keys); the planner passes installedCacheVariantFingerprint: null so that dimension stays dormant rather than comparing the token to itself.
  • The graph-version cache-reuse guard cannot reject in the single-document flow (the token's graphVersion is minted from the same manifest the planner verifies against). It activates once cross-document / segment reuse (PR 6/7) can carry a diverged graph version.
  • BFCache identity consumer is PR 6. This PR establishes the authority "before any new cache behavior", per the issue's start-order.

Validation

  • tests/operation-token.test.ts (new, 15 tests): every verdict dimension, the VerifiedOperationToken brand, the absent-vs-mismatch split, deterministic check order, and the require ⇒ evaluate fail-closed contract.
  • tests/navigation-planner.test.ts: two new tests cover the cache-reuse graph-version seam (stale graph → cacheReuseTokenRejected; matching graph → commits unchanged).
  • Existing app-browser-entry.test.ts (8 staleOperation assertions) and the full navigation-planner suite stay green, proving the gate refactor preserved behavior.
  • vp check (format, type-aware lint, knip) green; pre-commit full check + staged tests green.

Risks / follow-ups

  • cacheReuseTokenRejected is a new hard-navigation cause that cannot fire in the current single-document flow; first real activation is PR 6/7, which should add cross-document divergence coverage.
  • PR 6 (BFCache identity on the route graph) and PR 7 (segment cache) consume this authority: PR 6 supplies the BFCache write boundary, PR 7 supplies the real cacheVariantFingerprint + the segment-cache-write boundary (require: ["cacheVariant"]).

Refs #1790.

The navigation planner's public and internal types were suffixed `V0`
(RouteSnapshotV0, NavigationDecisionV0, FlightResultV0, and 19 others).
The suffix implied a versioning scheme that never materialized — there is
no V1, and the schema-version concerns it hinted at are carried explicitly
elsewhere (NAVIGATION_TRACE_SCHEMA_VERSION, CACHE_PROOF_MODEL_SCHEMA_VERSION).
The bare suffix was pure noise at every call site.

Rename all 22 `…V0` planner types to their unsuffixed names across the
planner, the browser state/entry/action modules, and the planner test
suite. Pure mechanical rename: no behavior change, all 295 planner and
browser-entry tests pass unchanged.
The App Router client carried an OperationToken on every navigation
decision but only read its `lane` field. The authority the token was
meant to own lived inline elsewhere: the commit-staleness gate was a raw
boolean (startedNavigationId !== activeNavigationId or a visible-commit
version mismatch) evaluated before the token was even built, and cache
reuse was gated separately. The token also lacked the lifecycle
navigation id, so it could not answer "does this result belong to the
active navigation?" at all — its operationId is a per-render counter.

Promote OperationToken to the single proof-of-eligibility object. A new
operation-token.ts module owns the token type and verifyOperationToken,
a pure verifier over four dimensions (active-navigation, visible-commit,
graph-version, cache-variant) that returns a branded VerifiedOperationToken
on success. The token verifies; ApprovedVisibleCommit still mutates — the
two stay separate.

- Add navigationId to the token (sourced from startedNavigationId) so it
  can carry the active-navigation authority.
- Route the commit-staleness gate through verifyOperationTokenForCommit
  and delete the inline boolean. The staleOperation skip + trace are
  byte-identical; planPendingRootBoundaryFlightResponse now requires the
  VerifiedOperationToken brand, making "verify before plan" a compile-time
  guarantee.
- Share the authority with cache reuse: planFlightResponseArrived gates an
  accepted cache-entry reuse decision through verifyOperationTokenForCacheReuse,
  hard-navigating (cacheReuseTokenRejected) when the proof's graph version no
  longer matches the installed route graph. Behavior-preserving today (the
  token's graphVersion is minted from the same manifest); a real guard once
  cross-document and segment reuse can diverge (PR 6/7).

Absence is not permission: a checked dimension tolerates an absent
authority fact, but a *required* one fails closed, and require implies
evaluation — the contract segment-cache/BFCache writes need in PR 6/7.

Tests: operation-token.test.ts covers every verdict dimension, the brand,
the absent/mismatch split, and require⇒evaluate; navigation-planner and
app-browser-entry suites stay green, proving the gate refactor preserved
behavior; two new planner tests cover the cache-reuse graph-version seam.
@pkg-pr-new

pkg-pr-new Bot commented Jun 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2009
npm i https://pkg.pr.new/vinext@2009

commit: 29af69f

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant