Skip to content

refactor(app-router): fence AppElements wire-key construction#1088

Merged
james-elicx merged 2 commits intocloudflare:mainfrom
NathanDrake2406:nathan/726-wire-02-03
May 6, 2026
Merged

refactor(app-router): fence AppElements wire-key construction#1088
james-elicx merged 2 commits intocloudflare:mainfrom
NathanDrake2406:nathan/726-wire-02-03

Conversation

@NathanDrake2406
Copy link
Copy Markdown
Contributor

What this changes

Implements task #726-WIRE-02/03 from issue #726 by moving AppElements wire-key construction and parsing behind AppElementsWire.

The codec now owns constructors and parsers for route, page, layout, template, slot, cache, metadata, and unmatched-slot wire values. Render wiring, layout classification, browser slot merging, mounted-slot header construction, and prefetch cache tests now call the codec instead of assembling or recognizing raw layout:, page:, template:, or slot: keys directly.

Why

Issue #726 calls out that the flat AppElements payload should stay as the transport, but raw wire keys must stop becoming router authority. Before this change, several code paths still constructed or interpreted wire keys with string templates or prefix checks outside the wire boundary, which made it easy for future router work to rebuild semantics around incidental payload strings.

Correctness oracle: Vinext internal invariant from #726. The wire format remains byte-compatible for existing payload keys and legacy metadata behavior, but ownership moves to AppElementsWire.

Approach

  • Added AppElementsWire constructors for layout, template, and slot element IDs, plus an element-key parser and slot-key predicate.
  • Kept route graph semantic ID minting separate from the AppElements wire boundary, because graph IDs are route graph facts, not the transport codec.
  • Made direct route/cache helper constructors private so callers use the codec object as the approved exported boundary.
  • Added source-boundary coverage that rejects new raw AppElements wire-key construction outside server/app-elements-wire.ts and routing/app-route-graph.ts.

Non-goals:

  • Does not promote route topology, slot preservation, cache authority, or NavigationPlanner semantics.
  • Does not change the flat payload shape or legacy key strings.

Bonk: please read issue #726 before reviewing this PR so the big-picture AppElementsWire boundary and router-spine migration context is visible.

Validation

  • vp test run tests/app-elements.test.ts tests/prefetch-cache.test.ts tests/app-page-element-builder.test.ts tests/slot.test.ts tests/app-page-route-wiring.test.ts tests/layout-classification.test.ts tests/app-browser-entry.test.ts tests/app-page-render.test.ts tests/app-page-execution.test.ts
  • vp check exits 0. It still reports the existing unrelated typescript-eslint(no-redundant-type-constituents) warning in packages/vinext/src/server/request-pipeline.ts:604.
  • Commit hook ran vp check --fix and knip --no-progress successfully.

Risks / follow-ups

The parser deliberately recognizes the existing legacy wire strings and rejects malformed keys as non-wire entries. Future #726 work can build stronger semantic IDs and planner decisions on top of this without relying on missing payload entries or raw key prefixes as proof.

AppElements transport keys could still be constructed or recognized through scattered raw string prefixes outside the wire codec. That kept the flat payload format acting as an informal semantic API and left legacy compatibility behavior covered only by indirect route tests.

Make AppElementsWire the exported constructor and parser boundary for route, page, layout, template, slot, cache, metadata, and unmatched-slot wire values. Route graph semantic ID minting stays separate, while render wiring, layout classification, browser slot merging, and prefetch cache tests now consume the codec boundary.

Tests cover canonical key construction and parsing, legacy metadata read/write behavior, unmatched-slot marker compatibility, and a source-boundary check that rejects new raw AppElements wire-key construction outside the codec and route graph.
Copilot AI review requested due to automatic review settings May 6, 2026 04:11
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 6, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1088

commit: 98c6609

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good boundary-fencing work. The codec ownership is clear: constructors and parsers live in app-elements-wire.ts, the route graph keeps its own semantic ID minting, and the source-boundary test enforces the invariant. A few things to consider:

  1. isSlotId performance: The current implementation delegates to the full parseAppElementsWireElementKey parser, which tries five prefix branches and allocates intermediate strings/objects for every call. isSlotId is called per-key in normalizeAppElements (two loops), mergeElements (two loops), and getMountedSlotIds — all hot per-navigation paths. See inline comment for a cheaper alternative. Not blocking, but worth a follow-up if profiling shows it matters.

  2. Boundary test is a strong addition. The filesystem-walking test that rejects raw wire-key construction outside the allowed set is a good structural invariant. One gap noted inline — the regex doesn't catch string concatenation patterns.

  3. Stale comment in app-elements.ts:24: The comment says createAppPayloadPageId stays private but this PR also made createAppPayloadCacheKey and createAppPayloadRouteId private. Consider updating it to mention all three, or dropping it since the AppElementsWireCodec type signature already documents the public surface.

Overall the change is clean and well-scoped. No correctness issues found.

}

function isAppElementsWireSlotId(key: string): boolean {
return parseAppElementsWireElementKey(key)?.kind === "slot";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This full-parses the key through all five prefix branches + allocates a result object just to check kind === "slot". Every call to isSlotId pays this cost, and it's called per-key in normalizeAppElements (×2 loops), mergeElements (×2 loops), and getMountedSlotIds.

A cheaper alternative that stays inside the codec boundary:

Suggested change
return parseAppElementsWireElementKey(key)?.kind === "slot";
function isAppElementsWireSlotId(key: string): boolean {
if (!key.startsWith("slot:")) return false;
const body = key.slice("slot:".length);
const sep = body.indexOf(":");
return sep > 0 && body.charCodeAt(sep + 1) === 0x2f; // next char is '/'
}

This validates the same structural invariant (slot:<name>:/<treePath>) without allocating. Not blocking — just a perf consideration if this becomes measurably hot.

}

function parseTreePath(input: string): string | null {
return input.startsWith("/") ? input : null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: parseTreePath requires a leading /, which means bare segment names like blog are rejected. This is correct for the current wire format where tree paths are always absolute (/, /blog, /(marketing)/blog/[slug]), but it's an implicit contract. A brief doc-comment on this function would help future contributors understand the invariant.

Comment thread tests/app-elements.test.ts Outdated
path.join(sourceRoot, "server/app-elements-wire.ts"),
]);
const rawWireConstruction =
/`(?:route|page|layout|template):\$\{|`slot:\$\{|\.startsWith\("(?:slot|layout|page|route|template):"\)/;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex catches template-literal construction and .startsWith() recognition, but wouldn't catch string concatenation patterns like "slot:" + name + ":" + path. That's probably fine for the current codebase since nobody uses concat, but worth noting in a comment near the regex documenting its intended scope — a future contributor might reach for concat and bypass the fence.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 6, 2026

Review posted successfully. Here's a summary of what I flagged:

3 inline comments:

  1. isSlotId performance (app-elements-wire.ts:187) — The current implementation delegates to the full 5-branch parser and allocates an object just to check kind === "slot". Suggested a cheaper startsWith + minimal validation alternative since this runs per-key in multiple hot navigation paths.

  2. parseTreePath implicit contract (app-elements-wire.ts:148) — The leading / requirement is correct but undocumented. Suggested a brief doc-comment for future contributors.

  3. Boundary test regex gap (tests/app-elements.test.ts:182) — The regex catches template literals and .startsWith() but not string concatenation patterns. Suggested documenting the intended scope.

1 top-level note:

  • Stale comment in app-elements.ts:24 only mentions createAppPayloadPageId being private, but this PR also privatized createAppPayloadCacheKey and createAppPayloadRouteId.

No correctness issues found — the change is clean and well-scoped.

github run

Make slot-key recognition a cheap structural predicate, document the absolute tree-path parser contract, and broaden the boundary coverage to catch raw wire-key concatenation.

Also route layout classification ids through AppElementsWire so raw layout-key construction stays fenced inside the codec boundary.
@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All prior review items addressed cleanly in the second commit. Tests pass (all 204 across 9 affected files). The boundary is well-drawn and the source-fence test is a strong structural invariant.

Verified:

  • isAppElementsWireSlotId uses the cheap startsWith + charCodeAt predicate (no allocation)
  • parseTreePath has a doc-comment explaining the absolute-path contract
  • Boundary test regex now catches concatenation patterns, with self-test assertions proving coverage
  • Stale comment updated to "Raw constructor helpers stay private"
  • app-page-dispatch.ts layout ID construction fenced through AppElementsWire.encodeLayoutId
  • Route graph functions intentionally kept separate from wire codec (graph-level semantic IDs, not transport) — allowed set in boundary test correctly includes both files
  • AppElementsWireElementKey type is module-private but inferrable from the codec return type — fine for now, future callers can narrow via TypeScript inference
  • No raw wire-key construction remains outside the two allowed files

One minor observation (non-blocking):

The route graph functions (createAppRouteGraphRouteId, createAppRouteGraphSlotId, etc. in routing/app-route-graph.ts) produce byte-identical strings to the wire codec constructors. The separation is conceptually correct — graph IDs are route-topology facts, wire IDs are transport keys — but if the wire format ever changes, these would need to change in lockstep. The boundary test catches construction outside the allowed set, but doesn't enforce that the two sets produce the same format. A future follow-up could add a shared format assertion (e.g., a test verifying createAppRouteGraphSlotId(name, path) === AppElementsWire.encodeSlotId(name, path)) to prevent silent drift. Low priority since any divergence would immediately break routing.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 6, 2026

Review submitted successfully. Here's a summary:

Approved — the PR cleanly addresses all prior review feedback and the code is correct.

What I verified:

  • All 204 tests pass across 9 affected test files
  • The isSlotId optimization now uses cheap startsWith + charCodeAt (no object allocation)
  • parseTreePath has a doc-comment explaining the absolute-path invariant
  • Boundary test regex catches template literals, startsWith, and string concatenation, with self-test assertions
  • Stale comment updated
  • No raw wire-key construction leaks outside the two allowed files (app-elements-wire.ts and app-route-graph.ts)

One non-blocking observation: The route graph and wire codec produce byte-identical strings from separate functions. A future cross-format assertion test could guard against silent drift if the wire format ever changes.

github run

@james-elicx james-elicx merged commit b94650b into cloudflare:main May 6, 2026
24 checks passed
james-elicx added a commit to piffie/vinext that referenced this pull request May 6, 2026
…ound-trip expectation

readAppElementsMetadata always returns an artifactCompatibility envelope after cloudflare#1062, but the round-trip test added in cloudflare#1088 still asserted the pre-cloudflare#1062 shape, breaking once both landed on main.
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.

3 participants