Repository navigation
feat(ppr): operability — snapshot size cap, capture debug sink, loud inert shell family - #708
Merged
Merged
Conversation
…inert shell family (closes #651) Three diagnostics/limits surfaces for the PPR capture pipeline: - ppr.maxSnapshotBytes (default 8 MiB, single defaulting site in captureAndStoreShell): an over-cap capture data snapshot is skipped — the shell still stores and serves (documented drift on later HITs) — reported once per key. Threaded through producer B and the dev /__rsc_shell endpoint so both producers apply one policy. - createRouter({ debugShellCapture }) structured sink mirroring CFCacheDebug: one ShellCaptureDebugEvent per capture attempt/skip (outcome, attempt/barrier/write-settle ms, prelude/snapshot bytes, backoff state); INTERNAL_RANGO_DEBUG folds in inside the resolver. In dev the terminal event per key mirrors into the next ppr GET's Server-Timing (ppr-capture;desc=..., plus a ppr:shell-read metric) via the new PerformanceMetric.desc field. - CFCacheStore warns once per isolate when a ppr route reads/writes its KV-less (inert) shell family, naming the fix — previously a silent permanent MISS.
ivogt
force-pushed
the
feat/ppr-operability-651
branch
from
July 6, 2026 19:37
f6a1b9c to
2608a94
Compare
commit: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #651.
Problem statement
Three PPR failure/opacity shapes were invisible from the consumer's seat. A page over a large
cache()segment could grow the shell entry's capture data snapshot toward the store value limit (Cloudflare KV: 25 MiB per value) and the entry silently vanished —kv.putrejects deep insidewaitUntil, so the route just never flipped to HIT. The capture pipeline itself (stored / no-shell / refused / backed-off, snapshot bytes, write-barrier waits) was observable only through scattered dev console warnings. And on Cloudflare without a KV binding,getShell/putShellno-op entirely: every ppr route is a permanent MISS with zero diagnostics.Before vs. after
Before — no cap, no sink, silent no-op:
After — per-route cap, structured sink, loud misconfiguration:
End-to-end consumer example
wrangler tailthen shows lines like:In dev with
debugPerformance: true, the last capture outcome for a key also rides the NEXT document GET'sServer-Timingasppr-capture;dur=812.00;desc="stored attempt=1 …", next to appr:shell-readhit/miss entry for the serve-side store read.Semantics table
kv.putrejects invisibly and the route never flips to HITppr: true/ nomaxSnapshotBytescaptureAndStoreShell), both producersdebugShellCaptureunsetINTERNAL_RANGO_DEBUG=1lights the console sinkdebugShellCapture: falseINTERNAL_RANGO_DEBUGkv, no ppr routesgetShell/putShell, which only ppr routes call)kv, ppr route servedCode-change summary
src/rsc/shell-capture.ts— the bulk:ShellCaptureDebugEvent/ShellCaptureDebugtypes +resolveShellCaptureDebugSink(owns theINTERNAL_RANGO_DEBUGfold; explicitfalsewins), per-attempt event publishing inrunShellCapture(aCaptureAttemptStatsbag rides throughattemptCapture/captureAndStoreShellcollecting barrier/write-settle/prelude/snapshot fields), skip/backoff events inscheduleShellCapture, the snapshot size guard incaptureAndStoreShell(measure after the loader family is appended; over cap → drop snapshot, warn once per key), and the FIFO-capped dev buffer backing the Server-Timing mirror (takeCaptureDebugEventForTiming, consumed on read).src/rsc/rsc-rendering.ts— descriptor threading (maxSnapshotBytes,debugSink), the devppr-captureServer-Timing mirror, and a metrics-gatedppr:shell-readmetric. The mirror lives here because a capture completes only after its triggering response commits, so its outcome can only ride a later response's header.src/cache/cf/cf-cache-store.ts—warnShellFamilyInertOncevia the existingwarnOncePerNamespacepattern, fired fromgetShell/putShellwhen!this.kv(only ppr routes call the shell family, so a KV-less store in a non-PPR app stays silent — the reason this is not a constructor warning).src/router/metrics.ts+src/server/context.ts—PerformanceMetric.desc, emitted as a Server-Timing quoted-string (delimiters escaped) and shown in the[RSC Perf]dev table.src/urls/pattern-types.ts,src/rsc/shell-serve.ts—PartialPrerenderProps.maxSnapshotBytes, passed throughresolvePprConfigundefaulted (likeswr/tags); the single defaulting site iscaptureAndStoreShell(DEFAULT_PPR_MAX_SNAPSHOT_BYTES).src/prerender/build-shell-capture.ts,src/vite/discovery/shell-prerender-phase.ts,src/vite/router-discovery.ts,src/rsc/shell-build-manifest.ts— producer B threading (build phase + dev/__rsc_shellmaxSnapshotBytesparam) so both producers apply one cap policy.src/router/router-options.ts,src/router.ts,src/router/router-interfaces.ts,src/index.rsc.ts— thedebugShellCaptureoption (joins thedebug*family) and public type exports.docs/design/ppr-shell-resume.md("Operability" section),docs/internal/feature-map.md,docs/internal/feature-file-map.md,skills/ppr/SKILL.md(options table + verification bullets).Test plan
src/rsc/__tests__/shell-capture.test.ts): over-cap snapshot skipped + shell still stored + exactly one report across recaptures (pins the issue's core contract); under-cap snapshot intact with the default cap; one debug event per attempt with sizes/waits (stored, andno-shell×2 on the retry path); a throwing sink never fails the capture; the dev Server-Timing buffer is consumed on read;skip-backoffevent carries failure count + remaining window.src/cache/cf/__tests__/cf-cache-store-shell.test.ts): KV-lessgetShell/putShellstill no-op AND warn exactly once per isolate (second store instance does not re-warn) — pins the missing-KV contract.src/router/__tests__/metrics.test.ts):descemitted as an escaped quoted-string; desc-less metrics byte-identical to the pre-change output.e2e/shell-cache.test.tsvia the sharedrunShellCacheSpec): new/shell-cache/snapshot-caproute (maxSnapshotBytes: 64) — the size-cap fallback still flips MISS→HIT and the HIT hydrates with zero hydration errors while the live hole advances (the issue's e2e ask). Full shell-cache suite green in both modes (27 dev + 1 known-flaky fixture dep-check retry, 28 production).pnpm run typecheck,pnpm run test:unit:all,pnpm run lint,pnpm run format,pnpm check:docs-api,pnpm check:e2e-bucketing— all green.router.named-routes.gen.tsregenerated and committed (only the test-app's routes changed).Notes
/simplifypass ran before this PR; notable applied cleanups: single defaulting site for the cap,INTERNAL_RANGO_DEBUGfolded into the sink resolver, FIFO cap on the dev event buffer, metrics-gated timing reads on the request path.hasShellFamilycannot see KV-lessness — methods exist on the prototype), which would also stop the doomed background captures a KV-less CF store still runs; (2) single-stringify snapshot measurement shared with the store envelope write (theVercelCacheStoreprecedent) — today the snapshot is serialized once for the cap check and once insideputShell, background-path only.