@rangojs/router 0.18.0 → 0.19.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/types/browser/event-controller.d.ts +3 -3
- package/dist/types/browser/history-state.d.ts +2 -0
- package/dist/types/browser/navigation-transaction.d.ts +20 -2
- package/dist/types/browser/partial-update.d.ts +16 -1
- package/dist/types/browser/prefetch/cache.d.ts +6 -6
- package/dist/types/browser/react/Link.d.ts +19 -9
- package/dist/types/browser/react/context.d.ts +2 -2
- package/dist/types/browser/react/location-state-shared.d.ts +95 -56
- package/dist/types/browser/react/location-state.d.ts +1 -1
- package/dist/types/browser/transition-when.d.ts +80 -0
- package/dist/types/browser/types.d.ts +104 -9
- package/dist/types/build/generate-manifest.d.ts +8 -0
- package/dist/types/cache/background-task.d.ts +24 -2
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/cache-key-utils.d.ts +44 -13
- package/dist/types/cache/cache-policy.d.ts +1 -1
- package/dist/types/cache/cache-runtime.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +117 -35
- package/dist/types/cache/cache-tag.d.ts +24 -4
- package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
- package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
- package/dist/types/cache/document-cache.d.ts +10 -0
- package/dist/types/cache/handle-snapshot.d.ts +40 -8
- package/dist/types/cache/invalidation-order.d.ts +23 -0
- package/dist/types/cache/request-tag-mask.d.ts +54 -0
- package/dist/types/cache/shell-snapshot.d.ts +88 -141
- package/dist/types/cache/tag-invalidation.d.ts +38 -14
- package/dist/types/cache/types.d.ts +68 -61
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
- package/dist/types/client-urls/navigation.d.ts +28 -2
- package/dist/types/client-urls/server-projection.d.ts +5 -2
- package/dist/types/client-urls/types.d.ts +5 -5
- package/dist/types/client.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/route-definition/helpers-types.d.ts +11 -11
- package/dist/types/route-name.d.ts +7 -0
- package/dist/types/router/error-handling.d.ts +1 -1
- package/dist/types/router/intercept-resolution.d.ts +2 -0
- package/dist/types/router/match-context.d.ts +5 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
- package/dist/types/router/router-options.d.ts +4 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
- package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
- package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
- package/dist/types/router/transition-when.d.ts +24 -10
- package/dist/types/rsc/attach-transition-when.d.ts +18 -0
- package/dist/types/rsc/full-payload.d.ts +14 -7
- package/dist/types/rsc/response-cache-serve.d.ts +15 -12
- package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
- package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
- package/dist/types/rsc/shell-capture.d.ts +78 -81
- package/dist/types/rsc/shell-serve.d.ts +19 -48
- package/dist/types/rsc/types.d.ts +2 -0
- package/dist/types/segment-system.d.ts +7 -0
- package/dist/types/server/context.d.ts +89 -61
- package/dist/types/server/cookie-store.d.ts +30 -18
- package/dist/types/server/handle-store.d.ts +52 -1
- package/dist/types/server/request-context.d.ts +66 -45
- package/dist/types/server/request-headers.d.ts +44 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/index.d.ts +3 -2
- package/dist/types/testing/internal/action-id.d.ts +7 -0
- package/dist/types/testing/location-state-key.d.ts +24 -0
- package/dist/types/testing/render-route.d.ts +17 -1
- package/dist/types/testing/run-transition-when.d.ts +44 -69
- package/dist/types/testing/serve-shell-request.d.ts +7 -6
- package/dist/types/testing/shell-status.d.ts +34 -22
- package/dist/types/transition-when-ref.d.ts +79 -0
- package/dist/types/types/cache-types.d.ts +15 -3
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/segments.d.ts +86 -71
- package/dist/types/urls/path-helper-types.d.ts +3 -4
- package/dist/types/urls/pattern-types.d.ts +3 -4
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
- package/dist/vite/index.js +960 -97
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +76 -49
- package/skills/caching/SKILL.md +167 -69
- package/skills/client-urls/SKILL.md +1 -1
- package/skills/comparison/references/framework-comparison.md +8 -5
- package/skills/document-cache/SKILL.md +24 -0
- package/skills/hooks/SKILL.md +3 -1
- package/skills/hooks/navigation.md +23 -2
- package/skills/hooks/state.md +98 -6
- package/skills/intercept/SKILL.md +29 -12
- package/skills/loader/SKILL.md +52 -21
- package/skills/middleware/SKILL.md +18 -0
- package/skills/migrate-nextjs/SKILL.md +3 -3
- package/skills/observability/SKILL.md +6 -5
- package/skills/parallel/SKILL.md +1 -1
- package/skills/ppr/SKILL.md +129 -77
- package/skills/prerender/SKILL.md +3 -3
- package/skills/rango/SKILL.md +25 -6
- package/skills/route/SKILL.md +2 -0
- package/skills/testing/SKILL.md +27 -25
- package/skills/testing/cache-prerender.md +9 -9
- package/skills/testing/client-components.md +45 -3
- package/skills/testing/loader.md +3 -2
- package/skills/testing/middleware.md +1 -0
- package/skills/testing/predicates.md +49 -49
- package/skills/testing/render-handler.md +1 -0
- package/skills/testing/serve-shell-request.md +14 -0
- package/skills/testing/server-actions.md +9 -3
- package/skills/theme/SKILL.md +8 -4
- package/skills/typesafety/env-and-bindings.md +8 -0
- package/skills/use-cache/SKILL.md +64 -19
- package/skills/view-transitions/SKILL.md +150 -29
- package/src/__augment-tests__/augmented.check.ts +23 -0
- package/src/browser/event-controller.ts +4 -4
- package/src/browser/history-state.ts +52 -1
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +77 -18
- package/src/browser/navigation-client.ts +4 -3
- package/src/browser/navigation-store.ts +43 -1
- package/src/browser/navigation-transaction.ts +31 -11
- package/src/browser/partial-update.ts +146 -35
- package/src/browser/prefetch/cache.ts +6 -6
- package/src/browser/prefetch/fetch.ts +2 -2
- package/src/browser/react/Link.tsx +38 -33
- package/src/browser/react/NavigationProvider.tsx +2 -2
- package/src/browser/react/context.ts +2 -2
- package/src/browser/react/location-state-shared.ts +241 -49
- package/src/browser/react/location-state.ts +5 -2
- package/src/browser/rsc-router.tsx +4 -0
- package/src/browser/server-action-bridge.ts +84 -15
- package/src/browser/transition-when.ts +205 -0
- package/src/browser/types.ts +124 -10
- package/src/build/generate-manifest.ts +77 -1
- package/src/cache/background-task.ts +55 -2
- package/src/cache/cache-exec-scope.ts +48 -1
- package/src/cache/cache-key-utils.ts +56 -16
- package/src/cache/cache-policy.ts +9 -4
- package/src/cache/cache-runtime.ts +197 -144
- package/src/cache/cache-scope.ts +304 -112
- package/src/cache/cache-tag.ts +105 -13
- package/src/cache/cf/cf-cache-store.ts +152 -76
- package/src/cache/cf/cf-shell-frame.ts +2 -2
- package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
- package/src/cache/document-cache.ts +53 -7
- package/src/cache/handle-snapshot.ts +52 -9
- package/src/cache/invalidation-order.ts +84 -0
- package/src/cache/request-tag-mask.ts +147 -0
- package/src/cache/shell-snapshot.ts +130 -348
- package/src/cache/tag-invalidation.ts +57 -51
- package/src/cache/types.ts +71 -67
- package/src/cache/vercel/vercel-cache-store.ts +61 -58
- package/src/client-urls/client-root.tsx +8 -4
- package/src/client-urls/client-urls.ts +8 -6
- package/src/client-urls/navigation.ts +55 -4
- package/src/client-urls/server-projection.ts +31 -10
- package/src/client-urls/types.ts +12 -5
- package/src/client.tsx +2 -0
- package/src/host/cookie-handler.ts +2 -1
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +3 -0
- package/src/loader-redirect.tsx +7 -3
- package/src/prerender/build-shell-capture.ts +155 -146
- package/src/route-definition/dsl-helpers.ts +2 -0
- package/src/route-definition/helpers-types.ts +11 -11
- package/src/route-name.ts +12 -0
- package/src/router/content-negotiation.ts +4 -1
- package/src/router/error-handling.ts +5 -2
- package/src/router/handler-context.ts +10 -7
- package/src/router/intercept-resolution.ts +11 -1
- package/src/router/loader-resolution.ts +28 -3
- package/src/router/logging.ts +5 -3
- package/src/router/match-api.ts +62 -34
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +9 -29
- package/src/router/match-middleware/background-revalidation.ts +3 -5
- package/src/router/match-middleware/cache-lookup.ts +35 -19
- package/src/router/match-middleware/cache-store.ts +15 -9
- package/src/router/match-result.ts +1 -0
- package/src/router/middleware.ts +2 -5
- package/src/router/navigation-snapshot.ts +6 -5
- package/src/router/prerender-match.ts +4 -0
- package/src/router/request-classification.ts +7 -4
- package/src/router/router-options.ts +4 -1
- package/src/router/segment-resolution/fresh.ts +0 -4
- package/src/router/segment-resolution/loader-cache.ts +150 -25
- package/src/router/segment-resolution/loader-mask.ts +3 -4
- package/src/router/segment-resolution/loader-snapshot.ts +15 -78
- package/src/router/segment-resolution/mask-nested.ts +47 -22
- package/src/router/segment-resolution/revalidation.ts +0 -3
- package/src/router/segment-resolution/view-transition-default.ts +6 -21
- package/src/router/telemetry.ts +6 -3
- package/src/router/transition-when.ts +34 -52
- package/src/router.ts +2 -1
- package/src/rsc/attach-transition-when.ts +47 -0
- package/src/rsc/full-payload.ts +30 -10
- package/src/rsc/handler.ts +12 -10
- package/src/rsc/loader-fetch.ts +2 -1
- package/src/rsc/manifest-init.ts +4 -0
- package/src/rsc/origin-guard.ts +8 -5
- package/src/rsc/progressive-enhancement.ts +7 -30
- package/src/rsc/response-cache-serve.ts +68 -80
- package/src/rsc/response-route-handler.ts +2 -2
- package/src/rsc/rsc-rendering.ts +206 -252
- package/src/rsc/server-action.ts +8 -20
- package/src/rsc/shell-build-manifest.ts +22 -20
- package/src/rsc/shell-capture-constants.ts +136 -20
- package/src/rsc/shell-capture.ts +352 -589
- package/src/rsc/shell-serve.ts +73 -109
- package/src/rsc/ssr-setup.ts +3 -2
- package/src/rsc/types.ts +2 -0
- package/src/segment-system.tsx +40 -7
- package/src/server/context.ts +189 -124
- package/src/server/cookie-store.ts +153 -127
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +123 -84
- package/src/server/request-headers.ts +134 -0
- package/src/server.ts +4 -0
- package/src/testing/dispatch.ts +4 -7
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/internal/action-id.ts +25 -0
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/location-state-key.ts +45 -0
- package/src/testing/render-handler.ts +1 -1
- package/src/testing/render-route.tsx +109 -16
- package/src/testing/run-client-revalidate.ts +3 -19
- package/src/testing/run-loader.ts +5 -1
- package/src/testing/run-transition-when.ts +109 -172
- package/src/testing/serve-shell-request.ts +10 -7
- package/src/testing/shell-status.ts +60 -56
- package/src/transition-when-ref.ts +220 -0
- package/src/types/cache-types.ts +15 -3
- package/src/types/index.ts +3 -0
- package/src/types/segments.ts +94 -91
- package/src/urls/path-helper-types.ts +3 -4
- package/src/urls/pattern-types.ts +3 -4
- package/src/vite/discovery/discover-routers.ts +3 -0
- package/src/vite/discovery/shell-prerender-phase.ts +0 -2
- package/src/vite/discovery/virtual-module-codegen.ts +16 -6
- package/src/vite/plugins/transition-when-hoist.ts +1064 -0
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +75 -5
- package/dist/types/rsc/transition-gate.d.ts +0 -23
- package/src/rsc/transition-gate.ts +0 -86
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* (initial page load) requests.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { requestHeaders } from "../server/request-headers.js";
|
|
8
9
|
import {
|
|
9
10
|
getRequestContext,
|
|
10
11
|
setRequestContextParams,
|
|
@@ -26,6 +27,7 @@ import { getSSRSetup, createSsrHtmlStage, isRscRequest } from "./ssr-setup.js";
|
|
|
26
27
|
import type { RscPayload } from "./types.js";
|
|
27
28
|
import type { SSRModule } from "./types.js";
|
|
28
29
|
import type { RequestContext } from "../server/request-context.js";
|
|
30
|
+
import type { PprReplayBypassReason } from "./shell-capture-constants.js";
|
|
29
31
|
import {
|
|
30
32
|
createResponseWithMergedHeaders,
|
|
31
33
|
createSimpleRedirectResponse,
|
|
@@ -46,7 +48,8 @@ import {
|
|
|
46
48
|
type RoutinePlan,
|
|
47
49
|
} from "./routine-plan.js";
|
|
48
50
|
import type { HandlerContext } from "./handler-context.js";
|
|
49
|
-
import {
|
|
51
|
+
import { attachTransitionWhen } from "./attach-transition-when.js";
|
|
52
|
+
import { publicRouteName } from "../route-name.js";
|
|
50
53
|
import { buildFullPayload } from "./full-payload.js";
|
|
51
54
|
import {
|
|
52
55
|
capShellWindow,
|
|
@@ -117,6 +120,7 @@ import type {
|
|
|
117
120
|
SegmentCacheStore,
|
|
118
121
|
ShellCacheEntry,
|
|
119
122
|
ShellDocumentRead,
|
|
123
|
+
ShellEntryHead,
|
|
120
124
|
ShellReadStats,
|
|
121
125
|
ShellSnapshotRecord,
|
|
122
126
|
} from "../cache/types.js";
|
|
@@ -125,25 +129,6 @@ import {
|
|
|
125
129
|
SEGMENT_FRAGMENT_RECOVERY_HEADER,
|
|
126
130
|
} from "../segment-fragments.js";
|
|
127
131
|
|
|
128
|
-
type PprReplayBypassReason =
|
|
129
|
-
| "method"
|
|
130
|
-
| "dynamic"
|
|
131
|
-
| "nonce"
|
|
132
|
-
| "store-unavailable"
|
|
133
|
-
| "passive-read-unsupported"
|
|
134
|
-
| "no-navigation-context"
|
|
135
|
-
| "prerender-store"
|
|
136
|
-
| "intercept"
|
|
137
|
-
| "cache-disabled"
|
|
138
|
-
| "read-error"
|
|
139
|
-
| "no-entry"
|
|
140
|
-
| "invalid-version"
|
|
141
|
-
| "corrupt-entry"
|
|
142
|
-
| "no-segment-snapshot"
|
|
143
|
-
| "snapshot-miss"
|
|
144
|
-
| "explicit-cache-hit"
|
|
145
|
-
| "stale-build-entry";
|
|
146
|
-
|
|
147
132
|
type PprReplayStatus =
|
|
148
133
|
| { outcome: "HIT"; freshness: "fresh" | "stale" }
|
|
149
134
|
| { outcome: "BYPASS"; reason: PprReplayBypassReason };
|
|
@@ -187,7 +172,6 @@ function createShellCaptureDescriptor(
|
|
|
187
172
|
tags: pprConfig.tags,
|
|
188
173
|
captureTimeout: pprConfig.captureTimeout,
|
|
189
174
|
store,
|
|
190
|
-
debug: INTERNAL_RANGO_DEBUG,
|
|
191
175
|
maxSnapshotBytes: pprConfig.maxSnapshotBytes,
|
|
192
176
|
debugSink: resolveShellCaptureDebugSink(ctx.router.debugShellCapture),
|
|
193
177
|
navigationOnly,
|
|
@@ -200,8 +184,7 @@ function shouldHealReplayMiss(
|
|
|
200
184
|
return (
|
|
201
185
|
reason === undefined ||
|
|
202
186
|
reason === "invalid-version" ||
|
|
203
|
-
reason === "corrupt-entry"
|
|
204
|
-
reason === "stale-build-entry"
|
|
187
|
+
reason === "corrupt-entry"
|
|
205
188
|
);
|
|
206
189
|
}
|
|
207
190
|
|
|
@@ -256,16 +239,11 @@ function replayableShellSnapshot(
|
|
|
256
239
|
if (!entry.navigationOnly && !hasIntactShellPayload(entry)) {
|
|
257
240
|
return { reason: "corrupt-entry" };
|
|
258
241
|
}
|
|
259
|
-
// Eligibility requires the CANONICAL doc segment record (`docKey`)
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
// captured before the field existed read as no-segment-snapshot and heal on
|
|
265
|
-
// recapture (pre-release, same treatment as the buildVersion field).
|
|
266
|
-
const snapshot = entry.snapshot;
|
|
267
|
-
return snapshot && hasDocRecord(snapshot, entry.docKey)
|
|
268
|
-
? { snapshot }
|
|
242
|
+
// Eligibility requires the CANONICAL doc segment record (`docKey`): replay
|
|
243
|
+
// resolves that record and nothing else. An entry without it (a
|
|
244
|
+
// prerender-served capture, a tombstone) reads as no-segment-snapshot.
|
|
245
|
+
return hasDocRecord(entry.snapshot, entry.docKey)
|
|
246
|
+
? { snapshot: entry.snapshot }
|
|
269
247
|
: { reason: "no-segment-snapshot" };
|
|
270
248
|
}
|
|
271
249
|
|
|
@@ -319,10 +297,11 @@ type PreparedRender =
|
|
|
319
297
|
| {
|
|
320
298
|
kind: "payload";
|
|
321
299
|
payload: RscPayload;
|
|
322
|
-
|
|
300
|
+
/** The response depends on the source page: an intercept targets the route. */
|
|
301
|
+
sourceScoped: boolean;
|
|
323
302
|
pprReplayStatus?: PprReplayStatus;
|
|
324
|
-
|
|
325
|
-
|
|
303
|
+
/** The key a navigation-only heal capture stores under, when one is needed. */
|
|
304
|
+
healKey?: string;
|
|
326
305
|
};
|
|
327
306
|
|
|
328
307
|
async function handleRscRenderingInner<TEnv>(
|
|
@@ -434,16 +413,15 @@ function* requestRenderPlan<TEnv>(
|
|
|
434
413
|
response.headers.set(SHELL_STATUS_HEADER, "MISS");
|
|
435
414
|
}
|
|
436
415
|
|
|
437
|
-
const
|
|
416
|
+
const healKey = prepared.healKey;
|
|
438
417
|
if (
|
|
439
418
|
isPartial &&
|
|
440
|
-
|
|
441
|
-
captureKey !== undefined &&
|
|
419
|
+
healKey !== undefined &&
|
|
442
420
|
!reqCtx._dynamic &&
|
|
443
421
|
response.status === 200
|
|
444
422
|
) {
|
|
445
423
|
yield* handoff("navigation-shell-capture", () =>
|
|
446
|
-
scheduleNavigationShellCapture(input,
|
|
424
|
+
scheduleNavigationShellCapture(input, healKey),
|
|
447
425
|
);
|
|
448
426
|
}
|
|
449
427
|
|
|
@@ -506,24 +484,21 @@ function* shellServePlan<TEnv>(
|
|
|
506
484
|
|
|
507
485
|
// A per-request CSP nonce pins the route to axis 1: a shared shell would
|
|
508
486
|
// freeze the capture request's nonce and CSP would reject it for every
|
|
509
|
-
// other visitor.
|
|
510
|
-
//
|
|
511
|
-
// the provider-only check missed the latter (issue #656).
|
|
512
|
-
const activeNonce = nonce ?? contextGet(reqCtx._variables, nonceToken);
|
|
487
|
+
// other visitor. Every step below, a HIT included, therefore runs without
|
|
488
|
+
// one.
|
|
513
489
|
const store = reqCtx._cacheStore;
|
|
514
490
|
const baseKey = buildShellKey(url, reqCtx._searchParamsFilter);
|
|
515
|
-
|
|
516
|
-
if (activeNonce !== undefined) {
|
|
491
|
+
if (activeRequestNonce(nonce, reqCtx) !== undefined) {
|
|
517
492
|
// Declared intent that cannot be honored deserves a diagnostic (unlike an
|
|
518
493
|
// undeclared route, which is silent): a ppr route gated off by an active
|
|
519
494
|
// per-request nonce warns once per key. Axis 1 after the warning.
|
|
520
|
-
warnPprNonceActiveOnce(
|
|
495
|
+
warnPprNonceActiveOnce(baseKey);
|
|
521
496
|
return { kind: "pass" };
|
|
522
497
|
}
|
|
523
498
|
if (!hasShellFamily(store)) {
|
|
524
499
|
// Declared intent that cannot be honored deserves a diagnostic (unlike an
|
|
525
500
|
// undeclared route, which is silent). Axis 1 after the warning.
|
|
526
|
-
warnShellStoreMissingOnce(
|
|
501
|
+
warnShellStoreMissingOnce(baseKey);
|
|
527
502
|
return { kind: "pass" };
|
|
528
503
|
}
|
|
529
504
|
// The route's own cache() opt-out — cache(false), or a condition() that
|
|
@@ -557,26 +532,18 @@ function* shellServePlan<TEnv>(
|
|
|
557
532
|
// or the store's keyGenerator) partitions its shell the same way: each
|
|
558
533
|
// partition captures and serves its own shell. A failed key resolution
|
|
559
534
|
// serves no shell, never another partition's.
|
|
560
|
-
const
|
|
535
|
+
const requestKey = resolveRequestShellKey(
|
|
561
536
|
baseKey,
|
|
562
|
-
route
|
|
537
|
+
route,
|
|
563
538
|
store,
|
|
564
|
-
reqCtx
|
|
565
|
-
|
|
539
|
+
reqCtx,
|
|
540
|
+
"ShellServe",
|
|
566
541
|
);
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
reportCacheError(error, "cache-read", "[ShellServe] shell key");
|
|
573
|
-
return undefined;
|
|
574
|
-
},
|
|
575
|
-
),
|
|
576
|
-
);
|
|
577
|
-
if (resolved === undefined) return { kind: "pass" };
|
|
578
|
-
key = resolved;
|
|
579
|
-
}
|
|
542
|
+
const key =
|
|
543
|
+
typeof requestKey === "string"
|
|
544
|
+
? requestKey
|
|
545
|
+
: yield* step("shell-partition", () => requestKey);
|
|
546
|
+
if (key === undefined) return { kind: "pass" };
|
|
580
547
|
reqCtx._shellKey = key;
|
|
581
548
|
// The buffered capture and tail timings are keyed by the partitioned key.
|
|
582
549
|
mirrorPprServerTimings(key, reqCtx);
|
|
@@ -612,6 +579,14 @@ function* shellServePlan<TEnv>(
|
|
|
612
579
|
pprConfig,
|
|
613
580
|
store,
|
|
614
581
|
);
|
|
582
|
+
// MISS (no entry, invalid reactVersion, a tombstone, or store read
|
|
583
|
+
// failure): axis 1 + a background capture scheduled once the response is
|
|
584
|
+
// known servable.
|
|
585
|
+
const miss: ShellServeOutcome = {
|
|
586
|
+
kind: "miss",
|
|
587
|
+
descriptor: { ...descriptor, storedSeqAtRead },
|
|
588
|
+
ssrModule,
|
|
589
|
+
};
|
|
615
590
|
|
|
616
591
|
// A HIT replays the handler layer from the entry's doc record (`docKey`),
|
|
617
592
|
// or, for a Prerender route, from the prerender store. An entry that has
|
|
@@ -622,11 +597,7 @@ function* shellServePlan<TEnv>(
|
|
|
622
597
|
(cached.entry.docKey !== undefined || route?.matched?.pr === true) &&
|
|
623
598
|
isValidShellHit(cached.entry, ctx.version)
|
|
624
599
|
) {
|
|
625
|
-
const document = openShellDocumentMetered(
|
|
626
|
-
reqCtx,
|
|
627
|
-
cached.entry,
|
|
628
|
-
cached.read,
|
|
629
|
-
);
|
|
600
|
+
const document = openShellDocumentMetered(reqCtx, cached);
|
|
630
601
|
if (!document) {
|
|
631
602
|
// Corrupt stored payload (undecodable prelude / unparseable
|
|
632
603
|
// postponed): a store-layer fault worth a diagnostic, unlike the
|
|
@@ -671,7 +642,6 @@ function* shellServePlan<TEnv>(
|
|
|
671
642
|
ssrModule,
|
|
672
643
|
document,
|
|
673
644
|
descriptor,
|
|
674
|
-
activeNonce,
|
|
675
645
|
),
|
|
676
646
|
);
|
|
677
647
|
return { kind: "serve", response };
|
|
@@ -686,11 +656,7 @@ function* shellServePlan<TEnv>(
|
|
|
686
656
|
cached?.entry.navigationOnly &&
|
|
687
657
|
isValidShellHit(cached.entry, ctx.version)
|
|
688
658
|
) {
|
|
689
|
-
return
|
|
690
|
-
kind: "miss",
|
|
691
|
-
descriptor: { ...descriptor, storedSeqAtRead },
|
|
692
|
-
ssrModule,
|
|
693
|
-
};
|
|
659
|
+
return miss;
|
|
694
660
|
}
|
|
695
661
|
|
|
696
662
|
// Build-time shell read-through (producer B, #699): on a runtime
|
|
@@ -736,7 +702,7 @@ function* shellServePlan<TEnv>(
|
|
|
736
702
|
// The document serve decodes the baked prelude once here; an undecodable
|
|
737
703
|
// one is a MISS like a corrupt runtime entry.
|
|
738
704
|
const buildDocument = buildHit
|
|
739
|
-
? openShellDocumentMetered(reqCtx, buildHit.entry)
|
|
705
|
+
? openShellDocumentMetered(reqCtx, { entry: buildHit.entry })
|
|
740
706
|
: null;
|
|
741
707
|
if (buildHit && buildDocument) {
|
|
742
708
|
// Past ppr.ttl: still serve the baked entry, recapture upgrades it.
|
|
@@ -764,19 +730,12 @@ function* shellServePlan<TEnv>(
|
|
|
764
730
|
ssrModule,
|
|
765
731
|
buildDocument,
|
|
766
732
|
descriptor,
|
|
767
|
-
activeNonce,
|
|
768
733
|
),
|
|
769
734
|
);
|
|
770
735
|
return { kind: "serve", response };
|
|
771
736
|
}
|
|
772
737
|
|
|
773
|
-
|
|
774
|
-
// + a background capture scheduled once the response is known servable.
|
|
775
|
-
return {
|
|
776
|
-
kind: "miss",
|
|
777
|
-
descriptor: { ...descriptor, storedSeqAtRead },
|
|
778
|
-
ssrModule,
|
|
779
|
-
};
|
|
738
|
+
return miss;
|
|
780
739
|
}
|
|
781
740
|
|
|
782
741
|
/**
|
|
@@ -796,10 +755,7 @@ function* preparePayloadPlan<TEnv>(
|
|
|
796
755
|
);
|
|
797
756
|
const result = replay.result;
|
|
798
757
|
const pprReplayStatus = replay.status;
|
|
799
|
-
const
|
|
800
|
-
"captureNeeded" in replay && replay.captureNeeded === true;
|
|
801
|
-
const partialCaptureKey =
|
|
802
|
-
"captureKey" in replay ? replay.captureKey : undefined;
|
|
758
|
+
const healKey = "healKey" in replay ? replay.healKey : undefined;
|
|
803
759
|
|
|
804
760
|
if (!result) {
|
|
805
761
|
// Fall back to full render
|
|
@@ -828,10 +784,9 @@ function* preparePayloadPlan<TEnv>(
|
|
|
828
784
|
return {
|
|
829
785
|
kind: "payload",
|
|
830
786
|
payload: buildFullPayload(match, ctx, url, reqCtx, handleStore),
|
|
831
|
-
|
|
787
|
+
sourceScoped: false,
|
|
832
788
|
pprReplayStatus,
|
|
833
|
-
|
|
834
|
-
partialCaptureKey,
|
|
789
|
+
healKey,
|
|
835
790
|
};
|
|
836
791
|
}
|
|
837
792
|
|
|
@@ -846,15 +801,12 @@ function* preparePayloadPlan<TEnv>(
|
|
|
846
801
|
// intercepted server-side (X-RSC-Reload) and never delivers a
|
|
847
802
|
// different-router payload to the client.
|
|
848
803
|
routerId: ctx.router.id,
|
|
849
|
-
segments:
|
|
850
|
-
result.segments,
|
|
851
|
-
reqCtx,
|
|
852
|
-
ctx.router.onError,
|
|
853
|
-
),
|
|
804
|
+
segments: attachTransitionWhen(result.segments, reqCtx),
|
|
854
805
|
matched: result.matched,
|
|
855
806
|
diff: result.diff,
|
|
856
807
|
resolvedIds: result.resolvedIds,
|
|
857
808
|
params: result.params,
|
|
809
|
+
routeName: publicRouteName(result.routeName),
|
|
858
810
|
isPartial: true,
|
|
859
811
|
slots: result.slots,
|
|
860
812
|
interceptTargets: result.interceptTargets,
|
|
@@ -867,10 +819,9 @@ function* preparePayloadPlan<TEnv>(
|
|
|
867
819
|
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
868
820
|
},
|
|
869
821
|
},
|
|
870
|
-
|
|
822
|
+
sourceScoped: result.interceptTargeted === true,
|
|
871
823
|
pprReplayStatus,
|
|
872
|
-
|
|
873
|
-
partialCaptureKey,
|
|
824
|
+
healKey,
|
|
874
825
|
};
|
|
875
826
|
}
|
|
876
827
|
|
|
@@ -889,14 +840,10 @@ function* preparePayloadPlan<TEnv>(
|
|
|
889
840
|
};
|
|
890
841
|
}
|
|
891
842
|
|
|
892
|
-
// Caching is now handled in router.match() via cache provider in request context
|
|
893
|
-
// match.segments already contains cached or fresh segments as appropriate
|
|
894
|
-
|
|
895
843
|
return {
|
|
896
844
|
kind: "payload",
|
|
897
845
|
payload: buildFullPayload(match, ctx, url, reqCtx, handleStore),
|
|
898
|
-
|
|
899
|
-
partialCaptureNeeded: false,
|
|
846
|
+
sourceScoped: false,
|
|
900
847
|
};
|
|
901
848
|
}
|
|
902
849
|
|
|
@@ -926,12 +873,18 @@ async function matchPartialAndRecordParams<TEnv>(
|
|
|
926
873
|
return replay;
|
|
927
874
|
}
|
|
928
875
|
|
|
929
|
-
/**
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
876
|
+
/**
|
|
877
|
+
* A document serve's store read: a getShell entry, or a prelude-first read
|
|
878
|
+
* (readShellDocument) whose entry is the head and whose snapshot arrives on
|
|
879
|
+
* its own promise.
|
|
880
|
+
*/
|
|
881
|
+
type ShellStoreRead =
|
|
882
|
+
| { entry: ShellCacheEntry; shouldRevalidate?: boolean; read?: undefined }
|
|
883
|
+
| {
|
|
884
|
+
entry: ShellEntryHead;
|
|
885
|
+
shouldRevalidate?: boolean;
|
|
886
|
+
read: ShellDocumentRead;
|
|
887
|
+
};
|
|
935
888
|
|
|
936
889
|
/**
|
|
937
890
|
* Shell store read that degrades to null on failure (axis 1 MISS, never a 500)
|
|
@@ -1030,13 +983,17 @@ function recordShellReadStats(
|
|
|
1030
983
|
*/
|
|
1031
984
|
function openShellDocumentMetered(
|
|
1032
985
|
reqCtx: RequestContext<any>,
|
|
1033
|
-
|
|
1034
|
-
read?: ShellDocumentRead,
|
|
986
|
+
stored: ShellStoreRead,
|
|
1035
987
|
): ShellDocument | null {
|
|
988
|
+
const read = stored.read;
|
|
989
|
+
const open = (): ShellDocument | null =>
|
|
990
|
+
stored.read
|
|
991
|
+
? openShellDocument(stored.entry, stored.read)
|
|
992
|
+
: openShellDocument(stored.entry);
|
|
1036
993
|
const store = reqCtx._metricsStore;
|
|
1037
|
-
if (!store) return
|
|
994
|
+
if (!store) return open();
|
|
1038
995
|
const start = performance.now();
|
|
1039
|
-
const document =
|
|
996
|
+
const document = open();
|
|
1040
997
|
appendMetric(
|
|
1041
998
|
store,
|
|
1042
999
|
"ppr:shell-open",
|
|
@@ -1048,22 +1005,82 @@ function openShellDocumentMetered(
|
|
|
1048
1005
|
return document;
|
|
1049
1006
|
}
|
|
1050
1007
|
|
|
1008
|
+
/**
|
|
1009
|
+
* The request's per-request CSP nonce. BOTH sources count: the
|
|
1010
|
+
* createRouter({ nonce }) provider (`nonce`) and a middleware
|
|
1011
|
+
* ctx.set(nonce, …) token write; the provider-only check missed the latter
|
|
1012
|
+
* (issue #656). Read at the commit point, after the whole middleware chain.
|
|
1013
|
+
*/
|
|
1014
|
+
function activeRequestNonce(
|
|
1015
|
+
nonce: string | undefined,
|
|
1016
|
+
reqCtx: RequestContext<any>,
|
|
1017
|
+
): string | undefined {
|
|
1018
|
+
return nonce ?? contextGet(reqCtx._variables, nonceToken);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1051
1021
|
/**
|
|
1052
1022
|
* The shell key a request reads and captures: `baseKey`, partitioned by the
|
|
1053
1023
|
* route's request partition (cache-scope.ts resolveShellPartition; a
|
|
1054
|
-
* partition equal to the default key leaves it unchanged).
|
|
1055
|
-
* synchronously and with no work, when nothing partitions the route; a
|
|
1056
|
-
*
|
|
1024
|
+
* partition equal to the default key leaves it unchanged). `baseKey` itself,
|
|
1025
|
+
* synchronously and with no work, when nothing partitions the route; else a
|
|
1026
|
+
* promise that resolves undefined when the key failed to resolve (reported
|
|
1027
|
+
* as `[source] shell key`): such a request reads and captures no shell,
|
|
1028
|
+
* never another partition's.
|
|
1057
1029
|
*/
|
|
1058
|
-
function
|
|
1030
|
+
function resolveRequestShellKey(
|
|
1059
1031
|
baseKey: string,
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1032
|
+
route: RouteSnapshot<any> | null,
|
|
1033
|
+
store: SegmentCacheStore,
|
|
1034
|
+
reqCtx: RequestContext<any>,
|
|
1035
|
+
source: string,
|
|
1036
|
+
): string | Promise<string | undefined> {
|
|
1037
|
+
const partition = resolveShellPartition(
|
|
1038
|
+
route?.cacheScope,
|
|
1039
|
+
store,
|
|
1040
|
+
reqCtx.url.pathname,
|
|
1041
|
+
route?.params,
|
|
1042
|
+
);
|
|
1043
|
+
if (!partition) return baseKey;
|
|
1044
|
+
return partition.then(
|
|
1045
|
+
(resolved) =>
|
|
1046
|
+
resolved === null ? baseKey : partitionShellKey(baseKey, resolved),
|
|
1047
|
+
(error: unknown) => {
|
|
1048
|
+
reportCacheError(error, "cache-read", `[${source}] shell key`);
|
|
1049
|
+
return undefined;
|
|
1050
|
+
},
|
|
1064
1051
|
);
|
|
1065
1052
|
}
|
|
1066
1053
|
|
|
1054
|
+
/**
|
|
1055
|
+
* Observe the explicit-scope composition in withCacheLookup: the route's own
|
|
1056
|
+
* cache() scope served the request (`explicit-cache-hit`) or refused it at
|
|
1057
|
+
* lookup time (`cache-disabled`). The two are exclusive per lookup, and
|
|
1058
|
+
* neither fires when the seeded doc record served.
|
|
1059
|
+
*/
|
|
1060
|
+
function observeExplicitScope(): {
|
|
1061
|
+
onExplicitHit: () => void;
|
|
1062
|
+
onExplicitBypass: () => void;
|
|
1063
|
+
bypassed: () => boolean;
|
|
1064
|
+
status: (otherwise: PprReplayStatus) => PprReplayStatus;
|
|
1065
|
+
} {
|
|
1066
|
+
let observed: "hit" | "bypass" | undefined;
|
|
1067
|
+
return {
|
|
1068
|
+
onExplicitHit: () => {
|
|
1069
|
+
observed = "hit";
|
|
1070
|
+
},
|
|
1071
|
+
onExplicitBypass: () => {
|
|
1072
|
+
observed = "bypass";
|
|
1073
|
+
},
|
|
1074
|
+
bypassed: () => observed === "bypass",
|
|
1075
|
+
status: (otherwise) =>
|
|
1076
|
+
observed === "hit"
|
|
1077
|
+
? { outcome: "BYPASS", reason: "explicit-cache-hit" }
|
|
1078
|
+
: observed === "bypass"
|
|
1079
|
+
? { outcome: "BYPASS", reason: "cache-disabled" }
|
|
1080
|
+
: otherwise,
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1067
1084
|
/**
|
|
1068
1085
|
* Server-Timing mirror (issue #651): a capture or a HIT tail completes AFTER
|
|
1069
1086
|
* its triggering response committed, so its numbers can only ride a LATER
|
|
@@ -1116,7 +1133,7 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
1116
1133
|
): Promise<Response> {
|
|
1117
1134
|
const { ctx, request, env, url, isPartial, nonce, reqCtx, renderSpan } =
|
|
1118
1135
|
input;
|
|
1119
|
-
const { payload, pprReplayStatus,
|
|
1136
|
+
const { payload, pprReplayStatus, sourceScoped } = prepared;
|
|
1120
1137
|
const metricsStore = reqCtx._metricsStore;
|
|
1121
1138
|
|
|
1122
1139
|
const rscHeaders: Record<string, string> = {
|
|
@@ -1133,18 +1150,20 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
1133
1150
|
serializePprReplayStatus(pprReplayStatus);
|
|
1134
1151
|
}
|
|
1135
1152
|
// Tell the client's prefetch cache to scope this response to its source
|
|
1136
|
-
// URL (instead of the default source-agnostic wildcard).
|
|
1137
|
-
//
|
|
1138
|
-
//
|
|
1139
|
-
|
|
1153
|
+
// URL (instead of the default source-agnostic wildcard). A route an
|
|
1154
|
+
// intercept targets renders the modal or the full page depending on the
|
|
1155
|
+
// source, so neither response may be reused from another source: a full
|
|
1156
|
+
// page prefetched where the intercept does not apply would otherwise
|
|
1157
|
+
// serve a later click where it does (#1007).
|
|
1158
|
+
if (sourceScoped) {
|
|
1140
1159
|
rscHeaders["x-rsc-prefetch-scope"] = "source";
|
|
1141
1160
|
}
|
|
1142
1161
|
// Enable browser HTTP caching for prefetch responses only.
|
|
1143
1162
|
// Requires X-Rango-Prefetch header (sent by Link prefetch fetch),
|
|
1144
|
-
//
|
|
1145
|
-
//
|
|
1146
|
-
const isPrefetch = request.
|
|
1147
|
-
if (isPrefetch && isPartial && !
|
|
1163
|
+
// a source-agnostic response (see sourceScoped above), and a configured
|
|
1164
|
+
// cache-control value (false disables caching).
|
|
1165
|
+
const isPrefetch = requestHeaders(request).has("X-Rango-Prefetch");
|
|
1166
|
+
if (isPrefetch && isPartial && !sourceScoped) {
|
|
1148
1167
|
const cc = ctx.router.prefetchCacheControl;
|
|
1149
1168
|
if (cc) {
|
|
1150
1169
|
rscHeaders["cache-control"] = cc;
|
|
@@ -1229,10 +1248,11 @@ function scheduleNavigationShellCapture<TEnv>(
|
|
|
1229
1248
|
/**
|
|
1230
1249
|
* Reuse a PPR capture's canonical segment record for a partial navigation.
|
|
1231
1250
|
* The ordinary matchPartial pipeline remains authoritative: it projects the
|
|
1232
|
-
* cached target tree against the client's segment ids
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1235
|
-
*
|
|
1251
|
+
* cached target tree against the client's segment ids and evaluates
|
|
1252
|
+
* revalidation. Only the segment family is seeded into the store overlay, so
|
|
1253
|
+
* item and response reads stay live; a record that served also arms the
|
|
1254
|
+
* bake-lane loader pins (onHit), which serve as on a document HIT, while live
|
|
1255
|
+
* loaders run fresh.
|
|
1236
1256
|
*/
|
|
1237
1257
|
async function matchPartialWithPprReplay<TEnv>(
|
|
1238
1258
|
ctx: HandlerContext<TEnv>,
|
|
@@ -1289,7 +1309,7 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1289
1309
|
return { result, status: finalStatus };
|
|
1290
1310
|
};
|
|
1291
1311
|
const pprConfig = resolvePprConfig(reqCtx._classifiedRoute?.manifestEntry);
|
|
1292
|
-
const activeNonce = nonce
|
|
1312
|
+
const activeNonce = activeRequestNonce(nonce, reqCtx);
|
|
1293
1313
|
const store = reqCtx._cacheStore;
|
|
1294
1314
|
|
|
1295
1315
|
if (!pprConfig) return runMatch();
|
|
@@ -1325,8 +1345,8 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1325
1345
|
// one-shot corruption recovery request still replay the same segment record,
|
|
1326
1346
|
// but through the server decode path.
|
|
1327
1347
|
armFragments =
|
|
1328
|
-
request.
|
|
1329
|
-
request.
|
|
1348
|
+
requestHeaders(request).get(SEGMENT_FRAGMENT_RECOVERY_HEADER) !== "1" &&
|
|
1349
|
+
requestHeaders(request).get(SEGMENT_FRAGMENT_CAPABILITY_HEADER) === "1";
|
|
1330
1350
|
// The prerender probe and the cache-opt-out gate below both need the
|
|
1331
1351
|
// classified snapshot; resolve (and persist) it once.
|
|
1332
1352
|
const routeSnapshot = classifiedRouteSnapshot(reqCtx);
|
|
@@ -1377,22 +1397,15 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1377
1397
|
|
|
1378
1398
|
// The visitor's own partition (resolveShellPartition): replay never
|
|
1379
1399
|
// crosses into another partition's shell.
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
baseKey,
|
|
1384
|
-
routeCacheScope,
|
|
1400
|
+
const key = await resolveRequestShellKey(
|
|
1401
|
+
buildShellKey(url, reqCtx._searchParamsFilter),
|
|
1402
|
+
routeSnapshot,
|
|
1385
1403
|
store,
|
|
1386
|
-
reqCtx
|
|
1387
|
-
|
|
1404
|
+
reqCtx,
|
|
1405
|
+
"NavigationPPR",
|
|
1388
1406
|
);
|
|
1389
|
-
if (
|
|
1390
|
-
|
|
1391
|
-
key = await partitionedKey;
|
|
1392
|
-
} catch (error) {
|
|
1393
|
-
reportCacheError(error, "cache-read", "[NavigationPPR] shell key");
|
|
1394
|
-
return runMatch({ outcome: "BYPASS", reason: "read-error" });
|
|
1395
|
-
}
|
|
1407
|
+
if (key === undefined) {
|
|
1408
|
+
return runMatch({ outcome: "BYPASS", reason: "read-error" });
|
|
1396
1409
|
}
|
|
1397
1410
|
const navigationKey = navigationShellKey(key);
|
|
1398
1411
|
let cached: Awaited<ReturnType<typeof store.getShell>> = null;
|
|
@@ -1443,35 +1456,6 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1443
1456
|
}
|
|
1444
1457
|
}
|
|
1445
1458
|
|
|
1446
|
-
if (!snapshot) {
|
|
1447
|
-
// Production build manifests are local module data. In dev, resolving a
|
|
1448
|
-
// missing build shell would foreground-fetch /__rsc_shell and block an
|
|
1449
|
-
// otherwise ordinary navigation on capture, so replay remains runtime-only.
|
|
1450
|
-
const buildHit =
|
|
1451
|
-
key !== baseKey
|
|
1452
|
-
? null
|
|
1453
|
-
: await lookupBuildShell(
|
|
1454
|
-
url,
|
|
1455
|
-
ctx.version,
|
|
1456
|
-
store,
|
|
1457
|
-
undefined,
|
|
1458
|
-
reqCtx._searchParamsFilter,
|
|
1459
|
-
);
|
|
1460
|
-
if (buildHit?.stale) {
|
|
1461
|
-
bypassReason ??= "stale-build-entry";
|
|
1462
|
-
} else if (buildHit) {
|
|
1463
|
-
const buildDecision = replayableShellSnapshot(
|
|
1464
|
-
buildHit.entry,
|
|
1465
|
-
ctx.version,
|
|
1466
|
-
);
|
|
1467
|
-
if ("snapshot" in buildDecision) {
|
|
1468
|
-
snapshot = buildDecision.snapshot;
|
|
1469
|
-
} else {
|
|
1470
|
-
bypassReason ??= buildDecision.reason;
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
1459
|
if (!snapshot) {
|
|
1476
1460
|
// Report-only marker for routes with an ENABLED route-derived scope: the
|
|
1477
1461
|
// lookup's own outcome must stay reportable even without a replayable
|
|
@@ -1486,25 +1470,20 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1486
1470
|
// implicit scope over the REAL doc: partition in
|
|
1487
1471
|
// resolveShellImplicitCacheScope).
|
|
1488
1472
|
if (routeCacheScope?.enabled) {
|
|
1489
|
-
|
|
1490
|
-
let explicitCacheBypass = false;
|
|
1473
|
+
const explicit = observeExplicitScope();
|
|
1491
1474
|
const previousImplicitCache = reqCtx._shellImplicitCache;
|
|
1492
1475
|
reqCtx._shellImplicitCache = {
|
|
1493
|
-
onExplicitHit:
|
|
1494
|
-
|
|
1495
|
-
},
|
|
1496
|
-
onExplicitBypass: () => {
|
|
1497
|
-
explicitCacheBypass = true;
|
|
1498
|
-
},
|
|
1476
|
+
onExplicitHit: explicit.onExplicitHit,
|
|
1477
|
+
onExplicitBypass: explicit.onExplicitBypass,
|
|
1499
1478
|
};
|
|
1500
1479
|
try {
|
|
1501
1480
|
const result = await matchPartialForReplay();
|
|
1502
|
-
const
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1481
|
+
const status = finalizeReplayStatus(
|
|
1482
|
+
explicit.status({
|
|
1483
|
+
outcome: "BYPASS",
|
|
1484
|
+
reason: bypassReason ?? "no-entry",
|
|
1485
|
+
}),
|
|
1486
|
+
);
|
|
1508
1487
|
return {
|
|
1509
1488
|
result,
|
|
1510
1489
|
status,
|
|
@@ -1518,13 +1497,14 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1518
1497
|
// capture derives from THIS request's context, so its doc record
|
|
1519
1498
|
// records. Excluding it left replay dead until the document
|
|
1520
1499
|
// recaptured. The always-false route stays protected: its every
|
|
1521
|
-
// lookup refuses, so
|
|
1522
|
-
|
|
1523
|
-
!
|
|
1500
|
+
// lookup refuses, so its bypass suppresses the heal.
|
|
1501
|
+
healKey:
|
|
1502
|
+
!explicit.bypassed() &&
|
|
1524
1503
|
reqCtx._pprReplayPostMatchReason === undefined &&
|
|
1525
1504
|
(shouldHealReplayMiss(bypassReason) ||
|
|
1526
|
-
bypassReason === "no-segment-snapshot")
|
|
1527
|
-
|
|
1505
|
+
bypassReason === "no-segment-snapshot")
|
|
1506
|
+
? navigationKey
|
|
1507
|
+
: undefined,
|
|
1528
1508
|
};
|
|
1529
1509
|
} finally {
|
|
1530
1510
|
reqCtx._shellImplicitCache = previousImplicitCache;
|
|
@@ -1536,10 +1516,11 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1536
1516
|
});
|
|
1537
1517
|
return {
|
|
1538
1518
|
...match,
|
|
1539
|
-
|
|
1519
|
+
healKey:
|
|
1540
1520
|
reqCtx._pprReplayPostMatchReason === undefined &&
|
|
1541
|
-
shouldHealReplayMiss(bypassReason)
|
|
1542
|
-
|
|
1521
|
+
shouldHealReplayMiss(bypassReason)
|
|
1522
|
+
? navigationKey
|
|
1523
|
+
: undefined,
|
|
1543
1524
|
};
|
|
1544
1525
|
}
|
|
1545
1526
|
|
|
@@ -1548,14 +1529,11 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1548
1529
|
let loaderSeed: ReturnType<typeof buildShellLoaderSeed> | undefined;
|
|
1549
1530
|
let segmentReplayHit = false;
|
|
1550
1531
|
let segmentReplayCorrupt = false;
|
|
1551
|
-
|
|
1552
|
-
let explicitCacheBypass = false;
|
|
1532
|
+
const explicit = observeExplicitScope();
|
|
1553
1533
|
reqCtx._shellImplicitCache = {
|
|
1554
1534
|
ttl: pprConfig.ttl,
|
|
1555
1535
|
swr: pprConfig.swr,
|
|
1556
|
-
store: new SeededShellStore(store, snapshot,
|
|
1557
|
-
segmentsOnly: true,
|
|
1558
|
-
}),
|
|
1536
|
+
store: new SeededShellStore(store, snapshot),
|
|
1559
1537
|
keyPrefix: "doc",
|
|
1560
1538
|
// Arms the bake-lane loader pins a document HIT serves (loader-cache.ts
|
|
1561
1539
|
// resolveLoaderData), decoded only now: the lookup awaits this before
|
|
@@ -1576,27 +1554,18 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1576
1554
|
// Arms the explicit-scope composition in withCacheLookup: a route-derived
|
|
1577
1555
|
// cache() scope stays authoritative, the seeded doc record supplies the
|
|
1578
1556
|
// match only on its miss. An explicit-tier hit must NOT report a replay
|
|
1579
|
-
// HIT — it served under the consumer's own semantics.
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
// deliberately does not pre-decide predicates, so the truthful
|
|
1585
|
-
// cache-disabled report comes from the lookup that actually refused.
|
|
1586
|
-
onExplicitBypass: () => {
|
|
1587
|
-
explicitCacheBypass = true;
|
|
1588
|
-
},
|
|
1557
|
+
// HIT — it served under the consumer's own semantics. A lookup-time
|
|
1558
|
+
// condition() refusal (or a scope with no store) reports cache-disabled:
|
|
1559
|
+
// the gate deliberately does not pre-decide predicates.
|
|
1560
|
+
onExplicitHit: explicit.onExplicitHit,
|
|
1561
|
+
onExplicitBypass: explicit.onExplicitBypass,
|
|
1589
1562
|
};
|
|
1590
1563
|
|
|
1591
1564
|
try {
|
|
1592
1565
|
const result = await matchPartialForReplay();
|
|
1593
1566
|
const provisionalStatus: PprReplayStatus = segmentReplayHit
|
|
1594
1567
|
? { outcome: "HIT", freshness }
|
|
1595
|
-
:
|
|
1596
|
-
? { outcome: "BYPASS", reason: "explicit-cache-hit" }
|
|
1597
|
-
: explicitCacheBypass
|
|
1598
|
-
? { outcome: "BYPASS", reason: "cache-disabled" }
|
|
1599
|
-
: { outcome: "BYPASS", reason: "snapshot-miss" };
|
|
1568
|
+
: explicit.status({ outcome: "BYPASS", reason: "snapshot-miss" });
|
|
1600
1569
|
// A prerender serve or intercept resolution leaves the seeded record
|
|
1601
1570
|
// unconsulted (prerender short-circuits before the scope; intercepts
|
|
1602
1571
|
// keep snapshot.cacheScope) — the "snapshot-miss" guess would blame the
|
|
@@ -1605,14 +1574,15 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1605
1574
|
return {
|
|
1606
1575
|
result,
|
|
1607
1576
|
status,
|
|
1608
|
-
captureNeeded:
|
|
1609
|
-
segmentReplayCorrupt && reqCtx._pprReplayPostMatchReason === undefined,
|
|
1610
1577
|
// A corrupt document snapshot must be overwritten at the document key;
|
|
1611
1578
|
// writing only the secondary navigation key would leave the preferred
|
|
1612
1579
|
// document entry shadowing the repair forever. The capture remains marked
|
|
1613
1580
|
// navigationOnly, so a document request ignores it and recaptures a
|
|
1614
1581
|
// document-safe shell under the same key.
|
|
1615
|
-
|
|
1582
|
+
healKey:
|
|
1583
|
+
segmentReplayCorrupt && reqCtx._pprReplayPostMatchReason === undefined
|
|
1584
|
+
? snapshotStoreKey
|
|
1585
|
+
: undefined,
|
|
1616
1586
|
};
|
|
1617
1587
|
} finally {
|
|
1618
1588
|
reqCtx._shellImplicitCache = previousImplicitCache;
|
|
@@ -1707,7 +1677,7 @@ async function isSettled(value: unknown): Promise<boolean> {
|
|
|
1707
1677
|
*/
|
|
1708
1678
|
async function degradeUnreplayableShell(
|
|
1709
1679
|
descriptor: ShellCaptureDescriptor,
|
|
1710
|
-
entry:
|
|
1680
|
+
entry: ShellEntryHead,
|
|
1711
1681
|
scheduleRecapture: () => void,
|
|
1712
1682
|
): Promise<void> {
|
|
1713
1683
|
const store = descriptor.store;
|
|
@@ -1717,6 +1687,7 @@ async function degradeUnreplayableShell(
|
|
|
1717
1687
|
{
|
|
1718
1688
|
reactVersion: entry.reactVersion,
|
|
1719
1689
|
buildVersion: entry.buildVersion,
|
|
1690
|
+
snapshot: [],
|
|
1720
1691
|
navigationOnly: true,
|
|
1721
1692
|
createdAt: Date.now(),
|
|
1722
1693
|
},
|
|
@@ -1749,7 +1720,6 @@ function serveShellHit(
|
|
|
1749
1720
|
ssrModule: SSRModule,
|
|
1750
1721
|
document: ShellDocument,
|
|
1751
1722
|
descriptor: ShellCaptureDescriptor,
|
|
1752
|
-
nonce: string | undefined,
|
|
1753
1723
|
): Response {
|
|
1754
1724
|
const { entry, prelude: preludeBytes } = document;
|
|
1755
1725
|
const metricsStore = reqCtx._metricsStore;
|
|
@@ -1859,10 +1829,10 @@ function serveShellHit(
|
|
|
1859
1829
|
ReadableStream<Uint8Array> | { redirect: string }
|
|
1860
1830
|
> = (async () => {
|
|
1861
1831
|
// Snapshot seeding (docs/design/ppr-shell-resume.md): the tail render must
|
|
1862
|
-
// match the frozen prelude, so
|
|
1863
|
-
//
|
|
1864
|
-
//
|
|
1865
|
-
//
|
|
1832
|
+
// match the frozen prelude, so it replays the doc record and serves the
|
|
1833
|
+
// bake-lane loader pins, on a DERIVED context so the shared reqCtx is
|
|
1834
|
+
// untouched. Every cache read the tail makes, a hole's included, goes to
|
|
1835
|
+
// the request's own store.
|
|
1866
1836
|
// A prelude-first read delivers the snapshot on its own promise: the
|
|
1867
1837
|
// prelude is already committed, so only the tail waits for it.
|
|
1868
1838
|
const inMemory = await isSettled(document.snapshot);
|
|
@@ -1886,9 +1856,6 @@ function serveShellHit(
|
|
|
1886
1856
|
}
|
|
1887
1857
|
const seededCtx = createTailContext();
|
|
1888
1858
|
const records = snapshot ?? [];
|
|
1889
|
-
if (reqCtx._cacheStore && records.length > 0) {
|
|
1890
|
-
seededCtx._cacheStore = new SeededShellStore(reqCtx._cacheStore, records);
|
|
1891
|
-
}
|
|
1892
1859
|
// Loader-family records (bake-lane containers, loader-container-bake):
|
|
1893
1860
|
// decode into a seed Map for the resolveLoaderData overlay, so the
|
|
1894
1861
|
// payload's baked container bytes match the frozen prelude while the
|
|
@@ -1922,9 +1889,7 @@ function serveShellHit(
|
|
|
1922
1889
|
seededCtx._shellImplicitCache = {
|
|
1923
1890
|
ttl: descriptor.ttl,
|
|
1924
1891
|
swr: descriptor.swr,
|
|
1925
|
-
store: new SeededShellStore(reqCtx._cacheStore!, records,
|
|
1926
|
-
segmentsOnly: true,
|
|
1927
|
-
}),
|
|
1892
|
+
store: new SeededShellStore(reqCtx._cacheStore!, records),
|
|
1928
1893
|
keyPrefix: "doc",
|
|
1929
1894
|
docTail: true,
|
|
1930
1895
|
fixedDocKey: entry.docKey,
|
|
@@ -2041,7 +2006,6 @@ function serveShellHit(
|
|
|
2041
2006
|
new TextEncoder().encode(
|
|
2042
2007
|
inlineShellScript(
|
|
2043
2008
|
`location.replace(${escapeJsonForScript(JSON.stringify(safeTarget))})`,
|
|
2044
|
-
nonce,
|
|
2045
2009
|
),
|
|
2046
2010
|
),
|
|
2047
2011
|
);
|
|
@@ -2067,9 +2031,7 @@ function serveShellHit(
|
|
|
2067
2031
|
scheduleRecapture,
|
|
2068
2032
|
);
|
|
2069
2033
|
}
|
|
2070
|
-
controller.enqueue(
|
|
2071
|
-
new TextEncoder().encode(shellReloadScript(nonce)),
|
|
2072
|
-
);
|
|
2034
|
+
controller.enqueue(new TextEncoder().encode(shellReloadScript()));
|
|
2073
2035
|
if (tailTiming) {
|
|
2074
2036
|
tailTiming.outcome = "error";
|
|
2075
2037
|
tailTiming.completeMs = Math.round(performance.now() - tailT0);
|
|
@@ -2084,15 +2046,7 @@ function serveShellHit(
|
|
|
2084
2046
|
// request until the entry ages out — recapture overwrites the entry.
|
|
2085
2047
|
// Client disconnects land here too; the recapture is idempotent and
|
|
2086
2048
|
// bounded by scheduleShellCapture's stampede guard + backoff.
|
|
2087
|
-
|
|
2088
|
-
ctx,
|
|
2089
|
-
request,
|
|
2090
|
-
env,
|
|
2091
|
-
url,
|
|
2092
|
-
reqCtx,
|
|
2093
|
-
ssrModule,
|
|
2094
|
-
descriptor,
|
|
2095
|
-
);
|
|
2049
|
+
scheduleRecapture();
|
|
2096
2050
|
if (tailTiming) {
|
|
2097
2051
|
tailTiming.outcome = "error";
|
|
2098
2052
|
tailTiming.completeMs = Math.round(performance.now() - tailT0);
|