@rangojs/router 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rango.js +1 -1
- package/dist/types/__internal.d.ts +7 -1
- package/dist/types/browser/action-fence.d.ts +2 -1
- package/dist/types/browser/event-controller.d.ts +8 -2
- package/dist/types/browser/history-state.d.ts +3 -9
- package/dist/types/browser/navigation-store.d.ts +8 -6
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
- package/dist/types/browser/react/use-action.d.ts +7 -2
- package/dist/types/browser/react/use-handle.d.ts +19 -16
- package/dist/types/browser/react/use-link-status.d.ts +5 -2
- package/dist/types/browser/segment-reconciler.d.ts +32 -4
- package/dist/types/browser/types.d.ts +24 -5
- package/dist/types/browser/wire-protocol.d.ts +26 -0
- package/dist/types/cache/cache-key-utils.d.ts +11 -5
- package/dist/types/cache/cache-scope.d.ts +34 -22
- package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
- package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
- package/dist/types/cache/handle-snapshot.d.ts +9 -4
- package/dist/types/cache/segment-codec.d.ts +14 -0
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/taint.d.ts +5 -3
- package/dist/types/cache/types.d.ts +7 -3
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
- package/dist/types/prefetch-markers.d.ts +5 -14
- package/dist/types/prerender/store.d.ts +31 -0
- package/dist/types/prerender/warm-request.d.ts +3 -3
- package/dist/types/prerender.d.ts +2 -2
- package/dist/types/route-name.d.ts +5 -0
- package/dist/types/router/error-handling.d.ts +11 -5
- package/dist/types/router/instrument.d.ts +4 -3
- package/dist/types/router/intercept-resolution.d.ts +25 -9
- package/dist/types/router/match-context.d.ts +6 -11
- package/dist/types/router/match-handlers.d.ts +2 -6
- package/dist/types/router/match-middleware/background-revalidation.d.ts +13 -22
- package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
- package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
- package/dist/types/router/navigation-snapshot.d.ts +7 -0
- package/dist/types/router/prerender-match.d.ts +21 -6
- package/dist/types/router/preview-match.d.ts +2 -7
- package/dist/types/router/request-classification.d.ts +5 -0
- package/dist/types/router/router-context.d.ts +7 -46
- package/dist/types/router/router-interfaces.d.ts +8 -14
- package/dist/types/router/segment-resolution/helpers.d.ts +19 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
- package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
- package/dist/types/router/segment-resolution/resolution-pass.d.ts +95 -0
- package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +2 -0
- package/dist/types/router/segment-resolution.d.ts +1 -2
- package/dist/types/router/segment-wrappers.d.ts +11 -48
- package/dist/types/router/telemetry.d.ts +2 -0
- package/dist/types/router/types.d.ts +11 -1
- package/dist/types/rsc/full-payload.d.ts +2 -2
- package/dist/types/rsc/handler-context.d.ts +6 -6
- package/dist/types/rsc/json-route-result.d.ts +5 -9
- package/dist/types/rsc/request-stages.d.ts +203 -0
- package/dist/types/rsc/response-cache-serve.d.ts +7 -12
- package/dist/types/rsc/response-route-handler.d.ts +2 -2
- package/dist/types/rsc/routine-plan.d.ts +1 -1
- package/dist/types/rsc/server-action.d.ts +2 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
- package/dist/types/rsc/shell-capture.d.ts +132 -15
- package/dist/types/rsc/shell-serve.d.ts +37 -12
- package/dist/types/server/context.d.ts +9 -8
- package/dist/types/server/handle-store.d.ts +16 -0
- package/dist/types/server/render-pass.d.ts +72 -0
- package/dist/types/server/request-context.d.ts +53 -104
- package/dist/types/server/rewrite-trail.d.ts +2 -2
- package/dist/types/testing/dispatch.d.ts +35 -18
- package/dist/types/testing/render-route.d.ts +43 -8
- package/dist/types/testing/serve-shell-request.d.ts +8 -1
- package/dist/types/types/cache-types.d.ts +6 -3
- package/dist/types/types/error-types.d.ts +30 -3
- package/dist/types/types/handler-context.d.ts +9 -1
- package/dist/types/types/segments.d.ts +1 -1
- package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
- package/dist/types/vite/discovery/build-env.d.ts +22 -0
- package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
- package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
- package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
- package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +12 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
- package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +8 -47
- package/dist/types/vite/temp-rsc-server.d.ts +39 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
- package/dist/types/wire-params.d.ts +39 -0
- package/dist/vite/index.js +3822 -3727
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +8 -5
- package/skills/caching/SKILL.md +37 -18
- package/skills/client-urls/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +13 -1
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/hooks/SKILL.md +1 -1
- package/skills/hooks/data.md +8 -1
- package/skills/hooks/handle-and-actions.md +33 -2
- package/skills/hooks/navigation.md +121 -15
- package/skills/hooks/state.md +6 -4
- package/skills/hooks/urls.md +16 -5
- package/skills/intercept/SKILL.md +23 -7
- package/skills/middleware/SKILL.md +18 -0
- package/skills/observability/SKILL.md +30 -0
- package/skills/ppr/SKILL.md +18 -3
- package/skills/prerender/SKILL.md +39 -15
- package/skills/rango/SKILL.md +5 -1
- package/skills/server-actions/SKILL.md +15 -1
- package/skills/testing/client-components.md +25 -18
- package/skills/testing/response-routes.md +6 -4
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/use-cache/SKILL.md +46 -8
- package/skills/view-transitions/SKILL.md +1 -1
- package/src/__internal.ts +7 -1
- package/src/browser/action-fence.ts +2 -1
- package/src/browser/connection-warmup.ts +3 -1
- package/src/browser/event-controller.ts +37 -13
- package/src/browser/history-state.ts +6 -12
- package/src/browser/navigation-bridge.ts +59 -26
- package/src/browser/navigation-client.ts +20 -25
- package/src/browser/navigation-store.ts +25 -27
- package/src/browser/navigation-transaction.ts +4 -1
- package/src/browser/network-error-handler.ts +4 -0
- package/src/browser/partial-update.ts +33 -6
- package/src/browser/prefetch/cache.ts +2 -1
- package/src/browser/prefetch/fetch.ts +8 -16
- package/src/browser/prefetch/queue.ts +4 -4
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/filter-segment-order.ts +2 -2
- package/src/browser/react/use-action.ts +24 -48
- package/src/browser/react/use-handle.ts +22 -18
- package/src/browser/react/use-link-status.ts +48 -60
- package/src/browser/react/use-navigation.ts +92 -41
- package/src/browser/rsc-router.tsx +1 -0
- package/src/browser/segment-reconciler.ts +109 -53
- package/src/browser/server-action-bridge.ts +81 -61
- package/src/browser/types.ts +25 -5
- package/src/browser/wire-protocol.ts +43 -0
- package/src/cache/cache-key-utils.ts +11 -5
- package/src/cache/cache-runtime.ts +132 -75
- package/src/cache/cache-scope.ts +110 -64
- package/src/cache/cf/cf-cache-constants.ts +2 -2
- package/src/cache/cf/cf-cache-store.ts +486 -874
- package/src/cache/cf/cf-kv-utils.ts +1 -1
- package/src/cache/document-cache.ts +64 -46
- package/src/cache/handle-snapshot.ts +19 -7
- package/src/cache/read-through-swr.ts +20 -18
- package/src/cache/segment-codec.ts +55 -0
- package/src/cache/shell-snapshot.ts +2 -2
- package/src/cache/tag-invalidation.ts +6 -2
- package/src/cache/taint.ts +5 -3
- package/src/cache/types.ts +7 -3
- package/src/cache/vercel/vercel-cache-store.ts +243 -310
- package/src/handles/pushed-by.ts +1 -1
- package/src/prefetch-markers.ts +5 -16
- package/src/prerender/store.ts +76 -0
- package/src/prerender/warm-request.ts +8 -4
- package/src/prerender.ts +2 -2
- package/src/route-name.ts +8 -0
- package/src/router/error-handling.ts +22 -10
- package/src/router/instrument.ts +4 -3
- package/src/router/intercept-resolution.ts +82 -75
- package/src/router/loader-resolution.ts +5 -4
- package/src/router/match-api.ts +5 -5
- package/src/router/match-context.ts +23 -12
- package/src/router/match-handlers.ts +8 -20
- package/src/router/match-middleware/background-revalidation.ts +46 -57
- package/src/router/match-middleware/cache-lookup.ts +677 -726
- package/src/router/match-middleware/cache-store.ts +4 -8
- package/src/router/match-middleware/intercept-resolution.ts +3 -23
- package/src/router/match-middleware/segment-resolution.ts +34 -61
- package/src/router/middleware.ts +69 -11
- package/src/router/navigation-snapshot.ts +14 -4
- package/src/router/prerender-match.ts +100 -44
- package/src/router/preview-match.ts +6 -41
- package/src/router/request-classification.ts +13 -7
- package/src/router/route-snapshot.ts +2 -5
- package/src/router/router-context.ts +10 -95
- package/src/router/router-interfaces.ts +8 -14
- package/src/router/segment-resolution/helpers.ts +49 -13
- package/src/router/segment-resolution/loader-cache.ts +15 -9
- package/src/router/segment-resolution/loader-mask.ts +5 -5
- package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
- package/src/router/segment-resolution/resolution-pass.ts +139 -0
- package/src/router/segment-resolution/revalidation.ts +939 -789
- package/src/router/segment-resolution/streamed-handler-boundary.ts +3 -0
- package/src/router/segment-resolution.ts +4 -13
- package/src/router/segment-wrappers.ts +51 -269
- package/src/router/telemetry.ts +5 -0
- package/src/router/types.ts +11 -1
- package/src/router.ts +19 -17
- package/src/rsc/full-payload.ts +6 -5
- package/src/rsc/handler-context.ts +8 -14
- package/src/rsc/handler.ts +126 -507
- package/src/rsc/json-route-result.ts +5 -9
- package/src/rsc/loader-fetch.ts +6 -5
- package/src/rsc/progressive-enhancement.ts +5 -0
- package/src/rsc/request-stages.ts +759 -0
- package/src/rsc/response-cache-serve.ts +10 -14
- package/src/rsc/response-route-handler.ts +13 -5
- package/src/rsc/rewrite-passes.ts +2 -1
- package/src/rsc/routine-plan.ts +1 -1
- package/src/rsc/rsc-rendering.ts +164 -349
- package/src/rsc/server-action.ts +87 -113
- package/src/rsc/shell-capture-constants.ts +1 -1
- package/src/rsc/shell-capture.ts +275 -227
- package/src/rsc/shell-serve.ts +121 -18
- package/src/rsc/ssr-setup.ts +5 -4
- package/src/segment-system.tsx +3 -3
- package/src/server/context.ts +22 -25
- package/src/server/handle-store.ts +55 -0
- package/src/server/render-pass.ts +291 -0
- package/src/server/request-context.ts +75 -115
- package/src/server/rewrite-trail.ts +6 -4
- package/src/ssr/index.tsx +1 -1
- package/src/testing/dispatch.ts +226 -710
- package/src/testing/e2e/page-helpers.ts +2 -2
- package/src/testing/render-route.tsx +199 -13
- package/src/testing/serve-shell-request.ts +36 -40
- package/src/types/cache-types.ts +6 -3
- package/src/types/error-types.ts +30 -3
- package/src/types/handler-context.ts +12 -1
- package/src/types/segments.ts +1 -1
- package/src/vite/discovery/build-discovery-phase.ts +204 -0
- package/src/vite/discovery/build-env.ts +148 -0
- package/src/vite/discovery/bundle-scan-phase.ts +128 -0
- package/src/vite/discovery/dev-prerender-cache.ts +8 -3
- package/src/vite/discovery/dev-server-phase.ts +1776 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +1 -1
- package/src/vite/discovery/prerender-collection.ts +24 -39
- package/src/vite/discovery/state.ts +15 -0
- package/src/vite/plugins/version-plugin.ts +1 -1
- package/src/vite/plugins/virtual-entries.ts +1 -1
- package/src/vite/router-discovery.ts +29 -2428
- package/src/vite/temp-rsc-server.ts +223 -0
- package/src/vite/utils/prerender-utils.ts +47 -0
- package/src/wire-params.ts +40 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
- package/src/router/segment-resolution/fresh.ts +0 -945
package/src/rsc/shell-capture.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Capture does NOT flow through the HTTP middleware pipeline. The integrated PPR
|
|
5
5
|
* serve path (rsc-rendering.ts + shell-serve.ts) builds a ShellCaptureDescriptor
|
|
6
6
|
* from the route's `ppr` path option after the served response is built and calls
|
|
7
|
-
*
|
|
7
|
+
* planShellCapture with it (ShellCaptureInput). The capture then runs as a background task that re-derives
|
|
8
8
|
* the page via `ctx.router.match()` under its OWN derived request context — fresh
|
|
9
|
-
* handle store, `
|
|
9
|
+
* handle store, `_shellCaptureState.run` so live-lane loaders mask (loader-mask.ts)
|
|
10
10
|
* and postpone at their loading() or inline <Suspense> boundary. The match is
|
|
11
11
|
* MIXED-CHAIN: cache()'d segments replay from ring 3, uncached segments execute
|
|
12
12
|
* their handlers. The record-first step (settleCaptureRecord) waits for the
|
|
@@ -48,13 +48,13 @@ import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
|
48
48
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
49
49
|
import type { TraceSpan } from "../router/tracing.js";
|
|
50
50
|
import {
|
|
51
|
+
runAsBackgroundWork,
|
|
51
52
|
runWithRequestContext,
|
|
52
53
|
setRequestContextParams,
|
|
53
|
-
wireRenderBarrier,
|
|
54
54
|
UNTRACKED_BACKGROUND_TASK,
|
|
55
55
|
type RequestContext,
|
|
56
|
-
type RouteRecordWindow,
|
|
57
56
|
} from "../server/request-context.js";
|
|
57
|
+
import { deriveRenderPass } from "../server/render-pass.js";
|
|
58
58
|
import {
|
|
59
59
|
DEFAULT_FUNCTION_TTL,
|
|
60
60
|
resolveSwrWindow,
|
|
@@ -149,7 +149,7 @@ const SHELL_SNAPSHOT_WRITE_SETTLE_MS = 1000;
|
|
|
149
149
|
* Upper bound on the pre-render WRITE BARRIER: before the capture's match/render,
|
|
150
150
|
* settle the background tasks the FOREGROUND request already scheduled — its
|
|
151
151
|
* deferred ring-3 cacheRoute and ring-1 setItem writes all go through
|
|
152
|
-
* reqCtx.waitUntil, and every one of them is scheduled BEFORE
|
|
152
|
+
* reqCtx.waitUntil, and every one of them is scheduled BEFORE planShellCapture
|
|
153
153
|
* runs (the response, and its onResponse callbacks, are committed first). Draining
|
|
154
154
|
* them turns the capture's cache reads from a RACE into an ORDERING EDGE: the
|
|
155
155
|
* capture deterministically observes the foreground's cache generation, replays it
|
|
@@ -226,7 +226,7 @@ async function raceTaskHardCap<T>(task: Promise<T>, key: string): Promise<T> {
|
|
|
226
226
|
* Module-level in-flight key map: the stampede guard for background captures, and
|
|
227
227
|
* its single owner. One capture runs per key per isolate; concurrent MISS/stale
|
|
228
228
|
* requests for the same key coalesce onto the first (the rest see the key present
|
|
229
|
-
* in
|
|
229
|
+
* in planShellCapture and skip). Added when a capture is scheduled and cleared
|
|
230
230
|
* in the task's settle paths via a token-guarded release, so a later request can
|
|
231
231
|
* recapture when TTL rolls. Living here (not split across the middleware) keeps
|
|
232
232
|
* the add/clear lifecycle in one layer.
|
|
@@ -234,7 +234,7 @@ async function raceTaskHardCap<T>(task: Promise<T>, key: string): Promise<T> {
|
|
|
234
234
|
* The value is the owning task's token ({@link CaptureGuardToken}) rather than
|
|
235
235
|
* a bare set entry: workerd can kill a capture's waitUntil context before its
|
|
236
236
|
* finally runs (or the task can wedge past every deadline), stranding the key.
|
|
237
|
-
*
|
|
237
|
+
* planShellCapture treats an entry older than
|
|
238
238
|
* SHELL_CAPTURE_TASK_HARD_CAP_MS as abandoned and reclaims it; the token guard
|
|
239
239
|
* keeps a late-settling stale task from releasing its replacement's entry.
|
|
240
240
|
*/
|
|
@@ -474,14 +474,14 @@ function refuseOnCaptureGuard(
|
|
|
474
474
|
);
|
|
475
475
|
return true;
|
|
476
476
|
}
|
|
477
|
-
const trip = derivedCtx.
|
|
477
|
+
const trip = derivedCtx._shellCaptureState?.guardTripped;
|
|
478
478
|
if (!trip) return false;
|
|
479
479
|
// Name the recorded source instead of hardcoding a lane: a trip can come
|
|
480
480
|
// from a bake-lane SEGMENT loader or from handler/render code, including a
|
|
481
481
|
// loader a handler awaits (issue #672). The guard recorded the read and its
|
|
482
482
|
// fix (server/context.ts guardIdentityRead): cookies()/headers(), a
|
|
483
483
|
// { cache: false } variable, and the theme reads (#971).
|
|
484
|
-
const loaderId = derivedCtx.
|
|
484
|
+
const loaderId = derivedCtx._shellCaptureState?.guardTrippedLoaderId;
|
|
485
485
|
const origin = loaderId
|
|
486
486
|
? `the loader "${loaderId}"`
|
|
487
487
|
: "handler/render code (no loader body was executing)";
|
|
@@ -625,7 +625,7 @@ export interface ShellCaptureDebugEvent {
|
|
|
625
625
|
* - skip-stored: a capture for the key stored after the scheduling request
|
|
626
626
|
* read the store and missed (ShellCaptureDescriptor.storedSeqAtRead), so
|
|
627
627
|
* the shell that request missed exists now
|
|
628
|
-
* - skip-in-flight:
|
|
628
|
+
* - skip-in-flight: planShellCapture found a capture already running for
|
|
629
629
|
* the key (stampede guard) and scheduled nothing
|
|
630
630
|
* - skip-backoff: the key is inside its refused-capture backoff window and
|
|
631
631
|
* the capture was not attempted
|
|
@@ -998,7 +998,7 @@ export function gateFlightForCapture(
|
|
|
998
998
|
* The background shell-capture descriptor: everything the capture task needs to
|
|
999
999
|
* store the shell. Built by the integrated PPR serve path (rsc-rendering.ts) from
|
|
1000
1000
|
* the route's `ppr` path option (`PartialPrerenderProps`) and the app-level cache
|
|
1001
|
-
* store, and passed to
|
|
1001
|
+
* store, and passed to planShellCapture in its input — it is NOT threaded through
|
|
1002
1002
|
* the request context. `tags` carries the route's OPERATIONAL `ppr.tags`; the
|
|
1003
1003
|
* capture UNIONS them with the shell's own auto-collected (non-loader) request
|
|
1004
1004
|
* tags from its derived render (the collected set stays authoritative). That
|
|
@@ -1066,7 +1066,7 @@ export interface ShellCaptureDescriptor {
|
|
|
1066
1066
|
/**
|
|
1067
1067
|
* Set for a shell keyed by the visitor's URL (`rewrite(...).shell("visitor")`,
|
|
1068
1068
|
* visitorShellKey): the capture renders the visitor's location
|
|
1069
|
-
* (`
|
|
1069
|
+
* (`ShellCaptureState.forVisitor`) and stores it as
|
|
1070
1070
|
* ShellCacheEntry.pathname.
|
|
1071
1071
|
*/
|
|
1072
1072
|
pathname?: string;
|
|
@@ -1081,7 +1081,7 @@ export interface ShellCaptureDescriptor {
|
|
|
1081
1081
|
/**
|
|
1082
1082
|
* A `router.prerender()` warm's capture (rsc-rendering.ts shellServePlan):
|
|
1083
1083
|
* one explicit call that must store, so the guards that shed a herd of
|
|
1084
|
-
* visitors do not apply to it.
|
|
1084
|
+
* visitors do not apply to it. planShellCapture skips the in-flight
|
|
1085
1085
|
* dedup and the refused-capture backoff, and attemptCapture waits longer on
|
|
1086
1086
|
* the write barrier. Every correctness guard, the inert-store skip and the
|
|
1087
1087
|
* queue limits still decide.
|
|
@@ -1108,6 +1108,25 @@ function noteCaptureStored(key: string): void {
|
|
|
1108
1108
|
storedCaptureSeqs.set(key, ++storedCaptureSeq);
|
|
1109
1109
|
}
|
|
1110
1110
|
|
|
1111
|
+
/** One shell capture: the request it re-renders and what it stores. */
|
|
1112
|
+
export interface ShellCaptureInput {
|
|
1113
|
+
ctx: HandlerContext<any>;
|
|
1114
|
+
request: Request;
|
|
1115
|
+
env: any;
|
|
1116
|
+
url: URL;
|
|
1117
|
+
reqCtx: RequestContext<any>;
|
|
1118
|
+
/**
|
|
1119
|
+
* The SSR module for the capture's own request and URL, called once the
|
|
1120
|
+
* capture leaves the queue. A document passes the module it already
|
|
1121
|
+
* resolved, returned as is, so the capture awaits only a pending one.
|
|
1122
|
+
*/
|
|
1123
|
+
ssr: (
|
|
1124
|
+
request: Request,
|
|
1125
|
+
url: URL,
|
|
1126
|
+
) => SSRModule | null | Promise<SSRModule | null>;
|
|
1127
|
+
descriptor: ShellCaptureDescriptor;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1111
1130
|
/**
|
|
1112
1131
|
* Schedule the background shell capture for a served document. Stampede-guarded:
|
|
1113
1132
|
* one capture per key per isolate. Runs via runBackground (waitUntil on workerd,
|
|
@@ -1115,21 +1134,10 @@ function noteCaptureStored(key: string): void {
|
|
|
1115
1134
|
* error is routed through reportCacheError — capture is best-effort; a failure just
|
|
1116
1135
|
* means the next request recaptures.
|
|
1117
1136
|
*
|
|
1118
|
-
* Eligibility (nonce/partial/status/strategy) is decided by the caller.
|
|
1119
|
-
* module loader may be passed for cold partial requests; it runs only after the
|
|
1120
|
-
* capture enters the guarded background queue, never on response latency.
|
|
1137
|
+
* Eligibility (nonce/partial/status/strategy) is decided by the caller.
|
|
1121
1138
|
*/
|
|
1122
|
-
export function
|
|
1123
|
-
ctx
|
|
1124
|
-
request: Request,
|
|
1125
|
-
env: any,
|
|
1126
|
-
url: URL,
|
|
1127
|
-
reqCtx: RequestContext<any>,
|
|
1128
|
-
ssrModule:
|
|
1129
|
-
| SSRModule
|
|
1130
|
-
| ((request: Request, url: URL) => Promise<SSRModule | null>),
|
|
1131
|
-
descriptor: ShellCaptureDescriptor,
|
|
1132
|
-
): void {
|
|
1139
|
+
export function planShellCapture(input: ShellCaptureInput): void {
|
|
1140
|
+
const { ctx, request, env, url, reqCtx, descriptor } = input;
|
|
1133
1141
|
const key = descriptor.key;
|
|
1134
1142
|
// A forced capture installs its own token below: a visitor's capture that
|
|
1135
1143
|
// started before the warm may have read data from before the change the
|
|
@@ -1191,10 +1199,9 @@ export function scheduleShellCapture(
|
|
|
1191
1199
|
const captureRequest = descriptor.navigationOnly
|
|
1192
1200
|
? createNavigationCaptureRequest(request, captureUrl)
|
|
1193
1201
|
: request;
|
|
1202
|
+
const ssrModule = input.ssr(captureRequest, captureUrl);
|
|
1194
1203
|
const resolvedSsrModule =
|
|
1195
|
-
|
|
1196
|
-
? await ssrModule(captureRequest, captureUrl)
|
|
1197
|
-
: ssrModule;
|
|
1204
|
+
ssrModule instanceof Promise ? await ssrModule : ssrModule;
|
|
1198
1205
|
if (
|
|
1199
1206
|
!resolvedSsrModule ||
|
|
1200
1207
|
!resolvedSsrModule.resumeShellHTML ||
|
|
@@ -1273,69 +1280,73 @@ export function scheduleShellCapture(
|
|
|
1273
1280
|
// (observed in production: ~24s of zero I/O before capture start).
|
|
1274
1281
|
// observePhase reads tracing off the ALS context captured when runBackground
|
|
1275
1282
|
// registered the task — the foreground request context, tracing intact.
|
|
1283
|
+
// onError marks every error of the capture `background`, the task's own
|
|
1284
|
+
// failure (captureTask's catch) included.
|
|
1276
1285
|
const serializedTask = async () => {
|
|
1277
|
-
await
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
(
|
|
1286
|
+
await runAsBackgroundWork(reqCtx, () =>
|
|
1287
|
+
observePhase(PHASES.background("shell-capture"), async (span) => {
|
|
1288
|
+
span.setAttribute("rango.shell_key", key);
|
|
1289
|
+
// A production page can enqueue several navigation-only captures through
|
|
1290
|
+
// viewport prefetching. Let a later document MISS overtake that queued
|
|
1291
|
+
// speculative work so it can become a shell HIT before the 15s queue
|
|
1292
|
+
// budget expires. Never preempts the active capture. Priority and the
|
|
1293
|
+
// backlog ahead at enqueue ride the span (and the skip event below), so
|
|
1294
|
+
// a skip-queue-timeout diagnoses itself instead of needing a trace dive.
|
|
1295
|
+
const queuePriority = descriptor.navigationOnly
|
|
1296
|
+
? ("navigation" as const)
|
|
1297
|
+
: ("document" as const);
|
|
1298
|
+
const depths = captureQueueDepths();
|
|
1299
|
+
const queueAhead =
|
|
1300
|
+
(depths.running ? 1 : 0) +
|
|
1301
|
+
depths.document +
|
|
1302
|
+
(queuePriority === "navigation" ? depths.navigation : 0);
|
|
1303
|
+
span.setAttribute("rango.background.queue_priority", queuePriority);
|
|
1304
|
+
span.setAttribute("rango.background.queue_ahead", queueAhead);
|
|
1305
|
+
const queueStart = performance.now();
|
|
1306
|
+
try {
|
|
1307
|
+
await enqueueSerializedCapture(
|
|
1308
|
+
() => {
|
|
1309
|
+
span.setAttribute(
|
|
1310
|
+
"rango.background.queue_wait_ms",
|
|
1311
|
+
Math.round(performance.now() - queueStart),
|
|
1312
|
+
);
|
|
1313
|
+
return captureTask(span);
|
|
1314
|
+
},
|
|
1315
|
+
{ priority: queuePriority },
|
|
1316
|
+
);
|
|
1317
|
+
} catch (error) {
|
|
1318
|
+
if (error instanceof CaptureQueueFullError) {
|
|
1319
|
+
releaseCaptureGuard(key, guardToken);
|
|
1320
|
+
span.setAttribute("rango.background.outcome", "skip-capacity");
|
|
1321
|
+
publishCaptureDebugEvent(descriptor, {
|
|
1322
|
+
key,
|
|
1323
|
+
outcome: "skip-capacity",
|
|
1324
|
+
});
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (error instanceof CaptureQueueWaitTimeoutError) {
|
|
1328
|
+
// Dropped unrun after waiting past the queue budget: no backoff (the
|
|
1329
|
+
// route is not doomed, the isolate was busy) — a later request
|
|
1330
|
+
// re-probes the key.
|
|
1331
|
+
releaseCaptureGuard(key, guardToken);
|
|
1332
|
+
span.setAttribute("rango.background.outcome", "skip-queue-timeout");
|
|
1299
1333
|
span.setAttribute(
|
|
1300
1334
|
"rango.background.queue_wait_ms",
|
|
1301
|
-
Math.round(
|
|
1335
|
+
Math.round(error.waitedMs),
|
|
1302
1336
|
);
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
outcome: "skip-capacity",
|
|
1314
|
-
});
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
if (error instanceof CaptureQueueWaitTimeoutError) {
|
|
1318
|
-
// Dropped unrun after waiting past the queue budget: no backoff (the
|
|
1319
|
-
// route is not doomed, the isolate was busy) — a later request
|
|
1320
|
-
// re-probes the key.
|
|
1321
|
-
releaseCaptureGuard(key, guardToken);
|
|
1322
|
-
span.setAttribute("rango.background.outcome", "skip-queue-timeout");
|
|
1323
|
-
span.setAttribute(
|
|
1324
|
-
"rango.background.queue_wait_ms",
|
|
1325
|
-
Math.round(error.waitedMs),
|
|
1326
|
-
);
|
|
1327
|
-
publishCaptureDebugEvent(descriptor, {
|
|
1328
|
-
key,
|
|
1329
|
-
outcome: "skip-queue-timeout",
|
|
1330
|
-
queueWaitMs: Math.round(error.waitedMs),
|
|
1331
|
-
queuePriority,
|
|
1332
|
-
queueAhead,
|
|
1333
|
-
});
|
|
1334
|
-
return;
|
|
1337
|
+
publishCaptureDebugEvent(descriptor, {
|
|
1338
|
+
key,
|
|
1339
|
+
outcome: "skip-queue-timeout",
|
|
1340
|
+
queueWaitMs: Math.round(error.waitedMs),
|
|
1341
|
+
queuePriority,
|
|
1342
|
+
queueAhead,
|
|
1343
|
+
});
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
throw error;
|
|
1335
1347
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
});
|
|
1348
|
+
}),
|
|
1349
|
+
);
|
|
1339
1350
|
};
|
|
1340
1351
|
// The capture's own task must NOT enter reqCtx._pendingBackgroundTasks: the
|
|
1341
1352
|
// capture drains that list before rendering (the write-barrier ordering edge),
|
|
@@ -1350,7 +1361,7 @@ export function scheduleShellCapture(
|
|
|
1350
1361
|
/**
|
|
1351
1362
|
* The ttl/swr a shell is stored with: the route's ppr policy, capped by the
|
|
1352
1363
|
* route cache() records the capture replayed or wrote
|
|
1353
|
-
* (
|
|
1364
|
+
* (ShellCaptureState.routeRecordWindow). A HIT replays the capture's handler
|
|
1354
1365
|
* output, while a client navigation reads the route's cache() tier first, so
|
|
1355
1366
|
* a shell that outlived its record would show a document request other
|
|
1356
1367
|
* handler output than a navigation of the same URL. Freshness and total
|
|
@@ -1518,9 +1529,9 @@ type CaptureAttemptStats = Pick<
|
|
|
1518
1529
|
* several HTTP requests before a capture stuck. We retry ONLY on `no-shell`
|
|
1519
1530
|
* that did not run out of ppr.captureTimeout (CaptureAttemptStats.noShellCause),
|
|
1520
1531
|
* and on `expired`; a genuine render error is NOT retried — it propagates to
|
|
1521
|
-
*
|
|
1532
|
+
* planShellCapture's reportCacheError. See docs/design/ppr-shell-resume.md.
|
|
1522
1533
|
*
|
|
1523
|
-
* `request` and `url` are the capture's own identity (
|
|
1534
|
+
* `request` and `url` are the capture's own identity (planShellCapture
|
|
1524
1535
|
* strips a navigation-only capture's transport parameters). `retryDelayMs` is
|
|
1525
1536
|
* a parameter (defaulting to the module const) so unit tests can drive the
|
|
1526
1537
|
* retry without a real 400ms wall-clock wait.
|
|
@@ -1541,7 +1552,7 @@ async function runShellCapture(
|
|
|
1541
1552
|
// attemptCapture/captureAndStoreShell collecting the observability fields
|
|
1542
1553
|
// (barrier wait, write-settle wait, prelude/snapshot bytes), and the event
|
|
1543
1554
|
// folds them with the outcome. A genuine render error skips the attempt
|
|
1544
|
-
// event —
|
|
1555
|
+
// event — planShellCapture's catch publishes the terminal `error` event.
|
|
1545
1556
|
const timedAttempt = async (
|
|
1546
1557
|
attempt: number,
|
|
1547
1558
|
): Promise<{
|
|
@@ -1626,7 +1637,7 @@ async function runShellCapture(
|
|
|
1626
1637
|
// lane rule: see resolveLoaderData, loader-cache.ts) is the likely cause —
|
|
1627
1638
|
// warn once per key. Ordering matters: because the retry
|
|
1628
1639
|
// absorbs cold-start, cold-start routes almost never reach this warning. The
|
|
1629
|
-
// caller (
|
|
1640
|
+
// caller (planShellCapture) reads this `no-shell` return to back the key off.
|
|
1630
1641
|
warnNullCaptureOnce(key, true, second.stats);
|
|
1631
1642
|
return "no-shell";
|
|
1632
1643
|
}
|
|
@@ -1673,7 +1684,8 @@ export type CaptureRecordOutcome =
|
|
|
1673
1684
|
* a HIT restores, and its handle encode has its own 5s timeout that must
|
|
1674
1685
|
* never be the one that fires); then the capture's onResponse callbacks fire
|
|
1675
1686
|
* with a synthetic 200 (the implicit doc scope's cacheRoute, an explicit
|
|
1676
|
-
* scope's own write), and the deferred writes settle.
|
|
1687
|
+
* scope's own write), and the deferred writes settle. A match holding an
|
|
1688
|
+
* error or notFound boundary throws before the callbacks fire.
|
|
1677
1689
|
*/
|
|
1678
1690
|
export async function settleCaptureRecord(
|
|
1679
1691
|
match: MatchResult,
|
|
@@ -1690,7 +1702,7 @@ export async function settleCaptureRecord(
|
|
|
1690
1702
|
// (ctx.use(Handle).defer()), so a defer whose resolver depends on a masked
|
|
1691
1703
|
// loader never settles and the wait below runs into the deadline instead.
|
|
1692
1704
|
freshHandleStore.seal();
|
|
1693
|
-
const loaderRecords = derivedCtx.
|
|
1705
|
+
const loaderRecords = derivedCtx._shellCaptureState?.loaderRecords;
|
|
1694
1706
|
const settled = await raceDeadline(
|
|
1695
1707
|
Promise.all([
|
|
1696
1708
|
freshHandleStore
|
|
@@ -1741,6 +1753,32 @@ export async function settleCaptureRecord(
|
|
|
1741
1753
|
return { kind: "refused", refusal: "loader" };
|
|
1742
1754
|
}
|
|
1743
1755
|
|
|
1756
|
+
// A boundary the match rendered in place of a handler (route, layout or
|
|
1757
|
+
// slot) that threw, or threw notFound(), on this pass. A MISS whose own
|
|
1758
|
+
// render failed answered 500/404 and scheduled no capture, so on a first
|
|
1759
|
+
// capture the failure is this pass's own (a flaky upstream); a recapture
|
|
1760
|
+
// with no visitor render behind it (stale HIT, warm, stale build shell)
|
|
1761
|
+
// can only have failed. The pass's _setStatus is the foreground's and the
|
|
1762
|
+
// callbacks below get a synthetic 200, so nothing else refuses it. A stale
|
|
1763
|
+
// shell whose recapture fails keeps serving, as a failed cache() refresh's
|
|
1764
|
+
// record does: until ttl + swr ends (a build-time shell, until a recapture
|
|
1765
|
+
// stores or a tag evicts it). Checked after the settle so a guard trip in
|
|
1766
|
+
// a handler or a loader names the read, and before any write starts.
|
|
1767
|
+
// Thrown like captureAndStoreShell's render errors: no retry,
|
|
1768
|
+
// reportCacheError and backoff in scheduleShellCapture.
|
|
1769
|
+
const boundary = match.segments.find(
|
|
1770
|
+
(s) => s.type === "error" || s.type === "notFound",
|
|
1771
|
+
);
|
|
1772
|
+
if (boundary) {
|
|
1773
|
+
if (refuseOnCaptureGuard(capture.key, derivedCtx))
|
|
1774
|
+
return { kind: "refused", refusal: guardRefusal(derivedCtx) };
|
|
1775
|
+
throw new Error(
|
|
1776
|
+
`[rango] Shell capture for "${capture.key}" rendered the ${boundary.type} ` +
|
|
1777
|
+
`boundary for segment "${boundary.id}"; a failed render is not stored.`,
|
|
1778
|
+
{ cause: boundary.error ?? boundary.notFoundInfo },
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1744
1782
|
const callbacks = derivedCtx._onResponseCallbacks?.splice(0) ?? [];
|
|
1745
1783
|
if (callbacks.length > 0) {
|
|
1746
1784
|
const synthetic = new Response(null, { status: 200 });
|
|
@@ -1770,7 +1808,7 @@ export async function settleCaptureRecord(
|
|
|
1770
1808
|
!Array.isArray(record.segments) ||
|
|
1771
1809
|
record.segments.length === 0
|
|
1772
1810
|
) {
|
|
1773
|
-
// Post-match serve-source truth (cache-lookup.ts
|
|
1811
|
+
// Post-match serve-source truth (cache-lookup.ts decodeStoredEntry).
|
|
1774
1812
|
if (derivedCtx._pprReplayPostMatchReason === "prerender-store") {
|
|
1775
1813
|
return { kind: "prerender", match };
|
|
1776
1814
|
}
|
|
@@ -1838,25 +1876,10 @@ function recordSegmentsHaveHandles(
|
|
|
1838
1876
|
/**
|
|
1839
1877
|
* One capture attempt in a DERIVED request context.
|
|
1840
1878
|
*
|
|
1841
|
-
* The derived context
|
|
1842
|
-
* post-middleware state (variables, cache store, env/request/url,
|
|
1843
|
-
*
|
|
1844
|
-
*
|
|
1845
|
-
* completion (its stream() flipped `completed` on settle) and would throw
|
|
1846
|
-
* LateHandlePushError on any re-push. Every downstream reader resolves the
|
|
1847
|
-
* store off the ambient context (setupLoaderAccess captures
|
|
1848
|
-
* _getRequestContext()._handleStore; trackHandler reads it), so the fresh
|
|
1849
|
-
* store on the derived context is what the capture match() writes handles to.
|
|
1850
|
-
* - _requestTags: a fresh Set. The capture collects its OWN shell tags here —
|
|
1851
|
-
* non-loader tags only, since loaders are masked — which is exactly the tag
|
|
1852
|
-
* set a shell entry should be invalidatable by (loader tags belong to holes).
|
|
1853
|
-
* - _shellCaptureRun: true — the switch loaders/cookies/headers guards read.
|
|
1854
|
-
* - _dynamic / dynamic(): an own latch the capture reads.
|
|
1855
|
-
* - _metricsStore: undefined so the capture never appends to the foreground's
|
|
1856
|
-
* (already-finalized) metrics.
|
|
1857
|
-
* - _renderBarrier family: an own barrier wired to the fresh handle store
|
|
1858
|
-
* (wireRenderBarrier), plus _treeHasStreaming/deadlock-guard resets — the
|
|
1859
|
-
* capture's rendered() lifecycle is its own, not the foreground's.
|
|
1879
|
+
* The derived context (deriveRenderPass, server/render-pass.ts) inherits the
|
|
1880
|
+
* foreground's post-middleware state (variables, cache store, env/request/url,
|
|
1881
|
+
* waitUntil) and owns the render-scoped accumulators, its render barrier
|
|
1882
|
+
* included: RENDER_PASS_FIELDS["shell-capture"] lists each and why.
|
|
1860
1883
|
*
|
|
1861
1884
|
* The capture is MIXED-CHAIN: its match() behaves like a normal render with
|
|
1862
1885
|
* respect to the segment cache — cache()'d segments replay from ring 3, UNCACHED
|
|
@@ -1913,7 +1936,7 @@ async function attemptCapture(
|
|
|
1913
1936
|
);
|
|
1914
1937
|
const { derivedCtx, freshHandleStore } = derivation;
|
|
1915
1938
|
if (descriptor.pathname !== undefined) {
|
|
1916
|
-
derivedCtx.
|
|
1939
|
+
derivedCtx._shellCaptureState!.forVisitor = true;
|
|
1917
1940
|
}
|
|
1918
1941
|
// The capture generation starts before matching or any snapshot read
|
|
1919
1942
|
// (ShellCacheEntry.createdAt): a tag invalidated after it wins.
|
|
@@ -2014,6 +2037,115 @@ async function attemptCapture(
|
|
|
2014
2037
|
});
|
|
2015
2038
|
}
|
|
2016
2039
|
|
|
2040
|
+
/**
|
|
2041
|
+
* @internal The freshness window of a route cache() record, as ms
|
|
2042
|
+
* timestamps: fresh until `freshUntil` (its `expiresAt`), stale until
|
|
2043
|
+
* `staleUntil` (plus its scope's swr). `ttl`, `swr` and `written` describe
|
|
2044
|
+
* the record that sets `staleUntil`: its cache() config, and whether this
|
|
2045
|
+
* capture wrote it (cacheRoute) rather than read it.
|
|
2046
|
+
*/
|
|
2047
|
+
export interface RouteRecordWindow {
|
|
2048
|
+
freshUntil: number;
|
|
2049
|
+
staleUntil: number;
|
|
2050
|
+
ttl: number;
|
|
2051
|
+
swr: number;
|
|
2052
|
+
written: boolean;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
/**
|
|
2056
|
+
* @internal What a shell capture's render records about itself, on its
|
|
2057
|
+
* derived request context as `_shellCaptureState`.
|
|
2058
|
+
* RENDER_PASS_FIELDS["shell-capture"] (server/render-pass.ts) creates it with
|
|
2059
|
+
* every field present, so it keeps one shape while the capture writes it. A
|
|
2060
|
+
* background re-render the capture starts has none: it writes a route
|
|
2061
|
+
* record, under cache() rules (RENDER_PASS_FIELDS["background-revalidation"]).
|
|
2062
|
+
*/
|
|
2063
|
+
export interface ShellCaptureState {
|
|
2064
|
+
/**
|
|
2065
|
+
* @internal PPR shell-capture ACTIVE marker. True ONLY inside the background
|
|
2066
|
+
* capture task's derived request context (built by shell-capture.ts). This is
|
|
2067
|
+
* the switch every capture-specific behavior reads: loader masking
|
|
2068
|
+
* (loader-mask.ts isShellCaptureActive / resolveOwnerLoaders) and the
|
|
2069
|
+
* identity-read capture guard (server/context.ts guardIdentityRead). The
|
|
2070
|
+
* foreground render never sets it, so the
|
|
2071
|
+
* served response is byte-identical to axis 1. The capture descriptor itself
|
|
2072
|
+
* (key/ttl/swr/tags/store) is NOT threaded through the request context — the
|
|
2073
|
+
* integrated PPR serve path (rsc/shell-serve.ts + rsc-rendering.ts) builds it
|
|
2074
|
+
* locally and passes it to planShellCapture in its input.
|
|
2075
|
+
*/
|
|
2076
|
+
readonly run: true;
|
|
2077
|
+
|
|
2078
|
+
/**
|
|
2079
|
+
* @internal Set beside `run` when the shell is keyed by the
|
|
2080
|
+
* visitor's URL (`rewrite(...).shell("visitor")`): the capture renders the
|
|
2081
|
+
* visitor's location and `ctx.rewrittenFrom`; the theme stays the default.
|
|
2082
|
+
*/
|
|
2083
|
+
forVisitor: boolean | undefined;
|
|
2084
|
+
|
|
2085
|
+
/**
|
|
2086
|
+
* @internal Bake-lane loader containers collected DURING a shell capture:
|
|
2087
|
+
* segment-key -> the loader's (pre-wrap) result promise. Populated by
|
|
2088
|
+
* resolveLoaderData for bake-lane loaders (lane rule: see its JSDoc in
|
|
2089
|
+
* loader-cache.ts). Drained by captureAndStoreShell after the shell
|
|
2090
|
+
* quiesces: settled containers are promise-elided, Flight-serialized, and
|
|
2091
|
+
* pinned into the snapshot's loader family; a REJECTED container refuses
|
|
2092
|
+
* the capture (error UI must never bake into the shared shell).
|
|
2093
|
+
*/
|
|
2094
|
+
loaderRecords: Map<string, Promise<unknown>>;
|
|
2095
|
+
|
|
2096
|
+
/**
|
|
2097
|
+
* @internal Set when a loader pushed a
|
|
2098
|
+
* value the record cannot keep (masked nested promises, a pushed promise
|
|
2099
|
+
* that settled to one, a live-lane loader's push, or a push outside a
|
|
2100
|
+
* loader body): the loader records then carry `runs: 1`, so a HIT still
|
|
2101
|
+
* runs their bodies for those pushes.
|
|
2102
|
+
*/
|
|
2103
|
+
unrecordedLoaderPush: true | undefined;
|
|
2104
|
+
|
|
2105
|
+
/**
|
|
2106
|
+
* @internal The lane of each loader the capture's matched route registers
|
|
2107
|
+
* (loader-cache.ts routeLoaderLanes), set by createMatchContextForFull
|
|
2108
|
+
* before any loader runs. The capture's push funnel credits a push made
|
|
2109
|
+
* under a live-lane loader's body, at any depth, to that loader
|
|
2110
|
+
* (shell-capture.ts deriveShellCaptureContext).
|
|
2111
|
+
*/
|
|
2112
|
+
loaderLanes: ReadonlyMap<string, "live" | "bake"> | undefined;
|
|
2113
|
+
|
|
2114
|
+
/**
|
|
2115
|
+
* @internal Set by the capture guard (server/context.ts
|
|
2116
|
+
* guardIdentityRead) when it throws DURING a capture render: the read
|
|
2117
|
+
* ("cookies()", "headers()", `ctx.get() for a non-cacheable variable`,
|
|
2118
|
+
* "ctx.theme") and the fix the
|
|
2119
|
+
* refusal warning gives for it. Load-bearing for the
|
|
2120
|
+
* bake lane: a guard throw inside an executing loader is swallowed by
|
|
2121
|
+
* wrapLoaderPromise into per-loader error UI, which would otherwise bake
|
|
2122
|
+
* silently into the shared shell — the capture checks this flag after the
|
|
2123
|
+
* render and refuses instead. Deterministic, so the capture does not retry.
|
|
2124
|
+
*/
|
|
2125
|
+
guardTripped: { surface: string; fix: string } | undefined;
|
|
2126
|
+
|
|
2127
|
+
/**
|
|
2128
|
+
* @internal The loader $$id whose BODY was executing when the capture guard
|
|
2129
|
+
* tripped (read off the loader-body ALS scope at trip time), or undefined
|
|
2130
|
+
* when the read came from handler/render code. Only used to make the
|
|
2131
|
+
* once-per-key refusal warning name the real source — the old text
|
|
2132
|
+
* hardcoded "a bake-lane loader", which misattributed handler-land reads
|
|
2133
|
+
* and sent users debugging the wrong lane (issue #672, secondary).
|
|
2134
|
+
*/
|
|
2135
|
+
guardTrippedLoaderId: string | undefined;
|
|
2136
|
+
|
|
2137
|
+
/**
|
|
2138
|
+
* @internal The freshness window of the route cache() records this
|
|
2139
|
+
* capture read or wrote (cache-scope.ts noteRouteRecordWindow), as ms
|
|
2140
|
+
* timestamps: `freshUntil` is the record's `expiresAt`, `staleUntil` adds
|
|
2141
|
+
* its scope's swr. The narrowest record wins. A shell capture caps its
|
|
2142
|
+
* entry's ttl and ttl+swr to it (shell-capture.ts capShellWindow), so a
|
|
2143
|
+
* shell is never fresher, or served longer, than the route cache() entry
|
|
2144
|
+
* it replayed or wrote.
|
|
2145
|
+
*/
|
|
2146
|
+
routeRecordWindow: RouteRecordWindow | undefined;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2017
2149
|
/**
|
|
2018
2150
|
* The derived capture context and its fresh (mask-funneled) handle store,
|
|
2019
2151
|
* shared by BOTH shell producers: the runtime background capture
|
|
@@ -2123,7 +2255,7 @@ export function deriveShellCaptureContext(
|
|
|
2123
2255
|
// A replay of a live-lane loader's push (a "use cache" or loader cache()
|
|
2124
2256
|
// hit a recapture makes inside a bake-lane body) is that hole's too.
|
|
2125
2257
|
const liveLaneOwner = (): string | undefined => {
|
|
2126
|
-
const lanes =
|
|
2258
|
+
const lanes = captureState.loaderLanes;
|
|
2127
2259
|
if (!lanes) return undefined;
|
|
2128
2260
|
return (
|
|
2129
2261
|
findEnclosingLoaderBody(
|
|
@@ -2190,7 +2322,7 @@ export function deriveShellCaptureContext(
|
|
|
2190
2322
|
const judged = mask(settled);
|
|
2191
2323
|
if (maskedNestedThenable) {
|
|
2192
2324
|
freshHandleStore.unrecordPush(handleName, segmentId, slot);
|
|
2193
|
-
|
|
2325
|
+
captureState.unrecordedLoaderPush = true;
|
|
2194
2326
|
}
|
|
2195
2327
|
return judged;
|
|
2196
2328
|
});
|
|
@@ -2220,7 +2352,7 @@ export function deriveShellCaptureContext(
|
|
|
2220
2352
|
// A push the record cannot keep reaches a HIT only from a live run of
|
|
2221
2353
|
// its loader, so the loader records ask for one (runs: 1).
|
|
2222
2354
|
const loaderPush = owner === undefined;
|
|
2223
|
-
if (loaderPush)
|
|
2355
|
+
if (loaderPush) captureState.unrecordedLoaderPush = true;
|
|
2224
2356
|
rawCapturePush(handleName, segmentId, masked, loaderPush, owner);
|
|
2225
2357
|
};
|
|
2226
2358
|
|
|
@@ -2229,111 +2361,25 @@ export function deriveShellCaptureContext(
|
|
|
2229
2361
|
// request before the derivation, or the capture would start its own and
|
|
2230
2362
|
// run key() under the capture guard.
|
|
2231
2363
|
reqCtx._resolvedCacheKeys ??= new Map();
|
|
2232
|
-
const
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2364
|
+
const recordingStore = reqCtx._cacheStore
|
|
2365
|
+
? new RecordingShellStore(reqCtx._cacheStore)
|
|
2366
|
+
: undefined;
|
|
2367
|
+
// What the capture owns, and why: RENDER_PASS_FIELDS["shell-capture"].
|
|
2368
|
+
const derivedCtx: RequestContext = deriveRenderPass(reqCtx, "shell-capture", {
|
|
2369
|
+
handleStore: freshHandleStore,
|
|
2370
|
+
identity,
|
|
2371
|
+
recording: recordingStore && {
|
|
2372
|
+
store: recordingStore,
|
|
2373
|
+
implicitCache: {
|
|
2374
|
+
ttl: descriptor.ttl,
|
|
2375
|
+
swr: descriptor.swr,
|
|
2376
|
+
store: new SnapshotOnlySegmentStore(recordingStore),
|
|
2377
|
+
keyPrefix: "doc",
|
|
2378
|
+
},
|
|
2379
|
+
},
|
|
2238
2380
|
});
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
derivedCtx.url = identity.url;
|
|
2242
|
-
derivedCtx.originalUrl = new URL(identity.url);
|
|
2243
|
-
derivedCtx.pathname = identity.url.pathname;
|
|
2244
|
-
derivedCtx.searchParams = identity.url.searchParams;
|
|
2245
|
-
}
|
|
2246
|
-
// Own render barrier, closure-bound to the derived ctx and the fresh store
|
|
2247
|
-
// (issue #684, plan 009). Without this every _renderBarrier* read fell
|
|
2248
|
-
// through the prototype to the foreground's ALREADY-RESOLVED barrier: a
|
|
2249
|
-
// bake-lane loader's `await ctx.rendered()` resolved instantly and
|
|
2250
|
-
// ctx.use(handle) read the FOREGROUND handle snapshot — foreground
|
|
2251
|
-
// per-request handle data could bake into the shared shell. wireRenderBarrier
|
|
2252
|
-
// also resets _treeHasStreaming (recomputed for the capture's tree) and the
|
|
2253
|
-
// deadlock-guard fields as own properties.
|
|
2254
|
-
wireRenderBarrier(derivedCtx, freshHandleStore);
|
|
2255
|
-
derivedCtx._requestTags = new Set<string>();
|
|
2256
|
-
// Own explicit-store registry: cache-store resolutions during the capture
|
|
2257
|
-
// (the implicit scope's SnapshotOnlySegmentStore, any per-capture explicit
|
|
2258
|
-
// store instance) must NOT register into the handler-lifetime
|
|
2259
|
-
// _explicitTaggedStores set — a capture-ephemeral store pinned there would
|
|
2260
|
-
// trip the partial-tag-store warning on every later updateTag() and retain
|
|
2261
|
-
// the whole capture snapshot in memory. Capture registrations die with this
|
|
2262
|
-
// context; module-singleton stores stay registered by normal renders.
|
|
2263
|
-
derivedCtx._explicitTaggedStores = new Set();
|
|
2264
|
-
// Own list: the capture's render errors refuse the capture only, and the
|
|
2265
|
-
// foreground's never reach it.
|
|
2266
|
-
derivedCtx._renderErrors = [];
|
|
2267
|
-
derivedCtx._recoveredHandlerErrors = undefined;
|
|
2268
|
-
derivedCtx._shellCaptureRun = true;
|
|
2269
|
-
// Own dynamic() latch. The inherited method writes the FOREGROUND context
|
|
2270
|
-
// (its closure), which already served, and nothing read it for a runtime
|
|
2271
|
-
// capture: handler code that opts out after an await (a handler promise the
|
|
2272
|
-
// capture now waits for) must refuse THIS capture instead
|
|
2273
|
-
// (settleCaptureRecord and captureAndStoreShell read _dynamic). The ppr
|
|
2274
|
-
// header latch is left alone: a capture's response is never sent.
|
|
2275
|
-
derivedCtx._dynamic = false;
|
|
2276
|
-
derivedCtx.dynamic = (): void => {
|
|
2277
|
-
derivedCtx._dynamic = true;
|
|
2278
|
-
};
|
|
2279
|
-
// Own-property reset: an inherited flag would make serializeSegments store
|
|
2280
|
-
// a double-encoded fragment. No current arming site reaches a capture
|
|
2281
|
-
// render (kept as defense-in-depth for a future one).
|
|
2282
|
-
derivedCtx._shellFragmentPayload = false;
|
|
2283
|
-
derivedCtx._metricsStore = undefined;
|
|
2284
|
-
// Spans, like perf metrics above, are a FOREGROUND surface: the capture
|
|
2285
|
-
// re-render must not emit a second rango.render/loader/ssr set after the
|
|
2286
|
-
// foreground rango.request span ended (orphan spans in the trace).
|
|
2287
|
-
// _tracing is otherwise inherited through Object.create(reqCtx).
|
|
2288
|
-
derivedCtx._tracing = undefined;
|
|
2289
|
-
// Bake-lane (`ssr: false`) loader containers, which execute during capture
|
|
2290
|
-
// (docs/design/loader-container-bake.md). resolveLoaderData registers each
|
|
2291
|
-
// container promise here; settleCaptureRecord waits for them, and the drain
|
|
2292
|
-
// in captureAndStoreShell elides + pins them into the snapshot's loader
|
|
2293
|
-
// family.
|
|
2294
|
-
derivedCtx._shellCaptureLoaderRecords = new Map();
|
|
2295
|
-
// Own onResponse list so the capture's match-middleware callbacks (the doc
|
|
2296
|
-
// record registers here) are ISOLATED from the foreground's shared array AND
|
|
2297
|
-
// can be fired by settleCaptureRecord. The segment write is gated behind
|
|
2298
|
-
// onResponse, which the capture never triggers (it builds no Response):
|
|
2299
|
-
// without the synthetic fire, no doc record is written.
|
|
2300
|
-
derivedCtx._onResponseCallbacks = [];
|
|
2301
|
-
|
|
2302
|
-
// Capture data snapshot: route the capture's cache-store calls through a
|
|
2303
|
-
// recording wrapper on the DERIVED context's store (own property, so the
|
|
2304
|
-
// shared reqCtx._cacheStore is untouched — the snapshot is per-capture). It
|
|
2305
|
-
// records the doc segment record, which rides inside the ShellCacheEntry
|
|
2306
|
-
// with the bake-lane loader pins; no cache read is recorded. See
|
|
2307
|
-
// cache/shell-snapshot.ts and the design doc.
|
|
2308
|
-
//
|
|
2309
|
-
// Cache writes are deferred (waitUntil): the doc record's write would
|
|
2310
|
-
// otherwise land after the match returns. Override the derived context's
|
|
2311
|
-
// waitUntil to COLLECT those write promises (still forwarding to the parent
|
|
2312
|
-
// so the write persists and the worker stays alive), then
|
|
2313
|
-
// settleCaptureRecord and captureAndStoreShell await them before reading
|
|
2314
|
-
// the record and draining.
|
|
2315
|
-
if (reqCtx._cacheStore) {
|
|
2316
|
-
const recordingStore = new RecordingShellStore(reqCtx._cacheStore);
|
|
2317
|
-
derivedCtx._cacheStore = recordingStore;
|
|
2318
|
-
derivedCtx.waitUntil = (fn: () => Promise<void>): void => {
|
|
2319
|
-
const p = Promise.resolve().then(fn);
|
|
2320
|
-
recordingStore.trackWrite(p);
|
|
2321
|
-
reqCtx.waitUntil(() => p);
|
|
2322
|
-
};
|
|
2323
|
-
// The doc record (capture side): the implicit doc-cache scope makes the
|
|
2324
|
-
// capture's match write ALL matched non-loader segments as one doc-keyed
|
|
2325
|
-
// segment record — into the snapshot only (SnapshotOnlySegmentStore), so
|
|
2326
|
-
// the record dies with the shell entry and the next capture's lookup
|
|
2327
|
-
// still misses (handlers re-run on recapture). A route deriving its own
|
|
2328
|
-
// cache() scope keeps it, and recordShellCaptureDocRecord (cache-store.ts)
|
|
2329
|
-
// writes the same record for it.
|
|
2330
|
-
derivedCtx._shellImplicitCache = {
|
|
2331
|
-
ttl: descriptor.ttl,
|
|
2332
|
-
swr: descriptor.swr,
|
|
2333
|
-
store: new SnapshotOnlySegmentStore(recordingStore),
|
|
2334
|
-
keyPrefix: "doc",
|
|
2335
|
-
};
|
|
2336
|
-
}
|
|
2381
|
+
// The row creates it; the push funnel above writes it.
|
|
2382
|
+
const captureState = derivedCtx._shellCaptureState!;
|
|
2337
2383
|
|
|
2338
2384
|
return {
|
|
2339
2385
|
derivedCtx,
|
|
@@ -2479,7 +2525,7 @@ async function captureAndStoreShell(
|
|
|
2479
2525
|
// container still pending here either pinned the tree (the trivial-prelude
|
|
2480
2526
|
// gate above already returned no-shell) or postponed under an ANCESTOR
|
|
2481
2527
|
// boundary (it is a hole; omitting the record keeps it live).
|
|
2482
|
-
const loaderRecords = reqCtx.
|
|
2528
|
+
const loaderRecords = reqCtx._shellCaptureState?.loaderRecords;
|
|
2483
2529
|
// Set once a bake-lane loader settles with real (non-hole) material: its data
|
|
2484
2530
|
// is frozen into the shell prelude regardless of whether snapshot
|
|
2485
2531
|
// serialization succeeds. Drives the opt-in debug metadata below.
|
|
@@ -2492,7 +2538,9 @@ async function captureAndStoreShell(
|
|
|
2492
2538
|
| typeof import("../cache/segment-codec.js").serializeResult
|
|
2493
2539
|
| undefined;
|
|
2494
2540
|
// Capture-wide, read once: every record of this snapshot agrees.
|
|
2495
|
-
const runs: 0 | 1 = reqCtx.
|
|
2541
|
+
const runs: 0 | 1 = reqCtx._shellCaptureState?.unrecordedLoaderPush
|
|
2542
|
+
? 1
|
|
2543
|
+
: 0;
|
|
2496
2544
|
for (const [segmentKey, containerPromise] of loaderRecords) {
|
|
2497
2545
|
const elided = await elideLoaderContainer(containerPromise);
|
|
2498
2546
|
if (elided.state === "rejected") {
|
|
@@ -2583,7 +2631,7 @@ async function captureAndStoreShell(
|
|
|
2583
2631
|
// The drain's container encode adds its Flight errors here too (#927).
|
|
2584
2632
|
// After the loader drain so a rejected bake-lane loader keeps its specific
|
|
2585
2633
|
// refusal. Thrown like a fatal shell error: no retry, reportCacheError and
|
|
2586
|
-
// backoff in
|
|
2634
|
+
// backoff in planShellCapture.
|
|
2587
2635
|
const renderErrors = reqCtx._renderErrors;
|
|
2588
2636
|
if (renderErrors && renderErrors.length > 0) throw renderErrors[0];
|
|
2589
2637
|
|
|
@@ -2744,7 +2792,7 @@ async function captureAndStoreShell(
|
|
|
2744
2792
|
// Capped to the route record the capture used: the policy the
|
|
2745
2793
|
// store would apply (resolveTtl/resolveSwrWindow, as putShell does),
|
|
2746
2794
|
// made explicit.
|
|
2747
|
-
const record = reqCtx.
|
|
2795
|
+
const record = reqCtx._shellCaptureState?.routeRecordWindow;
|
|
2748
2796
|
let window: { ttl?: number; swr?: number } = {
|
|
2749
2797
|
ttl: capture.ttl,
|
|
2750
2798
|
swr: capture.swr,
|