@rangojs/router 0.17.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/README.md +3 -2
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/event-controller.d.ts +3 -3
- package/dist/types/browser/history-state.d.ts +18 -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 +48 -2
- package/dist/types/cache/cache-policy.d.ts +18 -1
- package/dist/types/cache/cache-runtime.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +192 -26
- package/dist/types/cache/cache-tag.d.ts +116 -11
- 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 +224 -72
- 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 +19 -0
- package/dist/types/cache/document-cache.d.ts +10 -0
- package/dist/types/cache/handle-snapshot.d.ts +41 -8
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/invalidation-order.d.ts +23 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/request-tag-mask.d.ts +54 -0
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +141 -105
- package/dist/types/cache/tag-invalidation.d.ts +54 -3
- package/dist/types/cache/types.d.ts +267 -96
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
- 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/prerender/build-shell-capture.d.ts +11 -4
- 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/instrument.d.ts +8 -0
- 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/middleware-types.d.ts +6 -1
- package/dist/types/router/router-options.d.ts +4 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
- package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
- 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 +21 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/response-cache-serve.d.ts +15 -12
- package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
- package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
- package/dist/types/rsc/shell-capture.d.ts +202 -74
- package/dist/types/rsc/shell-serve.d.ts +144 -50
- package/dist/types/rsc/types.d.ts +9 -0
- package/dist/types/segment-system.d.ts +7 -0
- package/dist/types/server/context.d.ts +155 -33
- package/dist/types/server/cookie-store.d.ts +33 -0
- package/dist/types/server/handle-store.d.ts +52 -1
- package/dist/types/server/request-context.d.ts +210 -63
- package/dist/types/server/request-headers.d.ts +44 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +9 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- 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-loader.d.ts +18 -3
- package/dist/types/testing/run-transition-when.d.ts +44 -69
- package/dist/types/testing/serve-shell-request.d.ts +133 -0
- package/dist/types/testing/shell-status.d.ts +44 -19
- package/dist/types/transition-when-ref.d.ts +79 -0
- package/dist/types/types/cache-types.d.ts +15 -3
- package/dist/types/types/handler-context.d.ts +14 -1
- 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 +21 -18
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1005 -105
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +17 -7
- package/skills/cache-guide/SKILL.md +175 -76
- package/skills/caching/SKILL.md +361 -33
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +12 -7
- package/skills/cloudflare/SKILL.md +15 -0
- package/skills/comparison/references/framework-comparison.md +52 -37
- package/skills/defer-hydration/SKILL.md +50 -30
- package/skills/deployment-caching/SKILL.md +5 -2
- package/skills/document-cache/SKILL.md +32 -2
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/SKILL.md +3 -1
- package/skills/hooks/handle-and-actions.md +3 -1
- package/skills/hooks/navigation.md +23 -2
- package/skills/hooks/state.md +101 -7
- package/skills/intercept/SKILL.md +29 -12
- package/skills/loader/SKILL.md +197 -44
- package/skills/middleware/SKILL.md +34 -9
- package/skills/migrate-nextjs/SKILL.md +68 -37
- package/skills/migrate-react-router/SKILL.md +2 -1
- package/skills/migrate-react-router/component-migration.md +3 -1
- package/skills/migrate-react-router/data-and-actions.md +3 -1
- package/skills/migrate-react-router/route-mapping.md +7 -4
- package/skills/observability/SKILL.md +65 -0
- package/skills/parallel/SKILL.md +13 -8
- package/skills/ppr/SKILL.md +715 -371
- package/skills/prerender/SKILL.md +30 -17
- package/skills/rango/SKILL.md +72 -34
- package/skills/route/SKILL.md +14 -7
- package/skills/router-setup/SKILL.md +4 -2
- package/skills/scripts/SKILL.md +8 -1
- package/skills/server-actions/SKILL.md +11 -0
- package/skills/shell-manifest/SKILL.md +21 -12
- package/skills/testing/SKILL.md +49 -43
- package/skills/testing/cache-prerender.md +15 -14
- package/skills/testing/client-components.md +45 -3
- package/skills/testing/loader.md +19 -1
- 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 +202 -0
- package/skills/testing/server-actions.md +9 -3
- package/skills/theme/SKILL.md +56 -4
- package/skills/typesafety/env-and-bindings.md +18 -2
- package/skills/use-cache/SKILL.md +91 -23
- package/skills/vercel/SKILL.md +14 -2
- package/skills/view-transitions/SKILL.md +150 -29
- package/src/__augment-tests__/augmented.check.ts +23 -0
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/event-controller.ts +4 -4
- package/src/browser/history-state.ts +75 -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 +9 -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 +62 -2
- package/src/cache/cache-policy.ts +31 -4
- package/src/cache/cache-runtime.ts +209 -129
- package/src/cache/cache-scope.ts +573 -69
- package/src/cache/cache-tag.ts +475 -19
- 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 +1094 -376
- 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 +55 -40
- package/src/cache/document-cache.ts +53 -7
- package/src/cache/handle-snapshot.ts +53 -9
- package/src/cache/index.ts +2 -0
- package/src/cache/invalidation-order.ts +84 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/request-tag-mask.ts +147 -0
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +249 -277
- package/src/cache/tag-invalidation.ts +109 -18
- package/src/cache/types.ts +274 -102
- package/src/cache/vercel/vercel-cache-store.ts +561 -74
- 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 +173 -110
- package/src/route-definition/dsl-helpers.ts +6 -3
- 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 +42 -5
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +30 -8
- package/src/router/loader-resolution.ts +89 -33
- package/src/router/logging.ts +5 -3
- package/src/router/match-api.ts +89 -42
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +14 -29
- package/src/router/match-middleware/background-revalidation.ts +3 -5
- package/src/router/match-middleware/cache-lookup.ts +52 -19
- package/src/router/match-middleware/cache-store.ts +36 -10
- package/src/router/match-result.ts +1 -0
- package/src/router/middleware-types.ts +6 -1
- package/src/router/middleware.ts +20 -4
- package/src/router/navigation-snapshot.ts +6 -5
- package/src/router/prerender-match.ts +10 -0
- package/src/router/request-classification.ts +7 -4
- package/src/router/router-options.ts +4 -1
- package/src/router/segment-resolution/fresh.ts +18 -13
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +366 -70
- package/src/router/segment-resolution/loader-mask.ts +3 -4
- package/src/router/segment-resolution/loader-snapshot.ts +215 -126
- package/src/router/segment-resolution/mask-nested.ts +161 -42
- package/src/router/segment-resolution/revalidation.ts +27 -13
- 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 +47 -3
- package/src/rsc/handler.ts +24 -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 +10 -32
- package/src/rsc/response-cache-serve.ts +69 -78
- package/src/rsc/response-route-handler.ts +2 -2
- package/src/rsc/rsc-rendering.ts +712 -338
- package/src/rsc/server-action.ts +8 -20
- package/src/rsc/shell-build-manifest.ts +49 -29
- package/src/rsc/shell-capture-constants.ts +182 -12
- package/src/rsc/shell-capture.ts +1126 -612
- package/src/rsc/shell-serve.ts +331 -85
- package/src/rsc/ssr-setup.ts +3 -2
- package/src/rsc/types.ts +9 -0
- package/src/segment-system.tsx +40 -7
- package/src/server/context.ts +328 -72
- package/src/server/cookie-store.ts +184 -133
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +364 -100
- package/src/server/request-headers.ts +134 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +96 -37
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/testing/dispatch.ts +4 -7
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +14 -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 +67 -5
- package/src/testing/run-transition-when.ts +109 -172
- package/src/testing/serve-shell-request.ts +381 -0
- package/src/testing/shell-status.ts +73 -47
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/transition-when-ref.ts +220 -0
- package/src/types/cache-types.ts +15 -3
- package/src/types/handler-context.ts +14 -1
- 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 +21 -18
- package/src/vite/discovery/discover-routers.ts +3 -0
- package/src/vite/discovery/shell-prerender-phase.ts +9 -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 +89 -9
- 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,
|
|
@@ -13,15 +14,20 @@ import {
|
|
|
13
14
|
} from "../server/request-context.js";
|
|
14
15
|
import {
|
|
15
16
|
SeededShellStore,
|
|
17
|
+
ShellRecordUnavailableError,
|
|
16
18
|
buildShellLoaderSeed,
|
|
19
|
+
countSnapshotFamilies,
|
|
20
|
+
hasDocRecord,
|
|
17
21
|
} from "../cache/shell-snapshot.js";
|
|
18
22
|
import { appendMetric } from "../router/metrics.js";
|
|
23
|
+
import type { MetricsStore } from "../server/context.js";
|
|
19
24
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
20
25
|
import type { TraceSpan } from "../router/tracing.js";
|
|
21
26
|
import { getSSRSetup, createSsrHtmlStage, isRscRequest } from "./ssr-setup.js";
|
|
22
27
|
import type { RscPayload } from "./types.js";
|
|
23
28
|
import type { SSRModule } from "./types.js";
|
|
24
29
|
import type { RequestContext } from "../server/request-context.js";
|
|
30
|
+
import type { PprReplayBypassReason } from "./shell-capture-constants.js";
|
|
25
31
|
import {
|
|
26
32
|
createResponseWithMergedHeaders,
|
|
27
33
|
createSimpleRedirectResponse,
|
|
@@ -42,9 +48,12 @@ import {
|
|
|
42
48
|
type RoutinePlan,
|
|
43
49
|
} from "./routine-plan.js";
|
|
44
50
|
import type { HandlerContext } from "./handler-context.js";
|
|
45
|
-
import {
|
|
51
|
+
import { attachTransitionWhen } from "./attach-transition-when.js";
|
|
52
|
+
import { publicRouteName } from "../route-name.js";
|
|
46
53
|
import { buildFullPayload } from "./full-payload.js";
|
|
47
54
|
import {
|
|
55
|
+
capShellWindow,
|
|
56
|
+
lastStoredCaptureSeq,
|
|
48
57
|
scheduleShellCapture,
|
|
49
58
|
resolveShellCaptureDebugSink,
|
|
50
59
|
takeCaptureDebugEventForTiming,
|
|
@@ -59,7 +68,15 @@ import {
|
|
|
59
68
|
shellSearchSeed,
|
|
60
69
|
isValidShellHit,
|
|
61
70
|
hasIntactShellPayload,
|
|
62
|
-
|
|
71
|
+
openShellDocument,
|
|
72
|
+
navigationShellKey,
|
|
73
|
+
partitionShellKey,
|
|
74
|
+
inlineShellScript,
|
|
75
|
+
shellReloadScript,
|
|
76
|
+
partitionBuildShellCheckDone,
|
|
77
|
+
notePartitionBuildShellCheck,
|
|
78
|
+
warnPprWindowCappedOnce,
|
|
79
|
+
SHELL_PRELUDE_CHUNK_BYTES,
|
|
63
80
|
hasShellFamily,
|
|
64
81
|
warnShellStoreMissingOnce,
|
|
65
82
|
warnPprNonceActiveOnce,
|
|
@@ -67,13 +84,19 @@ import {
|
|
|
67
84
|
publishShellTailTiming,
|
|
68
85
|
takeShellTailTimingForServerTiming,
|
|
69
86
|
type ResolvedPprConfig,
|
|
87
|
+
type ShellDocument,
|
|
70
88
|
type ShellTailTiming,
|
|
71
89
|
} from "./shell-serve.js";
|
|
72
90
|
import {
|
|
91
|
+
hasBuildShell,
|
|
73
92
|
lookupBuildShell,
|
|
74
93
|
type DevShellLookup,
|
|
75
94
|
} from "./shell-build-manifest.js";
|
|
76
95
|
import { contextGet } from "../context-var.js";
|
|
96
|
+
import {
|
|
97
|
+
resolveSwrWindow,
|
|
98
|
+
staleShellRecaptureDue,
|
|
99
|
+
} from "../cache/cache-policy.js";
|
|
77
100
|
import {
|
|
78
101
|
getNavigationContextHeader,
|
|
79
102
|
prerenderStoreShortCircuits,
|
|
@@ -88,13 +111,17 @@ import {
|
|
|
88
111
|
safeSameOriginLanding,
|
|
89
112
|
} from "../redirect-origin.js";
|
|
90
113
|
import { nonce as nonceToken } from "./nonce.js";
|
|
114
|
+
import { escapeJsonForScript } from "../escape-script.js";
|
|
115
|
+
import { resolveShellPartition } from "../cache/cache-scope.js";
|
|
91
116
|
import { reportCacheError } from "../cache/cache-error.js";
|
|
92
117
|
import type { SearchParamsFilter } from "../cache/search-params-filter.js";
|
|
93
118
|
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
94
119
|
import type {
|
|
95
|
-
DocumentShellCacheEntry,
|
|
96
120
|
SegmentCacheStore,
|
|
97
121
|
ShellCacheEntry,
|
|
122
|
+
ShellDocumentRead,
|
|
123
|
+
ShellEntryHead,
|
|
124
|
+
ShellReadStats,
|
|
98
125
|
ShellSnapshotRecord,
|
|
99
126
|
} from "../cache/types.js";
|
|
100
127
|
import {
|
|
@@ -102,27 +129,6 @@ import {
|
|
|
102
129
|
SEGMENT_FRAGMENT_RECOVERY_HEADER,
|
|
103
130
|
} from "../segment-fragments.js";
|
|
104
131
|
|
|
105
|
-
type PprReplayBypassReason =
|
|
106
|
-
| "method"
|
|
107
|
-
| "dynamic"
|
|
108
|
-
| "nonce"
|
|
109
|
-
| "store-unavailable"
|
|
110
|
-
| "passive-read-unsupported"
|
|
111
|
-
| "no-navigation-context"
|
|
112
|
-
| "prerender-store"
|
|
113
|
-
| "intercept"
|
|
114
|
-
| "cache-disabled"
|
|
115
|
-
| "read-error"
|
|
116
|
-
| "no-entry"
|
|
117
|
-
| "invalid-version"
|
|
118
|
-
| "corrupt-entry"
|
|
119
|
-
| "handler-live-holes"
|
|
120
|
-
| "transition-when"
|
|
121
|
-
| "no-segment-snapshot"
|
|
122
|
-
| "snapshot-miss"
|
|
123
|
-
| "explicit-cache-hit"
|
|
124
|
-
| "stale-build-entry";
|
|
125
|
-
|
|
126
132
|
type PprReplayStatus =
|
|
127
133
|
| { outcome: "HIT"; freshness: "fresh" | "stale" }
|
|
128
134
|
| { outcome: "BYPASS"; reason: PprReplayBypassReason };
|
|
@@ -143,13 +149,6 @@ function describePprReplayStatus(status: PprReplayStatus): string {
|
|
|
143
149
|
: `bypass:${status.reason}`;
|
|
144
150
|
}
|
|
145
151
|
|
|
146
|
-
function buildNavigationShellKey(
|
|
147
|
-
url: URL,
|
|
148
|
-
filter?: SearchParamsFilter,
|
|
149
|
-
): string {
|
|
150
|
-
return `${buildShellKey(url, filter)}:navigation`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
152
|
function createShellCaptureDescriptor(
|
|
154
153
|
ctx: HandlerContext<any>,
|
|
155
154
|
key: string,
|
|
@@ -173,7 +172,6 @@ function createShellCaptureDescriptor(
|
|
|
173
172
|
tags: pprConfig.tags,
|
|
174
173
|
captureTimeout: pprConfig.captureTimeout,
|
|
175
174
|
store,
|
|
176
|
-
debug: INTERNAL_RANGO_DEBUG,
|
|
177
175
|
maxSnapshotBytes: pprConfig.maxSnapshotBytes,
|
|
178
176
|
debugSink: resolveShellCaptureDebugSink(ctx.router.debugShellCapture),
|
|
179
177
|
navigationOnly,
|
|
@@ -186,8 +184,7 @@ function shouldHealReplayMiss(
|
|
|
186
184
|
return (
|
|
187
185
|
reason === undefined ||
|
|
188
186
|
reason === "invalid-version" ||
|
|
189
|
-
reason === "corrupt-entry"
|
|
190
|
-
reason === "stale-build-entry"
|
|
187
|
+
reason === "corrupt-entry"
|
|
191
188
|
);
|
|
192
189
|
}
|
|
193
190
|
|
|
@@ -242,35 +239,11 @@ function replayableShellSnapshot(
|
|
|
242
239
|
if (!entry.navigationOnly && !hasIntactShellPayload(entry)) {
|
|
243
240
|
return { reason: "corrupt-entry" };
|
|
244
241
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// and counting those declared entries "replayable" that then always missed
|
|
251
|
-
// (a timing-dependent snapshot-miss/no-segment-snapshot flip-flop). Entries
|
|
252
|
-
// captured before the field existed read as no-segment-snapshot and heal on
|
|
253
|
-
// recapture (pre-release, same treatment as the buildVersion field).
|
|
254
|
-
const docKey = entry.docKey;
|
|
255
|
-
const snapshot = entry.snapshot;
|
|
256
|
-
const hasDocRecord =
|
|
257
|
-
docKey !== undefined &&
|
|
258
|
-
snapshot?.some((record) => {
|
|
259
|
-
if (
|
|
260
|
-
!record ||
|
|
261
|
-
typeof record !== "object" ||
|
|
262
|
-
record.family !== "segment" ||
|
|
263
|
-
record.key !== docKey ||
|
|
264
|
-
typeof record.value !== "object" ||
|
|
265
|
-
record.value === null
|
|
266
|
-
) {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
const segments = (record.value as { segments?: unknown }).segments;
|
|
270
|
-
return Array.isArray(segments) && segments.length > 0;
|
|
271
|
-
});
|
|
272
|
-
return hasDocRecord && snapshot
|
|
273
|
-
? { 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 }
|
|
274
247
|
: { reason: "no-segment-snapshot" };
|
|
275
248
|
}
|
|
276
249
|
|
|
@@ -324,10 +297,11 @@ type PreparedRender =
|
|
|
324
297
|
| {
|
|
325
298
|
kind: "payload";
|
|
326
299
|
payload: RscPayload;
|
|
327
|
-
|
|
300
|
+
/** The response depends on the source page: an intercept targets the route. */
|
|
301
|
+
sourceScoped: boolean;
|
|
328
302
|
pprReplayStatus?: PprReplayStatus;
|
|
329
|
-
|
|
330
|
-
|
|
303
|
+
/** The key a navigation-only heal capture stores under, when one is needed. */
|
|
304
|
+
healKey?: string;
|
|
331
305
|
};
|
|
332
306
|
|
|
333
307
|
async function handleRscRenderingInner<TEnv>(
|
|
@@ -439,21 +413,47 @@ function* requestRenderPlan<TEnv>(
|
|
|
439
413
|
response.headers.set(SHELL_STATUS_HEADER, "MISS");
|
|
440
414
|
}
|
|
441
415
|
|
|
416
|
+
const healKey = prepared.healKey;
|
|
442
417
|
if (
|
|
443
418
|
isPartial &&
|
|
444
|
-
|
|
419
|
+
healKey !== undefined &&
|
|
445
420
|
!reqCtx._dynamic &&
|
|
446
421
|
response.status === 200
|
|
447
422
|
) {
|
|
448
|
-
const captureKey = prepared.partialCaptureKey;
|
|
449
423
|
yield* handoff("navigation-shell-capture", () =>
|
|
450
|
-
scheduleNavigationShellCapture(input,
|
|
424
|
+
scheduleNavigationShellCapture(input, healKey),
|
|
451
425
|
);
|
|
452
426
|
}
|
|
453
427
|
|
|
454
428
|
return response;
|
|
455
429
|
}
|
|
456
430
|
|
|
431
|
+
/**
|
|
432
|
+
* Dev: warn once per route when the route's cache() entry reduces an explicit
|
|
433
|
+
* ppr window. The window is what capShellWindow gives a capture from a
|
|
434
|
+
* record written just now, the widest it can get; null (a cache() ttl + swr
|
|
435
|
+
* of 0) is the capture's own warning.
|
|
436
|
+
*/
|
|
437
|
+
function warnIfPprWindowReduced(
|
|
438
|
+
routeName: string,
|
|
439
|
+
pprOption: { ttl?: number; swr?: number },
|
|
440
|
+
pprConfig: ResolvedPprConfig,
|
|
441
|
+
cacheScope: { ttl: number; swr: number | undefined },
|
|
442
|
+
store: SegmentCacheStore<any>,
|
|
443
|
+
): void {
|
|
444
|
+
const cache = { ttl: cacheScope.ttl, swr: cacheScope.swr ?? 0 };
|
|
445
|
+
const shell = capShellWindow(
|
|
446
|
+
pprConfig.ttl,
|
|
447
|
+
resolveSwrWindow(pprConfig.swr, store.defaults),
|
|
448
|
+
{
|
|
449
|
+
freshUntil: cache.ttl * 1000,
|
|
450
|
+
staleUntil: (cache.ttl + cache.swr) * 1000,
|
|
451
|
+
},
|
|
452
|
+
0,
|
|
453
|
+
);
|
|
454
|
+
if (shell) warnPprWindowCappedOnce(routeName, pprOption, shell, cache);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
457
|
/**
|
|
458
458
|
* Axis 2 shell serve. Sync gates stay plan code; store and SSR effects run as
|
|
459
459
|
* named steps. Outcomes: serve a composed shell response, report a MISS (the
|
|
@@ -474,36 +474,88 @@ function* shellServePlan<TEnv>(
|
|
|
474
474
|
) {
|
|
475
475
|
return { kind: "pass" };
|
|
476
476
|
}
|
|
477
|
-
const
|
|
477
|
+
const manifestEntry = reqCtx._classifiedRoute?.manifestEntry;
|
|
478
|
+
const pprConfig = resolvePprConfig(manifestEntry);
|
|
478
479
|
if (!pprConfig) return { kind: "pass" };
|
|
480
|
+
// A degraded HIT's reload (shellReloadScript) renders like a cache miss:
|
|
481
|
+
// no shell read, no capture, so it can never degrade again. The handler
|
|
482
|
+
// stripped the marker from the request and left this flag.
|
|
483
|
+
if (reqCtx._shellForcedMiss) return { kind: "pass" };
|
|
479
484
|
|
|
480
485
|
// A per-request CSP nonce pins the route to axis 1: a shared shell would
|
|
481
486
|
// freeze the capture request's nonce and CSP would reject it for every
|
|
482
|
-
// other visitor.
|
|
483
|
-
//
|
|
484
|
-
// the provider-only check missed the latter (issue #656).
|
|
485
|
-
const activeNonce = nonce ?? contextGet(reqCtx._variables, nonceToken);
|
|
487
|
+
// other visitor. Every step below, a HIT included, therefore runs without
|
|
488
|
+
// one.
|
|
486
489
|
const store = reqCtx._cacheStore;
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
if (activeNonce !== undefined) {
|
|
490
|
+
const baseKey = buildShellKey(url, reqCtx._searchParamsFilter);
|
|
491
|
+
if (activeRequestNonce(nonce, reqCtx) !== undefined) {
|
|
490
492
|
// Declared intent that cannot be honored deserves a diagnostic (unlike an
|
|
491
493
|
// undeclared route, which is silent): a ppr route gated off by an active
|
|
492
494
|
// per-request nonce warns once per key. Axis 1 after the warning.
|
|
493
|
-
warnPprNonceActiveOnce(
|
|
495
|
+
warnPprNonceActiveOnce(baseKey);
|
|
494
496
|
return { kind: "pass" };
|
|
495
497
|
}
|
|
496
498
|
if (!hasShellFamily(store)) {
|
|
497
499
|
// Declared intent that cannot be honored deserves a diagnostic (unlike an
|
|
498
500
|
// undeclared route, which is silent). Axis 1 after the warning.
|
|
499
|
-
warnShellStoreMissingOnce(
|
|
501
|
+
warnShellStoreMissingOnce(baseKey);
|
|
500
502
|
return { kind: "pass" };
|
|
501
503
|
}
|
|
502
|
-
|
|
504
|
+
// The route's own cache() opt-out — cache(false), or a condition() that
|
|
505
|
+
// refuses THIS request — is absolute, and a HIT tail never consults the
|
|
506
|
+
// route scope (it replays the shell's doc record), so it is decided here,
|
|
507
|
+
// before any shell byte: the request renders like a cache miss (axis 1) and
|
|
508
|
+
// schedules no capture (the capture could not record the handler layer).
|
|
509
|
+
const route = classifiedRouteSnapshot(reqCtx);
|
|
510
|
+
if (route?.cacheScope && !route.cacheScope.allowsCache("read")) {
|
|
511
|
+
return { kind: "pass" };
|
|
512
|
+
}
|
|
513
|
+
// The shell is capped to the route cache() entry it captures from
|
|
514
|
+
// (capShellWindow); an explicit ppr window the cap reduces warns in dev.
|
|
515
|
+
// resolvePprConfig returned a config, so the entry is a ppr route.
|
|
516
|
+
const pprOption = manifestEntry?.type === "route" && manifestEntry.ppr;
|
|
517
|
+
if (
|
|
518
|
+
process.env.NODE_ENV !== "production" &&
|
|
519
|
+
route?.cacheScope?.enabled &&
|
|
520
|
+
typeof pprOption === "object" &&
|
|
521
|
+
(pprOption.ttl !== undefined || pprOption.swr !== undefined)
|
|
522
|
+
) {
|
|
523
|
+
warnIfPprWindowReduced(
|
|
524
|
+
reqCtx._classifiedRoute?.routeKey ?? url.pathname,
|
|
525
|
+
pprOption,
|
|
526
|
+
pprConfig,
|
|
527
|
+
route.cacheScope,
|
|
528
|
+
store,
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
// A route whose cache() record is partitioned by the request (its `key()`,
|
|
532
|
+
// or the store's keyGenerator) partitions its shell the same way: each
|
|
533
|
+
// partition captures and serves its own shell. A failed key resolution
|
|
534
|
+
// serves no shell, never another partition's.
|
|
535
|
+
const requestKey = resolveRequestShellKey(
|
|
536
|
+
baseKey,
|
|
537
|
+
route,
|
|
538
|
+
store,
|
|
539
|
+
reqCtx,
|
|
540
|
+
"ShellServe",
|
|
541
|
+
);
|
|
542
|
+
const key =
|
|
543
|
+
typeof requestKey === "string"
|
|
544
|
+
? requestKey
|
|
545
|
+
: yield* step("shell-partition", () => requestKey);
|
|
546
|
+
if (key === undefined) return { kind: "pass" };
|
|
547
|
+
reqCtx._shellKey = key;
|
|
548
|
+
// The buffered capture and tail timings are keyed by the partitioned key.
|
|
549
|
+
mirrorPprServerTimings(key, reqCtx);
|
|
550
|
+
|
|
551
|
+
// A MISS schedules its capture after rendering: taken before the read, this
|
|
552
|
+
// lets that capture skip itself when another request's capture stored the
|
|
553
|
+
// shell in between (ShellCaptureDescriptor.storedSeqAtRead).
|
|
554
|
+
const storedSeqAtRead = lastStoredCaptureSeq(key);
|
|
503
555
|
// SSR setup starts before route handling, so read the shell before joining it
|
|
504
556
|
// to overlap the remaining setup work with cache I/O.
|
|
505
557
|
const cached = yield* step("shell-read", () =>
|
|
506
|
-
readShellEntry(store, key, reqCtx),
|
|
558
|
+
readShellEntry(store, key, reqCtx, pprConfig.tags),
|
|
507
559
|
);
|
|
508
560
|
|
|
509
561
|
// allReady (ssr.resolveStreaming) bypasses PPR entirely: buffering defeats
|
|
@@ -527,13 +579,26 @@ function* shellServePlan<TEnv>(
|
|
|
527
579
|
pprConfig,
|
|
528
580
|
store,
|
|
529
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
|
+
};
|
|
530
590
|
|
|
591
|
+
// A HIT replays the handler layer from the entry's doc record (`docKey`),
|
|
592
|
+
// or, for a Prerender route, from the prerender store. An entry that has
|
|
593
|
+
// neither is a MISS: serving it would run handlers after the commit.
|
|
531
594
|
if (
|
|
532
595
|
cached &&
|
|
533
596
|
!cached.entry.navigationOnly &&
|
|
597
|
+
(cached.entry.docKey !== undefined || route?.matched?.pr === true) &&
|
|
534
598
|
isValidShellHit(cached.entry, ctx.version)
|
|
535
599
|
) {
|
|
536
|
-
|
|
600
|
+
const document = openShellDocumentMetered(reqCtx, cached);
|
|
601
|
+
if (!document) {
|
|
537
602
|
// Corrupt stored payload (undecodable prelude / unparseable
|
|
538
603
|
// postponed): a store-layer fault worth a diagnostic, unlike the
|
|
539
604
|
// silent version-mismatch lifecycle misses above. Degrade to MISS
|
|
@@ -548,8 +613,12 @@ function* shellServePlan<TEnv>(
|
|
|
548
613
|
);
|
|
549
614
|
} else {
|
|
550
615
|
// Stale (SWR) hit: serve the stale shell now, recapture in the
|
|
551
|
-
// background (stampede-guarded + backoff inside scheduleShellCapture)
|
|
552
|
-
|
|
616
|
+
// background (stampede-guarded + backoff inside scheduleShellCapture),
|
|
617
|
+
// at most once per SHELL_MIN_RECAPTURE_INTERVAL_MS of the shell's age.
|
|
618
|
+
if (
|
|
619
|
+
cached.shouldRevalidate &&
|
|
620
|
+
staleShellRecaptureDue(cached.entry, Date.now())
|
|
621
|
+
) {
|
|
553
622
|
yield* handoff("shell-recapture", () =>
|
|
554
623
|
scheduleShellCapture(
|
|
555
624
|
ctx,
|
|
@@ -562,7 +631,6 @@ function* shellServePlan<TEnv>(
|
|
|
562
631
|
),
|
|
563
632
|
);
|
|
564
633
|
}
|
|
565
|
-
const entry = cached.entry;
|
|
566
634
|
const response = yield* step("shell-hit", () =>
|
|
567
635
|
serveShellHit(
|
|
568
636
|
ctx,
|
|
@@ -572,7 +640,7 @@ function* shellServePlan<TEnv>(
|
|
|
572
640
|
reqCtx,
|
|
573
641
|
handleStore,
|
|
574
642
|
ssrModule,
|
|
575
|
-
|
|
643
|
+
document,
|
|
576
644
|
descriptor,
|
|
577
645
|
),
|
|
578
646
|
);
|
|
@@ -580,26 +648,63 @@ function* shellServePlan<TEnv>(
|
|
|
580
648
|
}
|
|
581
649
|
}
|
|
582
650
|
|
|
651
|
+
// A runtime tombstone (degradeUnreplayableShell: this key's HIT could not
|
|
652
|
+
// replay its handler layer) shadows the build shell too, or a build shell
|
|
653
|
+
// whose tail cannot be replayed would degrade on every request until a
|
|
654
|
+
// runtime capture lands.
|
|
655
|
+
if (
|
|
656
|
+
cached?.entry.navigationOnly &&
|
|
657
|
+
isValidShellHit(cached.entry, ctx.version)
|
|
658
|
+
) {
|
|
659
|
+
return miss;
|
|
660
|
+
}
|
|
661
|
+
|
|
583
662
|
// Build-time shell read-through (producer B, #699): on a runtime
|
|
584
663
|
// store MISS a Prerender+ppr route serves its `vite build`-baked
|
|
585
|
-
// shell through the SAME serve path.
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
//
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
664
|
+
// shell through the SAME serve path. A partitioned route has none: the
|
|
665
|
+
// build captured one partition, which no request may be assumed to
|
|
666
|
+
// share, and a route that has a build shell says so once. lookupBuildShell
|
|
667
|
+
// owns the validity gates and fails to null; openShellDocument below is
|
|
668
|
+
// the prelude's integrity check and only decode (either failing leaves the
|
|
669
|
+
// ordinary MISS path); past ppr.ttl the baked entry still serves while SWR
|
|
670
|
+
// recaptures — the upgrade path from build entry to runtime entry.
|
|
671
|
+
//
|
|
672
|
+
// Dev: no build manifest exists; producer B runs on demand via the dev
|
|
673
|
+
// server's /__rsc_shell endpoint for PRERENDERED routes only (production's
|
|
674
|
+
// exact candidate set). Folded away in production builds (NODE_ENV is a
|
|
675
|
+
// compile-time constant).
|
|
676
|
+
const devShellLookup = resolveDevShellLookup(reqCtx, pprConfig);
|
|
677
|
+
let buildHit: Awaited<ReturnType<typeof lookupBuildShell>> = null;
|
|
678
|
+
if (key !== baseKey) {
|
|
679
|
+
// Probed once per path without a build shell, and not again once the
|
|
680
|
+
// route warned. A search-bearing URL never reads one: nothing to probe.
|
|
681
|
+
const routeName = reqCtx._classifiedRoute?.routeKey;
|
|
682
|
+
if (
|
|
683
|
+
!partitionBuildShellCheckDone(url.pathname, routeName) &&
|
|
684
|
+
shellSearchSeed(url, reqCtx._searchParamsFilter) === ""
|
|
685
|
+
) {
|
|
686
|
+
const found = yield* step("build-shell-check", () =>
|
|
687
|
+
hasBuildShell(url.pathname, devShellLookup),
|
|
688
|
+
);
|
|
689
|
+
notePartitionBuildShellCheck(url.pathname, routeName, found);
|
|
690
|
+
}
|
|
691
|
+
} else {
|
|
692
|
+
buildHit = yield* step("build-shell-lookup", () =>
|
|
693
|
+
lookupBuildShell(
|
|
694
|
+
url,
|
|
695
|
+
ctx.version,
|
|
696
|
+
store,
|
|
697
|
+
devShellLookup,
|
|
698
|
+
reqCtx._searchParamsFilter,
|
|
699
|
+
),
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
// The document serve decodes the baked prelude once here; an undecodable
|
|
703
|
+
// one is a MISS like a corrupt runtime entry.
|
|
704
|
+
const buildDocument = buildHit
|
|
705
|
+
? openShellDocumentMetered(reqCtx, { entry: buildHit.entry })
|
|
706
|
+
: null;
|
|
707
|
+
if (buildHit && buildDocument) {
|
|
603
708
|
// Past ppr.ttl: still serve the baked entry, recapture upgrades it.
|
|
604
709
|
if (buildHit.stale) {
|
|
605
710
|
yield* handoff("shell-recapture", () =>
|
|
@@ -623,16 +728,14 @@ function* shellServePlan<TEnv>(
|
|
|
623
728
|
reqCtx,
|
|
624
729
|
handleStore,
|
|
625
730
|
ssrModule,
|
|
626
|
-
|
|
731
|
+
buildDocument,
|
|
627
732
|
descriptor,
|
|
628
733
|
),
|
|
629
734
|
);
|
|
630
735
|
return { kind: "serve", response };
|
|
631
736
|
}
|
|
632
737
|
|
|
633
|
-
|
|
634
|
-
// + a background capture scheduled once the response is known servable.
|
|
635
|
-
return { kind: "miss", descriptor, ssrModule };
|
|
738
|
+
return miss;
|
|
636
739
|
}
|
|
637
740
|
|
|
638
741
|
/**
|
|
@@ -652,10 +755,7 @@ function* preparePayloadPlan<TEnv>(
|
|
|
652
755
|
);
|
|
653
756
|
const result = replay.result;
|
|
654
757
|
const pprReplayStatus = replay.status;
|
|
655
|
-
const
|
|
656
|
-
"captureNeeded" in replay && replay.captureNeeded === true;
|
|
657
|
-
const partialCaptureKey =
|
|
658
|
-
"captureKey" in replay ? replay.captureKey : undefined;
|
|
758
|
+
const healKey = "healKey" in replay ? replay.healKey : undefined;
|
|
659
759
|
|
|
660
760
|
if (!result) {
|
|
661
761
|
// Fall back to full render
|
|
@@ -684,10 +784,9 @@ function* preparePayloadPlan<TEnv>(
|
|
|
684
784
|
return {
|
|
685
785
|
kind: "payload",
|
|
686
786
|
payload: buildFullPayload(match, ctx, url, reqCtx, handleStore),
|
|
687
|
-
|
|
787
|
+
sourceScoped: false,
|
|
688
788
|
pprReplayStatus,
|
|
689
|
-
|
|
690
|
-
partialCaptureKey,
|
|
789
|
+
healKey,
|
|
691
790
|
};
|
|
692
791
|
}
|
|
693
792
|
|
|
@@ -702,15 +801,12 @@ function* preparePayloadPlan<TEnv>(
|
|
|
702
801
|
// intercepted server-side (X-RSC-Reload) and never delivers a
|
|
703
802
|
// different-router payload to the client.
|
|
704
803
|
routerId: ctx.router.id,
|
|
705
|
-
segments:
|
|
706
|
-
result.segments,
|
|
707
|
-
reqCtx,
|
|
708
|
-
ctx.router.onError,
|
|
709
|
-
),
|
|
804
|
+
segments: attachTransitionWhen(result.segments, reqCtx),
|
|
710
805
|
matched: result.matched,
|
|
711
806
|
diff: result.diff,
|
|
712
807
|
resolvedIds: result.resolvedIds,
|
|
713
808
|
params: result.params,
|
|
809
|
+
routeName: publicRouteName(result.routeName),
|
|
714
810
|
isPartial: true,
|
|
715
811
|
slots: result.slots,
|
|
716
812
|
interceptTargets: result.interceptTargets,
|
|
@@ -723,10 +819,9 @@ function* preparePayloadPlan<TEnv>(
|
|
|
723
819
|
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
724
820
|
},
|
|
725
821
|
},
|
|
726
|
-
|
|
822
|
+
sourceScoped: result.interceptTargeted === true,
|
|
727
823
|
pprReplayStatus,
|
|
728
|
-
|
|
729
|
-
partialCaptureKey,
|
|
824
|
+
healKey,
|
|
730
825
|
};
|
|
731
826
|
}
|
|
732
827
|
|
|
@@ -745,14 +840,10 @@ function* preparePayloadPlan<TEnv>(
|
|
|
745
840
|
};
|
|
746
841
|
}
|
|
747
842
|
|
|
748
|
-
// Caching is now handled in router.match() via cache provider in request context
|
|
749
|
-
// match.segments already contains cached or fresh segments as appropriate
|
|
750
|
-
|
|
751
843
|
return {
|
|
752
844
|
kind: "payload",
|
|
753
845
|
payload: buildFullPayload(match, ctx, url, reqCtx, handleStore),
|
|
754
|
-
|
|
755
|
-
partialCaptureNeeded: false,
|
|
846
|
+
sourceScoped: false,
|
|
756
847
|
};
|
|
757
848
|
}
|
|
758
849
|
|
|
@@ -782,53 +873,227 @@ async function matchPartialAndRecordParams<TEnv>(
|
|
|
782
873
|
return replay;
|
|
783
874
|
}
|
|
784
875
|
|
|
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
|
+
};
|
|
888
|
+
|
|
785
889
|
/**
|
|
786
890
|
* Shell store read that degrades to null on failure (axis 1 MISS, never a 500)
|
|
787
891
|
* and records the raw `ppr:shell-read` outcome (pre-validity-gates, so a
|
|
788
892
|
* version-mismatch lifecycle miss stays distinguishable from a store miss).
|
|
893
|
+
* A store with a prelude-first read (readShellDocument) resolves before the
|
|
894
|
+
* snapshot is read, so the HIT commits without waiting for it.
|
|
789
895
|
*/
|
|
790
|
-
async function readShellEntry
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
896
|
+
async function readShellEntry(
|
|
897
|
+
store: SegmentCacheStore & {
|
|
898
|
+
getShell: NonNullable<SegmentCacheStore["getShell"]>;
|
|
899
|
+
},
|
|
794
900
|
key: string,
|
|
795
901
|
reqCtx: ReturnType<typeof getRequestContext>,
|
|
796
|
-
|
|
797
|
-
|
|
902
|
+
tagHints: readonly string[] | undefined,
|
|
903
|
+
): Promise<ShellStoreRead | null> {
|
|
904
|
+
let cached: ShellStoreRead | null = null;
|
|
798
905
|
const shellReadStart = reqCtx._metricsStore ? performance.now() : 0;
|
|
799
906
|
try {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
907
|
+
if (store.readShellDocument) {
|
|
908
|
+
const read = await store.readShellDocument(
|
|
909
|
+
key,
|
|
910
|
+
tagHints ? { tagHints } : undefined,
|
|
911
|
+
);
|
|
912
|
+
cached = read
|
|
913
|
+
? { entry: read.entry, shouldRevalidate: read.shouldRevalidate, read }
|
|
914
|
+
: null;
|
|
915
|
+
} else {
|
|
916
|
+
cached = await store.getShell(key);
|
|
917
|
+
}
|
|
805
918
|
} catch (error) {
|
|
806
919
|
reportCacheError(error, "cache-read", "[ShellServe] getShell");
|
|
807
920
|
}
|
|
808
921
|
if (reqCtx._metricsStore) {
|
|
922
|
+
const stats = cached?.read?.stats;
|
|
809
923
|
appendMetric(
|
|
810
924
|
reqCtx._metricsStore,
|
|
811
925
|
"ppr:shell-read",
|
|
812
926
|
shellReadStart,
|
|
813
927
|
performance.now() - shellReadStart,
|
|
814
928
|
undefined,
|
|
815
|
-
cached ? "hit" : "miss",
|
|
929
|
+
cached ? (stats ? `hit ${stats.tier}` : "hit") : "miss",
|
|
816
930
|
);
|
|
931
|
+
if (stats)
|
|
932
|
+
recordShellReadStats(reqCtx._metricsStore, shellReadStart, stats);
|
|
817
933
|
}
|
|
818
934
|
return cached;
|
|
819
935
|
}
|
|
820
936
|
|
|
821
937
|
/**
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
938
|
+
* A prelude-first read's sub-steps as `debugPerformance` rows under
|
|
939
|
+
* `ppr:shell-read`. The marker row starts with the prelude row because the
|
|
940
|
+
* two run in parallel; its desc says how long the read still waited on the
|
|
941
|
+
* marker once the prelude was in. On a deployed worker the clock only
|
|
942
|
+
* advances on I/O, so the byte counts are the cost signal for the CPU parts.
|
|
943
|
+
*/
|
|
944
|
+
function recordShellReadStats(
|
|
945
|
+
store: MetricsStore,
|
|
946
|
+
readStart: number,
|
|
947
|
+
stats: ShellReadStats,
|
|
948
|
+
): void {
|
|
949
|
+
let at = readStart;
|
|
950
|
+
const row = (label: string, ms: number | undefined, desc: string): void => {
|
|
951
|
+
if (ms !== undefined) appendMetric(store, label, at, ms, 1, desc);
|
|
952
|
+
};
|
|
953
|
+
if (stats.memo) {
|
|
954
|
+
row(
|
|
955
|
+
"ppr:shell-memo",
|
|
956
|
+
0,
|
|
957
|
+
`${stats.memo.hit ? "hit" : "miss"} size=${stats.memo.bytes}b`,
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
// A KV hit after an L1 miss: the L1 attempt first, then the KV read.
|
|
961
|
+
row("ppr:shell-l1-miss", stats.l1MissMs, stats.l1MissReason ?? "miss");
|
|
962
|
+
at += stats.l1MissMs ?? 0;
|
|
963
|
+
row("ppr:shell-match", stats.matchMs, stats.tier);
|
|
964
|
+
at += stats.matchMs ?? 0;
|
|
965
|
+
row("ppr:shell-head", stats.headMs, `bytes=${stats.headBytes ?? 0}`);
|
|
966
|
+
at += stats.headMs ?? 0;
|
|
967
|
+
row("ppr:shell-prelude", stats.preludeMs, `bytes=${stats.preludeBytes ?? 0}`);
|
|
968
|
+
let marker = `tags=${stats.tags ?? 0} ${stats.markerSerial ? "serial" : "parallel"} commit-wait=${(stats.markerWaitMs ?? 0).toFixed(2)}ms`;
|
|
969
|
+
if (stats.markerMemo) marker += ` memo=${stats.markerMemo}`;
|
|
970
|
+
if (stats.markerHinted) {
|
|
971
|
+
marker += ` hint=${stats.markerHintHits ?? 0}/${stats.markerHinted.length}`;
|
|
972
|
+
if (stats.markerLeadMs !== undefined) {
|
|
973
|
+
marker += ` lead=${stats.markerLeadMs.toFixed(2)}ms`;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (stats.freshReads) marker += " fresh-reads";
|
|
977
|
+
row("ppr:shell-marker", stats.markerMs, marker);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* openShellDocument (the HIT's integrity gate and only prelude decode) as a
|
|
982
|
+
* `ppr:shell-open` row: CPU only, so it reads 0 on a deployed worker.
|
|
983
|
+
*/
|
|
984
|
+
function openShellDocumentMetered(
|
|
985
|
+
reqCtx: RequestContext<any>,
|
|
986
|
+
stored: ShellStoreRead,
|
|
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);
|
|
993
|
+
const store = reqCtx._metricsStore;
|
|
994
|
+
if (!store) return open();
|
|
995
|
+
const start = performance.now();
|
|
996
|
+
const document = open();
|
|
997
|
+
appendMetric(
|
|
998
|
+
store,
|
|
999
|
+
"ppr:shell-open",
|
|
1000
|
+
start,
|
|
1001
|
+
performance.now() - start,
|
|
1002
|
+
undefined,
|
|
1003
|
+
`cpu ${read ? "raw" : "base64-decode"} prelude=${document?.prelude.length ?? 0}b`,
|
|
1004
|
+
);
|
|
1005
|
+
return document;
|
|
1006
|
+
}
|
|
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
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* The shell key a request reads and captures: `baseKey`, partitioned by the
|
|
1023
|
+
* route's request partition (cache-scope.ts resolveShellPartition; a
|
|
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.
|
|
1029
|
+
*/
|
|
1030
|
+
function resolveRequestShellKey(
|
|
1031
|
+
baseKey: string,
|
|
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
|
+
},
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
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
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Server-Timing mirror (issue #651): a capture or a HIT tail completes AFTER
|
|
1086
|
+
* its triggering response committed, so its numbers can only ride a LATER
|
|
1087
|
+
* response's header. Read-and-clear keeps one report per run. The capture
|
|
1088
|
+
* half is dev-only (see takeCaptureDebugEventForTiming); the tail half runs
|
|
1089
|
+
* wherever a tail was buffered (serveShellHit).
|
|
826
1090
|
*/
|
|
827
|
-
function
|
|
1091
|
+
function mirrorPprServerTimings(
|
|
828
1092
|
key: string,
|
|
829
1093
|
reqCtx: ReturnType<typeof getRequestContext>,
|
|
830
1094
|
): void {
|
|
831
|
-
if (
|
|
1095
|
+
if (!reqCtx._metricsStore) return;
|
|
1096
|
+
if (process.env.NODE_ENV !== "production") {
|
|
832
1097
|
const lastCapture = takeCaptureDebugEventForTiming(key);
|
|
833
1098
|
if (lastCapture) {
|
|
834
1099
|
appendMetric(
|
|
@@ -841,22 +1106,23 @@ function mirrorPprServerTimingsForDev(
|
|
|
841
1106
|
describeShellCaptureEvent({ ...lastCapture, attemptMs: undefined }),
|
|
842
1107
|
);
|
|
843
1108
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1109
|
+
}
|
|
1110
|
+
// Same mirror for the previous HIT's tail: its per-stage numbers
|
|
1111
|
+
// (snapshot/seed/match/handover/first-html/complete) finished after that
|
|
1112
|
+
// response's headers were committed, so they ride THIS request's
|
|
1113
|
+
// Server-Timing as `ppr:tail;dur=<complete ms>`. In production a tail is
|
|
1114
|
+
// buffered only when its own HIT collected metrics (serveShellHit).
|
|
1115
|
+
const lastTail = takeShellTailTimingForServerTiming(key);
|
|
1116
|
+
if (lastTail) {
|
|
1117
|
+
appendMetric(
|
|
1118
|
+
reqCtx._metricsStore,
|
|
1119
|
+
"ppr:tail",
|
|
1120
|
+
performance.now(),
|
|
1121
|
+
lastTail.completeMs ?? 0,
|
|
1122
|
+
undefined,
|
|
1123
|
+
// completeMs already rides as this entry's dur — drop it from desc.
|
|
1124
|
+
describeShellTailTiming({ ...lastTail, completeMs: undefined }),
|
|
1125
|
+
);
|
|
860
1126
|
}
|
|
861
1127
|
}
|
|
862
1128
|
|
|
@@ -867,7 +1133,7 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
867
1133
|
): Promise<Response> {
|
|
868
1134
|
const { ctx, request, env, url, isPartial, nonce, reqCtx, renderSpan } =
|
|
869
1135
|
input;
|
|
870
|
-
const { payload, pprReplayStatus,
|
|
1136
|
+
const { payload, pprReplayStatus, sourceScoped } = prepared;
|
|
871
1137
|
const metricsStore = reqCtx._metricsStore;
|
|
872
1138
|
|
|
873
1139
|
const rscHeaders: Record<string, string> = {
|
|
@@ -884,18 +1150,20 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
884
1150
|
serializePprReplayStatus(pprReplayStatus);
|
|
885
1151
|
}
|
|
886
1152
|
// Tell the client's prefetch cache to scope this response to its source
|
|
887
|
-
// URL (instead of the default source-agnostic wildcard).
|
|
888
|
-
//
|
|
889
|
-
//
|
|
890
|
-
|
|
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) {
|
|
891
1159
|
rscHeaders["x-rsc-prefetch-scope"] = "source";
|
|
892
1160
|
}
|
|
893
1161
|
// Enable browser HTTP caching for prefetch responses only.
|
|
894
1162
|
// Requires X-Rango-Prefetch header (sent by Link prefetch fetch),
|
|
895
|
-
//
|
|
896
|
-
//
|
|
897
|
-
const isPrefetch = request.
|
|
898
|
-
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) {
|
|
899
1167
|
const cc = ctx.router.prefetchCacheControl;
|
|
900
1168
|
if (cc) {
|
|
901
1169
|
rscHeaders["cache-control"] = cc;
|
|
@@ -944,7 +1212,7 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
944
1212
|
*/
|
|
945
1213
|
function scheduleNavigationShellCapture<TEnv>(
|
|
946
1214
|
input: RequestRenderInput<TEnv>,
|
|
947
|
-
|
|
1215
|
+
captureKey: string,
|
|
948
1216
|
): void {
|
|
949
1217
|
const { ctx, request, env, url, reqCtx } = input;
|
|
950
1218
|
const pprConfig = resolvePprConfig(reqCtx._classifiedRoute?.manifestEntry)!;
|
|
@@ -967,8 +1235,7 @@ function scheduleNavigationShellCapture<TEnv>(
|
|
|
967
1235
|
},
|
|
968
1236
|
createShellCaptureDescriptor(
|
|
969
1237
|
ctx,
|
|
970
|
-
|
|
971
|
-
buildNavigationShellKey(url, reqCtx._searchParamsFilter),
|
|
1238
|
+
captureKey,
|
|
972
1239
|
shellSearchSeed(url, reqCtx._searchParamsFilter),
|
|
973
1240
|
url.origin,
|
|
974
1241
|
pprConfig,
|
|
@@ -981,10 +1248,11 @@ function scheduleNavigationShellCapture<TEnv>(
|
|
|
981
1248
|
/**
|
|
982
1249
|
* Reuse a PPR capture's canonical segment record for a partial navigation.
|
|
983
1250
|
* The ordinary matchPartial pipeline remains authoritative: it projects the
|
|
984
|
-
* cached target tree against the client's segment ids
|
|
985
|
-
*
|
|
986
|
-
*
|
|
987
|
-
*
|
|
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.
|
|
988
1256
|
*/
|
|
989
1257
|
async function matchPartialWithPprReplay<TEnv>(
|
|
990
1258
|
ctx: HandlerContext<TEnv>,
|
|
@@ -1041,7 +1309,7 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1041
1309
|
return { result, status: finalStatus };
|
|
1042
1310
|
};
|
|
1043
1311
|
const pprConfig = resolvePprConfig(reqCtx._classifiedRoute?.manifestEntry);
|
|
1044
|
-
const activeNonce = nonce
|
|
1312
|
+
const activeNonce = activeRequestNonce(nonce, reqCtx);
|
|
1045
1313
|
const store = reqCtx._cacheStore;
|
|
1046
1314
|
|
|
1047
1315
|
if (!pprConfig) return runMatch();
|
|
@@ -1077,8 +1345,8 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1077
1345
|
// one-shot corruption recovery request still replay the same segment record,
|
|
1078
1346
|
// but through the server decode path.
|
|
1079
1347
|
armFragments =
|
|
1080
|
-
request.
|
|
1081
|
-
request.
|
|
1348
|
+
requestHeaders(request).get(SEGMENT_FRAGMENT_RECOVERY_HEADER) !== "1" &&
|
|
1349
|
+
requestHeaders(request).get(SEGMENT_FRAGMENT_CAPABILITY_HEADER) === "1";
|
|
1082
1350
|
// The prerender probe and the cache-opt-out gate below both need the
|
|
1083
1351
|
// classified snapshot; resolve (and persist) it once.
|
|
1084
1352
|
const routeSnapshot = classifiedRouteSnapshot(reqCtx);
|
|
@@ -1127,8 +1395,19 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1127
1395
|
return runMatch({ outcome: "BYPASS", reason: "cache-disabled" });
|
|
1128
1396
|
}
|
|
1129
1397
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1398
|
+
// The visitor's own partition (resolveShellPartition): replay never
|
|
1399
|
+
// crosses into another partition's shell.
|
|
1400
|
+
const key = await resolveRequestShellKey(
|
|
1401
|
+
buildShellKey(url, reqCtx._searchParamsFilter),
|
|
1402
|
+
routeSnapshot,
|
|
1403
|
+
store,
|
|
1404
|
+
reqCtx,
|
|
1405
|
+
"NavigationPPR",
|
|
1406
|
+
);
|
|
1407
|
+
if (key === undefined) {
|
|
1408
|
+
return runMatch({ outcome: "BYPASS", reason: "read-error" });
|
|
1409
|
+
}
|
|
1410
|
+
const navigationKey = navigationShellKey(key);
|
|
1132
1411
|
let cached: Awaited<ReturnType<typeof store.getShell>> = null;
|
|
1133
1412
|
try {
|
|
1134
1413
|
cached = await store.getShell(key, { claimRevalidation: false });
|
|
@@ -1152,11 +1431,7 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1152
1431
|
}
|
|
1153
1432
|
}
|
|
1154
1433
|
|
|
1155
|
-
if (
|
|
1156
|
-
!snapshot &&
|
|
1157
|
-
bypassReason !== "handler-live-holes" &&
|
|
1158
|
-
bypassReason !== "transition-when"
|
|
1159
|
-
) {
|
|
1434
|
+
if (!snapshot) {
|
|
1160
1435
|
let navigationCached: Awaited<ReturnType<typeof store.getShell>> = null;
|
|
1161
1436
|
try {
|
|
1162
1437
|
navigationCached = await store.getShell(navigationKey, {
|
|
@@ -1181,32 +1456,6 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1181
1456
|
}
|
|
1182
1457
|
}
|
|
1183
1458
|
|
|
1184
|
-
if (!snapshot) {
|
|
1185
|
-
// Production build manifests are local module data. In dev, resolving a
|
|
1186
|
-
// missing build shell would foreground-fetch /__rsc_shell and block an
|
|
1187
|
-
// otherwise ordinary navigation on capture, so replay remains runtime-only.
|
|
1188
|
-
const buildHit = await lookupBuildShell(
|
|
1189
|
-
url,
|
|
1190
|
-
ctx.version,
|
|
1191
|
-
store,
|
|
1192
|
-
undefined,
|
|
1193
|
-
reqCtx._searchParamsFilter,
|
|
1194
|
-
);
|
|
1195
|
-
if (buildHit?.stale) {
|
|
1196
|
-
bypassReason ??= "stale-build-entry";
|
|
1197
|
-
} else if (buildHit) {
|
|
1198
|
-
const buildDecision = replayableShellSnapshot(
|
|
1199
|
-
buildHit.entry,
|
|
1200
|
-
ctx.version,
|
|
1201
|
-
);
|
|
1202
|
-
if ("snapshot" in buildDecision) {
|
|
1203
|
-
snapshot = buildDecision.snapshot;
|
|
1204
|
-
} else {
|
|
1205
|
-
bypassReason ??= buildDecision.reason;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
1459
|
if (!snapshot) {
|
|
1211
1460
|
// Report-only marker for routes with an ENABLED route-derived scope: the
|
|
1212
1461
|
// lookup's own outcome must stay reportable even without a replayable
|
|
@@ -1221,25 +1470,20 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1221
1470
|
// implicit scope over the REAL doc: partition in
|
|
1222
1471
|
// resolveShellImplicitCacheScope).
|
|
1223
1472
|
if (routeCacheScope?.enabled) {
|
|
1224
|
-
|
|
1225
|
-
let explicitCacheBypass = false;
|
|
1473
|
+
const explicit = observeExplicitScope();
|
|
1226
1474
|
const previousImplicitCache = reqCtx._shellImplicitCache;
|
|
1227
1475
|
reqCtx._shellImplicitCache = {
|
|
1228
|
-
onExplicitHit:
|
|
1229
|
-
|
|
1230
|
-
},
|
|
1231
|
-
onExplicitBypass: () => {
|
|
1232
|
-
explicitCacheBypass = true;
|
|
1233
|
-
},
|
|
1476
|
+
onExplicitHit: explicit.onExplicitHit,
|
|
1477
|
+
onExplicitBypass: explicit.onExplicitBypass,
|
|
1234
1478
|
};
|
|
1235
1479
|
try {
|
|
1236
1480
|
const result = await matchPartialForReplay();
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1481
|
+
const status = finalizeReplayStatus(
|
|
1482
|
+
explicit.status({
|
|
1483
|
+
outcome: "BYPASS",
|
|
1484
|
+
reason: bypassReason ?? "no-entry",
|
|
1485
|
+
}),
|
|
1486
|
+
);
|
|
1243
1487
|
return {
|
|
1244
1488
|
result,
|
|
1245
1489
|
status,
|
|
@@ -1253,12 +1497,14 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1253
1497
|
// capture derives from THIS request's context, so its doc record
|
|
1254
1498
|
// records. Excluding it left replay dead until the document
|
|
1255
1499
|
// recaptured. The always-false route stays protected: its every
|
|
1256
|
-
// lookup refuses, so
|
|
1257
|
-
|
|
1258
|
-
!
|
|
1500
|
+
// lookup refuses, so its bypass suppresses the heal.
|
|
1501
|
+
healKey:
|
|
1502
|
+
!explicit.bypassed() &&
|
|
1259
1503
|
reqCtx._pprReplayPostMatchReason === undefined &&
|
|
1260
1504
|
(shouldHealReplayMiss(bypassReason) ||
|
|
1261
|
-
bypassReason === "no-segment-snapshot")
|
|
1505
|
+
bypassReason === "no-segment-snapshot")
|
|
1506
|
+
? navigationKey
|
|
1507
|
+
: undefined,
|
|
1262
1508
|
};
|
|
1263
1509
|
} finally {
|
|
1264
1510
|
reqCtx._shellImplicitCache = previousImplicitCache;
|
|
@@ -1270,26 +1516,37 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1270
1516
|
});
|
|
1271
1517
|
return {
|
|
1272
1518
|
...match,
|
|
1273
|
-
|
|
1519
|
+
healKey:
|
|
1274
1520
|
reqCtx._pprReplayPostMatchReason === undefined &&
|
|
1275
|
-
shouldHealReplayMiss(bypassReason)
|
|
1521
|
+
shouldHealReplayMiss(bypassReason)
|
|
1522
|
+
? navigationKey
|
|
1523
|
+
: undefined,
|
|
1276
1524
|
};
|
|
1277
1525
|
}
|
|
1278
1526
|
|
|
1279
1527
|
const previousImplicitCache = reqCtx._shellImplicitCache;
|
|
1528
|
+
const previousLoaderSeed = reqCtx._shellLoaderSeed;
|
|
1529
|
+
let loaderSeed: ReturnType<typeof buildShellLoaderSeed> | undefined;
|
|
1280
1530
|
let segmentReplayHit = false;
|
|
1281
1531
|
let segmentReplayCorrupt = false;
|
|
1282
|
-
|
|
1283
|
-
let explicitCacheBypass = false;
|
|
1532
|
+
const explicit = observeExplicitScope();
|
|
1284
1533
|
reqCtx._shellImplicitCache = {
|
|
1285
1534
|
ttl: pprConfig.ttl,
|
|
1286
1535
|
swr: pprConfig.swr,
|
|
1287
|
-
store: new SeededShellStore(store, snapshot,
|
|
1288
|
-
segmentsOnly: true,
|
|
1289
|
-
}),
|
|
1536
|
+
store: new SeededShellStore(store, snapshot),
|
|
1290
1537
|
keyPrefix: "doc",
|
|
1291
|
-
|
|
1538
|
+
// Arms the bake-lane loader pins a document HIT serves (loader-cache.ts
|
|
1539
|
+
// resolveLoaderData), decoded only now: the lookup awaits this before
|
|
1540
|
+
// the route's loaders resolve, and an explicit-tier hit or a miss runs
|
|
1541
|
+
// the loaders fresh and never reads a pin.
|
|
1542
|
+
onHit: async () => {
|
|
1292
1543
|
segmentReplayHit = true;
|
|
1544
|
+
// Never rejects: the lookup awaits this inside its own try, and an
|
|
1545
|
+
// unreadable seed leaves those loaders to run fresh.
|
|
1546
|
+
const seed = await (loaderSeed ??= buildShellLoaderSeed(snapshot).catch(
|
|
1547
|
+
() => undefined,
|
|
1548
|
+
));
|
|
1549
|
+
if (seed) reqCtx._shellLoaderSeed = seed;
|
|
1293
1550
|
},
|
|
1294
1551
|
onCorrupt: () => {
|
|
1295
1552
|
segmentReplayCorrupt = true;
|
|
@@ -1297,27 +1554,18 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1297
1554
|
// Arms the explicit-scope composition in withCacheLookup: a route-derived
|
|
1298
1555
|
// cache() scope stays authoritative, the seeded doc record supplies the
|
|
1299
1556
|
// match only on its miss. An explicit-tier hit must NOT report a replay
|
|
1300
|
-
// HIT — it served under the consumer's own semantics.
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
// deliberately does not pre-decide predicates, so the truthful
|
|
1306
|
-
// cache-disabled report comes from the lookup that actually refused.
|
|
1307
|
-
onExplicitBypass: () => {
|
|
1308
|
-
explicitCacheBypass = true;
|
|
1309
|
-
},
|
|
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,
|
|
1310
1562
|
};
|
|
1311
1563
|
|
|
1312
1564
|
try {
|
|
1313
1565
|
const result = await matchPartialForReplay();
|
|
1314
1566
|
const provisionalStatus: PprReplayStatus = segmentReplayHit
|
|
1315
1567
|
? { outcome: "HIT", freshness }
|
|
1316
|
-
:
|
|
1317
|
-
? { outcome: "BYPASS", reason: "explicit-cache-hit" }
|
|
1318
|
-
: explicitCacheBypass
|
|
1319
|
-
? { outcome: "BYPASS", reason: "cache-disabled" }
|
|
1320
|
-
: { outcome: "BYPASS", reason: "snapshot-miss" };
|
|
1568
|
+
: explicit.status({ outcome: "BYPASS", reason: "snapshot-miss" });
|
|
1321
1569
|
// A prerender serve or intercept resolution leaves the seeded record
|
|
1322
1570
|
// unconsulted (prerender short-circuits before the scope; intercepts
|
|
1323
1571
|
// keep snapshot.cacheScope) — the "snapshot-miss" guess would blame the
|
|
@@ -1326,17 +1574,19 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1326
1574
|
return {
|
|
1327
1575
|
result,
|
|
1328
1576
|
status,
|
|
1329
|
-
captureNeeded:
|
|
1330
|
-
segmentReplayCorrupt && reqCtx._pprReplayPostMatchReason === undefined,
|
|
1331
1577
|
// A corrupt document snapshot must be overwritten at the document key;
|
|
1332
1578
|
// writing only the secondary navigation key would leave the preferred
|
|
1333
1579
|
// document entry shadowing the repair forever. The capture remains marked
|
|
1334
1580
|
// navigationOnly, so a document request ignores it and recaptures a
|
|
1335
1581
|
// document-safe shell under the same key.
|
|
1336
|
-
|
|
1582
|
+
healKey:
|
|
1583
|
+
segmentReplayCorrupt && reqCtx._pprReplayPostMatchReason === undefined
|
|
1584
|
+
? snapshotStoreKey
|
|
1585
|
+
: undefined,
|
|
1337
1586
|
};
|
|
1338
1587
|
} finally {
|
|
1339
1588
|
reqCtx._shellImplicitCache = previousImplicitCache;
|
|
1589
|
+
reqCtx._shellLoaderSeed = previousLoaderSeed;
|
|
1340
1590
|
}
|
|
1341
1591
|
}
|
|
1342
1592
|
|
|
@@ -1381,6 +1631,76 @@ export function resolveShellHitRedirectTarget(
|
|
|
1381
1631
|
);
|
|
1382
1632
|
}
|
|
1383
1633
|
|
|
1634
|
+
/**
|
|
1635
|
+
* Buffer a finished tail timing for the next request's Server-Timing, and
|
|
1636
|
+
* under debugPerformance print it for this request (whose own Server-Timing
|
|
1637
|
+
* was sent at the commit, before the tail ran).
|
|
1638
|
+
*/
|
|
1639
|
+
function publishTailTiming(
|
|
1640
|
+
tailTiming: ShellTailTiming | null,
|
|
1641
|
+
metricsStore: MetricsStore | undefined,
|
|
1642
|
+
request: Request,
|
|
1643
|
+
url: URL,
|
|
1644
|
+
): void {
|
|
1645
|
+
if (!tailTiming) return;
|
|
1646
|
+
publishShellTailTiming(tailTiming);
|
|
1647
|
+
if (metricsStore) {
|
|
1648
|
+
console.log(
|
|
1649
|
+
`[RSC Perf] ${request.method} ${url.pathname} shell tail: ${describeShellTailTiming(tailTiming)}`,
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
const PENDING = Symbol("pending");
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* Whether `value` is not a promise, or a promise that has already settled:
|
|
1658
|
+
* racing it against an already-resolved promise, a settled one's reaction is
|
|
1659
|
+
* queued first and wins.
|
|
1660
|
+
*/
|
|
1661
|
+
async function isSettled(value: unknown): Promise<boolean> {
|
|
1662
|
+
if (!(value instanceof Promise)) return true;
|
|
1663
|
+
return Promise.race([value, PENDING]).then(
|
|
1664
|
+
(first) => first !== PENDING,
|
|
1665
|
+
() => true,
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/**
|
|
1670
|
+
* Evict a shell whose doc record cannot be replayed. The store has no shell
|
|
1671
|
+
* delete, so the entry is overwritten with a tombstone: a navigationOnly
|
|
1672
|
+
* entry with no document half and no snapshot, which document serving
|
|
1673
|
+
* treats as a MISS (shellServePlan, the build shell included) and partial
|
|
1674
|
+
* replay as `no-segment-snapshot`. The recapture overwrites it with a sound
|
|
1675
|
+
* entry. Only for a broken entry: a snapshot read that was merely slow
|
|
1676
|
+
* (`snapshotFailure: "unavailable"`) leaves the entry alone.
|
|
1677
|
+
*/
|
|
1678
|
+
async function degradeUnreplayableShell(
|
|
1679
|
+
descriptor: ShellCaptureDescriptor,
|
|
1680
|
+
entry: ShellEntryHead,
|
|
1681
|
+
scheduleRecapture: () => void,
|
|
1682
|
+
): Promise<void> {
|
|
1683
|
+
const store = descriptor.store;
|
|
1684
|
+
try {
|
|
1685
|
+
await store?.putShell?.(
|
|
1686
|
+
descriptor.key,
|
|
1687
|
+
{
|
|
1688
|
+
reactVersion: entry.reactVersion,
|
|
1689
|
+
buildVersion: entry.buildVersion,
|
|
1690
|
+
snapshot: [],
|
|
1691
|
+
navigationOnly: true,
|
|
1692
|
+
createdAt: Date.now(),
|
|
1693
|
+
},
|
|
1694
|
+
descriptor.ttl,
|
|
1695
|
+
0,
|
|
1696
|
+
);
|
|
1697
|
+
} catch (error) {
|
|
1698
|
+
reportCacheError(error, "cache-write", "[ShellServe] tombstone");
|
|
1699
|
+
}
|
|
1700
|
+
store?.dropShellMemo?.(descriptor.key);
|
|
1701
|
+
scheduleRecapture();
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1384
1704
|
/**
|
|
1385
1705
|
* Serve a validated shell HIT: commit the stored prelude bytes NOW and run the
|
|
1386
1706
|
* live tail behind them inside the response stream. Plain byte concatenation is
|
|
@@ -1398,15 +1718,17 @@ function serveShellHit(
|
|
|
1398
1718
|
reqCtx: RequestContext<any>,
|
|
1399
1719
|
handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
|
|
1400
1720
|
ssrModule: SSRModule,
|
|
1401
|
-
|
|
1721
|
+
document: ShellDocument,
|
|
1402
1722
|
descriptor: ShellCaptureDescriptor,
|
|
1403
1723
|
): Response {
|
|
1404
|
-
const preludeBytes =
|
|
1405
|
-
|
|
1406
|
-
//
|
|
1407
|
-
//
|
|
1724
|
+
const { entry, prelude: preludeBytes } = document;
|
|
1725
|
+
const metricsStore = reqCtx._metricsStore;
|
|
1726
|
+
// Per-stage tail timing for the `ppr:tail` Server-Timing mirror and the
|
|
1727
|
+
// perf console line: always in dev, in production only when this request
|
|
1728
|
+
// collects debugPerformance metrics. Offsets are relative to this commit
|
|
1729
|
+
// point.
|
|
1408
1730
|
const tailTiming: ShellTailTiming | null =
|
|
1409
|
-
process.env.NODE_ENV !== "production"
|
|
1731
|
+
process.env.NODE_ENV !== "production" || metricsStore
|
|
1410
1732
|
? {
|
|
1411
1733
|
key: descriptor.key,
|
|
1412
1734
|
outcome: "complete",
|
|
@@ -1414,6 +1736,8 @@ function serveShellHit(
|
|
|
1414
1736
|
}
|
|
1415
1737
|
: null;
|
|
1416
1738
|
const tailT0 = tailTiming ? performance.now() : 0;
|
|
1739
|
+
const scheduleRecapture = (): void =>
|
|
1740
|
+
scheduleShellCapture(ctx, request, env, url, reqCtx, ssrModule, descriptor);
|
|
1417
1741
|
|
|
1418
1742
|
const createTailContext = (): RequestContext<any> => {
|
|
1419
1743
|
const tailCtx: RequestContext<any> = Object.create(reqCtx);
|
|
@@ -1486,7 +1810,7 @@ function serveShellHit(
|
|
|
1486
1810
|
}
|
|
1487
1811
|
return observePhase(PHASES.ssr, () =>
|
|
1488
1812
|
ssrModule.resumeShellHTML!(rscStream, {
|
|
1489
|
-
postponed:
|
|
1813
|
+
postponed: document.postponed,
|
|
1490
1814
|
nonce: undefined,
|
|
1491
1815
|
// The shell key's own search — identical to the capture seed for
|
|
1492
1816
|
// this key, so the resume tree matches the captured tree.
|
|
@@ -1505,68 +1829,77 @@ function serveShellHit(
|
|
|
1505
1829
|
ReadableStream<Uint8Array> | { redirect: string }
|
|
1506
1830
|
> = (async () => {
|
|
1507
1831
|
// Snapshot seeding (docs/design/ppr-shell-resume.md): the tail render must
|
|
1508
|
-
// match the frozen prelude, so
|
|
1509
|
-
//
|
|
1510
|
-
//
|
|
1511
|
-
//
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
`[Server][ppr] shell HIT: loader seed built +${Math.round(performance.now() - seedStart)}ms (abs ${Math.round(performance.now())})`,
|
|
1532
|
-
);
|
|
1533
|
-
}
|
|
1534
|
-
if (loaderSeed) seededCtx._shellLoaderSeed = loaderSeed;
|
|
1535
|
-
// Shell fast path (serve side): if the capture recorded the implicit doc
|
|
1536
|
-
// segment record and the handler layer declared no liveness, the tail's
|
|
1537
|
-
// cache lookup HITs the seeded doc entry — the handler layer is REPLAYED,
|
|
1538
|
-
// not re-executed (loaders still run fresh). Anything else degrades to
|
|
1539
|
-
// the full tail automatically.
|
|
1540
|
-
if (!entry.handlerLiveHoles && !entry.transitionWhen) {
|
|
1541
|
-
seededCtx._shellImplicitCache = {
|
|
1542
|
-
ttl: descriptor.ttl,
|
|
1543
|
-
swr: descriptor.swr,
|
|
1544
|
-
keyPrefix: "doc",
|
|
1545
|
-
};
|
|
1546
|
-
if (INTERNAL_RANGO_DEBUG) {
|
|
1547
|
-
console.log(
|
|
1548
|
-
`[Server][ppr] shell HIT: fast path armed (implicit doc cache) (abs ${Math.round(performance.now())})`,
|
|
1549
|
-
);
|
|
1550
|
-
}
|
|
1551
|
-
} else if (INTERNAL_RANGO_DEBUG) {
|
|
1552
|
-
console.log(
|
|
1553
|
-
`[Server][ppr] shell HIT: fast path declined — request-dependent handler/transition state; tail re-runs handlers (abs ${Math.round(performance.now())})`,
|
|
1554
|
-
);
|
|
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.
|
|
1836
|
+
// A prelude-first read delivers the snapshot on its own promise: the
|
|
1837
|
+
// prelude is already committed, so only the tail waits for it.
|
|
1838
|
+
const inMemory = await isSettled(document.snapshot);
|
|
1839
|
+
const snapshot = await document.snapshot;
|
|
1840
|
+
// A snapshot already in memory (shell memo, memory store, build shells)
|
|
1841
|
+
// resolves at once, and the tail's seeding and match would run ahead of
|
|
1842
|
+
// the runtime writing the prelude: one macrotask first. A snapshot still
|
|
1843
|
+
// arriving on I/O has yielded by the time it resolves.
|
|
1844
|
+
if (inMemory) await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
1845
|
+
if (tailTiming) {
|
|
1846
|
+
tailTiming.snapshotMs = Math.round(performance.now() - tailT0);
|
|
1847
|
+
if (snapshot)
|
|
1848
|
+
tailTiming.snapshotRecords = countSnapshotFamilies(snapshot);
|
|
1849
|
+
tailTiming.snapshotPruned = entry.prunedRecords;
|
|
1850
|
+
const snapshotStats = document.stats?.snapshot;
|
|
1851
|
+
if (snapshotStats) {
|
|
1852
|
+
tailTiming.snapshotReadMs = Math.round(snapshotStats.readMs);
|
|
1853
|
+
tailTiming.snapshotBytes = snapshotStats.bytes;
|
|
1854
|
+
tailTiming.snapshotParseMs = Math.round(snapshotStats.parseMs);
|
|
1555
1855
|
}
|
|
1556
|
-
// Fragment splice (issue #700): store hits in THIS tail emit their stored
|
|
1557
|
-
// segment fragments verbatim (expanded by segment-fragments.ts). The flag
|
|
1558
|
-
// lives on the derived context so it can never leak into a capture render,
|
|
1559
|
-
// which serializes segments and must see real elements.
|
|
1560
|
-
seededCtx._shellFragmentPayload = true;
|
|
1561
|
-
return runWithRequestContext(seededCtx, () => renderTail(seededCtx));
|
|
1562
1856
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
//
|
|
1566
|
-
//
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1857
|
+
const seededCtx = createTailContext();
|
|
1858
|
+
const records = snapshot ?? [];
|
|
1859
|
+
// Loader-family records (bake-lane containers, loader-container-bake):
|
|
1860
|
+
// decode into a seed Map for the resolveLoaderData overlay, so the
|
|
1861
|
+
// payload's baked container bytes match the frozen prelude while the
|
|
1862
|
+
// hole-marker paths keep the fresh run's live nested promises.
|
|
1863
|
+
const seedStart =
|
|
1864
|
+
INTERNAL_RANGO_DEBUG || tailTiming ? performance.now() : 0;
|
|
1865
|
+
const loaderSeed =
|
|
1866
|
+
records.length > 0 ? await buildShellLoaderSeed(records) : undefined;
|
|
1867
|
+
if (tailTiming) {
|
|
1868
|
+
const seededAt = performance.now();
|
|
1869
|
+
tailTiming.seedMs = Math.round(seededAt - tailT0);
|
|
1870
|
+
tailTiming.seedCpuMs = Math.round(seededAt - seedStart);
|
|
1871
|
+
}
|
|
1872
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
1873
|
+
console.log(
|
|
1874
|
+
`[Server][ppr] shell HIT: loader seed built +${Math.round(performance.now() - seedStart)}ms (abs ${Math.round(performance.now())})`,
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
if (loaderSeed) seededCtx._shellLoaderSeed = loaderSeed;
|
|
1878
|
+
// The handler layer is REPLAYED from the entry's own doc record, never
|
|
1879
|
+
// executed: the implicit doc scope replaces any route-derived scope
|
|
1880
|
+
// (docTail), looks the record up by the key the capture wrote
|
|
1881
|
+
// (fixedDocKey), and reads it from a segment-only overlay of the snapshot
|
|
1882
|
+
// (the real store never holds a doc: record). A lookup that does not hit
|
|
1883
|
+
// throws ShellRecordUnavailableError (withCacheLookup), handled by the
|
|
1884
|
+
// degrade below. Live and promise-carrying bake-lane loaders still run
|
|
1885
|
+
// fresh; a promise-free bake-lane loader is served from its pin
|
|
1886
|
+
// (_shellLoaderSeed, loader-cache.ts). A Prerender route's tail takes
|
|
1887
|
+
// the handler layer from the prerender store before this scope is
|
|
1888
|
+
// consulted.
|
|
1889
|
+
seededCtx._shellImplicitCache = {
|
|
1890
|
+
ttl: descriptor.ttl,
|
|
1891
|
+
swr: descriptor.swr,
|
|
1892
|
+
store: new SeededShellStore(reqCtx._cacheStore!, records),
|
|
1893
|
+
keyPrefix: "doc",
|
|
1894
|
+
docTail: true,
|
|
1895
|
+
fixedDocKey: entry.docKey,
|
|
1896
|
+
};
|
|
1897
|
+
// Fragment splice (issue #700): store hits in THIS tail emit their stored
|
|
1898
|
+
// segment fragments verbatim (expanded by segment-fragments.ts). The flag
|
|
1899
|
+
// lives on the derived context so it can never leak into a capture render,
|
|
1900
|
+
// which serializes segments and must see real elements.
|
|
1901
|
+
seededCtx._shellFragmentPayload = true;
|
|
1902
|
+
return runWithRequestContext(seededCtx, () => renderTail(seededCtx));
|
|
1570
1903
|
})();
|
|
1571
1904
|
// The stream below is the only consumer; pre-attach a no-op catch so a tail
|
|
1572
1905
|
// failure before the stream is pulled never surfaces as an unhandled rejection.
|
|
@@ -1575,7 +1908,31 @@ function serveShellHit(
|
|
|
1575
1908
|
const serveStart = INTERNAL_RANGO_DEBUG ? performance.now() : 0;
|
|
1576
1909
|
const body = new ReadableStream<Uint8Array>({
|
|
1577
1910
|
async start(controller) {
|
|
1578
|
-
|
|
1911
|
+
// Fixed-size chunks (SHELL_PRELUDE_CHUNK_BYTES): a compressor in front
|
|
1912
|
+
// of the worker emits after each chunk instead of after the whole
|
|
1913
|
+
// prelude. slice() gives every chunk its own buffer.
|
|
1914
|
+
const commitStart = metricsStore ? performance.now() : 0;
|
|
1915
|
+
let chunks = 0;
|
|
1916
|
+
for (
|
|
1917
|
+
let offset = 0;
|
|
1918
|
+
offset < preludeBytes.length;
|
|
1919
|
+
offset += SHELL_PRELUDE_CHUNK_BYTES
|
|
1920
|
+
) {
|
|
1921
|
+
controller.enqueue(
|
|
1922
|
+
preludeBytes.slice(offset, offset + SHELL_PRELUDE_CHUNK_BYTES),
|
|
1923
|
+
);
|
|
1924
|
+
chunks++;
|
|
1925
|
+
}
|
|
1926
|
+
if (metricsStore) {
|
|
1927
|
+
appendMetric(
|
|
1928
|
+
metricsStore,
|
|
1929
|
+
"ppr:shell-commit",
|
|
1930
|
+
commitStart,
|
|
1931
|
+
performance.now() - commitStart,
|
|
1932
|
+
undefined,
|
|
1933
|
+
`cpu chunks=${chunks} prelude=${preludeBytes.length}b`,
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1579
1936
|
if (INTERNAL_RANGO_DEBUG) {
|
|
1580
1937
|
console.log(
|
|
1581
1938
|
`[Server][ppr] shell HIT: prelude enqueued (${preludeBytes.length}b) +${Math.round(performance.now() - serveStart)}ms`,
|
|
@@ -1647,7 +2004,9 @@ function serveShellHit(
|
|
|
1647
2004
|
);
|
|
1648
2005
|
controller.enqueue(
|
|
1649
2006
|
new TextEncoder().encode(
|
|
1650
|
-
|
|
2007
|
+
inlineShellScript(
|
|
2008
|
+
`location.replace(${escapeJsonForScript(JSON.stringify(safeTarget))})`,
|
|
2009
|
+
),
|
|
1651
2010
|
),
|
|
1652
2011
|
);
|
|
1653
2012
|
if (tailTiming) {
|
|
@@ -1655,29 +2014,44 @@ function serveShellHit(
|
|
|
1655
2014
|
tailTiming.completeMs = Math.round(performance.now() - tailT0);
|
|
1656
2015
|
}
|
|
1657
2016
|
}
|
|
1658
|
-
|
|
2017
|
+
publishTailTiming(tailTiming, metricsStore, request, url);
|
|
1659
2018
|
controller.close();
|
|
1660
2019
|
} catch (error) {
|
|
2020
|
+
if (error instanceof ShellRecordUnavailableError) {
|
|
2021
|
+
// The doc record could not supply the handler layer, and running
|
|
2022
|
+
// handlers behind a committed prelude is exactly what a HIT must
|
|
2023
|
+
// never do: reload the page into a forced MISS. A broken entry is
|
|
2024
|
+
// also replaced (tombstone, memo dropped, recapture); one whose
|
|
2025
|
+
// snapshot read was only slow stays, and the next request reads it
|
|
2026
|
+
// again.
|
|
2027
|
+
if ((await document.snapshotFailure) !== "unavailable") {
|
|
2028
|
+
await degradeUnreplayableShell(
|
|
2029
|
+
descriptor,
|
|
2030
|
+
entry,
|
|
2031
|
+
scheduleRecapture,
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
controller.enqueue(new TextEncoder().encode(shellReloadScript()));
|
|
2035
|
+
if (tailTiming) {
|
|
2036
|
+
tailTiming.outcome = "error";
|
|
2037
|
+
tailTiming.completeMs = Math.round(performance.now() - tailT0);
|
|
2038
|
+
}
|
|
2039
|
+
publishTailTiming(tailTiming, metricsStore, request, url);
|
|
2040
|
+
controller.close();
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
1661
2043
|
// Self-heal on a failed tail: errors the pre-commit gates cannot catch
|
|
1662
2044
|
// (mismatched postponed blob, hard render error above the holes) throw
|
|
1663
2045
|
// here AFTER the 200 + prelude flushed and would re-fail on every
|
|
1664
2046
|
// request until the entry ages out — recapture overwrites the entry.
|
|
1665
2047
|
// Client disconnects land here too; the recapture is idempotent and
|
|
1666
2048
|
// bounded by scheduleShellCapture's stampede guard + backoff.
|
|
1667
|
-
|
|
1668
|
-
ctx,
|
|
1669
|
-
request,
|
|
1670
|
-
env,
|
|
1671
|
-
url,
|
|
1672
|
-
reqCtx,
|
|
1673
|
-
ssrModule,
|
|
1674
|
-
descriptor,
|
|
1675
|
-
);
|
|
2049
|
+
scheduleRecapture();
|
|
1676
2050
|
if (tailTiming) {
|
|
1677
2051
|
tailTiming.outcome = "error";
|
|
1678
2052
|
tailTiming.completeMs = Math.round(performance.now() - tailT0);
|
|
1679
|
-
publishShellTailTiming(tailTiming);
|
|
1680
2053
|
}
|
|
2054
|
+
publishTailTiming(tailTiming, metricsStore, request, url);
|
|
1681
2055
|
controller.error(error);
|
|
1682
2056
|
}
|
|
1683
2057
|
},
|