@rangojs/router 0.16.0 → 0.18.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/README.md +255 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -0
- package/dist/types/browser/navigation-transaction.d.ts +6 -0
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/scroll-restoration.d.ts +7 -0
- package/dist/types/cache/cache-exec-scope.d.ts +18 -1
- package/dist/types/cache/cache-key-utils.d.ts +17 -2
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +132 -19
- package/dist/types/cache/cache-tag.d.ts +92 -7
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +54 -13
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +110 -21
- package/dist/types/cache/tag-invalidation.d.ts +30 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +220 -44
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- package/dist/types/client.d.ts +4 -4
- package/dist/types/client.rsc.d.ts +4 -5
- package/dist/types/components/DefaultDocument.d.ts +5 -2
- package/dist/types/context-var.d.ts +6 -5
- package/dist/types/escape-script.d.ts +1 -1
- package/dist/types/handles/MetaTags.d.ts +5 -4
- package/dist/types/handles/Scripts.d.ts +8 -7
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/handles/script.d.ts +17 -16
- package/dist/types/href-client.d.ts +1 -1
- package/dist/types/html.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +3 -2
- package/dist/types/loader-store.d.ts +7 -4
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/reverse.d.ts +36 -4
- package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
- package/dist/types/route-definition/helpers-types.d.ts +33 -9
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +1 -1
- package/dist/types/router/debug-manifest.d.ts +3 -2
- package/dist/types/router/error-handling.d.ts +20 -0
- package/dist/types/router/handler-context.d.ts +6 -0
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +9 -3
- package/dist/types/router/router-context.d.ts +3 -1
- package/dist/types/router/router-interfaces.d.ts +24 -2
- package/dist/types/router/router-options.d.ts +8 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
- package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
- package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
- package/dist/types/router/segment-wrappers.d.ts +1 -1
- package/dist/types/rsc/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
- package/dist/types/rsc/shell-capture.d.ts +151 -19
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +17 -0
- package/dist/types/server/context.d.ts +139 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +182 -53
- package/dist/types/ssr/index.d.ts +24 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/generated-routes.d.ts +2 -2
- package/dist/types/testing/internal/context.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +32 -6
- package/dist/types/testing/run-loader.d.ts +18 -3
- package/dist/types/testing/serve-shell-request.d.ts +132 -0
- package/dist/types/testing/shell-status.d.ts +23 -10
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
- package/dist/types/testing/vitest.d.ts +35 -1
- package/dist/types/theme/ThemeScript.d.ts +3 -3
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +28 -1
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/pattern-types.d.ts +22 -18
- package/dist/types/urls/response-types.d.ts +3 -12
- package/dist/types/use-loader.d.ts +12 -3
- package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
- package/dist/types/vite/rango.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1420 -1348
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +75 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +306 -124
- package/skills/caching/SKILL.md +530 -98
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +65 -38
- package/skills/cloudflare/SKILL.md +87 -20
- package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
- package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
- package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
- package/skills/comparison/SKILL.md +5 -0
- package/skills/comparison/references/framework-comparison.md +75 -57
- package/skills/composability/SKILL.md +38 -29
- package/skills/css/SKILL.md +10 -13
- package/skills/debug-manifest/SKILL.md +74 -31
- package/skills/defer-hydration/SKILL.md +52 -31
- package/skills/deployment-caching/SKILL.md +11 -3
- package/skills/document-cache/SKILL.md +98 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +55 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +34 -17
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +39 -15
- package/skills/hooks/urls.md +7 -5
- package/skills/host-router/SKILL.md +64 -12
- package/skills/i18n/SKILL.md +60 -34
- package/skills/intercept/SKILL.md +161 -26
- package/skills/layout/SKILL.md +106 -50
- package/skills/links/SKILL.md +51 -19
- package/skills/loader/SKILL.md +386 -128
- package/skills/middleware/SKILL.md +129 -26
- package/skills/migrate-nextjs/SKILL.md +156 -70
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +51 -39
- package/skills/migrate-react-router/component-migration.md +28 -7
- package/skills/migrate-react-router/data-and-actions.md +21 -6
- package/skills/migrate-react-router/route-mapping.md +19 -10
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +92 -14
- package/skills/parallel/SKILL.md +88 -47
- package/skills/ppr/SKILL.md +866 -379
- package/skills/prerender/SKILL.md +210 -108
- package/skills/rango/SKILL.md +115 -65
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +170 -60
- package/skills/router-setup/SKILL.md +420 -240
- package/skills/scripts/SKILL.md +39 -21
- package/skills/server-actions/SKILL.md +231 -59
- package/skills/shell-manifest/SKILL.md +28 -14
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +73 -33
- package/skills/testing/cache-prerender.md +60 -33
- package/skills/testing/client-components.md +59 -6
- package/skills/testing/e2e-parity.md +12 -2
- package/skills/testing/flight.md +19 -11
- package/skills/testing/handles.md +8 -5
- package/skills/testing/loader.md +26 -6
- package/skills/testing/middleware.md +8 -3
- package/skills/testing/predicates.md +120 -0
- package/skills/testing/render-handler.md +70 -22
- package/skills/testing/response-routes.md +11 -4
- package/skills/testing/reverse-and-types.md +20 -14
- package/skills/testing/serve-shell-request.md +188 -0
- package/skills/testing/server-actions.md +12 -3
- package/skills/testing/server-tree.md +9 -0
- package/skills/testing/setup.md +9 -5
- package/skills/theme/SKILL.md +155 -30
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +137 -50
- package/skills/typesafety/generated-files-and-cli.md +26 -14
- package/skills/typesafety/params-and-search.md +44 -19
- package/skills/typesafety/route-types.md +50 -14
- package/skills/use-cache/SKILL.md +224 -83
- package/skills/vercel/SKILL.md +35 -13
- package/skills/view-transitions/SKILL.md +42 -21
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- package/src/browser/index.ts +2 -2
- package/src/browser/navigation-bridge.ts +3 -4
- package/src/browser/navigation-transaction.ts +22 -2
- package/src/browser/partial-update.ts +35 -25
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +6 -1
- package/src/browser/scroll-restoration.ts +53 -6
- package/src/cache/cache-exec-scope.ts +37 -4
- package/src/cache/cache-key-utils.ts +23 -8
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +264 -68
- package/src/cache/cache-scope.ts +419 -43
- package/src/cache/cache-tag.ts +375 -11
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +990 -348
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +133 -28
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +108 -22
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +225 -44
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- package/src/client.rsc.tsx +10 -9
- package/src/client.tsx +5 -4
- package/src/components/DefaultDocument.tsx +7 -2
- package/src/context-var.ts +6 -5
- package/src/escape-script.ts +1 -1
- package/src/handles/MetaTags.tsx +13 -11
- package/src/handles/Scripts.tsx +11 -10
- package/src/handles/meta.ts +2 -2
- package/src/handles/script.ts +17 -16
- package/src/href-client.ts +1 -1
- package/src/html.ts +12 -0
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +1 -0
- package/src/loader-store.ts +7 -4
- package/src/prerender/build-shell-capture.ts +73 -22
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +121 -33
- package/src/route-definition/helpers-types.ts +33 -9
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +0 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +71 -28
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +141 -43
- package/src/router/loader-resolution.ts +115 -39
- package/src/router/match-api.ts +40 -26
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +110 -10
- package/src/router/match-middleware/cache-store.ts +48 -101
- package/src/router/middleware-types.ts +9 -6
- package/src/router/middleware.ts +75 -13
- package/src/router/prerender-match.ts +59 -19
- package/src/router/route-snapshot.ts +11 -1
- package/src/router/router-context.ts +2 -0
- package/src/router/router-interfaces.ts +25 -2
- package/src/router/router-options.ts +8 -5
- package/src/router/segment-resolution/fresh.ts +62 -21
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +407 -76
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +208 -56
- package/src/router/segment-resolution/mask-nested.ts +138 -44
- package/src/router/segment-resolution/revalidation.ts +61 -16
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/full-payload.ts +25 -1
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-cache-serve.ts +9 -6
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +608 -225
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +73 -6
- package/src/rsc/shell-capture.ts +1068 -296
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +18 -0
- package/src/server/context.ts +300 -32
- package/src/server/cookie-store.ts +87 -61
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +297 -67
- package/src/ssr/index.tsx +136 -44
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/generated-routes.ts +2 -2
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/internal/flight-client-globals.ts +7 -5
- package/src/testing/render-route.tsx +85 -24
- package/src/testing/run-loader.ts +68 -6
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +28 -1
- package/src/types/loader-types.ts +8 -6
- package/src/urls/pattern-types.ts +22 -18
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
- package/src/vite/router-discovery.ts +14 -4
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* RSC Rendering Handler (Navigation)
|
|
3
3
|
*
|
|
4
4
|
* Handles RSC rendering for both partial (client-side navigation) and full
|
|
5
|
-
* (initial page load) requests.
|
|
6
|
-
* static generation.
|
|
5
|
+
* (initial page load) requests.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import {
|
|
@@ -14,9 +13,13 @@ import {
|
|
|
14
13
|
} from "../server/request-context.js";
|
|
15
14
|
import {
|
|
16
15
|
SeededShellStore,
|
|
16
|
+
ShellRecordUnavailableError,
|
|
17
17
|
buildShellLoaderSeed,
|
|
18
|
+
countSnapshotFamilies,
|
|
19
|
+
hasDocRecord,
|
|
18
20
|
} from "../cache/shell-snapshot.js";
|
|
19
21
|
import { appendMetric } from "../router/metrics.js";
|
|
22
|
+
import type { MetricsStore } from "../server/context.js";
|
|
20
23
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
21
24
|
import type { TraceSpan } from "../router/tracing.js";
|
|
22
25
|
import { getSSRSetup, createSsrHtmlStage, isRscRequest } from "./ssr-setup.js";
|
|
@@ -46,6 +49,8 @@ import type { HandlerContext } from "./handler-context.js";
|
|
|
46
49
|
import { gateTransitions } from "./transition-gate.js";
|
|
47
50
|
import { buildFullPayload } from "./full-payload.js";
|
|
48
51
|
import {
|
|
52
|
+
capShellWindow,
|
|
53
|
+
lastStoredCaptureSeq,
|
|
49
54
|
scheduleShellCapture,
|
|
50
55
|
resolveShellCaptureDebugSink,
|
|
51
56
|
takeCaptureDebugEventForTiming,
|
|
@@ -60,7 +65,15 @@ import {
|
|
|
60
65
|
shellSearchSeed,
|
|
61
66
|
isValidShellHit,
|
|
62
67
|
hasIntactShellPayload,
|
|
63
|
-
|
|
68
|
+
openShellDocument,
|
|
69
|
+
navigationShellKey,
|
|
70
|
+
partitionShellKey,
|
|
71
|
+
inlineShellScript,
|
|
72
|
+
shellReloadScript,
|
|
73
|
+
partitionBuildShellCheckDone,
|
|
74
|
+
notePartitionBuildShellCheck,
|
|
75
|
+
warnPprWindowCappedOnce,
|
|
76
|
+
SHELL_PRELUDE_CHUNK_BYTES,
|
|
64
77
|
hasShellFamily,
|
|
65
78
|
warnShellStoreMissingOnce,
|
|
66
79
|
warnPprNonceActiveOnce,
|
|
@@ -68,13 +81,19 @@ import {
|
|
|
68
81
|
publishShellTailTiming,
|
|
69
82
|
takeShellTailTimingForServerTiming,
|
|
70
83
|
type ResolvedPprConfig,
|
|
84
|
+
type ShellDocument,
|
|
71
85
|
type ShellTailTiming,
|
|
72
86
|
} from "./shell-serve.js";
|
|
73
87
|
import {
|
|
88
|
+
hasBuildShell,
|
|
74
89
|
lookupBuildShell,
|
|
75
90
|
type DevShellLookup,
|
|
76
91
|
} from "./shell-build-manifest.js";
|
|
77
92
|
import { contextGet } from "../context-var.js";
|
|
93
|
+
import {
|
|
94
|
+
resolveSwrWindow,
|
|
95
|
+
staleShellRecaptureDue,
|
|
96
|
+
} from "../cache/cache-policy.js";
|
|
78
97
|
import {
|
|
79
98
|
getNavigationContextHeader,
|
|
80
99
|
prerenderStoreShortCircuits,
|
|
@@ -89,13 +108,16 @@ import {
|
|
|
89
108
|
safeSameOriginLanding,
|
|
90
109
|
} from "../redirect-origin.js";
|
|
91
110
|
import { nonce as nonceToken } from "./nonce.js";
|
|
111
|
+
import { escapeJsonForScript } from "../escape-script.js";
|
|
112
|
+
import { resolveShellPartition } from "../cache/cache-scope.js";
|
|
92
113
|
import { reportCacheError } from "../cache/cache-error.js";
|
|
93
114
|
import type { SearchParamsFilter } from "../cache/search-params-filter.js";
|
|
94
115
|
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
95
116
|
import type {
|
|
96
|
-
DocumentShellCacheEntry,
|
|
97
117
|
SegmentCacheStore,
|
|
98
118
|
ShellCacheEntry,
|
|
119
|
+
ShellDocumentRead,
|
|
120
|
+
ShellReadStats,
|
|
99
121
|
ShellSnapshotRecord,
|
|
100
122
|
} from "../cache/types.js";
|
|
101
123
|
import {
|
|
@@ -117,8 +139,6 @@ type PprReplayBypassReason =
|
|
|
117
139
|
| "no-entry"
|
|
118
140
|
| "invalid-version"
|
|
119
141
|
| "corrupt-entry"
|
|
120
|
-
| "handler-live-holes"
|
|
121
|
-
| "transition-when"
|
|
122
142
|
| "no-segment-snapshot"
|
|
123
143
|
| "snapshot-miss"
|
|
124
144
|
| "explicit-cache-hit"
|
|
@@ -144,13 +164,6 @@ function describePprReplayStatus(status: PprReplayStatus): string {
|
|
|
144
164
|
: `bypass:${status.reason}`;
|
|
145
165
|
}
|
|
146
166
|
|
|
147
|
-
function buildNavigationShellKey(
|
|
148
|
-
url: URL,
|
|
149
|
-
filter?: SearchParamsFilter,
|
|
150
|
-
): string {
|
|
151
|
-
return `${buildShellKey(url, filter)}:navigation`;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
167
|
function createShellCaptureDescriptor(
|
|
155
168
|
ctx: HandlerContext<any>,
|
|
156
169
|
key: string,
|
|
@@ -243,8 +256,6 @@ function replayableShellSnapshot(
|
|
|
243
256
|
if (!entry.navigationOnly && !hasIntactShellPayload(entry)) {
|
|
244
257
|
return { reason: "corrupt-entry" };
|
|
245
258
|
}
|
|
246
|
-
if (entry.handlerLiveHoles) return { reason: "handler-live-holes" };
|
|
247
|
-
if (entry.transitionWhen) return { reason: "transition-when" };
|
|
248
259
|
// Eligibility requires the CANONICAL doc segment record (`docKey`), not just
|
|
249
260
|
// any segment record: captures under an explicit cache() scope also record
|
|
250
261
|
// that tier's reads/writes under keys a partial lookup can never resolve,
|
|
@@ -252,25 +263,8 @@ function replayableShellSnapshot(
|
|
|
252
263
|
// (a timing-dependent snapshot-miss/no-segment-snapshot flip-flop). Entries
|
|
253
264
|
// captured before the field existed read as no-segment-snapshot and heal on
|
|
254
265
|
// recapture (pre-release, same treatment as the buildVersion field).
|
|
255
|
-
const docKey = entry.docKey;
|
|
256
266
|
const snapshot = entry.snapshot;
|
|
257
|
-
|
|
258
|
-
docKey !== undefined &&
|
|
259
|
-
snapshot?.some((record) => {
|
|
260
|
-
if (
|
|
261
|
-
!record ||
|
|
262
|
-
typeof record !== "object" ||
|
|
263
|
-
record.family !== "segment" ||
|
|
264
|
-
record.key !== docKey ||
|
|
265
|
-
typeof record.value !== "object" ||
|
|
266
|
-
record.value === null
|
|
267
|
-
) {
|
|
268
|
-
return false;
|
|
269
|
-
}
|
|
270
|
-
const segments = (record.value as { segments?: unknown }).segments;
|
|
271
|
-
return Array.isArray(segments) && segments.length > 0;
|
|
272
|
-
});
|
|
273
|
-
return hasDocRecord && snapshot
|
|
267
|
+
return snapshot && hasDocRecord(snapshot, entry.docKey)
|
|
274
268
|
? { snapshot }
|
|
275
269
|
: { reason: "no-segment-snapshot" };
|
|
276
270
|
}
|
|
@@ -394,8 +388,8 @@ function* requestRenderPlan<TEnv>(
|
|
|
394
388
|
const shell = yield* scope("shell-serve", shellServePlan(input));
|
|
395
389
|
if (shell.kind === "serve") return shell.response;
|
|
396
390
|
|
|
397
|
-
// Match + payload. A control response (redirect
|
|
398
|
-
//
|
|
391
|
+
// Match + payload. A control response (redirect) ends the plan before any
|
|
392
|
+
// rendering.
|
|
399
393
|
const prepared = yield* scope(
|
|
400
394
|
isPartial ? "prepare:partial" : "prepare:full",
|
|
401
395
|
preparePayloadPlan(input),
|
|
@@ -440,13 +434,14 @@ function* requestRenderPlan<TEnv>(
|
|
|
440
434
|
response.headers.set(SHELL_STATUS_HEADER, "MISS");
|
|
441
435
|
}
|
|
442
436
|
|
|
437
|
+
const captureKey = prepared.partialCaptureKey;
|
|
443
438
|
if (
|
|
444
439
|
isPartial &&
|
|
445
440
|
prepared.partialCaptureNeeded &&
|
|
441
|
+
captureKey !== undefined &&
|
|
446
442
|
!reqCtx._dynamic &&
|
|
447
443
|
response.status === 200
|
|
448
444
|
) {
|
|
449
|
-
const captureKey = prepared.partialCaptureKey;
|
|
450
445
|
yield* handoff("navigation-shell-capture", () =>
|
|
451
446
|
scheduleNavigationShellCapture(input, captureKey),
|
|
452
447
|
);
|
|
@@ -455,6 +450,32 @@ function* requestRenderPlan<TEnv>(
|
|
|
455
450
|
return response;
|
|
456
451
|
}
|
|
457
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Dev: warn once per route when the route's cache() entry reduces an explicit
|
|
455
|
+
* ppr window. The window is what capShellWindow gives a capture from a
|
|
456
|
+
* record written just now, the widest it can get; null (a cache() ttl + swr
|
|
457
|
+
* of 0) is the capture's own warning.
|
|
458
|
+
*/
|
|
459
|
+
function warnIfPprWindowReduced(
|
|
460
|
+
routeName: string,
|
|
461
|
+
pprOption: { ttl?: number; swr?: number },
|
|
462
|
+
pprConfig: ResolvedPprConfig,
|
|
463
|
+
cacheScope: { ttl: number; swr: number | undefined },
|
|
464
|
+
store: SegmentCacheStore<any>,
|
|
465
|
+
): void {
|
|
466
|
+
const cache = { ttl: cacheScope.ttl, swr: cacheScope.swr ?? 0 };
|
|
467
|
+
const shell = capShellWindow(
|
|
468
|
+
pprConfig.ttl,
|
|
469
|
+
resolveSwrWindow(pprConfig.swr, store.defaults),
|
|
470
|
+
{
|
|
471
|
+
freshUntil: cache.ttl * 1000,
|
|
472
|
+
staleUntil: (cache.ttl + cache.swr) * 1000,
|
|
473
|
+
},
|
|
474
|
+
0,
|
|
475
|
+
);
|
|
476
|
+
if (shell) warnPprWindowCappedOnce(routeName, pprOption, shell, cache);
|
|
477
|
+
}
|
|
478
|
+
|
|
458
479
|
/**
|
|
459
480
|
* Axis 2 shell serve. Sync gates stay plan code; store and SSR effects run as
|
|
460
481
|
* named steps. Outcomes: serve a composed shell response, report a MISS (the
|
|
@@ -470,14 +491,18 @@ function* shellServePlan<TEnv>(
|
|
|
470
491
|
if (
|
|
471
492
|
isPartial ||
|
|
472
493
|
request.method !== "GET" ||
|
|
473
|
-
url.searchParams.has("__prerender_collect") ||
|
|
474
494
|
isRscRequest(request, url, false) ||
|
|
475
495
|
reqCtx._dynamic
|
|
476
496
|
) {
|
|
477
497
|
return { kind: "pass" };
|
|
478
498
|
}
|
|
479
|
-
const
|
|
499
|
+
const manifestEntry = reqCtx._classifiedRoute?.manifestEntry;
|
|
500
|
+
const pprConfig = resolvePprConfig(manifestEntry);
|
|
480
501
|
if (!pprConfig) return { kind: "pass" };
|
|
502
|
+
// A degraded HIT's reload (shellReloadScript) renders like a cache miss:
|
|
503
|
+
// no shell read, no capture, so it can never degrade again. The handler
|
|
504
|
+
// stripped the marker from the request and left this flag.
|
|
505
|
+
if (reqCtx._shellForcedMiss) return { kind: "pass" };
|
|
481
506
|
|
|
482
507
|
// A per-request CSP nonce pins the route to axis 1: a shared shell would
|
|
483
508
|
// freeze the capture request's nonce and CSP would reject it for every
|
|
@@ -486,8 +511,8 @@ function* shellServePlan<TEnv>(
|
|
|
486
511
|
// the provider-only check missed the latter (issue #656).
|
|
487
512
|
const activeNonce = nonce ?? contextGet(reqCtx._variables, nonceToken);
|
|
488
513
|
const store = reqCtx._cacheStore;
|
|
489
|
-
const
|
|
490
|
-
|
|
514
|
+
const baseKey = buildShellKey(url, reqCtx._searchParamsFilter);
|
|
515
|
+
let key = baseKey;
|
|
491
516
|
if (activeNonce !== undefined) {
|
|
492
517
|
// Declared intent that cannot be honored deserves a diagnostic (unlike an
|
|
493
518
|
// undeclared route, which is silent): a ppr route gated off by an active
|
|
@@ -501,11 +526,69 @@ function* shellServePlan<TEnv>(
|
|
|
501
526
|
warnShellStoreMissingOnce(key);
|
|
502
527
|
return { kind: "pass" };
|
|
503
528
|
}
|
|
504
|
-
|
|
529
|
+
// The route's own cache() opt-out — cache(false), or a condition() that
|
|
530
|
+
// refuses THIS request — is absolute, and a HIT tail never consults the
|
|
531
|
+
// route scope (it replays the shell's doc record), so it is decided here,
|
|
532
|
+
// before any shell byte: the request renders like a cache miss (axis 1) and
|
|
533
|
+
// schedules no capture (the capture could not record the handler layer).
|
|
534
|
+
const route = classifiedRouteSnapshot(reqCtx);
|
|
535
|
+
if (route?.cacheScope && !route.cacheScope.allowsCache("read")) {
|
|
536
|
+
return { kind: "pass" };
|
|
537
|
+
}
|
|
538
|
+
// The shell is capped to the route cache() entry it captures from
|
|
539
|
+
// (capShellWindow); an explicit ppr window the cap reduces warns in dev.
|
|
540
|
+
// resolvePprConfig returned a config, so the entry is a ppr route.
|
|
541
|
+
const pprOption = manifestEntry?.type === "route" && manifestEntry.ppr;
|
|
542
|
+
if (
|
|
543
|
+
process.env.NODE_ENV !== "production" &&
|
|
544
|
+
route?.cacheScope?.enabled &&
|
|
545
|
+
typeof pprOption === "object" &&
|
|
546
|
+
(pprOption.ttl !== undefined || pprOption.swr !== undefined)
|
|
547
|
+
) {
|
|
548
|
+
warnIfPprWindowReduced(
|
|
549
|
+
reqCtx._classifiedRoute?.routeKey ?? url.pathname,
|
|
550
|
+
pprOption,
|
|
551
|
+
pprConfig,
|
|
552
|
+
route.cacheScope,
|
|
553
|
+
store,
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
// A route whose cache() record is partitioned by the request (its `key()`,
|
|
557
|
+
// or the store's keyGenerator) partitions its shell the same way: each
|
|
558
|
+
// partition captures and serves its own shell. A failed key resolution
|
|
559
|
+
// serves no shell, never another partition's.
|
|
560
|
+
const partitionedKey = partitionedShellKey(
|
|
561
|
+
baseKey,
|
|
562
|
+
route?.cacheScope,
|
|
563
|
+
store,
|
|
564
|
+
reqCtx.url.pathname,
|
|
565
|
+
route?.params,
|
|
566
|
+
);
|
|
567
|
+
if (partitionedKey) {
|
|
568
|
+
const resolved = yield* step("shell-partition", () =>
|
|
569
|
+
partitionedKey.then(
|
|
570
|
+
(value): string | undefined => value,
|
|
571
|
+
(error: unknown) => {
|
|
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
|
+
}
|
|
580
|
+
reqCtx._shellKey = key;
|
|
581
|
+
// The buffered capture and tail timings are keyed by the partitioned key.
|
|
582
|
+
mirrorPprServerTimings(key, reqCtx);
|
|
583
|
+
|
|
584
|
+
// A MISS schedules its capture after rendering: taken before the read, this
|
|
585
|
+
// lets that capture skip itself when another request's capture stored the
|
|
586
|
+
// shell in between (ShellCaptureDescriptor.storedSeqAtRead).
|
|
587
|
+
const storedSeqAtRead = lastStoredCaptureSeq(key);
|
|
505
588
|
// SSR setup starts before route handling, so read the shell before joining it
|
|
506
589
|
// to overlap the remaining setup work with cache I/O.
|
|
507
590
|
const cached = yield* step("shell-read", () =>
|
|
508
|
-
readShellEntry(store, key, reqCtx),
|
|
591
|
+
readShellEntry(store, key, reqCtx, pprConfig.tags),
|
|
509
592
|
);
|
|
510
593
|
|
|
511
594
|
// allReady (ssr.resolveStreaming) bypasses PPR entirely: buffering defeats
|
|
@@ -530,12 +613,21 @@ function* shellServePlan<TEnv>(
|
|
|
530
613
|
store,
|
|
531
614
|
);
|
|
532
615
|
|
|
616
|
+
// A HIT replays the handler layer from the entry's doc record (`docKey`),
|
|
617
|
+
// or, for a Prerender route, from the prerender store. An entry that has
|
|
618
|
+
// neither is a MISS: serving it would run handlers after the commit.
|
|
533
619
|
if (
|
|
534
620
|
cached &&
|
|
535
621
|
!cached.entry.navigationOnly &&
|
|
622
|
+
(cached.entry.docKey !== undefined || route?.matched?.pr === true) &&
|
|
536
623
|
isValidShellHit(cached.entry, ctx.version)
|
|
537
624
|
) {
|
|
538
|
-
|
|
625
|
+
const document = openShellDocumentMetered(
|
|
626
|
+
reqCtx,
|
|
627
|
+
cached.entry,
|
|
628
|
+
cached.read,
|
|
629
|
+
);
|
|
630
|
+
if (!document) {
|
|
539
631
|
// Corrupt stored payload (undecodable prelude / unparseable
|
|
540
632
|
// postponed): a store-layer fault worth a diagnostic, unlike the
|
|
541
633
|
// silent version-mismatch lifecycle misses above. Degrade to MISS
|
|
@@ -550,8 +642,12 @@ function* shellServePlan<TEnv>(
|
|
|
550
642
|
);
|
|
551
643
|
} else {
|
|
552
644
|
// Stale (SWR) hit: serve the stale shell now, recapture in the
|
|
553
|
-
// background (stampede-guarded + backoff inside scheduleShellCapture)
|
|
554
|
-
|
|
645
|
+
// background (stampede-guarded + backoff inside scheduleShellCapture),
|
|
646
|
+
// at most once per SHELL_MIN_RECAPTURE_INTERVAL_MS of the shell's age.
|
|
647
|
+
if (
|
|
648
|
+
cached.shouldRevalidate &&
|
|
649
|
+
staleShellRecaptureDue(cached.entry, Date.now())
|
|
650
|
+
) {
|
|
555
651
|
yield* handoff("shell-recapture", () =>
|
|
556
652
|
scheduleShellCapture(
|
|
557
653
|
ctx,
|
|
@@ -564,7 +660,6 @@ function* shellServePlan<TEnv>(
|
|
|
564
660
|
),
|
|
565
661
|
);
|
|
566
662
|
}
|
|
567
|
-
const entry = cached.entry;
|
|
568
663
|
const response = yield* step("shell-hit", () =>
|
|
569
664
|
serveShellHit(
|
|
570
665
|
ctx,
|
|
@@ -574,34 +669,76 @@ function* shellServePlan<TEnv>(
|
|
|
574
669
|
reqCtx,
|
|
575
670
|
handleStore,
|
|
576
671
|
ssrModule,
|
|
577
|
-
|
|
672
|
+
document,
|
|
578
673
|
descriptor,
|
|
674
|
+
activeNonce,
|
|
579
675
|
),
|
|
580
676
|
);
|
|
581
677
|
return { kind: "serve", response };
|
|
582
678
|
}
|
|
583
679
|
}
|
|
584
680
|
|
|
681
|
+
// A runtime tombstone (degradeUnreplayableShell: this key's HIT could not
|
|
682
|
+
// replay its handler layer) shadows the build shell too, or a build shell
|
|
683
|
+
// whose tail cannot be replayed would degrade on every request until a
|
|
684
|
+
// runtime capture lands.
|
|
685
|
+
if (
|
|
686
|
+
cached?.entry.navigationOnly &&
|
|
687
|
+
isValidShellHit(cached.entry, ctx.version)
|
|
688
|
+
) {
|
|
689
|
+
return {
|
|
690
|
+
kind: "miss",
|
|
691
|
+
descriptor: { ...descriptor, storedSeqAtRead },
|
|
692
|
+
ssrModule,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
585
696
|
// Build-time shell read-through (producer B, #699): on a runtime
|
|
586
697
|
// store MISS a Prerender+ppr route serves its `vite build`-baked
|
|
587
|
-
// shell through the SAME serve path.
|
|
588
|
-
//
|
|
589
|
-
//
|
|
590
|
-
//
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
698
|
+
// shell through the SAME serve path. A partitioned route has none: the
|
|
699
|
+
// build captured one partition, which no request may be assumed to
|
|
700
|
+
// share, and a route that has a build shell says so once. lookupBuildShell
|
|
701
|
+
// owns the validity gates and fails to null; openShellDocument below is
|
|
702
|
+
// the prelude's integrity check and only decode (either failing leaves the
|
|
703
|
+
// ordinary MISS path); past ppr.ttl the baked entry still serves while SWR
|
|
704
|
+
// recaptures — the upgrade path from build entry to runtime entry.
|
|
705
|
+
//
|
|
706
|
+
// Dev: no build manifest exists; producer B runs on demand via the dev
|
|
707
|
+
// server's /__rsc_shell endpoint for PRERENDERED routes only (production's
|
|
708
|
+
// exact candidate set). Folded away in production builds (NODE_ENV is a
|
|
709
|
+
// compile-time constant).
|
|
710
|
+
const devShellLookup = resolveDevShellLookup(reqCtx, pprConfig);
|
|
711
|
+
let buildHit: Awaited<ReturnType<typeof lookupBuildShell>> = null;
|
|
712
|
+
if (key !== baseKey) {
|
|
713
|
+
// Probed once per path without a build shell, and not again once the
|
|
714
|
+
// route warned. A search-bearing URL never reads one: nothing to probe.
|
|
715
|
+
const routeName = reqCtx._classifiedRoute?.routeKey;
|
|
716
|
+
if (
|
|
717
|
+
!partitionBuildShellCheckDone(url.pathname, routeName) &&
|
|
718
|
+
shellSearchSeed(url, reqCtx._searchParamsFilter) === ""
|
|
719
|
+
) {
|
|
720
|
+
const found = yield* step("build-shell-check", () =>
|
|
721
|
+
hasBuildShell(url.pathname, devShellLookup),
|
|
722
|
+
);
|
|
723
|
+
notePartitionBuildShellCheck(url.pathname, routeName, found);
|
|
724
|
+
}
|
|
725
|
+
} else {
|
|
726
|
+
buildHit = yield* step("build-shell-lookup", () =>
|
|
727
|
+
lookupBuildShell(
|
|
728
|
+
url,
|
|
729
|
+
ctx.version,
|
|
730
|
+
store,
|
|
731
|
+
devShellLookup,
|
|
732
|
+
reqCtx._searchParamsFilter,
|
|
733
|
+
),
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
// The document serve decodes the baked prelude once here; an undecodable
|
|
737
|
+
// one is a MISS like a corrupt runtime entry.
|
|
738
|
+
const buildDocument = buildHit
|
|
739
|
+
? openShellDocumentMetered(reqCtx, buildHit.entry)
|
|
740
|
+
: null;
|
|
741
|
+
if (buildHit && buildDocument) {
|
|
605
742
|
// Past ppr.ttl: still serve the baked entry, recapture upgrades it.
|
|
606
743
|
if (buildHit.stale) {
|
|
607
744
|
yield* handoff("shell-recapture", () =>
|
|
@@ -625,8 +762,9 @@ function* shellServePlan<TEnv>(
|
|
|
625
762
|
reqCtx,
|
|
626
763
|
handleStore,
|
|
627
764
|
ssrModule,
|
|
628
|
-
|
|
765
|
+
buildDocument,
|
|
629
766
|
descriptor,
|
|
767
|
+
activeNonce,
|
|
630
768
|
),
|
|
631
769
|
);
|
|
632
770
|
return { kind: "serve", response };
|
|
@@ -634,12 +772,16 @@ function* shellServePlan<TEnv>(
|
|
|
634
772
|
|
|
635
773
|
// MISS (no entry, invalid reactVersion, or store read failure): axis 1
|
|
636
774
|
// + a background capture scheduled once the response is known servable.
|
|
637
|
-
return {
|
|
775
|
+
return {
|
|
776
|
+
kind: "miss",
|
|
777
|
+
descriptor: { ...descriptor, storedSeqAtRead },
|
|
778
|
+
ssrModule,
|
|
779
|
+
};
|
|
638
780
|
}
|
|
639
781
|
|
|
640
782
|
/**
|
|
641
783
|
* Match the request and assemble the RSC payload, or produce a control
|
|
642
|
-
* response (redirect
|
|
784
|
+
* response (redirect) that ends the request plan.
|
|
643
785
|
*/
|
|
644
786
|
function* preparePayloadPlan<TEnv>(
|
|
645
787
|
input: RequestRenderInput<TEnv>,
|
|
@@ -750,13 +892,6 @@ function* preparePayloadPlan<TEnv>(
|
|
|
750
892
|
// Caching is now handled in router.match() via cache provider in request context
|
|
751
893
|
// match.segments already contains cached or fresh segments as appropriate
|
|
752
894
|
|
|
753
|
-
if (url.searchParams.has("__prerender_collect")) {
|
|
754
|
-
const response = yield* step("prerender-collect", () =>
|
|
755
|
-
collectPrerenderArtifacts(match, handleStore),
|
|
756
|
-
);
|
|
757
|
-
return { kind: "control", response };
|
|
758
|
-
}
|
|
759
|
-
|
|
760
895
|
return {
|
|
761
896
|
kind: "payload",
|
|
762
897
|
payload: buildFullPayload(match, ctx, url, reqCtx, handleStore),
|
|
@@ -791,53 +926,157 @@ async function matchPartialAndRecordParams<TEnv>(
|
|
|
791
926
|
return replay;
|
|
792
927
|
}
|
|
793
928
|
|
|
929
|
+
/** A document serve's store read: the entry, plus the prelude-first read when the store has one. */
|
|
930
|
+
interface ShellStoreRead {
|
|
931
|
+
entry: ShellCacheEntry;
|
|
932
|
+
shouldRevalidate?: boolean;
|
|
933
|
+
read?: ShellDocumentRead;
|
|
934
|
+
}
|
|
935
|
+
|
|
794
936
|
/**
|
|
795
937
|
* Shell store read that degrades to null on failure (axis 1 MISS, never a 500)
|
|
796
938
|
* and records the raw `ppr:shell-read` outcome (pre-validity-gates, so a
|
|
797
939
|
* version-mismatch lifecycle miss stays distinguishable from a store miss).
|
|
940
|
+
* A store with a prelude-first read (readShellDocument) resolves before the
|
|
941
|
+
* snapshot is read, so the HIT commits without waiting for it.
|
|
798
942
|
*/
|
|
799
|
-
async function readShellEntry
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
943
|
+
async function readShellEntry(
|
|
944
|
+
store: SegmentCacheStore & {
|
|
945
|
+
getShell: NonNullable<SegmentCacheStore["getShell"]>;
|
|
946
|
+
},
|
|
803
947
|
key: string,
|
|
804
948
|
reqCtx: ReturnType<typeof getRequestContext>,
|
|
805
|
-
|
|
806
|
-
|
|
949
|
+
tagHints: readonly string[] | undefined,
|
|
950
|
+
): Promise<ShellStoreRead | null> {
|
|
951
|
+
let cached: ShellStoreRead | null = null;
|
|
807
952
|
const shellReadStart = reqCtx._metricsStore ? performance.now() : 0;
|
|
808
953
|
try {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
954
|
+
if (store.readShellDocument) {
|
|
955
|
+
const read = await store.readShellDocument(
|
|
956
|
+
key,
|
|
957
|
+
tagHints ? { tagHints } : undefined,
|
|
958
|
+
);
|
|
959
|
+
cached = read
|
|
960
|
+
? { entry: read.entry, shouldRevalidate: read.shouldRevalidate, read }
|
|
961
|
+
: null;
|
|
962
|
+
} else {
|
|
963
|
+
cached = await store.getShell(key);
|
|
964
|
+
}
|
|
814
965
|
} catch (error) {
|
|
815
966
|
reportCacheError(error, "cache-read", "[ShellServe] getShell");
|
|
816
967
|
}
|
|
817
968
|
if (reqCtx._metricsStore) {
|
|
969
|
+
const stats = cached?.read?.stats;
|
|
818
970
|
appendMetric(
|
|
819
971
|
reqCtx._metricsStore,
|
|
820
972
|
"ppr:shell-read",
|
|
821
973
|
shellReadStart,
|
|
822
974
|
performance.now() - shellReadStart,
|
|
823
975
|
undefined,
|
|
824
|
-
cached ? "hit" : "miss",
|
|
976
|
+
cached ? (stats ? `hit ${stats.tier}` : "hit") : "miss",
|
|
825
977
|
);
|
|
978
|
+
if (stats)
|
|
979
|
+
recordShellReadStats(reqCtx._metricsStore, shellReadStart, stats);
|
|
826
980
|
}
|
|
827
981
|
return cached;
|
|
828
982
|
}
|
|
829
983
|
|
|
830
984
|
/**
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
985
|
+
* A prelude-first read's sub-steps as `debugPerformance` rows under
|
|
986
|
+
* `ppr:shell-read`. The marker row starts with the prelude row because the
|
|
987
|
+
* two run in parallel; its desc says how long the read still waited on the
|
|
988
|
+
* marker once the prelude was in. On a deployed worker the clock only
|
|
989
|
+
* advances on I/O, so the byte counts are the cost signal for the CPU parts.
|
|
990
|
+
*/
|
|
991
|
+
function recordShellReadStats(
|
|
992
|
+
store: MetricsStore,
|
|
993
|
+
readStart: number,
|
|
994
|
+
stats: ShellReadStats,
|
|
995
|
+
): void {
|
|
996
|
+
let at = readStart;
|
|
997
|
+
const row = (label: string, ms: number | undefined, desc: string): void => {
|
|
998
|
+
if (ms !== undefined) appendMetric(store, label, at, ms, 1, desc);
|
|
999
|
+
};
|
|
1000
|
+
if (stats.memo) {
|
|
1001
|
+
row(
|
|
1002
|
+
"ppr:shell-memo",
|
|
1003
|
+
0,
|
|
1004
|
+
`${stats.memo.hit ? "hit" : "miss"} size=${stats.memo.bytes}b`,
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
// A KV hit after an L1 miss: the L1 attempt first, then the KV read.
|
|
1008
|
+
row("ppr:shell-l1-miss", stats.l1MissMs, stats.l1MissReason ?? "miss");
|
|
1009
|
+
at += stats.l1MissMs ?? 0;
|
|
1010
|
+
row("ppr:shell-match", stats.matchMs, stats.tier);
|
|
1011
|
+
at += stats.matchMs ?? 0;
|
|
1012
|
+
row("ppr:shell-head", stats.headMs, `bytes=${stats.headBytes ?? 0}`);
|
|
1013
|
+
at += stats.headMs ?? 0;
|
|
1014
|
+
row("ppr:shell-prelude", stats.preludeMs, `bytes=${stats.preludeBytes ?? 0}`);
|
|
1015
|
+
let marker = `tags=${stats.tags ?? 0} ${stats.markerSerial ? "serial" : "parallel"} commit-wait=${(stats.markerWaitMs ?? 0).toFixed(2)}ms`;
|
|
1016
|
+
if (stats.markerMemo) marker += ` memo=${stats.markerMemo}`;
|
|
1017
|
+
if (stats.markerHinted) {
|
|
1018
|
+
marker += ` hint=${stats.markerHintHits ?? 0}/${stats.markerHinted.length}`;
|
|
1019
|
+
if (stats.markerLeadMs !== undefined) {
|
|
1020
|
+
marker += ` lead=${stats.markerLeadMs.toFixed(2)}ms`;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (stats.freshReads) marker += " fresh-reads";
|
|
1024
|
+
row("ppr:shell-marker", stats.markerMs, marker);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* openShellDocument (the HIT's integrity gate and only prelude decode) as a
|
|
1029
|
+
* `ppr:shell-open` row: CPU only, so it reads 0 on a deployed worker.
|
|
1030
|
+
*/
|
|
1031
|
+
function openShellDocumentMetered(
|
|
1032
|
+
reqCtx: RequestContext<any>,
|
|
1033
|
+
entry: ShellCacheEntry,
|
|
1034
|
+
read?: ShellDocumentRead,
|
|
1035
|
+
): ShellDocument | null {
|
|
1036
|
+
const store = reqCtx._metricsStore;
|
|
1037
|
+
if (!store) return openShellDocument(entry, read);
|
|
1038
|
+
const start = performance.now();
|
|
1039
|
+
const document = openShellDocument(entry, read);
|
|
1040
|
+
appendMetric(
|
|
1041
|
+
store,
|
|
1042
|
+
"ppr:shell-open",
|
|
1043
|
+
start,
|
|
1044
|
+
performance.now() - start,
|
|
1045
|
+
undefined,
|
|
1046
|
+
`cpu ${read ? "raw" : "base64-decode"} prelude=${document?.prelude.length ?? 0}b`,
|
|
1047
|
+
);
|
|
1048
|
+
return document;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* The shell key a request reads and captures: `baseKey`, partitioned by the
|
|
1053
|
+
* route's request partition (cache-scope.ts resolveShellPartition; a
|
|
1054
|
+
* partition equal to the default key leaves it unchanged). Undefined,
|
|
1055
|
+
* synchronously and with no work, when nothing partitions the route; a
|
|
1056
|
+
* rejection is a failed key resolution.
|
|
1057
|
+
*/
|
|
1058
|
+
function partitionedShellKey(
|
|
1059
|
+
baseKey: string,
|
|
1060
|
+
...partition: Parameters<typeof resolveShellPartition>
|
|
1061
|
+
): Promise<string> | undefined {
|
|
1062
|
+
return resolveShellPartition(...partition)?.then((resolved) =>
|
|
1063
|
+
resolved === null ? baseKey : partitionShellKey(baseKey, resolved),
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Server-Timing mirror (issue #651): a capture or a HIT tail completes AFTER
|
|
1069
|
+
* its triggering response committed, so its numbers can only ride a LATER
|
|
1070
|
+
* response's header. Read-and-clear keeps one report per run. The capture
|
|
1071
|
+
* half is dev-only (see takeCaptureDebugEventForTiming); the tail half runs
|
|
1072
|
+
* wherever a tail was buffered (serveShellHit).
|
|
835
1073
|
*/
|
|
836
|
-
function
|
|
1074
|
+
function mirrorPprServerTimings(
|
|
837
1075
|
key: string,
|
|
838
1076
|
reqCtx: ReturnType<typeof getRequestContext>,
|
|
839
1077
|
): void {
|
|
840
|
-
if (
|
|
1078
|
+
if (!reqCtx._metricsStore) return;
|
|
1079
|
+
if (process.env.NODE_ENV !== "production") {
|
|
841
1080
|
const lastCapture = takeCaptureDebugEventForTiming(key);
|
|
842
1081
|
if (lastCapture) {
|
|
843
1082
|
appendMetric(
|
|
@@ -850,55 +1089,24 @@ function mirrorPprServerTimingsForDev(
|
|
|
850
1089
|
describeShellCaptureEvent({ ...lastCapture, attemptMs: undefined }),
|
|
851
1090
|
);
|
|
852
1091
|
}
|
|
853
|
-
// Same mirror for the previous HIT's tail: its per-stage numbers
|
|
854
|
-
// (seed/match/handover/first-html/complete) finished after that
|
|
855
|
-
// response's headers were committed, so they ride THIS request's
|
|
856
|
-
// Server-Timing as `ppr:tail;dur=<complete ms>`.
|
|
857
|
-
const lastTail = takeShellTailTimingForServerTiming(key);
|
|
858
|
-
if (lastTail) {
|
|
859
|
-
appendMetric(
|
|
860
|
-
reqCtx._metricsStore,
|
|
861
|
-
"ppr:tail",
|
|
862
|
-
performance.now(),
|
|
863
|
-
lastTail.completeMs ?? 0,
|
|
864
|
-
undefined,
|
|
865
|
-
// completeMs already rides as this entry's dur — drop it from desc.
|
|
866
|
-
describeShellTailTiming({ ...lastTail, completeMs: undefined }),
|
|
867
|
-
);
|
|
868
|
-
}
|
|
869
1092
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
)
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
const handles: Record<string, Record<string, unknown[]>> = {};
|
|
887
|
-
for (const seg of nonLoaderSegments) {
|
|
888
|
-
const segHandles = handleStore.getDataForSegment(seg.id);
|
|
889
|
-
if (Object.keys(segHandles).length > 0) {
|
|
890
|
-
handles[seg.id] = segHandles;
|
|
891
|
-
}
|
|
1093
|
+
// Same mirror for the previous HIT's tail: its per-stage numbers
|
|
1094
|
+
// (snapshot/seed/match/handover/first-html/complete) finished after that
|
|
1095
|
+
// response's headers were committed, so they ride THIS request's
|
|
1096
|
+
// Server-Timing as `ppr:tail;dur=<complete ms>`. In production a tail is
|
|
1097
|
+
// buffered only when its own HIT collected metrics (serveShellHit).
|
|
1098
|
+
const lastTail = takeShellTailTimingForServerTiming(key);
|
|
1099
|
+
if (lastTail) {
|
|
1100
|
+
appendMetric(
|
|
1101
|
+
reqCtx._metricsStore,
|
|
1102
|
+
"ppr:tail",
|
|
1103
|
+
performance.now(),
|
|
1104
|
+
lastTail.completeMs ?? 0,
|
|
1105
|
+
undefined,
|
|
1106
|
+
// completeMs already rides as this entry's dur — drop it from desc.
|
|
1107
|
+
describeShellTailTiming({ ...lastTail, completeMs: undefined }),
|
|
1108
|
+
);
|
|
892
1109
|
}
|
|
893
|
-
return new Response(
|
|
894
|
-
JSON.stringify({
|
|
895
|
-
segments: serializedSegments,
|
|
896
|
-
handles,
|
|
897
|
-
routeName: match.routeName,
|
|
898
|
-
params: match.params,
|
|
899
|
-
}),
|
|
900
|
-
{ headers: { "Content-Type": "application/json" } },
|
|
901
|
-
);
|
|
902
1110
|
}
|
|
903
1111
|
|
|
904
1112
|
/** Assemble headers + tracking and run the foreground stage-driver render. */
|
|
@@ -985,7 +1193,7 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
985
1193
|
*/
|
|
986
1194
|
function scheduleNavigationShellCapture<TEnv>(
|
|
987
1195
|
input: RequestRenderInput<TEnv>,
|
|
988
|
-
|
|
1196
|
+
captureKey: string,
|
|
989
1197
|
): void {
|
|
990
1198
|
const { ctx, request, env, url, reqCtx } = input;
|
|
991
1199
|
const pprConfig = resolvePprConfig(reqCtx._classifiedRoute?.manifestEntry)!;
|
|
@@ -1008,8 +1216,7 @@ function scheduleNavigationShellCapture<TEnv>(
|
|
|
1008
1216
|
},
|
|
1009
1217
|
createShellCaptureDescriptor(
|
|
1010
1218
|
ctx,
|
|
1011
|
-
|
|
1012
|
-
buildNavigationShellKey(url, reqCtx._searchParamsFilter),
|
|
1219
|
+
captureKey,
|
|
1013
1220
|
shellSearchSeed(url, reqCtx._searchParamsFilter),
|
|
1014
1221
|
url.origin,
|
|
1015
1222
|
pprConfig,
|
|
@@ -1168,8 +1375,26 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1168
1375
|
return runMatch({ outcome: "BYPASS", reason: "cache-disabled" });
|
|
1169
1376
|
}
|
|
1170
1377
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1378
|
+
// The visitor's own partition (resolveShellPartition): replay never
|
|
1379
|
+
// crosses into another partition's shell.
|
|
1380
|
+
const baseKey = buildShellKey(url, reqCtx._searchParamsFilter);
|
|
1381
|
+
let key = baseKey;
|
|
1382
|
+
const partitionedKey = partitionedShellKey(
|
|
1383
|
+
baseKey,
|
|
1384
|
+
routeCacheScope,
|
|
1385
|
+
store,
|
|
1386
|
+
reqCtx.url.pathname,
|
|
1387
|
+
routeSnapshot?.params,
|
|
1388
|
+
);
|
|
1389
|
+
if (partitionedKey) {
|
|
1390
|
+
try {
|
|
1391
|
+
key = await partitionedKey;
|
|
1392
|
+
} catch (error) {
|
|
1393
|
+
reportCacheError(error, "cache-read", "[NavigationPPR] shell key");
|
|
1394
|
+
return runMatch({ outcome: "BYPASS", reason: "read-error" });
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
const navigationKey = navigationShellKey(key);
|
|
1173
1398
|
let cached: Awaited<ReturnType<typeof store.getShell>> = null;
|
|
1174
1399
|
try {
|
|
1175
1400
|
cached = await store.getShell(key, { claimRevalidation: false });
|
|
@@ -1193,11 +1418,7 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1193
1418
|
}
|
|
1194
1419
|
}
|
|
1195
1420
|
|
|
1196
|
-
if (
|
|
1197
|
-
!snapshot &&
|
|
1198
|
-
bypassReason !== "handler-live-holes" &&
|
|
1199
|
-
bypassReason !== "transition-when"
|
|
1200
|
-
) {
|
|
1421
|
+
if (!snapshot) {
|
|
1201
1422
|
let navigationCached: Awaited<ReturnType<typeof store.getShell>> = null;
|
|
1202
1423
|
try {
|
|
1203
1424
|
navigationCached = await store.getShell(navigationKey, {
|
|
@@ -1226,13 +1447,16 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1226
1447
|
// Production build manifests are local module data. In dev, resolving a
|
|
1227
1448
|
// missing build shell would foreground-fetch /__rsc_shell and block an
|
|
1228
1449
|
// otherwise ordinary navigation on capture, so replay remains runtime-only.
|
|
1229
|
-
const buildHit =
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1450
|
+
const buildHit =
|
|
1451
|
+
key !== baseKey
|
|
1452
|
+
? null
|
|
1453
|
+
: await lookupBuildShell(
|
|
1454
|
+
url,
|
|
1455
|
+
ctx.version,
|
|
1456
|
+
store,
|
|
1457
|
+
undefined,
|
|
1458
|
+
reqCtx._searchParamsFilter,
|
|
1459
|
+
);
|
|
1236
1460
|
if (buildHit?.stale) {
|
|
1237
1461
|
bypassReason ??= "stale-build-entry";
|
|
1238
1462
|
} else if (buildHit) {
|
|
@@ -1300,6 +1524,7 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1300
1524
|
reqCtx._pprReplayPostMatchReason === undefined &&
|
|
1301
1525
|
(shouldHealReplayMiss(bypassReason) ||
|
|
1302
1526
|
bypassReason === "no-segment-snapshot"),
|
|
1527
|
+
captureKey: navigationKey,
|
|
1303
1528
|
};
|
|
1304
1529
|
} finally {
|
|
1305
1530
|
reqCtx._shellImplicitCache = previousImplicitCache;
|
|
@@ -1314,10 +1539,13 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1314
1539
|
captureNeeded:
|
|
1315
1540
|
reqCtx._pprReplayPostMatchReason === undefined &&
|
|
1316
1541
|
shouldHealReplayMiss(bypassReason),
|
|
1542
|
+
captureKey: navigationKey,
|
|
1317
1543
|
};
|
|
1318
1544
|
}
|
|
1319
1545
|
|
|
1320
1546
|
const previousImplicitCache = reqCtx._shellImplicitCache;
|
|
1547
|
+
const previousLoaderSeed = reqCtx._shellLoaderSeed;
|
|
1548
|
+
let loaderSeed: ReturnType<typeof buildShellLoaderSeed> | undefined;
|
|
1321
1549
|
let segmentReplayHit = false;
|
|
1322
1550
|
let segmentReplayCorrupt = false;
|
|
1323
1551
|
let explicitCacheHit = false;
|
|
@@ -1329,8 +1557,18 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1329
1557
|
segmentsOnly: true,
|
|
1330
1558
|
}),
|
|
1331
1559
|
keyPrefix: "doc",
|
|
1332
|
-
|
|
1560
|
+
// Arms the bake-lane loader pins a document HIT serves (loader-cache.ts
|
|
1561
|
+
// resolveLoaderData), decoded only now: the lookup awaits this before
|
|
1562
|
+
// the route's loaders resolve, and an explicit-tier hit or a miss runs
|
|
1563
|
+
// the loaders fresh and never reads a pin.
|
|
1564
|
+
onHit: async () => {
|
|
1333
1565
|
segmentReplayHit = true;
|
|
1566
|
+
// Never rejects: the lookup awaits this inside its own try, and an
|
|
1567
|
+
// unreadable seed leaves those loaders to run fresh.
|
|
1568
|
+
const seed = await (loaderSeed ??= buildShellLoaderSeed(snapshot).catch(
|
|
1569
|
+
() => undefined,
|
|
1570
|
+
));
|
|
1571
|
+
if (seed) reqCtx._shellLoaderSeed = seed;
|
|
1334
1572
|
},
|
|
1335
1573
|
onCorrupt: () => {
|
|
1336
1574
|
segmentReplayCorrupt = true;
|
|
@@ -1378,6 +1616,7 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1378
1616
|
};
|
|
1379
1617
|
} finally {
|
|
1380
1618
|
reqCtx._shellImplicitCache = previousImplicitCache;
|
|
1619
|
+
reqCtx._shellLoaderSeed = previousLoaderSeed;
|
|
1381
1620
|
}
|
|
1382
1621
|
}
|
|
1383
1622
|
|
|
@@ -1422,6 +1661,75 @@ export function resolveShellHitRedirectTarget(
|
|
|
1422
1661
|
);
|
|
1423
1662
|
}
|
|
1424
1663
|
|
|
1664
|
+
/**
|
|
1665
|
+
* Buffer a finished tail timing for the next request's Server-Timing, and
|
|
1666
|
+
* under debugPerformance print it for this request (whose own Server-Timing
|
|
1667
|
+
* was sent at the commit, before the tail ran).
|
|
1668
|
+
*/
|
|
1669
|
+
function publishTailTiming(
|
|
1670
|
+
tailTiming: ShellTailTiming | null,
|
|
1671
|
+
metricsStore: MetricsStore | undefined,
|
|
1672
|
+
request: Request,
|
|
1673
|
+
url: URL,
|
|
1674
|
+
): void {
|
|
1675
|
+
if (!tailTiming) return;
|
|
1676
|
+
publishShellTailTiming(tailTiming);
|
|
1677
|
+
if (metricsStore) {
|
|
1678
|
+
console.log(
|
|
1679
|
+
`[RSC Perf] ${request.method} ${url.pathname} shell tail: ${describeShellTailTiming(tailTiming)}`,
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
const PENDING = Symbol("pending");
|
|
1685
|
+
|
|
1686
|
+
/**
|
|
1687
|
+
* Whether `value` is not a promise, or a promise that has already settled:
|
|
1688
|
+
* racing it against an already-resolved promise, a settled one's reaction is
|
|
1689
|
+
* queued first and wins.
|
|
1690
|
+
*/
|
|
1691
|
+
async function isSettled(value: unknown): Promise<boolean> {
|
|
1692
|
+
if (!(value instanceof Promise)) return true;
|
|
1693
|
+
return Promise.race([value, PENDING]).then(
|
|
1694
|
+
(first) => first !== PENDING,
|
|
1695
|
+
() => true,
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
/**
|
|
1700
|
+
* Evict a shell whose doc record cannot be replayed. The store has no shell
|
|
1701
|
+
* delete, so the entry is overwritten with a tombstone: a navigationOnly
|
|
1702
|
+
* entry with no document half and no snapshot, which document serving
|
|
1703
|
+
* treats as a MISS (shellServePlan, the build shell included) and partial
|
|
1704
|
+
* replay as `no-segment-snapshot`. The recapture overwrites it with a sound
|
|
1705
|
+
* entry. Only for a broken entry: a snapshot read that was merely slow
|
|
1706
|
+
* (`snapshotFailure: "unavailable"`) leaves the entry alone.
|
|
1707
|
+
*/
|
|
1708
|
+
async function degradeUnreplayableShell(
|
|
1709
|
+
descriptor: ShellCaptureDescriptor,
|
|
1710
|
+
entry: ShellCacheEntry,
|
|
1711
|
+
scheduleRecapture: () => void,
|
|
1712
|
+
): Promise<void> {
|
|
1713
|
+
const store = descriptor.store;
|
|
1714
|
+
try {
|
|
1715
|
+
await store?.putShell?.(
|
|
1716
|
+
descriptor.key,
|
|
1717
|
+
{
|
|
1718
|
+
reactVersion: entry.reactVersion,
|
|
1719
|
+
buildVersion: entry.buildVersion,
|
|
1720
|
+
navigationOnly: true,
|
|
1721
|
+
createdAt: Date.now(),
|
|
1722
|
+
},
|
|
1723
|
+
descriptor.ttl,
|
|
1724
|
+
0,
|
|
1725
|
+
);
|
|
1726
|
+
} catch (error) {
|
|
1727
|
+
reportCacheError(error, "cache-write", "[ShellServe] tombstone");
|
|
1728
|
+
}
|
|
1729
|
+
store?.dropShellMemo?.(descriptor.key);
|
|
1730
|
+
scheduleRecapture();
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1425
1733
|
/**
|
|
1426
1734
|
* Serve a validated shell HIT: commit the stored prelude bytes NOW and run the
|
|
1427
1735
|
* live tail behind them inside the response stream. Plain byte concatenation is
|
|
@@ -1439,15 +1747,18 @@ function serveShellHit(
|
|
|
1439
1747
|
reqCtx: RequestContext<any>,
|
|
1440
1748
|
handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
|
|
1441
1749
|
ssrModule: SSRModule,
|
|
1442
|
-
|
|
1750
|
+
document: ShellDocument,
|
|
1443
1751
|
descriptor: ShellCaptureDescriptor,
|
|
1752
|
+
nonce: string | undefined,
|
|
1444
1753
|
): Response {
|
|
1445
|
-
const preludeBytes =
|
|
1446
|
-
|
|
1447
|
-
//
|
|
1448
|
-
//
|
|
1754
|
+
const { entry, prelude: preludeBytes } = document;
|
|
1755
|
+
const metricsStore = reqCtx._metricsStore;
|
|
1756
|
+
// Per-stage tail timing for the `ppr:tail` Server-Timing mirror and the
|
|
1757
|
+
// perf console line: always in dev, in production only when this request
|
|
1758
|
+
// collects debugPerformance metrics. Offsets are relative to this commit
|
|
1759
|
+
// point.
|
|
1449
1760
|
const tailTiming: ShellTailTiming | null =
|
|
1450
|
-
process.env.NODE_ENV !== "production"
|
|
1761
|
+
process.env.NODE_ENV !== "production" || metricsStore
|
|
1451
1762
|
? {
|
|
1452
1763
|
key: descriptor.key,
|
|
1453
1764
|
outcome: "complete",
|
|
@@ -1455,6 +1766,8 @@ function serveShellHit(
|
|
|
1455
1766
|
}
|
|
1456
1767
|
: null;
|
|
1457
1768
|
const tailT0 = tailTiming ? performance.now() : 0;
|
|
1769
|
+
const scheduleRecapture = (): void =>
|
|
1770
|
+
scheduleShellCapture(ctx, request, env, url, reqCtx, ssrModule, descriptor);
|
|
1458
1771
|
|
|
1459
1772
|
const createTailContext = (): RequestContext<any> => {
|
|
1460
1773
|
const tailCtx: RequestContext<any> = Object.create(reqCtx);
|
|
@@ -1527,13 +1840,17 @@ function serveShellHit(
|
|
|
1527
1840
|
}
|
|
1528
1841
|
return observePhase(PHASES.ssr, () =>
|
|
1529
1842
|
ssrModule.resumeShellHTML!(rscStream, {
|
|
1530
|
-
postponed:
|
|
1843
|
+
postponed: document.postponed,
|
|
1531
1844
|
nonce: undefined,
|
|
1532
1845
|
// The shell key's own search — identical to the capture seed for
|
|
1533
1846
|
// this key, so the resume tree matches the captured tree.
|
|
1534
1847
|
search: shellSearchSeed(url, reqCtx._searchParamsFilter),
|
|
1535
1848
|
// The HIT request's origin — same host as the capture's (key-scoped).
|
|
1536
1849
|
origin: url.origin,
|
|
1850
|
+
// The document cache reads the list before storing this composite.
|
|
1851
|
+
onError: (error) => {
|
|
1852
|
+
reqCtx._renderErrors?.push(error);
|
|
1853
|
+
},
|
|
1537
1854
|
}),
|
|
1538
1855
|
);
|
|
1539
1856
|
};
|
|
@@ -1546,64 +1863,78 @@ function serveShellHit(
|
|
|
1546
1863
|
// values via a SeededShellStore overlay while unpinned reads (the holes)
|
|
1547
1864
|
// stay live. The overlay lives on a DERIVED context so the shared reqCtx is
|
|
1548
1865
|
// untouched.
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
`[Server][ppr] shell HIT: loader seed built +${Math.round(performance.now() - seedStart)}ms (abs ${Math.round(performance.now())})`,
|
|
1569
|
-
);
|
|
1570
|
-
}
|
|
1571
|
-
if (loaderSeed) seededCtx._shellLoaderSeed = loaderSeed;
|
|
1572
|
-
// Shell fast path (serve side): if the capture recorded the implicit doc
|
|
1573
|
-
// segment record and the handler layer declared no liveness, the tail's
|
|
1574
|
-
// cache lookup HITs the seeded doc entry — the handler layer is REPLAYED,
|
|
1575
|
-
// not re-executed (loaders still run fresh). Anything else degrades to
|
|
1576
|
-
// the full tail automatically.
|
|
1577
|
-
if (!entry.handlerLiveHoles && !entry.transitionWhen) {
|
|
1578
|
-
seededCtx._shellImplicitCache = {
|
|
1579
|
-
ttl: descriptor.ttl,
|
|
1580
|
-
swr: descriptor.swr,
|
|
1581
|
-
keyPrefix: "doc",
|
|
1582
|
-
};
|
|
1583
|
-
if (INTERNAL_RANGO_DEBUG) {
|
|
1584
|
-
console.log(
|
|
1585
|
-
`[Server][ppr] shell HIT: fast path armed (implicit doc cache) (abs ${Math.round(performance.now())})`,
|
|
1586
|
-
);
|
|
1587
|
-
}
|
|
1588
|
-
} else if (INTERNAL_RANGO_DEBUG) {
|
|
1589
|
-
console.log(
|
|
1590
|
-
`[Server][ppr] shell HIT: fast path declined — request-dependent handler/transition state; tail re-runs handlers (abs ${Math.round(performance.now())})`,
|
|
1591
|
-
);
|
|
1866
|
+
// A prelude-first read delivers the snapshot on its own promise: the
|
|
1867
|
+
// prelude is already committed, so only the tail waits for it.
|
|
1868
|
+
const inMemory = await isSettled(document.snapshot);
|
|
1869
|
+
const snapshot = await document.snapshot;
|
|
1870
|
+
// A snapshot already in memory (shell memo, memory store, build shells)
|
|
1871
|
+
// resolves at once, and the tail's seeding and match would run ahead of
|
|
1872
|
+
// the runtime writing the prelude: one macrotask first. A snapshot still
|
|
1873
|
+
// arriving on I/O has yielded by the time it resolves.
|
|
1874
|
+
if (inMemory) await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
1875
|
+
if (tailTiming) {
|
|
1876
|
+
tailTiming.snapshotMs = Math.round(performance.now() - tailT0);
|
|
1877
|
+
if (snapshot)
|
|
1878
|
+
tailTiming.snapshotRecords = countSnapshotFamilies(snapshot);
|
|
1879
|
+
tailTiming.snapshotPruned = entry.prunedRecords;
|
|
1880
|
+
const snapshotStats = document.stats?.snapshot;
|
|
1881
|
+
if (snapshotStats) {
|
|
1882
|
+
tailTiming.snapshotReadMs = Math.round(snapshotStats.readMs);
|
|
1883
|
+
tailTiming.snapshotBytes = snapshotStats.bytes;
|
|
1884
|
+
tailTiming.snapshotParseMs = Math.round(snapshotStats.parseMs);
|
|
1592
1885
|
}
|
|
1593
|
-
// Fragment splice (issue #700): store hits in THIS tail emit their stored
|
|
1594
|
-
// segment fragments verbatim (expanded by segment-fragments.ts). The flag
|
|
1595
|
-
// lives on the derived context so it can never leak into a capture render,
|
|
1596
|
-
// which serializes segments and must see real elements.
|
|
1597
|
-
seededCtx._shellFragmentPayload = true;
|
|
1598
|
-
return runWithRequestContext(seededCtx, () => renderTail(seededCtx));
|
|
1599
1886
|
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1887
|
+
const seededCtx = createTailContext();
|
|
1888
|
+
const records = snapshot ?? [];
|
|
1889
|
+
if (reqCtx._cacheStore && records.length > 0) {
|
|
1890
|
+
seededCtx._cacheStore = new SeededShellStore(reqCtx._cacheStore, records);
|
|
1891
|
+
}
|
|
1892
|
+
// Loader-family records (bake-lane containers, loader-container-bake):
|
|
1893
|
+
// decode into a seed Map for the resolveLoaderData overlay, so the
|
|
1894
|
+
// payload's baked container bytes match the frozen prelude while the
|
|
1895
|
+
// hole-marker paths keep the fresh run's live nested promises.
|
|
1896
|
+
const seedStart =
|
|
1897
|
+
INTERNAL_RANGO_DEBUG || tailTiming ? performance.now() : 0;
|
|
1898
|
+
const loaderSeed =
|
|
1899
|
+
records.length > 0 ? await buildShellLoaderSeed(records) : undefined;
|
|
1900
|
+
if (tailTiming) {
|
|
1901
|
+
const seededAt = performance.now();
|
|
1902
|
+
tailTiming.seedMs = Math.round(seededAt - tailT0);
|
|
1903
|
+
tailTiming.seedCpuMs = Math.round(seededAt - seedStart);
|
|
1904
|
+
}
|
|
1905
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
1906
|
+
console.log(
|
|
1907
|
+
`[Server][ppr] shell HIT: loader seed built +${Math.round(performance.now() - seedStart)}ms (abs ${Math.round(performance.now())})`,
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
if (loaderSeed) seededCtx._shellLoaderSeed = loaderSeed;
|
|
1911
|
+
// The handler layer is REPLAYED from the entry's own doc record, never
|
|
1912
|
+
// executed: the implicit doc scope replaces any route-derived scope
|
|
1913
|
+
// (docTail), looks the record up by the key the capture wrote
|
|
1914
|
+
// (fixedDocKey), and reads it from a segment-only overlay of the snapshot
|
|
1915
|
+
// (the real store never holds a doc: record). A lookup that does not hit
|
|
1916
|
+
// throws ShellRecordUnavailableError (withCacheLookup), handled by the
|
|
1917
|
+
// degrade below. Live and promise-carrying bake-lane loaders still run
|
|
1918
|
+
// fresh; a promise-free bake-lane loader is served from its pin
|
|
1919
|
+
// (_shellLoaderSeed, loader-cache.ts). A Prerender route's tail takes
|
|
1920
|
+
// the handler layer from the prerender store before this scope is
|
|
1921
|
+
// consulted.
|
|
1922
|
+
seededCtx._shellImplicitCache = {
|
|
1923
|
+
ttl: descriptor.ttl,
|
|
1924
|
+
swr: descriptor.swr,
|
|
1925
|
+
store: new SeededShellStore(reqCtx._cacheStore!, records, {
|
|
1926
|
+
segmentsOnly: true,
|
|
1927
|
+
}),
|
|
1928
|
+
keyPrefix: "doc",
|
|
1929
|
+
docTail: true,
|
|
1930
|
+
fixedDocKey: entry.docKey,
|
|
1931
|
+
};
|
|
1932
|
+
// Fragment splice (issue #700): store hits in THIS tail emit their stored
|
|
1933
|
+
// segment fragments verbatim (expanded by segment-fragments.ts). The flag
|
|
1934
|
+
// lives on the derived context so it can never leak into a capture render,
|
|
1935
|
+
// which serializes segments and must see real elements.
|
|
1936
|
+
seededCtx._shellFragmentPayload = true;
|
|
1937
|
+
return runWithRequestContext(seededCtx, () => renderTail(seededCtx));
|
|
1607
1938
|
})();
|
|
1608
1939
|
// The stream below is the only consumer; pre-attach a no-op catch so a tail
|
|
1609
1940
|
// failure before the stream is pulled never surfaces as an unhandled rejection.
|
|
@@ -1612,7 +1943,31 @@ function serveShellHit(
|
|
|
1612
1943
|
const serveStart = INTERNAL_RANGO_DEBUG ? performance.now() : 0;
|
|
1613
1944
|
const body = new ReadableStream<Uint8Array>({
|
|
1614
1945
|
async start(controller) {
|
|
1615
|
-
|
|
1946
|
+
// Fixed-size chunks (SHELL_PRELUDE_CHUNK_BYTES): a compressor in front
|
|
1947
|
+
// of the worker emits after each chunk instead of after the whole
|
|
1948
|
+
// prelude. slice() gives every chunk its own buffer.
|
|
1949
|
+
const commitStart = metricsStore ? performance.now() : 0;
|
|
1950
|
+
let chunks = 0;
|
|
1951
|
+
for (
|
|
1952
|
+
let offset = 0;
|
|
1953
|
+
offset < preludeBytes.length;
|
|
1954
|
+
offset += SHELL_PRELUDE_CHUNK_BYTES
|
|
1955
|
+
) {
|
|
1956
|
+
controller.enqueue(
|
|
1957
|
+
preludeBytes.slice(offset, offset + SHELL_PRELUDE_CHUNK_BYTES),
|
|
1958
|
+
);
|
|
1959
|
+
chunks++;
|
|
1960
|
+
}
|
|
1961
|
+
if (metricsStore) {
|
|
1962
|
+
appendMetric(
|
|
1963
|
+
metricsStore,
|
|
1964
|
+
"ppr:shell-commit",
|
|
1965
|
+
commitStart,
|
|
1966
|
+
performance.now() - commitStart,
|
|
1967
|
+
undefined,
|
|
1968
|
+
`cpu chunks=${chunks} prelude=${preludeBytes.length}b`,
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1616
1971
|
if (INTERNAL_RANGO_DEBUG) {
|
|
1617
1972
|
console.log(
|
|
1618
1973
|
`[Server][ppr] shell HIT: prelude enqueued (${preludeBytes.length}b) +${Math.round(performance.now() - serveStart)}ms`,
|
|
@@ -1684,7 +2039,10 @@ function serveShellHit(
|
|
|
1684
2039
|
);
|
|
1685
2040
|
controller.enqueue(
|
|
1686
2041
|
new TextEncoder().encode(
|
|
1687
|
-
|
|
2042
|
+
inlineShellScript(
|
|
2043
|
+
`location.replace(${escapeJsonForScript(JSON.stringify(safeTarget))})`,
|
|
2044
|
+
nonce,
|
|
2045
|
+
),
|
|
1688
2046
|
),
|
|
1689
2047
|
);
|
|
1690
2048
|
if (tailTiming) {
|
|
@@ -1692,9 +2050,34 @@ function serveShellHit(
|
|
|
1692
2050
|
tailTiming.completeMs = Math.round(performance.now() - tailT0);
|
|
1693
2051
|
}
|
|
1694
2052
|
}
|
|
1695
|
-
|
|
2053
|
+
publishTailTiming(tailTiming, metricsStore, request, url);
|
|
1696
2054
|
controller.close();
|
|
1697
2055
|
} catch (error) {
|
|
2056
|
+
if (error instanceof ShellRecordUnavailableError) {
|
|
2057
|
+
// The doc record could not supply the handler layer, and running
|
|
2058
|
+
// handlers behind a committed prelude is exactly what a HIT must
|
|
2059
|
+
// never do: reload the page into a forced MISS. A broken entry is
|
|
2060
|
+
// also replaced (tombstone, memo dropped, recapture); one whose
|
|
2061
|
+
// snapshot read was only slow stays, and the next request reads it
|
|
2062
|
+
// again.
|
|
2063
|
+
if ((await document.snapshotFailure) !== "unavailable") {
|
|
2064
|
+
await degradeUnreplayableShell(
|
|
2065
|
+
descriptor,
|
|
2066
|
+
entry,
|
|
2067
|
+
scheduleRecapture,
|
|
2068
|
+
);
|
|
2069
|
+
}
|
|
2070
|
+
controller.enqueue(
|
|
2071
|
+
new TextEncoder().encode(shellReloadScript(nonce)),
|
|
2072
|
+
);
|
|
2073
|
+
if (tailTiming) {
|
|
2074
|
+
tailTiming.outcome = "error";
|
|
2075
|
+
tailTiming.completeMs = Math.round(performance.now() - tailT0);
|
|
2076
|
+
}
|
|
2077
|
+
publishTailTiming(tailTiming, metricsStore, request, url);
|
|
2078
|
+
controller.close();
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
1698
2081
|
// Self-heal on a failed tail: errors the pre-commit gates cannot catch
|
|
1699
2082
|
// (mismatched postponed blob, hard render error above the holes) throw
|
|
1700
2083
|
// here AFTER the 200 + prelude flushed and would re-fail on every
|
|
@@ -1713,8 +2096,8 @@ function serveShellHit(
|
|
|
1713
2096
|
if (tailTiming) {
|
|
1714
2097
|
tailTiming.outcome = "error";
|
|
1715
2098
|
tailTiming.completeMs = Math.round(performance.now() - tailT0);
|
|
1716
|
-
publishShellTailTiming(tailTiming);
|
|
1717
2099
|
}
|
|
2100
|
+
publishTailTiming(tailTiming, metricsStore, request, url);
|
|
1718
2101
|
controller.error(error);
|
|
1719
2102
|
}
|
|
1720
2103
|
},
|