Repository navigation
Description
Follow-ups from the caching/PPR/render-pipeline audit and the advisor/caching-ppr-fixes change wave (8 fixes: shell-HIT redirect guard, document-cache host key, render-callable cacheTag() docs, Vercel base64 parity, CF no-TTL marker warning, navigation-loading dev+prod e2e parity, untagged-bake + action-refetch diagnostics, SSR capture hardening). Those 8 landed and pass the full gate; the items below were deliberately deferred out of that wave. Plan files with full context live under plans/ (001–011 + README).
Grouped by type. None is blocking; each is independently pickup-able.
A. DRY consolidations (from the /simplify review)
All three are small, apply to files outside the change wave's scope, and were skipped to keep the wave file-disjoint. Each new site correctly mirrors an established idiom — these consolidate the idiom, not fix a defect.
-
safeSameOriginLanding(raw, origin, basename)shared helper. The "neutralize a blocked redirect to a safe same-origin landing" fallback (basename && basename !== "/" ? basename : "/") is inlined in bothpackages/rangojs-router/src/rsc/redirect-guard.ts:82and the newresolveShellHitRedirectTargetinpackages/rangojs-router/src/rsc/rsc-rendering.ts. Extract one helper (insrc/redirect-origin.ts, alongsideresolveSameOriginRedirect) so the "where does a blocked redirect go" answer lives in one place. Effort: S. -
documentKeyBase(url)shared helper. Host+path+search key composition now lives in two near-duplicate spots with slightly different search formatting:packages/rangojs-router/src/cache/cache-scope.tsgetCacheKeyBase(${host}${pathname}…) and the inline default-key template inpackages/rangojs-router/src/cache/document-cache.ts. Extract a shared base so the host-namespacing rule can't drift between the segment and document tiers. Effort: S. -
warnOncePerNamespace(seen, message)private method onCFCacheStore. Theconst id = this.namespace ?? "default"; if (!seen.has(id)) { seen.add(id); console.warn(...) }dance is now open-coded three times inpackages/rangojs-router/src/cache/cf/cf-cache-store.ts(the two pre-existing floor/read-side warnings + the new no-TTL one). Collapse to a private method. Effort: S. -
Migrate warn-once console emitters to a structured diagnostics surface when the operability surface from PPR operability: snapshot size guard, capture diagnostics, and the silent missing-KV no-op #651/PPR capture pipeline hardening: narrow the write barrier, unify bounded drains, tame the backoff-vs-CI flake #652 lands. Do all of them as one batch: the two new emitters (
warnedNoTagInvalidationTtlin cf-cache-store,warnUntaggedShellBakeOncein shell-capture) plus their four existing siblings (warnedNoKvReadInvalidation,warnedTagInvalidationTtlFloor,warnedNullCaptures,warnedRefusedCaptures). Neither new warning is a special candidate to move alone. Depends on: PPR operability: snapshot size guard, capture diagnostics, and the silent missing-KV no-op #651/PPR capture pipeline hardening: narrow the write barrier, unify bounded drains, tame the backoff-vs-CI flake #652.
B. Deferred test coverage
- Shell-HIT redirect flip-to-redirect e2e (dev + production). Plan 001 shipped the fix (
resolveShellHitRedirectTarget) + a unit test on the extracted helper, but deferred the end-to-end case: capture a PPR shell, flip the route tomatch.redirectwithin the shell TTL, and assert a cross-origin flip lands on the safe same-origin target — in BOTH dev and(production). Needs a switchable-redirect route inpackages/rangojs-router/e2e/test-app(+ its.gen.ts). The branch is documented as near-unreachable, so this is thoroughness coverage of the guard, not a hot path. Seeplans/001-shell-hit-redirect-guard.mdStep 3. Effort: M (fixture design is the cost).
C. Held / investigate items
-
[HELD — needs design] Hole-free shell-HIT loader optimization (plan 007). Original idea: skip the fresh bake-lane loader run on hole-free shell HITs since the overlay discards its result. An adversarial fact-check proved this UNSAFE as a skip —
executeLoaderDataon the HIT path has unconditional side effects the system relies on: loader telemetry (phase metric +rango.loaderspan viactx.use→observePhase), cache-tag recording into_requestTags, and the_loaderCacheOverridesinterceptor install that lets a handler'sctx.use(loader)get the overlaid value instead of re-executing. A real win is a capture-time-replay redesign (record tags/metrics/override at capture, replay on HIT without running the body), not a surgical skip. This is a design spike, not an executor task. Full reasoning inplans/007-hole-free-shell-hit-skip-loader.md. -
[INVESTIGATE] Capture derived-context render-barrier inheritance (plan 009).
attemptCapture'sderivedCtx = Object.create(reqCtx)resets eight render-scoped accumulators but NOT the_renderBarrier*family, and the barrier getter/resolver are closure-bound to the FOREGROUND ctx — so a bake-lane loader callingctx.rendered()thenctx.use(handle)during capture reads the foreground request's handle snapshot, and the capture lane is coupled to the foreground's single barrier instance. Reachable only viarendered()in a bake-lane loader (experimental surface). Deliverable is a verdict + reproduction (or documented non-repro), not a speculative fix. Note: this touchessrc/rsc/shell-capture.ts, which fix(ppr): collect shell tags at the putShell write barrier — async-recorded tags evict the shell (#676) #680 already restructured. Seeplans/009-investigate-capture-barrier-inheritance.md. -
[INVESTIGATE] SWR background handle-store swap (plan 010). On a stale
"use cache"HIT, the background revalidation swaps the SHAREDrequestCtx._handleStoreand stampsstampCacheExecon shared args, restoring only in afinally— andwaitUntilschedules on the next microtask, so the prologue can run while the foreground is still rendering. Hypotheses: (a) foreground handle pushes get routed into the isolated background store and lost from the live document; (b) a concurrent foregroundctx.set()/ctx.header()throwsINSIDE_CACHE_EXEC. The siblingbackground-revalidation.tsswaps only AFTER draining the foreground — the "safe ordering" reference. Deliverable is a verdict + reproduction per hazard. Seeplans/010-investigate-swr-bg-handle-store-swap.md. -
[INVESTIGATE] SSR resume errors are unrecoverable after prelude commit (SSR-03).
resumeShellHTMLrunsJSON.parse(postponed)/resume()atpackages/rangojs-router/src/ssr/index.tsxAFTERrsc-rendering.tshas already enqueuedpreludeBytes(committed HTTP 200 + static shell). A corrupt/truncated storedpostponedblob or anyresume()throw yields a permanently broken page for that cache key until it ages out, with no fallback to axis-1. Fix direction: parse + startresume()setup (or an integrity check on the postponed blob) BEFORE the serve path commits the prelude. Effort: M. Interacts with SSR-04. -
[INVESTIGATE] Shell cache invalidated only by
React.version, not app build/generation (SSR-04).buildShellKey(packages/rangojs-router/src/rsc/shell-serve.ts) keys on host+path+search only, andisValidShellHitgates solely onentry.reactVersion === React.version. With a persistent shared store (KV / runtime-cache / D1) that survives deploys, an app-code change that keeps the same React version leaves a stale-build prelude+postponed live under the same key → resuming an old build's postponed holes against new Flight → tree-mismatch inresume()(the SSR-03 broken-serve). Fold a build/generation id (orctx.version) into the key or the validity check. Effort: M. Confirm overlap with PPR operability: snapshot size guard, capture diagnostics, and the silent missing-KV no-op #651/PPR: capture-time taint guard — refuse to store shells that read per-request data into shell content #653 first.
D. Tooling
- Extend an API-existence guard to the caching skills.
pnpm check:docs-apiguards onlydocs/why-rango.md. ThecacheTag()render-callable drift fixed in plan 003 (skills teaching a"use cache"-only API after the code shipped a second form) recurred silently because the caching skills aren't guarded. Extend the check to the caching/PPR skills so referenced identifiers/signatures are verified againstsrc/. Effort: M.
Deferred-and-rejected items (recorded so they aren't re-audited): the 32-bit param-hash / cache-key collision ceilings (PPR-03, CACHE-04) are documented in-code as bounded tradeoffs with explicit revisit triggers; the refused-capture backoff map retention (PPR-04) is intentional escalation, folded into #652. See plans/README.md.
Activity
ivogt commentedon Jul 5, 2026
Change wave landed in #685 (branch
advisor/caching-ppr-fixes, 8 commits). The follow-ups tracked here are the items deliberately deferred out of that PR.ivogt commentedon Jul 5, 2026
Status update. Groups A + D are implemented in draft PR #686 (shared
safeSameOriginLanding, sharedcacheKeyBase,warnOncePerNamespace, docs-api guard over the 8 caching/PPR skills — the guard shakedown also fixed a class-declaration false-negative that hidSkip). A4 stays blocked on #651/#652. Groups B and C were investigated; verdicts below, each independently reproduced or traced withpath:lineevidence.B. Shell-HIT redirect flip e2e — NOT DETERMINISTICALLY REACHABLE, recommend closing as unit-covered
The planned fixture (capture shell → flip route to redirect → HIT within TTL) cannot fire the branch through real routing, in either mode:
plan.mode === "redirect", which leaves_classifiedRouteunset (rsc/handler.ts:827), which nullsresolvePprConfigat the shell gate (rsc/rsc-rendering.ts:124) — the stored shell entry is never consulted; the request exits as a plain 3xx throughguardOutgoingRedirect.match()reuses the classification snapshot (router/match-api.ts:67-69), so classification and tail can only disagree via a mid-request dev-HMR invalidation race — not deterministically testable.match.redirectis only ever produced by URL normalization (trie-matching.ts:325-343,pattern-matching.ts:484-568) — always same-origin, which is why the branch comment says near-unreachable.The branch is correct defense-in-depth and its neutralization matrix is fully pinned by
src/rsc/__tests__/shell-hit-redirect-guard.test.ts(cross-origin → root/basename, protocol-relative, same-origin pass-through). A monkey-patched e2e would test the mock, not the router. Proposing to check this box off as unit-covered with this analysis as the record.C. Investigation verdicts
Plan 009 — capture barrier inheritance: CONFIRMED (survived #680/#685).
attemptCapture's derived ctx (shell-capture.ts:748-770) resets 8 accumulators but inherits the whole_renderBarrier*family; the barrier is closure-bound to the foreground ctx (request-context.ts:1061-1107), and its resolver no-ops once resolved — so the capture lane never gets its own barrier. Reproduced against production wiring (realcreateRequestContext+setupLoaderAccess): a bake-lane loader'sawait ctx.rendered(); ctx.use(handle)resolves instantly and reads the FOREGROUND handle snapshot; the capture's fresh_handleStoreis invisible. 2/2 repro tests green. Consequence: baked loader values can embed foreground handle data inside a capture-lane shell — an internal inconsistency whenever an uncached handler pushes non-deterministic handle data. Fix: extract the barrier wiring into a helper and wire the derived ctx its own barrier + reset_renderBarrierSegmentOrder/Waiters/HandleSnapshot/GuardClosed/_handlerLoaderDeps/_treeHasStreaming. Effort S code / M with mandated coverage. One un-repro'd corner (code-read only): with_treeHasStreaminginherited true, a capture-lanerendered()canseal()the capture's fresh store mid-capture, pairing it with the foreground's segment order.Plan 010 — SWR background handle-store swap: BOTH HAZARDS CONFIRMED, plus a third. Swap site is
cache/cache-runtime.ts:429-430(stale-hit branch), stamp:459-464, restore-in-finally:510-517; productionwaitUntilruns the prologue on the next microtask (request-context.ts:996), while the foreground is mid-render. Unit repro with production-faithful scheduling (the existing suite's serializing waitUntil mock is exactly why it never saw this), all green in one run: (a) foreground handle push lands in the background's isolated store — lost from the live document; (b) foregroundctx.set()throws INSIDE_CACHE_EXEC during the window; (c) NEW: the misrouted foreground push is persisted into the background's cache entry (cross-contamination). Note two existing tests PIN the hazardous behavior (cache-runtime-stale.test.ts:259, :478). Fix: derived-context isolation (the exact patternshell-capture.ts:748-753uses) or drain-then-swap likebackground-revalidation.ts:129-155. Effort M. This is the highest-priority C item — real bug, not theoretical.SSR-03 — resume errors after prelude commit: CONFIRMED (not addressed by #685 — that hardening was capture-side only).
resumeShellHTML's only failure handling is report-and-rethrow (ssr/index.tsx:638-646); the rejection lands incontroller.error(rsc-rendering.ts:577) AFTER the 200 + full static prelude flushed. Client gets a visually-complete page that never hydrates (holes stuck on fallbacks, no Flight payload). No eviction exists (deleteShellappears nowhere; store family is get/put only) and failure triggers no recapture — every request re-serves the broken entry until TTL (default 300s, consumer-set can be hours). Corrupt-prelude variant (base64ToBytes, bareatobatshell-serve.ts:88-93) throws pre-commit instead — 500-path, equally no degrade, equally repeating. Fix (S code / M with coverage): extend the HIT validity gate (rsc-rendering.ts:180) to trybase64ToBytes(prelude)+JSON.parse(postponed)and fall through to the existing MISS path (axis 1 + recapture overwrites the corrupt entry); additionally schedule a recapture in the tail catch so parseable-but-mismatched blobs self-heal instead of persisting the TTL. Movingresume()itself pre-commit would forfeit the streaming design — don't.SSR-04 — shell validity ignores build id: CONFIRMED-GAP (engine), PARTIALLY MITIGATED (default CF only). Gate is
entry.reactVersion === React.versiononly (shell-serve.ts:83-85); theShellCacheEntrydoc atcache/types.ts:227-229even promises "never mixed across … a build change" — documented, unenforced. Store reality: CFCacheStore'sv/${version}/key prefix rotates per deploy (mitigated unless a consumer pinsoptions.version); VercelCacheStore has NO default version (vercel-cache-store.ts:361) — naivegetCache()wiring serves stale-build shells across deploys, feeding exactly the SSR-03 broken-serve; named memory stores survive dev HMR via the globalThis registry. Fix (S, and cheaper than key-folding): stampbuildVersion: ctx.version(the@rangojs/router:versionbuild stamp, already read in the same function atrsc-rendering.ts:256) into the entry at capture and extendisValidShellHit— mismatch = miss = recapture overwrites the same key (self-healing, same doctrine as the reactVersion gate; key-folding would orphan dead entries + double-version on CF). Entries without the field = miss (pre-release, no shim). No overlap with #651/#653 beyond #651's debug sink being the natural place for a mismatch outcome. Sequencing: SSR-03 + SSR-04 + plan 009 all touch the same gate/capture area — one PR for the pair SSR-03/04 is the efficient cut.Plan 007 — hole-free shell-HIT replay: NO-GO (design spike complete, safety analysis holds, numbers don't). Key traced fact: for a CACHED bake-lane loader the HIT tail already never runs the body (
SeededShellStorepins the item-family record —shell-snapshot.ts:293-301,loader-cache.ts:279-293). The only body that runs on a hole-free HIT is an UNCACHED bake-lane loader — the exact shape the untagged-bake warning (shell-capture.ts:330-361) already discourages, with a one-line user remedy (cache the loader). TTFB is untouched either way (prelude flushes first). Full record-at-capture/replay-on-HIT design (what to record, storage, degrade rules, tag-attribution hazard) is preserved in the spike output should the reopen trigger fire: real-app profiling showing hole-free HIT tails dominated by slow uncached bake-lane loaders that genuinely can't be cached. Sequencing dependency: any future implementation goes AFTER SSR-04's build-id gate. Cheap interim: a /ppr skill note saying "cache your bake-lane loaders" is the sanctioned way to get this win today.Suggested next steps
ivogt commentedon Jul 5, 2026
SSR-03 + SSR-04 are implemented in draft PR #687 (buildVersion validity gate + pre-commit payload integrity check + tail-failure healing recapture; red-before-green 8→0, dev+prod e2e via a test-app fault injector). Remaining from this issue: plan 010 (SWR handle-store swap — next up), plan 009 (capture barrier wiring), and the A4 diagnostics batch blocked on #651/#652.
fix(ppr): shell serve-gate hardening — buildVersion validity gate, pr…
chore(router): #684 follow-ups — DRY consolidations + docs-api guard …
ivogt commentedon Jul 5, 2026
Plan 010 is implemented in draft PR #689: the stale-hit background revalidation now runs under a derived context (own
_handleStore, shell-capture pattern) instead of swapping the shared store, and no longer stampsINSIDE_CACHE_EXEConto shared args (the miss-path stamp owns in-fn misuse detection; invariant documented at the stamp). Red-before-green 2→20 with a production-faithful microtask-scheduling race test; /simplify ran pre-push (3 findings applied). Remaining here: plan 009 + the A4 diagnostics batch (blocked on #651/#652).fix(cache): SWR background revalidation under a derived context — no …
fix(ppr): shell serve-gate hardening — buildVersion gate, payload int…
ivogt commentedon Jul 5, 2026
Status: #687 (SSR-03 + SSR-04, squash
a1cae548) and #689 (plan 010, squash518a9e74) are both MERGED to main, each with a fully green non-draft CI matrix. The dev-shard flake both PRs tripped over during the runs is documented and mechanism-pinned in #690 (inception-level, unrelated to any recent PPR work). Remaining from this issue: plan 009 (capture barrier wiring for the derived ctx) and the A4 warn-once→diagnostics batch (blocked on #651/#652). B and 007 dispositions recorded above.ivogt commentedon Jul 6, 2026
Plan 009 is implemented in draft PR #704: the shell capture's derived context now gets its own render barrier via a shared
wireRenderBarrier(ctx, handleStore)helper (extracted fromcreateRequestContext, called fromderiveShellCaptureContext— covers both shell producers), with the six barrier-family fields reset as own properties. A capture-lane bake-lane loader'sawait ctx.rendered(); ctx.use(handle)now reads the CAPTURE's handle store instead of the foreground snapshot, and_treeHasStreamingis recomputed for the capture's tree (the streaming corner — premature seal of the capture's fresh store — is covered by the same reset). Red-before-green 3→0 in a new suite driving the real match path under the real derivation; full gate green (typecheck 16/16, test:unit:all, lint, format). Remaining from this issue: only the A4 warn-once→diagnostics batch, blocked on #651/#652.fix(ppr): capture lane gets its own render barrier — rendered() no lo…
ivogt commentedon Jul 6, 2026
Status: #704 (plan 009, squash
cc830514) is MERGED to main with a fully green non-draft CI matrix — all dev and production e2e shards passed on the run (no #690 flake this time). With that, every actionable item from this issue is closed out: A+D (#686), SSR-03/04 (#687), plan 010 (#689), plan 009 (#704); B and 007 dispositions recorded above. The only remainder is the A4 warn-once→diagnostics batch, which stays blocked on #651/#652 — tracking it there is probably cleaner than keeping this issue open for it.ivogt commentedon Aug 30, 2026
All actionable items from this tracker are already on main, as the 2026-07-06 status comment recorded.
A+D (#686): shared
safeSameOriginLanding(src/redirect-origin.ts), sharedcacheKeyBase(src/cache/cache-key-utils.ts),warnOncePerNamespaceonCFCacheStore. SSR-03/04 (#687), plan 010 (#689), plan 009 (#704). Group B (shell-HIT redirect e2e) is not deterministically reachable through real routing — unit-covered insrc/rsc/__tests__/shell-hit-redirect-guard.test.ts. Plan 007 (skip hole-free HIT loaders) was investigated unsafe.The leftover A4 warn-once→diagnostics batch was parked on #651/#652; #651 is closed. Not keeping this issue open for that cleanup.
docs(router): trim render-pass.ts comments to the invariants