@rangojs/router 0.5.0 → 0.5.2
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 +5 -1
- package/dist/types/browser/event-controller.d.ts +6 -0
- package/dist/types/browser/prefetch/cache.d.ts +31 -2
- package/dist/types/cache/cf/cf-cache-constants.d.ts +8 -1
- package/dist/types/cache/cf/cf-cache-store.d.ts +15 -1
- package/dist/types/cache/cf/cf-cache-types.d.ts +1 -1
- package/dist/types/cache/cf/cf-kv-utils.d.ts +21 -0
- package/dist/types/handles/is-thenable.d.ts +2 -4
- package/dist/types/route-definition/helpers-types.d.ts +5 -4
- package/dist/types/rsc/helpers.d.ts +3 -0
- package/dist/types/rsc/render-pipeline.d.ts +9 -0
- package/dist/types/rsc/routine-plan.d.ts +124 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +17 -0
- package/dist/types/server/request-context.d.ts +4 -1
- package/dist/types/static-handler.d.ts +15 -1
- package/dist/types/urls/path-helper-types.d.ts +6 -7
- package/dist/types/vite/encryption-key.d.ts +2 -0
- package/dist/types/vite/plugins/expose-internal-ids.d.ts +10 -0
- package/dist/types/vite/plugins/server-ref-hashing.d.ts +24 -0
- package/dist/types/vite/plugins/server-reference-pattern.d.ts +1 -0
- package/dist/types/vite/utils/shared-utils.d.ts +12 -0
- package/dist/vite/index.js +154 -57
- package/package.json +3 -3
- package/skills/caching/SKILL.md +1 -1
- package/skills/mime-routes/SKILL.md +3 -1
- package/skills/parallel/SKILL.md +1 -1
- package/skills/response-routes/SKILL.md +4 -2
- package/skills/typesafety/generated-files-and-cli.md +16 -9
- package/skills/typesafety/route-types.md +5 -1
- package/skills/use-cache/SKILL.md +47 -0
- package/src/browser/event-controller.ts +40 -15
- package/src/browser/partial-update.ts +26 -11
- package/src/browser/prefetch/cache.ts +53 -9
- package/src/browser/prefetch/fetch.ts +83 -2
- package/src/browser/react/NavigationProvider.tsx +7 -0
- package/src/cache/cache-runtime.ts +89 -15
- package/src/cache/cf/cf-cache-constants.ts +8 -1
- package/src/cache/cf/cf-cache-store.ts +41 -64
- package/src/cache/cf/cf-cache-types.ts +1 -1
- package/src/cache/cf/cf-kv-utils.ts +38 -0
- package/src/cache/segment-codec.ts +21 -5
- package/src/handles/is-thenable.ts +2 -4
- package/src/route-definition/helpers-types.ts +5 -3
- package/src/router/match-middleware/cache-lookup.ts +24 -15
- package/src/rsc/handler.ts +26 -5
- package/src/rsc/helpers.ts +13 -0
- package/src/rsc/progressive-enhancement.ts +247 -70
- package/src/rsc/render-pipeline.ts +68 -24
- package/src/rsc/routine-plan.ts +359 -0
- package/src/rsc/rsc-rendering.ts +555 -302
- package/src/rsc/server-action.ts +180 -71
- package/src/rsc/shell-capture-constants.ts +18 -0
- package/src/rsc/shell-capture.ts +92 -21
- package/src/server/request-context.ts +6 -0
- package/src/ssr/ssr-root.tsx +1 -0
- package/src/static-handler.ts +18 -2
- package/src/urls/path-helper-types.ts +9 -10
- package/src/vite/encryption-key.ts +29 -0
- package/src/vite/plugins/expose-action-id.ts +2 -2
- package/src/vite/plugins/expose-internal-ids.ts +46 -0
- package/src/vite/plugins/server-ref-hashing.ts +74 -0
- package/src/vite/plugins/server-reference-pattern.ts +10 -0
- package/src/vite/rango.ts +9 -0
- package/src/vite/router-discovery.ts +21 -2
- package/src/vite/utils/shared-utils.ts +12 -7
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -27,8 +27,21 @@ import {
|
|
|
27
27
|
createResponseWithMergedHeaders,
|
|
28
28
|
createSimpleRedirectResponse,
|
|
29
29
|
attachLocationStateIfPresent,
|
|
30
|
+
matchAndRecordParams,
|
|
30
31
|
} from "./helpers.js";
|
|
31
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
createRenderStageTraceBridge,
|
|
34
|
+
renderRscFlightStage,
|
|
35
|
+
renderRscResponse,
|
|
36
|
+
} from "./render-pipeline.js";
|
|
37
|
+
import {
|
|
38
|
+
createRoutineTrace,
|
|
39
|
+
handoff,
|
|
40
|
+
runRoutine,
|
|
41
|
+
scope,
|
|
42
|
+
step,
|
|
43
|
+
type RoutinePlan,
|
|
44
|
+
} from "./routine-plan.js";
|
|
32
45
|
import type { HandlerContext } from "./handler-context.js";
|
|
33
46
|
import { gateTransitions } from "./transition-gate.js";
|
|
34
47
|
import { buildFullPayload } from "./full-payload.js";
|
|
@@ -281,6 +294,34 @@ export function handleRscRendering<TEnv>(
|
|
|
281
294
|
);
|
|
282
295
|
}
|
|
283
296
|
|
|
297
|
+
interface RequestRenderInput<TEnv> {
|
|
298
|
+
ctx: HandlerContext<TEnv>;
|
|
299
|
+
request: Request;
|
|
300
|
+
env: TEnv;
|
|
301
|
+
url: URL;
|
|
302
|
+
isPartial: boolean;
|
|
303
|
+
handleStore: ReturnType<typeof getRequestContext>["_handleStore"];
|
|
304
|
+
nonce: string | undefined;
|
|
305
|
+
renderSpan: TraceSpan;
|
|
306
|
+
reqCtx: ReturnType<typeof getRequestContext>;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
type ShellServeOutcome =
|
|
310
|
+
| { kind: "serve"; response: Response }
|
|
311
|
+
| { kind: "miss"; descriptor: ShellCaptureDescriptor; ssrModule: SSRModule }
|
|
312
|
+
| { kind: "pass" };
|
|
313
|
+
|
|
314
|
+
type PreparedRender =
|
|
315
|
+
| { kind: "control"; response: Response }
|
|
316
|
+
| {
|
|
317
|
+
kind: "payload";
|
|
318
|
+
payload: RscPayload;
|
|
319
|
+
hasInterceptSlots: boolean;
|
|
320
|
+
pprReplayStatus?: PprReplayStatus;
|
|
321
|
+
partialCaptureNeeded: boolean;
|
|
322
|
+
partialCaptureKey?: string;
|
|
323
|
+
};
|
|
324
|
+
|
|
284
325
|
async function handleRscRenderingInner<TEnv>(
|
|
285
326
|
ctx: HandlerContext<TEnv>,
|
|
286
327
|
request: Request,
|
|
@@ -291,13 +332,49 @@ async function handleRscRenderingInner<TEnv>(
|
|
|
291
332
|
nonce: string | undefined,
|
|
292
333
|
renderSpan: TraceSpan,
|
|
293
334
|
): Promise<Response> {
|
|
335
|
+
// Flow trace rides the internal debug surface (INTERNAL_RANGO_DEBUG=1): the
|
|
336
|
+
// Vite pipeline bakes the flag at build/dev-server start (inject-client-debug),
|
|
337
|
+
// so trace allocation and output fold out of ordinary production builds while
|
|
338
|
+
// an explicit debug build keeps them. The routine itself remains the executor.
|
|
339
|
+
const mode = isPartial ? "partial" : "document";
|
|
340
|
+
const trace = INTERNAL_RANGO_DEBUG ? createRoutineTrace(mode) : undefined;
|
|
294
341
|
const reqCtx = getRequestContext();
|
|
342
|
+
const input: RequestRenderInput<TEnv> = {
|
|
343
|
+
ctx,
|
|
344
|
+
request,
|
|
345
|
+
env,
|
|
346
|
+
url,
|
|
347
|
+
isPartial,
|
|
348
|
+
handleStore,
|
|
349
|
+
nonce,
|
|
350
|
+
renderSpan,
|
|
351
|
+
reqCtx,
|
|
352
|
+
};
|
|
353
|
+
try {
|
|
354
|
+
return await runRoutine(requestRenderPlan(input), {
|
|
355
|
+
trace,
|
|
356
|
+
owner: reqCtx,
|
|
357
|
+
});
|
|
358
|
+
} finally {
|
|
359
|
+
if (trace) {
|
|
360
|
+
console.log(
|
|
361
|
+
`[routine] ${request.method} ${url.pathname} (${trace.name})\n${trace.format()}`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
295
366
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
367
|
+
/**
|
|
368
|
+
* The request-level render plan. Reads top to bottom as the request executes:
|
|
369
|
+
* serve a stored PPR shell if one commits, otherwise match and build the
|
|
370
|
+
* payload, render it through the stage driver, then hand off background
|
|
371
|
+
* captures. Effects run through the routine runner (yield-before-execute,
|
|
372
|
+
* exact result/error identity — see routine-plan.ts).
|
|
373
|
+
*/
|
|
374
|
+
function* requestRenderPlan<TEnv>(
|
|
375
|
+
input: RequestRenderInput<TEnv>,
|
|
376
|
+
): RoutinePlan<Response> {
|
|
377
|
+
const { isPartial, reqCtx } = input;
|
|
301
378
|
|
|
302
379
|
// --- Axis 2: integrated PPR shell serve (docs/design/ppr-shell-resume.md) ---
|
|
303
380
|
//
|
|
@@ -305,214 +382,272 @@ async function handleRscRenderingInner<TEnv>(
|
|
|
305
382
|
// chain (executeRender wraps it), so no shell byte can precede a guard
|
|
306
383
|
// decision — that ordering is what makes a shared shell safe. Routes
|
|
307
384
|
// without the `ppr` option stay pure axis 1 at zero cost.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
385
|
+
const shell = yield* scope("shell-serve", shellServePlan(input));
|
|
386
|
+
if (shell.kind === "serve") return shell.response;
|
|
387
|
+
|
|
388
|
+
// Match + payload. A control response (redirect, prerender collection)
|
|
389
|
+
// ends the plan before any rendering.
|
|
390
|
+
const prepared = yield* scope(
|
|
391
|
+
isPartial ? "prepare:partial" : "prepare:full",
|
|
392
|
+
preparePayloadPlan(input),
|
|
393
|
+
);
|
|
394
|
+
if (prepared.kind === "control") return prepared.response;
|
|
395
|
+
|
|
396
|
+
// For partial requests, include any server-set location state in the payload.
|
|
397
|
+
// SSR (full page) requests ignore location state since there's no history.state
|
|
398
|
+
// to write to on a fresh page load.
|
|
399
|
+
if (isPartial && prepared.payload.metadata) {
|
|
400
|
+
attachLocationStateIfPresent(prepared.payload);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const response = yield* step("render", () =>
|
|
404
|
+
renderPreparedRscResponse(input, prepared),
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
// --- Axis 2: PPR shell CAPTURE on MISS (background task; see design doc) ---
|
|
408
|
+
// Capture only a 200 HTML document (a 404/error render is not a cacheable
|
|
409
|
+
// shell). Capture does not flow through the HTTP pipeline — middleware never
|
|
410
|
+
// re-runs (it already ran for this request; guarding is serve-time).
|
|
411
|
+
// reqCtx._dynamic is re-read here on purpose: a loader may have marked the
|
|
412
|
+
// request dynamic while rendering.
|
|
413
|
+
if (shell.kind === "miss" && !reqCtx._dynamic) {
|
|
414
|
+
if (
|
|
415
|
+
response.status === 200 &&
|
|
416
|
+
(response.headers.get("content-type") ?? "").includes("text/html")
|
|
417
|
+
) {
|
|
418
|
+
const { descriptor, ssrModule } = shell;
|
|
419
|
+
yield* handoff("shell-capture", () =>
|
|
420
|
+
scheduleShellCapture(
|
|
421
|
+
input.ctx,
|
|
422
|
+
input.request,
|
|
423
|
+
input.env,
|
|
424
|
+
input.url,
|
|
425
|
+
reqCtx,
|
|
426
|
+
ssrModule,
|
|
427
|
+
descriptor,
|
|
428
|
+
),
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
response.headers.set(SHELL_STATUS_HEADER, "MISS");
|
|
432
|
+
}
|
|
433
|
+
|
|
312
434
|
if (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
!
|
|
316
|
-
|
|
317
|
-
!reqCtx._dynamic
|
|
435
|
+
isPartial &&
|
|
436
|
+
prepared.partialCaptureNeeded &&
|
|
437
|
+
!reqCtx._dynamic &&
|
|
438
|
+
response.status === 200
|
|
318
439
|
) {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
440
|
+
const captureKey = prepared.partialCaptureKey;
|
|
441
|
+
yield* handoff("navigation-shell-capture", () =>
|
|
442
|
+
scheduleNavigationShellCapture(input, captureKey),
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return response;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Axis 2 shell serve. Sync gates stay plan code; store and SSR effects run as
|
|
451
|
+
* named steps. Outcomes: serve a composed shell response, report a MISS (the
|
|
452
|
+
* top-level plan schedules the capture after the response commits), or pass
|
|
453
|
+
* (route not eligible — pure axis 1).
|
|
454
|
+
*/
|
|
455
|
+
function* shellServePlan<TEnv>(
|
|
456
|
+
input: RequestRenderInput<TEnv>,
|
|
457
|
+
): RoutinePlan<ShellServeOutcome> {
|
|
458
|
+
const { ctx, request, env, url, isPartial, handleStore, nonce, reqCtx } =
|
|
459
|
+
input;
|
|
460
|
+
|
|
461
|
+
if (
|
|
462
|
+
isPartial ||
|
|
463
|
+
request.method !== "GET" ||
|
|
464
|
+
url.searchParams.has("__prerender_collect") ||
|
|
465
|
+
isRscRequest(request, url, false) ||
|
|
466
|
+
reqCtx._dynamic
|
|
467
|
+
) {
|
|
468
|
+
return { kind: "pass" };
|
|
469
|
+
}
|
|
470
|
+
const pprConfig = resolvePprConfig(reqCtx._classifiedRoute?.manifestEntry);
|
|
471
|
+
if (!pprConfig) return { kind: "pass" };
|
|
472
|
+
|
|
473
|
+
// A per-request CSP nonce pins the route to axis 1: a shared shell would
|
|
474
|
+
// freeze the capture request's nonce and CSP would reject it for every
|
|
475
|
+
// other visitor. BOTH nonce sources must gate — the createRouter({ nonce })
|
|
476
|
+
// provider (`nonce` param) and a middleware ctx.set(nonce, …) token write;
|
|
477
|
+
// the provider-only check missed the latter (issue #656).
|
|
478
|
+
const activeNonce = nonce ?? contextGet(reqCtx._variables, nonceToken);
|
|
479
|
+
const store = reqCtx._cacheStore;
|
|
480
|
+
const key = buildShellKey(url, reqCtx._searchParamsFilter);
|
|
481
|
+
mirrorPprServerTimingsForDev(key, reqCtx);
|
|
482
|
+
if (activeNonce !== undefined) {
|
|
483
|
+
// Declared intent that cannot be honored deserves a diagnostic (unlike an
|
|
484
|
+
// undeclared route, which is silent): a ppr route gated off by an active
|
|
485
|
+
// per-request nonce warns once per key. Axis 1 after the warning.
|
|
486
|
+
warnPprNonceActiveOnce(key);
|
|
487
|
+
return { kind: "pass" };
|
|
488
|
+
}
|
|
489
|
+
if (!hasShellFamily(store)) {
|
|
490
|
+
// Declared intent that cannot be honored deserves a diagnostic (unlike an
|
|
491
|
+
// undeclared route, which is silent). Axis 1 after the warning.
|
|
492
|
+
warnShellStoreMissingOnce(key);
|
|
493
|
+
return { kind: "pass" };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// SSR setup starts before route handling, so read the shell before joining it
|
|
497
|
+
// to overlap the remaining setup work with cache I/O.
|
|
498
|
+
const cached = yield* step("shell-read", () =>
|
|
499
|
+
readShellEntry(store, key, reqCtx),
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
// allReady (ssr.resolveStreaming) bypasses PPR entirely: buffering defeats
|
|
503
|
+
// streaming, so bots/SEO crawlers get one complete axis-1 document. These
|
|
504
|
+
// routes pay one unnecessary shell read to keep the PPR hot path concurrent.
|
|
505
|
+
const [ssrModule, streamMode] = yield* step("ssr-setup", () =>
|
|
506
|
+
getSSRSetup(ctx, request, env, url, reqCtx._metricsStore),
|
|
507
|
+
);
|
|
508
|
+
if (
|
|
509
|
+
streamMode === "allReady" ||
|
|
510
|
+
!ssrModule.resumeShellHTML ||
|
|
511
|
+
!ssrModule.captureShellHTML
|
|
512
|
+
) {
|
|
513
|
+
return { kind: "pass" };
|
|
514
|
+
}
|
|
515
|
+
const descriptor = createShellCaptureDescriptor(ctx, key, pprConfig, store);
|
|
516
|
+
|
|
517
|
+
if (
|
|
518
|
+
cached &&
|
|
519
|
+
!cached.entry.navigationOnly &&
|
|
520
|
+
isValidShellHit(cached.entry, ctx.version)
|
|
521
|
+
) {
|
|
522
|
+
if (!hasIntactShellPayload(cached.entry)) {
|
|
523
|
+
// Corrupt stored payload (undecodable prelude / unparseable
|
|
524
|
+
// postponed): a store-layer fault worth a diagnostic, unlike the
|
|
525
|
+
// silent version-mismatch lifecycle misses above. Degrade to MISS
|
|
526
|
+
// — the top-level plan schedules the recapture that overwrites it.
|
|
527
|
+
reportCacheError(
|
|
528
|
+
new Error(
|
|
529
|
+
`corrupt shell entry for "${key}": prelude/postponed failed ` +
|
|
530
|
+
"the integrity check; serving axis 1 and recapturing",
|
|
531
|
+
),
|
|
532
|
+
"cache-read",
|
|
533
|
+
"[ShellServe] getShell",
|
|
534
|
+
);
|
|
535
|
+
} else {
|
|
536
|
+
// Stale (SWR) hit: serve the stale shell now, recapture in the
|
|
537
|
+
// background (stampede-guarded + backoff inside scheduleShellCapture).
|
|
538
|
+
if (cached.shouldRevalidate) {
|
|
539
|
+
yield* handoff("shell-recapture", () =>
|
|
540
|
+
scheduleShellCapture(
|
|
541
|
+
ctx,
|
|
542
|
+
request,
|
|
543
|
+
env,
|
|
544
|
+
url,
|
|
545
|
+
reqCtx,
|
|
546
|
+
ssrModule,
|
|
547
|
+
descriptor,
|
|
548
|
+
),
|
|
549
|
+
);
|
|
362
550
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
// per-request nonce warns once per key. Axis 1 after the warning.
|
|
367
|
-
warnPprNonceActiveOnce(key);
|
|
368
|
-
} else if (!hasShellFamily(store)) {
|
|
369
|
-
// Declared intent that cannot be honored deserves a diagnostic (unlike an
|
|
370
|
-
// undeclared route, which is silent). Axis 1 after the warning.
|
|
371
|
-
warnShellStoreMissingOnce(key);
|
|
372
|
-
} else {
|
|
373
|
-
// allReady (ssr.resolveStreaming) bypasses PPR entirely: buffering defeats
|
|
374
|
-
// streaming, so bots/SEO crawlers get one complete axis-1 document.
|
|
375
|
-
const [ssrModule, streamMode] = await getSSRSetup(
|
|
551
|
+
const entry = cached.entry;
|
|
552
|
+
const response = yield* step("shell-hit", () =>
|
|
553
|
+
serveShellHit(
|
|
376
554
|
ctx,
|
|
377
555
|
request,
|
|
378
556
|
env,
|
|
379
557
|
url,
|
|
380
|
-
reqCtx
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
ctx,
|
|
389
|
-
key,
|
|
390
|
-
pprConfig,
|
|
391
|
-
store,
|
|
392
|
-
);
|
|
393
|
-
// One serve funnel for BOTH entry sources (runtime store hit below,
|
|
394
|
-
// build-manifest hit further down): schedule the background
|
|
395
|
-
// recapture when asked, then commit the composed response.
|
|
396
|
-
const serveHit = (
|
|
397
|
-
entry: DocumentShellCacheEntry,
|
|
398
|
-
revalidate: boolean | undefined,
|
|
399
|
-
): Response => {
|
|
400
|
-
if (revalidate) {
|
|
401
|
-
scheduleShellCapture(
|
|
402
|
-
ctx,
|
|
403
|
-
request,
|
|
404
|
-
env,
|
|
405
|
-
url,
|
|
406
|
-
reqCtx,
|
|
407
|
-
ssrModule,
|
|
408
|
-
descriptor,
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
return serveShellHit(
|
|
412
|
-
ctx,
|
|
413
|
-
request,
|
|
414
|
-
env,
|
|
415
|
-
url,
|
|
416
|
-
reqCtx,
|
|
417
|
-
handleStore,
|
|
418
|
-
ssrModule,
|
|
419
|
-
entry,
|
|
420
|
-
descriptor,
|
|
421
|
-
);
|
|
422
|
-
};
|
|
423
|
-
let cached: Awaited<ReturnType<typeof store.getShell>> = null;
|
|
424
|
-
const shellReadStart = reqCtx._metricsStore ? performance.now() : 0;
|
|
425
|
-
try {
|
|
426
|
-
cached = await store.getShell(key);
|
|
427
|
-
} catch (error) {
|
|
428
|
-
// A failing store read degrades to axis 1 (MISS), never a 500.
|
|
429
|
-
reportCacheError(error, "cache-read", "[ShellServe] getShell");
|
|
430
|
-
}
|
|
431
|
-
if (reqCtx._metricsStore) {
|
|
432
|
-
// Raw store outcome (pre-validity-gates), so a version-mismatch
|
|
433
|
-
// lifecycle miss is still distinguishable from a store miss.
|
|
434
|
-
appendMetric(
|
|
435
|
-
reqCtx._metricsStore,
|
|
436
|
-
"ppr:shell-read",
|
|
437
|
-
shellReadStart,
|
|
438
|
-
performance.now() - shellReadStart,
|
|
439
|
-
undefined,
|
|
440
|
-
cached ? "hit" : "miss",
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
if (
|
|
444
|
-
cached &&
|
|
445
|
-
!cached.entry.navigationOnly &&
|
|
446
|
-
isValidShellHit(cached.entry, ctx.version)
|
|
447
|
-
) {
|
|
448
|
-
if (!hasIntactShellPayload(cached.entry)) {
|
|
449
|
-
// Corrupt stored payload (undecodable prelude / unparseable
|
|
450
|
-
// postponed): a store-layer fault worth a diagnostic, unlike the
|
|
451
|
-
// silent version-mismatch lifecycle misses above. Degrade to MISS
|
|
452
|
-
// — pprMiss below schedules the recapture that overwrites it.
|
|
453
|
-
reportCacheError(
|
|
454
|
-
new Error(
|
|
455
|
-
`corrupt shell entry for "${key}": prelude/postponed failed ` +
|
|
456
|
-
"the integrity check; serving axis 1 and recapturing",
|
|
457
|
-
),
|
|
458
|
-
"cache-read",
|
|
459
|
-
"[ShellServe] getShell",
|
|
460
|
-
);
|
|
461
|
-
} else {
|
|
462
|
-
// Stale (SWR) hit: serve the stale shell now, recapture in the
|
|
463
|
-
// background (stampede-guarded + backoff inside scheduleShellCapture).
|
|
464
|
-
return serveHit(cached.entry, cached.shouldRevalidate);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
// Build-time shell read-through (producer B, #699): on a runtime
|
|
468
|
-
// store MISS a Prerender+ppr route serves its `vite build`-baked
|
|
469
|
-
// shell through the SAME serveShellHit. lookupBuildShell owns every
|
|
470
|
-
// gate and fails to null (ordinary MISS path takes over); past
|
|
471
|
-
// ppr.ttl the baked entry still serves while SWR recaptures — the
|
|
472
|
-
// upgrade path from build entry to runtime entry.
|
|
473
|
-
const buildHit = await lookupBuildShell(
|
|
474
|
-
url,
|
|
475
|
-
ctx.version,
|
|
476
|
-
store,
|
|
477
|
-
// Dev: no build manifest exists; producer B runs on demand via
|
|
478
|
-
// the dev server's /__rsc_shell endpoint for PRERENDERED routes
|
|
479
|
-
// only (production's exact candidate set). Folded away in
|
|
480
|
-
// production builds (NODE_ENV is a compile-time constant).
|
|
481
|
-
resolveDevShellLookup(reqCtx, pprConfig),
|
|
482
|
-
reqCtx._searchParamsFilter,
|
|
483
|
-
);
|
|
484
|
-
if (buildHit) {
|
|
485
|
-
// Past ppr.ttl: still serve the baked entry, recapture upgrades it.
|
|
486
|
-
return serveHit(buildHit.entry, buildHit.stale);
|
|
487
|
-
}
|
|
488
|
-
// MISS (no entry, invalid reactVersion, or store read failure): axis 1
|
|
489
|
-
// + a background capture scheduled once the response is known servable.
|
|
490
|
-
pprMiss = { descriptor, ssrModule };
|
|
491
|
-
}
|
|
492
|
-
}
|
|
558
|
+
reqCtx,
|
|
559
|
+
handleStore,
|
|
560
|
+
ssrModule,
|
|
561
|
+
entry,
|
|
562
|
+
descriptor,
|
|
563
|
+
),
|
|
564
|
+
);
|
|
565
|
+
return { kind: "serve", response };
|
|
493
566
|
}
|
|
494
567
|
}
|
|
495
568
|
|
|
569
|
+
// Build-time shell read-through (producer B, #699): on a runtime
|
|
570
|
+
// store MISS a Prerender+ppr route serves its `vite build`-baked
|
|
571
|
+
// shell through the SAME serve path. lookupBuildShell owns every
|
|
572
|
+
// gate and fails to null (ordinary MISS path takes over); past
|
|
573
|
+
// ppr.ttl the baked entry still serves while SWR recaptures — the
|
|
574
|
+
// upgrade path from build entry to runtime entry.
|
|
575
|
+
const buildHit = yield* step("build-shell-lookup", () =>
|
|
576
|
+
lookupBuildShell(
|
|
577
|
+
url,
|
|
578
|
+
ctx.version,
|
|
579
|
+
store,
|
|
580
|
+
// Dev: no build manifest exists; producer B runs on demand via
|
|
581
|
+
// the dev server's /__rsc_shell endpoint for PRERENDERED routes
|
|
582
|
+
// only (production's exact candidate set). Folded away in
|
|
583
|
+
// production builds (NODE_ENV is a compile-time constant).
|
|
584
|
+
resolveDevShellLookup(reqCtx, pprConfig),
|
|
585
|
+
reqCtx._searchParamsFilter,
|
|
586
|
+
),
|
|
587
|
+
);
|
|
588
|
+
if (buildHit) {
|
|
589
|
+
// Past ppr.ttl: still serve the baked entry, recapture upgrades it.
|
|
590
|
+
if (buildHit.stale) {
|
|
591
|
+
yield* handoff("shell-recapture", () =>
|
|
592
|
+
scheduleShellCapture(
|
|
593
|
+
ctx,
|
|
594
|
+
request,
|
|
595
|
+
env,
|
|
596
|
+
url,
|
|
597
|
+
reqCtx,
|
|
598
|
+
ssrModule,
|
|
599
|
+
descriptor,
|
|
600
|
+
),
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
const response = yield* step("shell-hit", () =>
|
|
604
|
+
serveShellHit(
|
|
605
|
+
ctx,
|
|
606
|
+
request,
|
|
607
|
+
env,
|
|
608
|
+
url,
|
|
609
|
+
reqCtx,
|
|
610
|
+
handleStore,
|
|
611
|
+
ssrModule,
|
|
612
|
+
buildHit.entry,
|
|
613
|
+
descriptor,
|
|
614
|
+
),
|
|
615
|
+
);
|
|
616
|
+
return { kind: "serve", response };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// MISS (no entry, invalid reactVersion, or store read failure): axis 1
|
|
620
|
+
// + a background capture scheduled once the response is known servable.
|
|
621
|
+
return { kind: "miss", descriptor, ssrModule };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Match the request and assemble the RSC payload, or produce a control
|
|
626
|
+
* response (redirect, prerender collection) that ends the request plan.
|
|
627
|
+
*/
|
|
628
|
+
function* preparePayloadPlan<TEnv>(
|
|
629
|
+
input: RequestRenderInput<TEnv>,
|
|
630
|
+
): RoutinePlan<PreparedRender> {
|
|
631
|
+
const { ctx, request, env, url, isPartial, handleStore, nonce, reqCtx } =
|
|
632
|
+
input;
|
|
633
|
+
|
|
496
634
|
if (isPartial) {
|
|
497
635
|
// Partial render (navigation)
|
|
498
|
-
const replay =
|
|
499
|
-
ctx,
|
|
500
|
-
request,
|
|
501
|
-
env,
|
|
502
|
-
url,
|
|
503
|
-
reqCtx,
|
|
504
|
-
nonce,
|
|
636
|
+
const replay = yield* step("match:ppr-replay", () =>
|
|
637
|
+
matchPartialAndRecordParams(ctx, request, env, url, reqCtx, nonce),
|
|
505
638
|
);
|
|
506
639
|
const result = replay.result;
|
|
507
|
-
pprReplayStatus = replay.status;
|
|
508
|
-
partialCaptureNeeded =
|
|
640
|
+
const pprReplayStatus = replay.status;
|
|
641
|
+
const partialCaptureNeeded =
|
|
509
642
|
"captureNeeded" in replay && replay.captureNeeded === true;
|
|
510
|
-
partialCaptureKey =
|
|
643
|
+
const partialCaptureKey =
|
|
644
|
+
"captureKey" in replay ? replay.captureKey : undefined;
|
|
511
645
|
|
|
512
646
|
if (!result) {
|
|
513
647
|
// Fall back to full render
|
|
514
|
-
const match =
|
|
515
|
-
|
|
648
|
+
const match = yield* step("match:fallback", () =>
|
|
649
|
+
matchAndRecordParams(ctx, request, env),
|
|
650
|
+
);
|
|
516
651
|
|
|
517
652
|
if (match.redirect) {
|
|
518
653
|
// Partial request: use X-RSC-Redirect header so the client can
|
|
@@ -529,16 +664,22 @@ async function handleRscRenderingInner<TEnv>(
|
|
|
529
664
|
serializePprReplayStatus(pprReplayStatus),
|
|
530
665
|
);
|
|
531
666
|
}
|
|
532
|
-
return redirectResponse;
|
|
667
|
+
return { kind: "control", response: redirectResponse };
|
|
533
668
|
}
|
|
534
669
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
670
|
+
return {
|
|
671
|
+
kind: "payload",
|
|
672
|
+
payload: buildFullPayload(match, ctx, url, reqCtx, handleStore),
|
|
673
|
+
hasInterceptSlots: false,
|
|
674
|
+
pprReplayStatus,
|
|
675
|
+
partialCaptureNeeded,
|
|
676
|
+
partialCaptureKey,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
540
679
|
|
|
541
|
-
|
|
680
|
+
return {
|
|
681
|
+
kind: "payload",
|
|
682
|
+
payload: {
|
|
542
683
|
metadata: {
|
|
543
684
|
pathname: url.pathname,
|
|
544
685
|
// routerId is serialized on every payload (including within-session
|
|
@@ -566,62 +707,191 @@ async function handleRscRenderingInner<TEnv>(
|
|
|
566
707
|
defaultPrefetch: ctx.router.defaultPrefetch,
|
|
567
708
|
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
568
709
|
},
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
710
|
+
},
|
|
711
|
+
hasInterceptSlots: !!result.slots,
|
|
712
|
+
pprReplayStatus,
|
|
713
|
+
partialCaptureNeeded,
|
|
714
|
+
partialCaptureKey,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Full render (initial page load)
|
|
719
|
+
const match = yield* step("match", () =>
|
|
720
|
+
matchAndRecordParams(ctx, request, env),
|
|
721
|
+
);
|
|
575
722
|
|
|
576
|
-
|
|
577
|
-
|
|
723
|
+
if (match.redirect) {
|
|
724
|
+
return {
|
|
725
|
+
kind: "control",
|
|
726
|
+
response: createResponseWithMergedHeaders(null, {
|
|
578
727
|
status: 308,
|
|
579
728
|
headers: { Location: match.redirect },
|
|
580
|
-
})
|
|
581
|
-
}
|
|
729
|
+
}),
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Caching is now handled in router.match() via cache provider in request context
|
|
734
|
+
// match.segments already contains cached or fresh segments as appropriate
|
|
582
735
|
|
|
583
|
-
|
|
584
|
-
|
|
736
|
+
if (url.searchParams.has("__prerender_collect")) {
|
|
737
|
+
const response = yield* step("prerender-collect", () =>
|
|
738
|
+
collectPrerenderArtifacts(match, handleStore),
|
|
739
|
+
);
|
|
740
|
+
return { kind: "control", response };
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return {
|
|
744
|
+
kind: "payload",
|
|
745
|
+
payload: buildFullPayload(match, ctx, url, reqCtx, handleStore),
|
|
746
|
+
hasInterceptSlots: false,
|
|
747
|
+
partialCaptureNeeded: false,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* PPR replay match that stamps params/routeName when a replay result exists.
|
|
753
|
+
* A null result records nothing — the fallback match records its own.
|
|
754
|
+
*/
|
|
755
|
+
async function matchPartialAndRecordParams<TEnv>(
|
|
756
|
+
ctx: HandlerContext<TEnv>,
|
|
757
|
+
request: Request,
|
|
758
|
+
env: TEnv,
|
|
759
|
+
url: URL,
|
|
760
|
+
reqCtx: RequestContext<any>,
|
|
761
|
+
nonce: string | undefined,
|
|
762
|
+
): Promise<Awaited<ReturnType<typeof matchPartialWithPprReplay<TEnv>>>> {
|
|
763
|
+
const replay = await matchPartialWithPprReplay(
|
|
764
|
+
ctx,
|
|
765
|
+
request,
|
|
766
|
+
env,
|
|
767
|
+
url,
|
|
768
|
+
reqCtx,
|
|
769
|
+
nonce,
|
|
770
|
+
);
|
|
771
|
+
if (replay.result) {
|
|
772
|
+
setRequestContextParams(replay.result.params, replay.result.routeName);
|
|
773
|
+
}
|
|
774
|
+
return replay;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Shell store read that degrades to null on failure (axis 1 MISS, never a 500)
|
|
779
|
+
* and records the raw `ppr:shell-read` outcome (pre-validity-gates, so a
|
|
780
|
+
* version-mismatch lifecycle miss stays distinguishable from a store miss).
|
|
781
|
+
*/
|
|
782
|
+
async function readShellEntry<
|
|
783
|
+
TStore extends { getShell(key: string): Promise<unknown> },
|
|
784
|
+
>(
|
|
785
|
+
store: TStore,
|
|
786
|
+
key: string,
|
|
787
|
+
reqCtx: ReturnType<typeof getRequestContext>,
|
|
788
|
+
): Promise<Awaited<ReturnType<TStore["getShell"]>> | null> {
|
|
789
|
+
let cached: Awaited<ReturnType<TStore["getShell"]>> | null = null;
|
|
790
|
+
const shellReadStart = reqCtx._metricsStore ? performance.now() : 0;
|
|
791
|
+
try {
|
|
792
|
+
// The constraint types the call as Promise<unknown>; the instantiated
|
|
793
|
+
// TStore carries the real entry type, so the assertion restores it.
|
|
794
|
+
cached = (await store.getShell(key)) as Awaited<
|
|
795
|
+
ReturnType<TStore["getShell"]>
|
|
796
|
+
>;
|
|
797
|
+
} catch (error) {
|
|
798
|
+
reportCacheError(error, "cache-read", "[ShellServe] getShell");
|
|
799
|
+
}
|
|
800
|
+
if (reqCtx._metricsStore) {
|
|
801
|
+
appendMetric(
|
|
802
|
+
reqCtx._metricsStore,
|
|
803
|
+
"ppr:shell-read",
|
|
804
|
+
shellReadStart,
|
|
805
|
+
performance.now() - shellReadStart,
|
|
806
|
+
undefined,
|
|
807
|
+
cached ? "hit" : "miss",
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
return cached;
|
|
811
|
+
}
|
|
585
812
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
813
|
+
/**
|
|
814
|
+
* Dev Server-Timing mirror (issue #651): a capture completes AFTER its
|
|
815
|
+
* triggering response committed, so its outcome can only ride a LATER
|
|
816
|
+
* response's header. Read-and-clear keeps one capture = one report;
|
|
817
|
+
* dev-only (see takeCaptureDebugEventForTiming).
|
|
818
|
+
*/
|
|
819
|
+
function mirrorPprServerTimingsForDev(
|
|
820
|
+
key: string,
|
|
821
|
+
reqCtx: ReturnType<typeof getRequestContext>,
|
|
822
|
+
): void {
|
|
823
|
+
if (process.env.NODE_ENV !== "production" && reqCtx._metricsStore) {
|
|
824
|
+
const lastCapture = takeCaptureDebugEventForTiming(key);
|
|
825
|
+
if (lastCapture) {
|
|
826
|
+
appendMetric(
|
|
827
|
+
reqCtx._metricsStore,
|
|
828
|
+
"ppr:capture",
|
|
829
|
+
performance.now(),
|
|
830
|
+
lastCapture.attemptMs ?? 0,
|
|
831
|
+
undefined,
|
|
832
|
+
// attemptMs already rides as this entry's dur — drop it from desc.
|
|
833
|
+
describeShellCaptureEvent({ ...lastCapture, attemptMs: undefined }),
|
|
592
834
|
);
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
routeName: match.routeName,
|
|
609
|
-
params: match.params,
|
|
610
|
-
}),
|
|
611
|
-
{ headers: { "Content-Type": "application/json" } },
|
|
835
|
+
}
|
|
836
|
+
// Same mirror for the previous HIT's tail: its per-stage numbers
|
|
837
|
+
// (seed/match/handover/first-html/complete) finished after that
|
|
838
|
+
// response's headers were committed, so they ride THIS request's
|
|
839
|
+
// Server-Timing as `ppr:tail;dur=<complete ms>`.
|
|
840
|
+
const lastTail = takeShellTailTimingForServerTiming(key);
|
|
841
|
+
if (lastTail) {
|
|
842
|
+
appendMetric(
|
|
843
|
+
reqCtx._metricsStore,
|
|
844
|
+
"ppr:tail",
|
|
845
|
+
performance.now(),
|
|
846
|
+
lastTail.completeMs ?? 0,
|
|
847
|
+
undefined,
|
|
848
|
+
// completeMs already rides as this entry's dur — drop it from desc.
|
|
849
|
+
describeShellTailTiming({ ...lastTail, completeMs: undefined }),
|
|
612
850
|
);
|
|
613
|
-
} else {
|
|
614
|
-
payload = buildFullPayload(match, ctx, url, reqCtx, handleStore);
|
|
615
851
|
}
|
|
616
852
|
}
|
|
853
|
+
}
|
|
617
854
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
855
|
+
/**
|
|
856
|
+
* Build-time prerender collection: serialize segments and handle data
|
|
857
|
+
* to JSON for storage as build artifacts. At runtime the worker
|
|
858
|
+
* deserializes these and feeds them through the normal segment pipeline.
|
|
859
|
+
*/
|
|
860
|
+
async function collectPrerenderArtifacts<TEnv>(
|
|
861
|
+
match: Awaited<ReturnType<HandlerContext<TEnv>["router"]["match"]>>,
|
|
862
|
+
handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
|
|
863
|
+
): Promise<Response> {
|
|
864
|
+
const nonLoaderSegments = match.segments.filter((s) => s.type !== "loader");
|
|
865
|
+
handleStore.seal();
|
|
866
|
+
await handleStore.settled;
|
|
867
|
+
const { serializeSegments } = await import("../cache/segment-codec.js");
|
|
868
|
+
const serializedSegments = await serializeSegments(nonLoaderSegments);
|
|
869
|
+
const handles: Record<string, Record<string, unknown[]>> = {};
|
|
870
|
+
for (const seg of nonLoaderSegments) {
|
|
871
|
+
const segHandles = handleStore.getDataForSegment(seg.id);
|
|
872
|
+
if (Object.keys(segHandles).length > 0) {
|
|
873
|
+
handles[seg.id] = segHandles;
|
|
874
|
+
}
|
|
623
875
|
}
|
|
876
|
+
return new Response(
|
|
877
|
+
JSON.stringify({
|
|
878
|
+
segments: serializedSegments,
|
|
879
|
+
handles,
|
|
880
|
+
routeName: match.routeName,
|
|
881
|
+
params: match.params,
|
|
882
|
+
}),
|
|
883
|
+
{ headers: { "Content-Type": "application/json" } },
|
|
884
|
+
);
|
|
885
|
+
}
|
|
624
886
|
|
|
887
|
+
/** Assemble headers + tracking and run the foreground stage-driver render. */
|
|
888
|
+
function renderPreparedRscResponse<TEnv>(
|
|
889
|
+
input: RequestRenderInput<TEnv>,
|
|
890
|
+
prepared: Extract<PreparedRender, { kind: "payload" }>,
|
|
891
|
+
): Promise<Response> {
|
|
892
|
+
const { ctx, request, env, url, isPartial, nonce, reqCtx, renderSpan } =
|
|
893
|
+
input;
|
|
894
|
+
const { payload, pprReplayStatus, hasInterceptSlots } = prepared;
|
|
625
895
|
const metricsStore = reqCtx._metricsStore;
|
|
626
896
|
|
|
627
897
|
const rscHeaders: Record<string, string> = {
|
|
@@ -661,8 +931,11 @@ async function handleRscRenderingInner<TEnv>(
|
|
|
661
931
|
mode: isPartial ? ("partial" as const) : ("full" as const),
|
|
662
932
|
routeKey: reqCtx._routeName,
|
|
663
933
|
span: renderSpan,
|
|
934
|
+
onEvent:
|
|
935
|
+
reqCtx._activeRoutine &&
|
|
936
|
+
createRenderStageTraceBridge(reqCtx._activeRoutine),
|
|
664
937
|
};
|
|
665
|
-
|
|
938
|
+
return renderRscResponse(
|
|
666
939
|
{
|
|
667
940
|
ctx,
|
|
668
941
|
request,
|
|
@@ -686,65 +959,45 @@ async function handleRscRenderingInner<TEnv>(
|
|
|
686
959
|
}),
|
|
687
960
|
},
|
|
688
961
|
);
|
|
962
|
+
}
|
|
689
963
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
964
|
+
/**
|
|
965
|
+
* Background capture for a navigation-only shell (partial replay reported
|
|
966
|
+
* captureNeeded). SSR setup is resolved lazily inside the capture because the
|
|
967
|
+
* partial request itself never touched HTML rendering.
|
|
968
|
+
*/
|
|
969
|
+
function scheduleNavigationShellCapture<TEnv>(
|
|
970
|
+
input: RequestRenderInput<TEnv>,
|
|
971
|
+
partialCaptureKey: string | undefined,
|
|
972
|
+
): void {
|
|
973
|
+
const { ctx, request, env, url, reqCtx } = input;
|
|
974
|
+
const pprConfig = resolvePprConfig(reqCtx._classifiedRoute?.manifestEntry)!;
|
|
975
|
+
const store = reqCtx._cacheStore!;
|
|
976
|
+
scheduleShellCapture(
|
|
977
|
+
ctx,
|
|
978
|
+
request,
|
|
979
|
+
env,
|
|
980
|
+
url,
|
|
981
|
+
reqCtx,
|
|
982
|
+
async (captureRequest, captureUrl) => {
|
|
983
|
+
const [ssrModule, streamMode] = await getSSRSetup(
|
|
700
984
|
ctx,
|
|
701
|
-
|
|
985
|
+
captureRequest,
|
|
702
986
|
env,
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
pprMiss.ssrModule,
|
|
706
|
-
pprMiss.descriptor,
|
|
987
|
+
captureUrl,
|
|
988
|
+
undefined,
|
|
707
989
|
);
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
if (
|
|
713
|
-
isPartial &&
|
|
714
|
-
partialCaptureNeeded &&
|
|
715
|
-
!reqCtx._dynamic &&
|
|
716
|
-
response.status === 200
|
|
717
|
-
) {
|
|
718
|
-
const pprConfig = resolvePprConfig(reqCtx._classifiedRoute?.manifestEntry)!;
|
|
719
|
-
const store = reqCtx._cacheStore!;
|
|
720
|
-
scheduleShellCapture(
|
|
990
|
+
return streamMode === "allReady" ? null : ssrModule;
|
|
991
|
+
},
|
|
992
|
+
createShellCaptureDescriptor(
|
|
721
993
|
ctx,
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
captureRequest,
|
|
730
|
-
env,
|
|
731
|
-
captureUrl,
|
|
732
|
-
undefined,
|
|
733
|
-
);
|
|
734
|
-
return streamMode === "allReady" ? null : ssrModule;
|
|
735
|
-
},
|
|
736
|
-
createShellCaptureDescriptor(
|
|
737
|
-
ctx,
|
|
738
|
-
partialCaptureKey ??
|
|
739
|
-
buildNavigationShellKey(url, reqCtx._searchParamsFilter),
|
|
740
|
-
pprConfig,
|
|
741
|
-
store,
|
|
742
|
-
true,
|
|
743
|
-
),
|
|
744
|
-
);
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
return response;
|
|
994
|
+
partialCaptureKey ??
|
|
995
|
+
buildNavigationShellKey(url, reqCtx._searchParamsFilter),
|
|
996
|
+
pprConfig,
|
|
997
|
+
store,
|
|
998
|
+
true,
|
|
999
|
+
),
|
|
1000
|
+
);
|
|
748
1001
|
}
|
|
749
1002
|
|
|
750
1003
|
/**
|