@rangojs/router 0.17.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 +3 -2
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -0
- 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 +98 -14
- 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-snapshot.d.ts +4 -3
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- 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/types.d.ts +207 -43
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/middleware-types.d.ts +6 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +40 -11
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +21 -10
- package/dist/types/router/segment-resolution/mask-nested.d.ts +65 -24
- package/dist/types/rsc/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +53 -6
- package/dist/types/rsc/shell-capture.d.ts +146 -15
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/types.d.ts +7 -0
- package/dist/types/server/context.d.ts +109 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/request-context.d.ts +159 -33
- package/dist/types/ssr/index.d.ts +9 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/testing/flight.entry.d.ts +7 -0
- 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/types/handler-context.d.ts +14 -1
- package/dist/types/urls/pattern-types.d.ts +22 -18
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +46 -9
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +17 -7
- package/skills/cache-guide/SKILL.md +123 -51
- package/skills/caching/SKILL.md +253 -23
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +11 -6
- package/skills/cloudflare/SKILL.md +15 -0
- package/skills/comparison/references/framework-comparison.md +44 -32
- package/skills/defer-hydration/SKILL.md +50 -30
- package/skills/deployment-caching/SKILL.md +5 -2
- package/skills/document-cache/SKILL.md +8 -2
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/handle-and-actions.md +3 -1
- package/skills/hooks/state.md +3 -1
- package/skills/loader/SKILL.md +161 -39
- package/skills/middleware/SKILL.md +16 -9
- package/skills/migrate-nextjs/SKILL.md +67 -36
- 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 +64 -0
- package/skills/parallel/SKILL.md +13 -8
- package/skills/ppr/SKILL.md +655 -363
- package/skills/prerender/SKILL.md +27 -14
- package/skills/rango/SKILL.md +52 -33
- package/skills/route/SKILL.md +12 -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 +25 -21
- package/skills/testing/cache-prerender.md +15 -14
- package/skills/testing/loader.md +18 -1
- package/skills/testing/serve-shell-request.md +188 -0
- package/skills/theme/SKILL.md +52 -4
- package/skills/typesafety/env-and-bindings.md +10 -2
- package/skills/use-cache/SKILL.md +39 -16
- package/skills/vercel/SKILL.md +14 -2
- package/skills/view-transitions/SKILL.md +1 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- package/src/browser/rsc-router.tsx +5 -0
- package/src/cache/cache-key-utils.ts +22 -2
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +34 -7
- package/src/cache/cache-scope.ts +345 -33
- 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/handle-snapshot.ts +4 -3
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +104 -19
- package/src/cache/types.ts +211 -43
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- package/src/prerender/build-shell-capture.ts +65 -11
- package/src/route-definition/dsl-helpers.ts +4 -3
- package/src/router/handler-context.ts +38 -4
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +19 -7
- package/src/router/loader-resolution.ts +63 -32
- package/src/router/match-api.ts +28 -9
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/cache-lookup.ts +17 -0
- package/src/router/match-middleware/cache-store.ts +31 -11
- package/src/router/middleware-types.ts +6 -1
- package/src/router/middleware.ts +23 -4
- package/src/router/prerender-match.ts +6 -0
- package/src/router/segment-resolution/fresh.ts +18 -9
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +227 -56
- package/src/router/segment-resolution/loader-snapshot.ts +203 -51
- package/src/router/segment-resolution/mask-nested.ts +136 -42
- package/src/router/segment-resolution/revalidation.ts +27 -10
- 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/response-cache-serve.ts +9 -6
- package/src/rsc/rsc-rendering.ts +601 -181
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +60 -6
- package/src/rsc/shell-capture.ts +1001 -250
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/types.ts +7 -0
- package/src/server/context.ts +219 -28
- package/src/server/cookie-store.ts +87 -62
- package/src/server/request-context.ts +263 -38
- package/src/ssr/index.tsx +96 -37
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/run-loader.ts +63 -5
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/types/handler-context.ts +14 -1
- package/src/urls/pattern-types.ts +22 -18
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/router-discovery.ts +14 -4
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -13,9 +13,13 @@ import {
|
|
|
13
13
|
} from "../server/request-context.js";
|
|
14
14
|
import {
|
|
15
15
|
SeededShellStore,
|
|
16
|
+
ShellRecordUnavailableError,
|
|
16
17
|
buildShellLoaderSeed,
|
|
18
|
+
countSnapshotFamilies,
|
|
19
|
+
hasDocRecord,
|
|
17
20
|
} from "../cache/shell-snapshot.js";
|
|
18
21
|
import { appendMetric } from "../router/metrics.js";
|
|
22
|
+
import type { MetricsStore } from "../server/context.js";
|
|
19
23
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
20
24
|
import type { TraceSpan } from "../router/tracing.js";
|
|
21
25
|
import { getSSRSetup, createSsrHtmlStage, isRscRequest } from "./ssr-setup.js";
|
|
@@ -45,6 +49,8 @@ import type { HandlerContext } from "./handler-context.js";
|
|
|
45
49
|
import { gateTransitions } from "./transition-gate.js";
|
|
46
50
|
import { buildFullPayload } from "./full-payload.js";
|
|
47
51
|
import {
|
|
52
|
+
capShellWindow,
|
|
53
|
+
lastStoredCaptureSeq,
|
|
48
54
|
scheduleShellCapture,
|
|
49
55
|
resolveShellCaptureDebugSink,
|
|
50
56
|
takeCaptureDebugEventForTiming,
|
|
@@ -59,7 +65,15 @@ import {
|
|
|
59
65
|
shellSearchSeed,
|
|
60
66
|
isValidShellHit,
|
|
61
67
|
hasIntactShellPayload,
|
|
62
|
-
|
|
68
|
+
openShellDocument,
|
|
69
|
+
navigationShellKey,
|
|
70
|
+
partitionShellKey,
|
|
71
|
+
inlineShellScript,
|
|
72
|
+
shellReloadScript,
|
|
73
|
+
partitionBuildShellCheckDone,
|
|
74
|
+
notePartitionBuildShellCheck,
|
|
75
|
+
warnPprWindowCappedOnce,
|
|
76
|
+
SHELL_PRELUDE_CHUNK_BYTES,
|
|
63
77
|
hasShellFamily,
|
|
64
78
|
warnShellStoreMissingOnce,
|
|
65
79
|
warnPprNonceActiveOnce,
|
|
@@ -67,13 +81,19 @@ import {
|
|
|
67
81
|
publishShellTailTiming,
|
|
68
82
|
takeShellTailTimingForServerTiming,
|
|
69
83
|
type ResolvedPprConfig,
|
|
84
|
+
type ShellDocument,
|
|
70
85
|
type ShellTailTiming,
|
|
71
86
|
} from "./shell-serve.js";
|
|
72
87
|
import {
|
|
88
|
+
hasBuildShell,
|
|
73
89
|
lookupBuildShell,
|
|
74
90
|
type DevShellLookup,
|
|
75
91
|
} from "./shell-build-manifest.js";
|
|
76
92
|
import { contextGet } from "../context-var.js";
|
|
93
|
+
import {
|
|
94
|
+
resolveSwrWindow,
|
|
95
|
+
staleShellRecaptureDue,
|
|
96
|
+
} from "../cache/cache-policy.js";
|
|
77
97
|
import {
|
|
78
98
|
getNavigationContextHeader,
|
|
79
99
|
prerenderStoreShortCircuits,
|
|
@@ -88,13 +108,16 @@ import {
|
|
|
88
108
|
safeSameOriginLanding,
|
|
89
109
|
} from "../redirect-origin.js";
|
|
90
110
|
import { nonce as nonceToken } from "./nonce.js";
|
|
111
|
+
import { escapeJsonForScript } from "../escape-script.js";
|
|
112
|
+
import { resolveShellPartition } from "../cache/cache-scope.js";
|
|
91
113
|
import { reportCacheError } from "../cache/cache-error.js";
|
|
92
114
|
import type { SearchParamsFilter } from "../cache/search-params-filter.js";
|
|
93
115
|
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
94
116
|
import type {
|
|
95
|
-
DocumentShellCacheEntry,
|
|
96
117
|
SegmentCacheStore,
|
|
97
118
|
ShellCacheEntry,
|
|
119
|
+
ShellDocumentRead,
|
|
120
|
+
ShellReadStats,
|
|
98
121
|
ShellSnapshotRecord,
|
|
99
122
|
} from "../cache/types.js";
|
|
100
123
|
import {
|
|
@@ -116,8 +139,6 @@ type PprReplayBypassReason =
|
|
|
116
139
|
| "no-entry"
|
|
117
140
|
| "invalid-version"
|
|
118
141
|
| "corrupt-entry"
|
|
119
|
-
| "handler-live-holes"
|
|
120
|
-
| "transition-when"
|
|
121
142
|
| "no-segment-snapshot"
|
|
122
143
|
| "snapshot-miss"
|
|
123
144
|
| "explicit-cache-hit"
|
|
@@ -143,13 +164,6 @@ function describePprReplayStatus(status: PprReplayStatus): string {
|
|
|
143
164
|
: `bypass:${status.reason}`;
|
|
144
165
|
}
|
|
145
166
|
|
|
146
|
-
function buildNavigationShellKey(
|
|
147
|
-
url: URL,
|
|
148
|
-
filter?: SearchParamsFilter,
|
|
149
|
-
): string {
|
|
150
|
-
return `${buildShellKey(url, filter)}:navigation`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
167
|
function createShellCaptureDescriptor(
|
|
154
168
|
ctx: HandlerContext<any>,
|
|
155
169
|
key: string,
|
|
@@ -242,8 +256,6 @@ function replayableShellSnapshot(
|
|
|
242
256
|
if (!entry.navigationOnly && !hasIntactShellPayload(entry)) {
|
|
243
257
|
return { reason: "corrupt-entry" };
|
|
244
258
|
}
|
|
245
|
-
if (entry.handlerLiveHoles) return { reason: "handler-live-holes" };
|
|
246
|
-
if (entry.transitionWhen) return { reason: "transition-when" };
|
|
247
259
|
// Eligibility requires the CANONICAL doc segment record (`docKey`), not just
|
|
248
260
|
// any segment record: captures under an explicit cache() scope also record
|
|
249
261
|
// that tier's reads/writes under keys a partial lookup can never resolve,
|
|
@@ -251,25 +263,8 @@ function replayableShellSnapshot(
|
|
|
251
263
|
// (a timing-dependent snapshot-miss/no-segment-snapshot flip-flop). Entries
|
|
252
264
|
// captured before the field existed read as no-segment-snapshot and heal on
|
|
253
265
|
// recapture (pre-release, same treatment as the buildVersion field).
|
|
254
|
-
const docKey = entry.docKey;
|
|
255
266
|
const snapshot = entry.snapshot;
|
|
256
|
-
|
|
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
|
|
267
|
+
return snapshot && hasDocRecord(snapshot, entry.docKey)
|
|
273
268
|
? { snapshot }
|
|
274
269
|
: { reason: "no-segment-snapshot" };
|
|
275
270
|
}
|
|
@@ -439,13 +434,14 @@ function* requestRenderPlan<TEnv>(
|
|
|
439
434
|
response.headers.set(SHELL_STATUS_HEADER, "MISS");
|
|
440
435
|
}
|
|
441
436
|
|
|
437
|
+
const captureKey = prepared.partialCaptureKey;
|
|
442
438
|
if (
|
|
443
439
|
isPartial &&
|
|
444
440
|
prepared.partialCaptureNeeded &&
|
|
441
|
+
captureKey !== undefined &&
|
|
445
442
|
!reqCtx._dynamic &&
|
|
446
443
|
response.status === 200
|
|
447
444
|
) {
|
|
448
|
-
const captureKey = prepared.partialCaptureKey;
|
|
449
445
|
yield* handoff("navigation-shell-capture", () =>
|
|
450
446
|
scheduleNavigationShellCapture(input, captureKey),
|
|
451
447
|
);
|
|
@@ -454,6 +450,32 @@ function* requestRenderPlan<TEnv>(
|
|
|
454
450
|
return response;
|
|
455
451
|
}
|
|
456
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
|
+
|
|
457
479
|
/**
|
|
458
480
|
* Axis 2 shell serve. Sync gates stay plan code; store and SSR effects run as
|
|
459
481
|
* named steps. Outcomes: serve a composed shell response, report a MISS (the
|
|
@@ -474,8 +496,13 @@ function* shellServePlan<TEnv>(
|
|
|
474
496
|
) {
|
|
475
497
|
return { kind: "pass" };
|
|
476
498
|
}
|
|
477
|
-
const
|
|
499
|
+
const manifestEntry = reqCtx._classifiedRoute?.manifestEntry;
|
|
500
|
+
const pprConfig = resolvePprConfig(manifestEntry);
|
|
478
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" };
|
|
479
506
|
|
|
480
507
|
// A per-request CSP nonce pins the route to axis 1: a shared shell would
|
|
481
508
|
// freeze the capture request's nonce and CSP would reject it for every
|
|
@@ -484,8 +511,8 @@ function* shellServePlan<TEnv>(
|
|
|
484
511
|
// the provider-only check missed the latter (issue #656).
|
|
485
512
|
const activeNonce = nonce ?? contextGet(reqCtx._variables, nonceToken);
|
|
486
513
|
const store = reqCtx._cacheStore;
|
|
487
|
-
const
|
|
488
|
-
|
|
514
|
+
const baseKey = buildShellKey(url, reqCtx._searchParamsFilter);
|
|
515
|
+
let key = baseKey;
|
|
489
516
|
if (activeNonce !== undefined) {
|
|
490
517
|
// Declared intent that cannot be honored deserves a diagnostic (unlike an
|
|
491
518
|
// undeclared route, which is silent): a ppr route gated off by an active
|
|
@@ -499,11 +526,69 @@ function* shellServePlan<TEnv>(
|
|
|
499
526
|
warnShellStoreMissingOnce(key);
|
|
500
527
|
return { kind: "pass" };
|
|
501
528
|
}
|
|
502
|
-
|
|
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);
|
|
503
588
|
// SSR setup starts before route handling, so read the shell before joining it
|
|
504
589
|
// to overlap the remaining setup work with cache I/O.
|
|
505
590
|
const cached = yield* step("shell-read", () =>
|
|
506
|
-
readShellEntry(store, key, reqCtx),
|
|
591
|
+
readShellEntry(store, key, reqCtx, pprConfig.tags),
|
|
507
592
|
);
|
|
508
593
|
|
|
509
594
|
// allReady (ssr.resolveStreaming) bypasses PPR entirely: buffering defeats
|
|
@@ -528,12 +613,21 @@ function* shellServePlan<TEnv>(
|
|
|
528
613
|
store,
|
|
529
614
|
);
|
|
530
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.
|
|
531
619
|
if (
|
|
532
620
|
cached &&
|
|
533
621
|
!cached.entry.navigationOnly &&
|
|
622
|
+
(cached.entry.docKey !== undefined || route?.matched?.pr === true) &&
|
|
534
623
|
isValidShellHit(cached.entry, ctx.version)
|
|
535
624
|
) {
|
|
536
|
-
|
|
625
|
+
const document = openShellDocumentMetered(
|
|
626
|
+
reqCtx,
|
|
627
|
+
cached.entry,
|
|
628
|
+
cached.read,
|
|
629
|
+
);
|
|
630
|
+
if (!document) {
|
|
537
631
|
// Corrupt stored payload (undecodable prelude / unparseable
|
|
538
632
|
// postponed): a store-layer fault worth a diagnostic, unlike the
|
|
539
633
|
// silent version-mismatch lifecycle misses above. Degrade to MISS
|
|
@@ -548,8 +642,12 @@ function* shellServePlan<TEnv>(
|
|
|
548
642
|
);
|
|
549
643
|
} else {
|
|
550
644
|
// Stale (SWR) hit: serve the stale shell now, recapture in the
|
|
551
|
-
// background (stampede-guarded + backoff inside scheduleShellCapture)
|
|
552
|
-
|
|
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
|
+
) {
|
|
553
651
|
yield* handoff("shell-recapture", () =>
|
|
554
652
|
scheduleShellCapture(
|
|
555
653
|
ctx,
|
|
@@ -562,7 +660,6 @@ function* shellServePlan<TEnv>(
|
|
|
562
660
|
),
|
|
563
661
|
);
|
|
564
662
|
}
|
|
565
|
-
const entry = cached.entry;
|
|
566
663
|
const response = yield* step("shell-hit", () =>
|
|
567
664
|
serveShellHit(
|
|
568
665
|
ctx,
|
|
@@ -572,34 +669,76 @@ function* shellServePlan<TEnv>(
|
|
|
572
669
|
reqCtx,
|
|
573
670
|
handleStore,
|
|
574
671
|
ssrModule,
|
|
575
|
-
|
|
672
|
+
document,
|
|
576
673
|
descriptor,
|
|
674
|
+
activeNonce,
|
|
577
675
|
),
|
|
578
676
|
);
|
|
579
677
|
return { kind: "serve", response };
|
|
580
678
|
}
|
|
581
679
|
}
|
|
582
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
|
+
|
|
583
696
|
// Build-time shell read-through (producer B, #699): on a runtime
|
|
584
697
|
// 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
|
-
|
|
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) {
|
|
603
742
|
// Past ppr.ttl: still serve the baked entry, recapture upgrades it.
|
|
604
743
|
if (buildHit.stale) {
|
|
605
744
|
yield* handoff("shell-recapture", () =>
|
|
@@ -623,8 +762,9 @@ function* shellServePlan<TEnv>(
|
|
|
623
762
|
reqCtx,
|
|
624
763
|
handleStore,
|
|
625
764
|
ssrModule,
|
|
626
|
-
|
|
765
|
+
buildDocument,
|
|
627
766
|
descriptor,
|
|
767
|
+
activeNonce,
|
|
628
768
|
),
|
|
629
769
|
);
|
|
630
770
|
return { kind: "serve", response };
|
|
@@ -632,7 +772,11 @@ function* shellServePlan<TEnv>(
|
|
|
632
772
|
|
|
633
773
|
// MISS (no entry, invalid reactVersion, or store read failure): axis 1
|
|
634
774
|
// + a background capture scheduled once the response is known servable.
|
|
635
|
-
return {
|
|
775
|
+
return {
|
|
776
|
+
kind: "miss",
|
|
777
|
+
descriptor: { ...descriptor, storedSeqAtRead },
|
|
778
|
+
ssrModule,
|
|
779
|
+
};
|
|
636
780
|
}
|
|
637
781
|
|
|
638
782
|
/**
|
|
@@ -782,53 +926,157 @@ async function matchPartialAndRecordParams<TEnv>(
|
|
|
782
926
|
return replay;
|
|
783
927
|
}
|
|
784
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
|
+
|
|
785
936
|
/**
|
|
786
937
|
* Shell store read that degrades to null on failure (axis 1 MISS, never a 500)
|
|
787
938
|
* and records the raw `ppr:shell-read` outcome (pre-validity-gates, so a
|
|
788
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.
|
|
789
942
|
*/
|
|
790
|
-
async function readShellEntry
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
943
|
+
async function readShellEntry(
|
|
944
|
+
store: SegmentCacheStore & {
|
|
945
|
+
getShell: NonNullable<SegmentCacheStore["getShell"]>;
|
|
946
|
+
},
|
|
794
947
|
key: string,
|
|
795
948
|
reqCtx: ReturnType<typeof getRequestContext>,
|
|
796
|
-
|
|
797
|
-
|
|
949
|
+
tagHints: readonly string[] | undefined,
|
|
950
|
+
): Promise<ShellStoreRead | null> {
|
|
951
|
+
let cached: ShellStoreRead | null = null;
|
|
798
952
|
const shellReadStart = reqCtx._metricsStore ? performance.now() : 0;
|
|
799
953
|
try {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
+
}
|
|
805
965
|
} catch (error) {
|
|
806
966
|
reportCacheError(error, "cache-read", "[ShellServe] getShell");
|
|
807
967
|
}
|
|
808
968
|
if (reqCtx._metricsStore) {
|
|
969
|
+
const stats = cached?.read?.stats;
|
|
809
970
|
appendMetric(
|
|
810
971
|
reqCtx._metricsStore,
|
|
811
972
|
"ppr:shell-read",
|
|
812
973
|
shellReadStart,
|
|
813
974
|
performance.now() - shellReadStart,
|
|
814
975
|
undefined,
|
|
815
|
-
cached ? "hit" : "miss",
|
|
976
|
+
cached ? (stats ? `hit ${stats.tier}` : "hit") : "miss",
|
|
816
977
|
);
|
|
978
|
+
if (stats)
|
|
979
|
+
recordShellReadStats(reqCtx._metricsStore, shellReadStart, stats);
|
|
817
980
|
}
|
|
818
981
|
return cached;
|
|
819
982
|
}
|
|
820
983
|
|
|
821
984
|
/**
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
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.
|
|
826
990
|
*/
|
|
827
|
-
function
|
|
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).
|
|
1073
|
+
*/
|
|
1074
|
+
function mirrorPprServerTimings(
|
|
828
1075
|
key: string,
|
|
829
1076
|
reqCtx: ReturnType<typeof getRequestContext>,
|
|
830
1077
|
): void {
|
|
831
|
-
if (
|
|
1078
|
+
if (!reqCtx._metricsStore) return;
|
|
1079
|
+
if (process.env.NODE_ENV !== "production") {
|
|
832
1080
|
const lastCapture = takeCaptureDebugEventForTiming(key);
|
|
833
1081
|
if (lastCapture) {
|
|
834
1082
|
appendMetric(
|
|
@@ -841,22 +1089,23 @@ function mirrorPprServerTimingsForDev(
|
|
|
841
1089
|
describeShellCaptureEvent({ ...lastCapture, attemptMs: undefined }),
|
|
842
1090
|
);
|
|
843
1091
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1092
|
+
}
|
|
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
|
+
);
|
|
860
1109
|
}
|
|
861
1110
|
}
|
|
862
1111
|
|
|
@@ -944,7 +1193,7 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
944
1193
|
*/
|
|
945
1194
|
function scheduleNavigationShellCapture<TEnv>(
|
|
946
1195
|
input: RequestRenderInput<TEnv>,
|
|
947
|
-
|
|
1196
|
+
captureKey: string,
|
|
948
1197
|
): void {
|
|
949
1198
|
const { ctx, request, env, url, reqCtx } = input;
|
|
950
1199
|
const pprConfig = resolvePprConfig(reqCtx._classifiedRoute?.manifestEntry)!;
|
|
@@ -967,8 +1216,7 @@ function scheduleNavigationShellCapture<TEnv>(
|
|
|
967
1216
|
},
|
|
968
1217
|
createShellCaptureDescriptor(
|
|
969
1218
|
ctx,
|
|
970
|
-
|
|
971
|
-
buildNavigationShellKey(url, reqCtx._searchParamsFilter),
|
|
1219
|
+
captureKey,
|
|
972
1220
|
shellSearchSeed(url, reqCtx._searchParamsFilter),
|
|
973
1221
|
url.origin,
|
|
974
1222
|
pprConfig,
|
|
@@ -1127,8 +1375,26 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1127
1375
|
return runMatch({ outcome: "BYPASS", reason: "cache-disabled" });
|
|
1128
1376
|
}
|
|
1129
1377
|
|
|
1130
|
-
|
|
1131
|
-
|
|
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);
|
|
1132
1398
|
let cached: Awaited<ReturnType<typeof store.getShell>> = null;
|
|
1133
1399
|
try {
|
|
1134
1400
|
cached = await store.getShell(key, { claimRevalidation: false });
|
|
@@ -1152,11 +1418,7 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1152
1418
|
}
|
|
1153
1419
|
}
|
|
1154
1420
|
|
|
1155
|
-
if (
|
|
1156
|
-
!snapshot &&
|
|
1157
|
-
bypassReason !== "handler-live-holes" &&
|
|
1158
|
-
bypassReason !== "transition-when"
|
|
1159
|
-
) {
|
|
1421
|
+
if (!snapshot) {
|
|
1160
1422
|
let navigationCached: Awaited<ReturnType<typeof store.getShell>> = null;
|
|
1161
1423
|
try {
|
|
1162
1424
|
navigationCached = await store.getShell(navigationKey, {
|
|
@@ -1185,13 +1447,16 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1185
1447
|
// Production build manifests are local module data. In dev, resolving a
|
|
1186
1448
|
// missing build shell would foreground-fetch /__rsc_shell and block an
|
|
1187
1449
|
// otherwise ordinary navigation on capture, so replay remains runtime-only.
|
|
1188
|
-
const buildHit =
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1450
|
+
const buildHit =
|
|
1451
|
+
key !== baseKey
|
|
1452
|
+
? null
|
|
1453
|
+
: await lookupBuildShell(
|
|
1454
|
+
url,
|
|
1455
|
+
ctx.version,
|
|
1456
|
+
store,
|
|
1457
|
+
undefined,
|
|
1458
|
+
reqCtx._searchParamsFilter,
|
|
1459
|
+
);
|
|
1195
1460
|
if (buildHit?.stale) {
|
|
1196
1461
|
bypassReason ??= "stale-build-entry";
|
|
1197
1462
|
} else if (buildHit) {
|
|
@@ -1259,6 +1524,7 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1259
1524
|
reqCtx._pprReplayPostMatchReason === undefined &&
|
|
1260
1525
|
(shouldHealReplayMiss(bypassReason) ||
|
|
1261
1526
|
bypassReason === "no-segment-snapshot"),
|
|
1527
|
+
captureKey: navigationKey,
|
|
1262
1528
|
};
|
|
1263
1529
|
} finally {
|
|
1264
1530
|
reqCtx._shellImplicitCache = previousImplicitCache;
|
|
@@ -1273,10 +1539,13 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1273
1539
|
captureNeeded:
|
|
1274
1540
|
reqCtx._pprReplayPostMatchReason === undefined &&
|
|
1275
1541
|
shouldHealReplayMiss(bypassReason),
|
|
1542
|
+
captureKey: navigationKey,
|
|
1276
1543
|
};
|
|
1277
1544
|
}
|
|
1278
1545
|
|
|
1279
1546
|
const previousImplicitCache = reqCtx._shellImplicitCache;
|
|
1547
|
+
const previousLoaderSeed = reqCtx._shellLoaderSeed;
|
|
1548
|
+
let loaderSeed: ReturnType<typeof buildShellLoaderSeed> | undefined;
|
|
1280
1549
|
let segmentReplayHit = false;
|
|
1281
1550
|
let segmentReplayCorrupt = false;
|
|
1282
1551
|
let explicitCacheHit = false;
|
|
@@ -1288,8 +1557,18 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1288
1557
|
segmentsOnly: true,
|
|
1289
1558
|
}),
|
|
1290
1559
|
keyPrefix: "doc",
|
|
1291
|
-
|
|
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 () => {
|
|
1292
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;
|
|
1293
1572
|
},
|
|
1294
1573
|
onCorrupt: () => {
|
|
1295
1574
|
segmentReplayCorrupt = true;
|
|
@@ -1337,6 +1616,7 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1337
1616
|
};
|
|
1338
1617
|
} finally {
|
|
1339
1618
|
reqCtx._shellImplicitCache = previousImplicitCache;
|
|
1619
|
+
reqCtx._shellLoaderSeed = previousLoaderSeed;
|
|
1340
1620
|
}
|
|
1341
1621
|
}
|
|
1342
1622
|
|
|
@@ -1381,6 +1661,75 @@ export function resolveShellHitRedirectTarget(
|
|
|
1381
1661
|
);
|
|
1382
1662
|
}
|
|
1383
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
|
+
|
|
1384
1733
|
/**
|
|
1385
1734
|
* Serve a validated shell HIT: commit the stored prelude bytes NOW and run the
|
|
1386
1735
|
* live tail behind them inside the response stream. Plain byte concatenation is
|
|
@@ -1398,15 +1747,18 @@ function serveShellHit(
|
|
|
1398
1747
|
reqCtx: RequestContext<any>,
|
|
1399
1748
|
handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
|
|
1400
1749
|
ssrModule: SSRModule,
|
|
1401
|
-
|
|
1750
|
+
document: ShellDocument,
|
|
1402
1751
|
descriptor: ShellCaptureDescriptor,
|
|
1752
|
+
nonce: string | undefined,
|
|
1403
1753
|
): Response {
|
|
1404
|
-
const preludeBytes =
|
|
1405
|
-
|
|
1406
|
-
//
|
|
1407
|
-
//
|
|
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.
|
|
1408
1760
|
const tailTiming: ShellTailTiming | null =
|
|
1409
|
-
process.env.NODE_ENV !== "production"
|
|
1761
|
+
process.env.NODE_ENV !== "production" || metricsStore
|
|
1410
1762
|
? {
|
|
1411
1763
|
key: descriptor.key,
|
|
1412
1764
|
outcome: "complete",
|
|
@@ -1414,6 +1766,8 @@ function serveShellHit(
|
|
|
1414
1766
|
}
|
|
1415
1767
|
: null;
|
|
1416
1768
|
const tailT0 = tailTiming ? performance.now() : 0;
|
|
1769
|
+
const scheduleRecapture = (): void =>
|
|
1770
|
+
scheduleShellCapture(ctx, request, env, url, reqCtx, ssrModule, descriptor);
|
|
1417
1771
|
|
|
1418
1772
|
const createTailContext = (): RequestContext<any> => {
|
|
1419
1773
|
const tailCtx: RequestContext<any> = Object.create(reqCtx);
|
|
@@ -1486,7 +1840,7 @@ function serveShellHit(
|
|
|
1486
1840
|
}
|
|
1487
1841
|
return observePhase(PHASES.ssr, () =>
|
|
1488
1842
|
ssrModule.resumeShellHTML!(rscStream, {
|
|
1489
|
-
postponed:
|
|
1843
|
+
postponed: document.postponed,
|
|
1490
1844
|
nonce: undefined,
|
|
1491
1845
|
// The shell key's own search — identical to the capture seed for
|
|
1492
1846
|
// this key, so the resume tree matches the captured tree.
|
|
@@ -1509,64 +1863,78 @@ function serveShellHit(
|
|
|
1509
1863
|
// values via a SeededShellStore overlay while unpinned reads (the holes)
|
|
1510
1864
|
// stay live. The overlay lives on a DERIVED context so the shared reqCtx is
|
|
1511
1865
|
// untouched.
|
|
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
|
-
);
|
|
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);
|
|
1555
1885
|
}
|
|
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
1886
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
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));
|
|
1570
1938
|
})();
|
|
1571
1939
|
// The stream below is the only consumer; pre-attach a no-op catch so a tail
|
|
1572
1940
|
// failure before the stream is pulled never surfaces as an unhandled rejection.
|
|
@@ -1575,7 +1943,31 @@ function serveShellHit(
|
|
|
1575
1943
|
const serveStart = INTERNAL_RANGO_DEBUG ? performance.now() : 0;
|
|
1576
1944
|
const body = new ReadableStream<Uint8Array>({
|
|
1577
1945
|
async start(controller) {
|
|
1578
|
-
|
|
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
|
+
}
|
|
1579
1971
|
if (INTERNAL_RANGO_DEBUG) {
|
|
1580
1972
|
console.log(
|
|
1581
1973
|
`[Server][ppr] shell HIT: prelude enqueued (${preludeBytes.length}b) +${Math.round(performance.now() - serveStart)}ms`,
|
|
@@ -1647,7 +2039,10 @@ function serveShellHit(
|
|
|
1647
2039
|
);
|
|
1648
2040
|
controller.enqueue(
|
|
1649
2041
|
new TextEncoder().encode(
|
|
1650
|
-
|
|
2042
|
+
inlineShellScript(
|
|
2043
|
+
`location.replace(${escapeJsonForScript(JSON.stringify(safeTarget))})`,
|
|
2044
|
+
nonce,
|
|
2045
|
+
),
|
|
1651
2046
|
),
|
|
1652
2047
|
);
|
|
1653
2048
|
if (tailTiming) {
|
|
@@ -1655,9 +2050,34 @@ function serveShellHit(
|
|
|
1655
2050
|
tailTiming.completeMs = Math.round(performance.now() - tailT0);
|
|
1656
2051
|
}
|
|
1657
2052
|
}
|
|
1658
|
-
|
|
2053
|
+
publishTailTiming(tailTiming, metricsStore, request, url);
|
|
1659
2054
|
controller.close();
|
|
1660
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
|
+
}
|
|
1661
2081
|
// Self-heal on a failed tail: errors the pre-commit gates cannot catch
|
|
1662
2082
|
// (mismatched postponed blob, hard render error above the holes) throw
|
|
1663
2083
|
// here AFTER the 200 + prelude flushed and would re-fail on every
|
|
@@ -1676,8 +2096,8 @@ function serveShellHit(
|
|
|
1676
2096
|
if (tailTiming) {
|
|
1677
2097
|
tailTiming.outcome = "error";
|
|
1678
2098
|
tailTiming.completeMs = Math.round(performance.now() - tailT0);
|
|
1679
|
-
publishShellTailTiming(tailTiming);
|
|
1680
2099
|
}
|
|
2100
|
+
publishTailTiming(tailTiming, metricsStore, request, url);
|
|
1681
2101
|
controller.error(error);
|
|
1682
2102
|
}
|
|
1683
2103
|
},
|