Skip to content

Matrix-critical refactors from review (fresh/revalidation unification, trie suffix order, loader-cache rebind) #568

Closed

Description

@ivogt

Summary

Internal refactors and one correctness item flagged by the multi-agent review that sit on semantic-matrix-critical paths (middleware scope, handler-first ordering, segment resolution, the matching trie). Each requires a deliberate change plus a semantic-matrix re-run (pnpm --filter @rangojs/router exec playwright test semantic-matrix), so they were intentionally deferred from the stabilization PR rather than applied as "safe cleanups". Mostly internal — the one consumer-observable item (trie suffix ordering) has a userland example.

Correctness (consumer-observable)

Trie suffix-param matching has no longest-suffix-wins rule

walkTrie's suffix-param tier (node.xp) iterates suffix keys in object order; overlapping suffixes resolve by declaration order, not by specificity.

// Consumer expects the more specific suffix to win:
path("/:file.min.js", MinJs)   // more specific
path("/:file.js", Js)
// Request /app.min.js may match :file.js (file="app.min") depending on declaration order.

Fix: pre-sort node.xp suffix keys by descending length at build time (src/build/route-trie.ts, where .xp is constructed — allocation-free on the match hot path). Add trie tests + a semantic-matrix row.

Internal refactors (no consumer-facing change; do for maintainability + measured before/after)

  • fresh.ts vs revalidation.ts are near-duplicate resolution trees. resolveSegment/resolveOrphanLayout/resolveParallelEntry/resolveLoaders + collectEntryLoaders (byte-identical) mirror each other, both encoding the handler-first guarantee. Unify behind a single traversal with a per-segment strategy. Matrix-critical — the review explicitly says treat this as a deliberate change with a full matrix re-run.
  • M11: orphan-revalidation inlines ~166 lines of parallel-slot resolution instead of calling the shared helper (revalidation.ts:1154-1319). Two deliberate divergences (unconditional defaultOverride:{value:true}, loader-resolve-before-push order) must become explicit policy args, pinned by parallel-revalidate-fns-invocation.test.ts. PR feat(router): opt out of router-placed ViewTransition via transition({ viewTransition }) #526 shows the copies have drifted before.
  • Parallel-slot component-resolution block duplicated within the two revalidation functions (~40 lines, on the partial-revalidation hot path, entangles _currentSegmentId/handlerRan mutation + streamed-handler call).
  • Duplicated merge-and-clone Response builder in executeMiddleware (+ a related block in executeInterceptMiddleware) — touches the response-header-precedence contract pinned by middleware-throw-response.test.ts / intercept-middleware-headers.test.ts.
  • yieldFromStore vs runtime cache-hit fresh-loader resolution duplicated in match-middleware/cache-lookup.ts — parity-sensitive, on the cache hot path.
  • loader-cache permanently rebinds ctx.use, chaining one wrapper per cached loader (O(N) dispatch); correctness relies on an undocumented synchronous capture-before-overwrite invariant. Replace mutate-and-chain with a stable interceptor + Map prime (touches setupLoaderAccess in loader-resolution.ts); needs multi-cached-loader accumulation tests.

Acceptance

  • Each item: behavior-preserving (or matrix rows updated if intentionally changed), execution-model.md updated where the contract is restated, semantic matrix green, debugPerformance before/after for the hot-path ones.

Activity

  1. ivogt commented on Jun 14, 2026

    @ivogt
    CollaboratorAuthor

    Reopening note: #576 shipped 4 of the 6 internal refactors + the trie-suffix fix, but two sub-items remain (orphan→shared-helper unification with policy args, and the fresh/reval traversal merge). They were deferred because divergence (A) is intentional and the existing pinning test doesn't actually lock it — see #577 for the forensic verdict and the coverage-first plan.

  2. reopened this on Jun 14, 2026
  3. ivogt commented on Jun 14, 2026

    @ivogt
    CollaboratorAuthor

    Status: 5 of 7 items done; 2 sub-items remain (this issue tracks them)

    Done:

    All shipped behavior-preserving (semantic matrix 61/61 dev+prod, no row changes; 4-way adversarial refutation, all refuted: false).

    Remaining (deferred — real semantic risk the current tests don't cover):

    1. Orphan-revalidation calls the shared parallel helper (M11). resolveOrphanLayoutWithRevalidation still inlines ~166 lines. Forensic verdict on the two divergences:

    2. Unify fresh.ts vs revalidation.ts resolution trees. The "byte-identical" premise is false — collectEntryLoaders twins differ in the loader call (+8 args) and matchedIds accumulation; resolveSegment twins diverge in revalidate gating + route-handler ordering. Strategy-parameterized traversal, not a literal dedup; do after sub-item 1.

    Blocker — coverage first. parallel-revalidate-fns-invocation.test.ts does not lock either divergence (every orphan case uses belongsToRoute=true, where (A) coincides; fns override the seed). Before refactoring, add a pinning test for the divergent (A) case: orphan parallel with belongsToRoute=false, slot ∉ clientSegmentIds, parent ∈ clientSegmentIds, zero revalidate fns → assert the slot resolves non-null and is in matchedIds.

    (Consolidating the duplicate follow-up #577 into this issue, since this one is the natural home.)

  4. ivogt commented on Jun 14, 2026

    @ivogt
    CollaboratorAuthor

    Sub-item 1 (fresh.ts vs revalidation.ts traversal unify): experiment run, mostly not worth it

    Attempted the strategy-parameterized traversal unification on a throwaway branch (fully reverted). Findings, so it doesn't get re-run:

    • collectEntryLoaders DOES unify cleanly — it was the one genuinely byte-identical walk (skip-already-seen, dedup, parallel/layout recursion, orphan-inherited loaders); only "how one entry's loaders resolve" differed, a clean one-line strategy. Implemented + green (matrix 61/61 dev+prod, cache e2e 135/135), then dropped because it's a modest win not worth the file churn on its own.
    • resolveLoaders breaks the premise — fresh has a loadingDisabled await-all branch (+ its own metrics) that the revalidation twin doesn't. Unifying = either change partial-render behavior or fork inside the "shared" fn anyway.
    • resolveSegment/resolveOrphanLayout is the wall — five genuinely-divergent behavioral hooks (per-segment client-cache gate, component:null reuse signal, _handlerRan bookkeeping, parallel-slot policy, matchedIds threading) and two different return shapes (ResolvedSegment[] vs {segments, matchedIds}). A strategy object there is more abstraction than the duplication it removes, and would itself become the thing that drifts.

    One durable artifact landed: #579 pins the revalidation skipped-loader-stays-matched behavior the suite never covered.

    Recommendation: keep this sub-item deferred / consider WON'T-FIX. The remaining 'duplication' is two genuinely different resolution engines (full render vs partial-delta), not a dedup opportunity. The other 5 #568 items (trie suffix + 4 internal de-dups in #575/#576, orphan→shared helper in #578) are done.

  5. ivogt commented on Jun 14, 2026

    @ivogt
    CollaboratorAuthor

    Closing — all 7 items resolved

    Both remaining sub-items have landed on main, so this issue is done.

    Final ledger

    # Item Resolution
    1 Trie suffix longest-suffix-wins (consumer-observable) ✅ #575
    2 Merge-and-clone Response builder dedup (executeMiddleware/executeInterceptMiddleware) ✅ #576
    3 yieldFromStore vs cache-hit fresh-loader resolution dedup ✅ #576
    4 Parallel-slot component-resolution block dedup ✅ #576
    5 loader-cache O(N) ctx.use rebind → stable interceptor + Map ✅ #576
    6 Orphan-revalidation → shared parallel helper (M11) ✅ #578
    7 Unify fresh.ts vs revalidation.ts resolution trees ⛔ WON'T-FIX (see below); behavior pinned by #579

    Sub-item 6 (#578) — how the divergences were carried, not normalized

    resolveOrphanLayoutWithRevalidation no longer inlines ~166 lines of parallel-slot resolution; it calls the shared helper with the two divergences expressed as explicit policy args:

    Output is byte-identical on both paths (4-way adversarial refutation, all refuted:false). The coverage-first blocker was cleared first: a pinning test for the divergent (A) case (belongsToRoute=false, slot ∉ clientSegmentIds, parent ∈ it, zero revalidate fns) that goes red if (A) is normalized to type-derived.

    Sub-item 7 (#578's sibling, #579) — WON'T-FIX, with rationale

    The strategy-parameterized traversal unification was attempted on a throwaway branch (fully reverted). The "byte-identical twins" premise is false:

    • collectEntryLoaders does unify cleanly — but it's a modest win not worth the file churn alone.
    • resolveLoaders breaks the premise (fresh has a loadingDisabled await-all branch the reval twin lacks).
    • resolveSegment/resolveOrphanLayout is the wall: five genuinely-divergent behavioral hooks (per-segment client-cache gate, component:null reuse signal, _handlerRan bookkeeping, parallel-slot policy, matchedIds threading) and two different return shapes (ResolvedSegment[] vs {segments, matchedIds}). A strategy object there is more abstraction than the duplication it removes — it would itself become the thing that drifts.

    These are two genuinely different resolution engines (full render vs partial-delta), not a dedup opportunity. The one durable artifact from the experiment landed as #579, which pins the revalidation skipped-loader-stays-matched behavior (skipped loader dropped from emitted segments but its id kept in matchedIds) that the suite never covered.

    All matrix-critical work shipped behavior-preserving: semantic matrix 61/61 dev+prod, no row changes.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions