@rangojs/router 0.0.0-experimental.150 → 0.0.0-experimental.152
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/vite/index.js +146 -76
- package/package.json +2 -2
- package/skills/debug-manifest/SKILL.md +2 -10
- package/skills/middleware/SKILL.md +33 -0
- package/skills/ppr/SKILL.md +37 -4
- package/skills/prerender/SKILL.md +44 -1
- package/src/build/generate-manifest.ts +5 -36
- package/src/build/route-trie.ts +5 -50
- package/src/cache/cache-key-utils.ts +0 -1
- package/src/prerender/build-shell-capture.ts +215 -21
- package/src/router/handler-context.ts +10 -1
- package/src/router/middleware-types.ts +17 -0
- package/src/router/middleware.ts +4 -0
- package/src/router/prerender-match.ts +7 -0
- package/src/router/router-interfaces.ts +0 -6
- package/src/router/router-options.ts +0 -8
- package/src/router.ts +0 -4
- package/src/rsc/handler.ts +55 -51
- package/src/rsc/helpers.ts +461 -0
- package/src/rsc/progressive-enhancement.ts +66 -27
- package/src/rsc/rsc-rendering.ts +91 -57
- package/src/rsc/server-action.ts +49 -46
- package/src/rsc/shell-build-manifest.ts +50 -8
- package/src/rsc/shell-capture.ts +17 -6
- package/src/server/context.ts +67 -10
- package/src/server/request-context.ts +37 -0
- package/src/testing/internal/context.ts +9 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/run-middleware.ts +14 -0
- package/src/types/handler-context.ts +12 -1
- package/src/vite/discovery/discover-routers.ts +44 -52
- package/src/vite/discovery/virtual-module-codegen.ts +112 -2
- package/src/vite/rango.ts +31 -0
- package/src/vite/router-discovery.ts +131 -22
|
@@ -18,9 +18,14 @@ import { gateTransitions } from "./transition-gate.js";
|
|
|
18
18
|
import { resolvedHandleStream } from "../handles/deferred-resolution.js";
|
|
19
19
|
import type { RscPayload, ReactFormState } from "./types.js";
|
|
20
20
|
import {
|
|
21
|
+
RSC_RENDER_HTML_RESPONSE_PHASES,
|
|
21
22
|
createResponseWithMergedHeaders,
|
|
22
23
|
finalizeResponse,
|
|
23
24
|
buildRouteMiddlewareEntries,
|
|
25
|
+
createRscRenderStages,
|
|
26
|
+
finishRscRenderStages,
|
|
27
|
+
observeRscHtmlStage,
|
|
28
|
+
readRscFlightStage,
|
|
24
29
|
} from "./helpers.js";
|
|
25
30
|
import type { HandlerContext } from "./handler-context.js";
|
|
26
31
|
import {
|
|
@@ -327,11 +332,31 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
327
332
|
},
|
|
328
333
|
};
|
|
329
334
|
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
335
|
+
const stageTracking = {
|
|
336
|
+
mode: "progressive-enhancement" as const,
|
|
337
|
+
routeKey: getRequestContext()._routeName,
|
|
338
|
+
actionId: directActionId ?? undefined,
|
|
339
|
+
phases: RSC_RENDER_HTML_RESPONSE_PHASES,
|
|
340
|
+
};
|
|
341
|
+
const renderStages = createRscRenderStages({
|
|
342
|
+
ctx,
|
|
343
|
+
request,
|
|
344
|
+
env,
|
|
345
|
+
url,
|
|
346
|
+
payload,
|
|
347
|
+
init: {
|
|
348
|
+
// boundarylessErrorStatus is set only when the action threw and no error
|
|
349
|
+
// boundary matched; it makes the re-render carry 500 like the JS path.
|
|
350
|
+
// The redirect branch above returns before this, so a redirect re-render
|
|
351
|
+
// keeps its 308 and is never overridden.
|
|
352
|
+
...(boundarylessErrorStatus !== undefined
|
|
353
|
+
? { status: boundarylessErrorStatus }
|
|
354
|
+
: {}),
|
|
355
|
+
headers: { "content-type": "text/html;charset=utf-8" },
|
|
333
356
|
},
|
|
357
|
+
tracking: stageTracking,
|
|
334
358
|
});
|
|
359
|
+
const flightStage = await readRscFlightStage(renderStages);
|
|
335
360
|
// metricsStore=undefined is safe: the handler already stashed the early
|
|
336
361
|
// SSR setup promise on request variables, so getSSRSetup returns it
|
|
337
362
|
// without falling back to a fresh startSSRSetup.
|
|
@@ -344,21 +369,18 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
344
369
|
);
|
|
345
370
|
// reactFormState carries the useActionState payload via the SSR-option path
|
|
346
371
|
// (renderToReadableStream({ formState })); it does NOT travel on RscPayload.
|
|
347
|
-
const htmlStream = await
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
372
|
+
const htmlStream = await observeRscHtmlStage(
|
|
373
|
+
{ url, tracking: stageTracking },
|
|
374
|
+
() =>
|
|
375
|
+
ssrModule.renderHTML(flightStage.stream, {
|
|
376
|
+
formState: reactFormState,
|
|
377
|
+
nonce,
|
|
378
|
+
streamMode,
|
|
379
|
+
}),
|
|
380
|
+
);
|
|
352
381
|
|
|
353
|
-
return
|
|
354
|
-
|
|
355
|
-
// boundary matched; it makes the re-render carry 500 like the JS path.
|
|
356
|
-
// The redirect branch above returns before this, so a redirect re-render
|
|
357
|
-
// keeps its 308 and is never overridden.
|
|
358
|
-
...(boundarylessErrorStatus !== undefined
|
|
359
|
-
? { status: boundarylessErrorStatus }
|
|
360
|
-
: {}),
|
|
361
|
-
headers: { "content-type": "text/html;charset=utf-8" },
|
|
382
|
+
return finishRscRenderStages(renderStages, {
|
|
383
|
+
body: htmlStream,
|
|
362
384
|
});
|
|
363
385
|
};
|
|
364
386
|
|
|
@@ -475,11 +497,25 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
475
497
|
},
|
|
476
498
|
};
|
|
477
499
|
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
500
|
+
const stageTracking = {
|
|
501
|
+
mode: "progressive-enhancement-error" as const,
|
|
502
|
+
routeKey: getRequestContext()._routeName,
|
|
503
|
+
actionId: actionId ?? undefined,
|
|
504
|
+
phases: RSC_RENDER_HTML_RESPONSE_PHASES,
|
|
505
|
+
};
|
|
506
|
+
const renderStages = createRscRenderStages({
|
|
507
|
+
ctx,
|
|
508
|
+
request,
|
|
509
|
+
env,
|
|
510
|
+
url,
|
|
511
|
+
payload,
|
|
512
|
+
init: {
|
|
513
|
+
status: 500,
|
|
514
|
+
headers: { "content-type": "text/html;charset=utf-8" },
|
|
481
515
|
},
|
|
516
|
+
tracking: stageTracking,
|
|
482
517
|
});
|
|
518
|
+
const flightStage = await readRscFlightStage(renderStages);
|
|
483
519
|
// metricsStore=undefined is safe: the handler already stashed the early
|
|
484
520
|
// SSR setup promise on request variables, so getSSRSetup returns it
|
|
485
521
|
// without falling back to a fresh startSSRSetup.
|
|
@@ -490,13 +526,16 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
490
526
|
url,
|
|
491
527
|
undefined,
|
|
492
528
|
);
|
|
493
|
-
const htmlStream = await
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
529
|
+
const htmlStream = await observeRscHtmlStage(
|
|
530
|
+
{ url, tracking: stageTracking },
|
|
531
|
+
() =>
|
|
532
|
+
ssrModule.renderHTML(flightStage.stream, {
|
|
533
|
+
nonce,
|
|
534
|
+
streamMode,
|
|
535
|
+
}),
|
|
536
|
+
);
|
|
497
537
|
|
|
498
|
-
return
|
|
499
|
-
|
|
500
|
-
headers: { "content-type": "text/html;charset=utf-8" },
|
|
538
|
+
return finishRscRenderStages(renderStages, {
|
|
539
|
+
body: htmlStream,
|
|
501
540
|
});
|
|
502
541
|
}
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -22,9 +22,18 @@ import type { RscPayload } from "./types.js";
|
|
|
22
22
|
import type { SSRModule } from "./types.js";
|
|
23
23
|
import type { RequestContext } from "../server/request-context.js";
|
|
24
24
|
import {
|
|
25
|
+
RSC_FLIGHT_ONLY_PHASES,
|
|
26
|
+
RSC_RENDER_FLIGHT_RESPONSE_PHASES,
|
|
27
|
+
RSC_RENDER_HTML_RESPONSE_PHASES,
|
|
25
28
|
createResponseWithMergedHeaders,
|
|
26
29
|
createSimpleRedirectResponse,
|
|
27
30
|
attachLocationStateIfPresent,
|
|
31
|
+
createRscRenderStages,
|
|
32
|
+
finishRscRenderStages,
|
|
33
|
+
observeRscHtmlStage,
|
|
34
|
+
readRscFlightStage,
|
|
35
|
+
renderRscFlightStage,
|
|
36
|
+
runRscRenderStages,
|
|
28
37
|
} from "./helpers.js";
|
|
29
38
|
import type { HandlerContext } from "./handler-context.js";
|
|
30
39
|
import { gateTransitions } from "./transition-gate.js";
|
|
@@ -124,7 +133,8 @@ async function handleRscRenderingInner<TEnv>(
|
|
|
124
133
|
!isPartial &&
|
|
125
134
|
request.method === "GET" &&
|
|
126
135
|
!url.searchParams.has("__prerender_collect") &&
|
|
127
|
-
!isRscRequest(request, url, false)
|
|
136
|
+
!isRscRequest(request, url, false) &&
|
|
137
|
+
!reqCtx._dynamic
|
|
128
138
|
) {
|
|
129
139
|
const pprConfig = resolvePprConfig(reqCtx._classifiedRoute?.manifestEntry);
|
|
130
140
|
if (pprConfig) {
|
|
@@ -428,54 +438,60 @@ async function handleRscRenderingInner<TEnv>(
|
|
|
428
438
|
|
|
429
439
|
const metricsStore = reqCtx._metricsStore;
|
|
430
440
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
441
|
+
const rscHeaders: Record<string, string> = {
|
|
442
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
443
|
+
vary: "accept, X-Rango-State, X-RSC-Router-Client-Path",
|
|
444
|
+
// Router identity, so the client can verify pre-decode (before importing
|
|
445
|
+
// chunks) that this content payload belongs to its app and refuse a
|
|
446
|
+
// foreign one (cache/proxy/bug). Control-only reload/redirect responses
|
|
447
|
+
// are deliberately NOT stamped. See browser/response-adapter.ts.
|
|
448
|
+
"X-RSC-Router-Id": ctx.router.id,
|
|
449
|
+
};
|
|
450
|
+
// Tell the client's prefetch cache to scope this response to its source
|
|
451
|
+
// URL (instead of the default source-agnostic wildcard). Intercept
|
|
452
|
+
// responses depend on the source page matching an intercept rule, so
|
|
453
|
+
// they must not be reused for navigations from other sources.
|
|
454
|
+
if (hasInterceptSlots) {
|
|
455
|
+
rscHeaders["x-rsc-prefetch-scope"] = "source";
|
|
456
|
+
}
|
|
457
|
+
// Enable browser HTTP caching for prefetch responses only.
|
|
458
|
+
// Requires X-Rango-Prefetch header (sent by Link prefetch fetch),
|
|
459
|
+
// non-intercept context (intercept responses depend on source page),
|
|
460
|
+
// and a configured cache-control value (false disables caching).
|
|
461
|
+
const isPrefetch = request.headers.has("X-Rango-Prefetch");
|
|
462
|
+
if (isPrefetch && isPartial && !hasInterceptSlots) {
|
|
463
|
+
const cc = ctx.router.prefetchCacheControl;
|
|
464
|
+
if (cc) {
|
|
465
|
+
rscHeaders["cache-control"] = cc;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const isFlightResponse = isRscRequest(request, url, isPartial);
|
|
470
|
+
const stageTracking = {
|
|
471
|
+
mode: isPartial ? ("partial" as const) : ("full" as const),
|
|
472
|
+
routeKey: reqCtx._routeName,
|
|
473
|
+
phases: isFlightResponse
|
|
474
|
+
? RSC_RENDER_FLIGHT_RESPONSE_PHASES
|
|
475
|
+
: RSC_RENDER_HTML_RESPONSE_PHASES,
|
|
476
|
+
};
|
|
477
|
+
const renderStages = createRscRenderStages({
|
|
478
|
+
ctx,
|
|
479
|
+
request,
|
|
480
|
+
env,
|
|
481
|
+
url,
|
|
482
|
+
payload,
|
|
483
|
+
init: {
|
|
484
|
+
headers: rscHeaders,
|
|
436
485
|
},
|
|
486
|
+
tracking: stageTracking,
|
|
437
487
|
});
|
|
438
|
-
const rscSerializeDur = performance.now() - rscSerializeStart;
|
|
439
|
-
// This measures synchronous stream creation, not end-to-end stream consumption.
|
|
440
|
-
appendMetric(
|
|
441
|
-
metricsStore,
|
|
442
|
-
"rsc-serialize",
|
|
443
|
-
rscSerializeStart,
|
|
444
|
-
rscSerializeDur,
|
|
445
|
-
);
|
|
446
488
|
|
|
447
|
-
|
|
489
|
+
const flightStage = await readRscFlightStage(renderStages);
|
|
490
|
+
const rscStream = flightStage.stream;
|
|
491
|
+
|
|
492
|
+
if (isFlightResponse) {
|
|
448
493
|
// render:total is recorded by the observePhase wrapper around this function.
|
|
449
|
-
|
|
450
|
-
"content-type": "text/x-component;charset=utf-8",
|
|
451
|
-
vary: "accept, X-Rango-State, X-RSC-Router-Client-Path",
|
|
452
|
-
// Router identity, so the client can verify pre-decode (before importing
|
|
453
|
-
// chunks) that this content payload belongs to its app and refuse a
|
|
454
|
-
// foreign one (cache/proxy/bug). Control-only reload/redirect responses
|
|
455
|
-
// are deliberately NOT stamped. See browser/response-adapter.ts.
|
|
456
|
-
"X-RSC-Router-Id": ctx.router.id,
|
|
457
|
-
};
|
|
458
|
-
// Tell the client's prefetch cache to scope this response to its source
|
|
459
|
-
// URL (instead of the default source-agnostic wildcard). Intercept
|
|
460
|
-
// responses depend on the source page matching an intercept rule, so
|
|
461
|
-
// they must not be reused for navigations from other sources.
|
|
462
|
-
if (hasInterceptSlots) {
|
|
463
|
-
rscHeaders["x-rsc-prefetch-scope"] = "source";
|
|
464
|
-
}
|
|
465
|
-
// Enable browser HTTP caching for prefetch responses only.
|
|
466
|
-
// Requires X-Rango-Prefetch header (sent by Link prefetch fetch),
|
|
467
|
-
// non-intercept context (intercept responses depend on source page),
|
|
468
|
-
// and a configured cache-control value (false disables caching).
|
|
469
|
-
const isPrefetch = request.headers.has("X-Rango-Prefetch");
|
|
470
|
-
if (isPrefetch && isPartial && !hasInterceptSlots) {
|
|
471
|
-
const cc = ctx.router.prefetchCacheControl;
|
|
472
|
-
if (cc) {
|
|
473
|
-
rscHeaders["cache-control"] = cc;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
return createResponseWithMergedHeaders(rscStream, {
|
|
477
|
-
headers: rscHeaders,
|
|
478
|
-
});
|
|
494
|
+
return runRscRenderStages(renderStages);
|
|
479
495
|
}
|
|
480
496
|
|
|
481
497
|
// Delegate to SSR for HTML response (reuse early setup if available)
|
|
@@ -489,14 +505,21 @@ async function handleRscRenderingInner<TEnv>(
|
|
|
489
505
|
|
|
490
506
|
// ssr-render-html metric + rango.ssr span from one boundary. render:total is
|
|
491
507
|
// recorded by the observePhase wrapper around this function.
|
|
492
|
-
const htmlStream = await
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
508
|
+
const htmlStream = await observeRscHtmlStage(
|
|
509
|
+
{ url, tracking: stageTracking },
|
|
510
|
+
() =>
|
|
511
|
+
observePhase(PHASES.ssr, () =>
|
|
512
|
+
ssrModule.renderHTML(rscStream, {
|
|
513
|
+
nonce,
|
|
514
|
+
streamMode,
|
|
515
|
+
}),
|
|
516
|
+
),
|
|
497
517
|
);
|
|
498
|
-
const response =
|
|
499
|
-
|
|
518
|
+
const response = await finishRscRenderStages(renderStages, {
|
|
519
|
+
body: htmlStream,
|
|
520
|
+
init: {
|
|
521
|
+
headers: { "content-type": "text/html;charset=utf-8" },
|
|
522
|
+
},
|
|
500
523
|
});
|
|
501
524
|
|
|
502
525
|
// --- Axis 2: PPR shell CAPTURE on MISS (background task; see design doc) ---
|
|
@@ -507,7 +530,7 @@ async function handleRscRenderingInner<TEnv>(
|
|
|
507
530
|
// page via router.match() under a derived context (fresh handle store,
|
|
508
531
|
// _shellCaptureRun: true) — middleware never re-runs; it already ran for this
|
|
509
532
|
// request and guarding is serve-time.
|
|
510
|
-
if (pprMiss) {
|
|
533
|
+
if (pprMiss && !reqCtx._dynamic) {
|
|
511
534
|
if (
|
|
512
535
|
response.status === 200 &&
|
|
513
536
|
(response.headers.get("content-type") ?? "").includes("text/html")
|
|
@@ -611,11 +634,22 @@ function serveShellHit(
|
|
|
611
634
|
}
|
|
612
635
|
// Full Flight render per request: hydration needs the whole payload (there
|
|
613
636
|
// is no Flight-side resume — a React limitation, not ours).
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
ctx
|
|
637
|
+
const flightStage = renderRscFlightStage(
|
|
638
|
+
{
|
|
639
|
+
ctx,
|
|
640
|
+
request,
|
|
641
|
+
env,
|
|
642
|
+
url,
|
|
643
|
+
payload,
|
|
644
|
+
tracking: {
|
|
645
|
+
mode: "full",
|
|
646
|
+
routeKey: activeCtx._routeName,
|
|
647
|
+
phases: RSC_FLIGHT_ONLY_PHASES,
|
|
648
|
+
},
|
|
617
649
|
},
|
|
618
|
-
|
|
650
|
+
performance.now(),
|
|
651
|
+
);
|
|
652
|
+
let rscStream = flightStage.stream;
|
|
619
653
|
// Timing tap: when does the Flight render produce its FIRST byte? Compared
|
|
620
654
|
// with the eager-inject/first-tail logs this proves whether hydration-start
|
|
621
655
|
// latency is genuine server work (loaders) or stream plumbing holding
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
getRequestContext,
|
|
20
20
|
setRequestContextParams,
|
|
21
21
|
} from "../server/request-context.js";
|
|
22
|
-
import { appendMetric } from "../router/metrics.js";
|
|
23
22
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
24
23
|
import { gateTransitions } from "./transition-gate.js";
|
|
25
24
|
import type { RscPayload } from "./types.js";
|
|
@@ -29,6 +28,8 @@ import {
|
|
|
29
28
|
createSimpleRedirectResponse,
|
|
30
29
|
interceptRedirectForPartial,
|
|
31
30
|
attachLocationStateIfPresent,
|
|
31
|
+
createRscRenderStages,
|
|
32
|
+
runRscRenderStages,
|
|
32
33
|
} from "./helpers.js";
|
|
33
34
|
import { warnNonRedirectActionResponse } from "./runtime-warnings.js";
|
|
34
35
|
import type { HandlerContext } from "./handler-context.js";
|
|
@@ -362,8 +363,6 @@ async function revalidateAfterActionInner<TEnv>(
|
|
|
362
363
|
errorBoundary,
|
|
363
364
|
} = continuation;
|
|
364
365
|
const reqCtx = getRequestContext();
|
|
365
|
-
const metricsStore = reqCtx._metricsStore;
|
|
366
|
-
|
|
367
366
|
// Expose the action that triggered this revalidation to the transition({ when })
|
|
368
367
|
// gate (covers both the error-boundary and success gate calls below). Mirrors
|
|
369
368
|
// the action fields a revalidate() predicate sees.
|
|
@@ -412,29 +411,30 @@ async function revalidateAfterActionInner<TEnv>(
|
|
|
412
411
|
// is a success-only semantic. Error boundary responses update the error UI
|
|
413
412
|
// but should not mutate browser history state.
|
|
414
413
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
414
|
+
return runRscRenderStages(
|
|
415
|
+
createRscRenderStages({
|
|
416
|
+
ctx,
|
|
417
|
+
request,
|
|
418
|
+
env,
|
|
419
|
+
url,
|
|
420
|
+
payload: errorPayload,
|
|
421
|
+
temporaryReferences,
|
|
422
|
+
init: {
|
|
423
|
+
status: actionStatus,
|
|
424
|
+
headers: {
|
|
425
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
426
|
+
// Router identity for the client's pre-decode integrity check (the
|
|
427
|
+
// action apply path has no post-decode guard). See response-adapter.
|
|
428
|
+
"X-RSC-Router-Id": ctx.router.id,
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
tracking: {
|
|
432
|
+
mode: "action-revalidation",
|
|
433
|
+
routeKey: reqCtx._routeName,
|
|
434
|
+
actionId: actionContext?.actionId,
|
|
435
|
+
},
|
|
436
|
+
}),
|
|
427
437
|
);
|
|
428
|
-
|
|
429
|
-
return createResponseWithMergedHeaders(errorStream, {
|
|
430
|
-
status: actionStatus,
|
|
431
|
-
headers: {
|
|
432
|
-
"content-type": "text/x-component;charset=utf-8",
|
|
433
|
-
// Router identity for the client's pre-decode integrity check (the
|
|
434
|
-
// action apply path has no post-decode guard). See response-adapter.
|
|
435
|
-
"X-RSC-Router-Id": ctx.router.id,
|
|
436
|
-
},
|
|
437
|
-
});
|
|
438
438
|
}
|
|
439
439
|
|
|
440
440
|
const matchResult = await ctx.router.matchPartial(
|
|
@@ -498,25 +498,28 @@ async function revalidateAfterActionInner<TEnv>(
|
|
|
498
498
|
|
|
499
499
|
attachLocationStateIfPresent(payload);
|
|
500
500
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
501
|
+
return runRscRenderStages(
|
|
502
|
+
createRscRenderStages({
|
|
503
|
+
ctx,
|
|
504
|
+
request,
|
|
505
|
+
env,
|
|
506
|
+
url,
|
|
507
|
+
payload,
|
|
508
|
+
temporaryReferences,
|
|
509
|
+
init: {
|
|
510
|
+
status: actionStatus,
|
|
511
|
+
headers: {
|
|
512
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
513
|
+
// Router identity for the client's pre-decode integrity check (the
|
|
514
|
+
// action apply path has no post-decode guard). See response-adapter.
|
|
515
|
+
"X-RSC-Router-Id": ctx.router.id,
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
tracking: {
|
|
519
|
+
mode: "action-revalidation",
|
|
520
|
+
routeKey: reqCtx._routeName,
|
|
521
|
+
actionId: actionContext?.actionId,
|
|
522
|
+
},
|
|
523
|
+
}),
|
|
524
|
+
);
|
|
522
525
|
}
|
|
@@ -146,6 +146,32 @@ export interface DevShellLookup {
|
|
|
146
146
|
/** Retry delay (~400ms) plus quiesce/fizz/store headroom past the budgets. */
|
|
147
147
|
const DEV_SHELL_RETRY_MARGIN_MS = 5_000;
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Dev boot-race readiness (issue #719). The `/__rsc_shell` endpoint stands up
|
|
151
|
+
* the capture realm lazily on its first hit — the temp server (Cloudflare
|
|
152
|
+
* preset), the registry import, and, on either preset, a Vite dependency
|
|
153
|
+
* re-optimization that the first import of the shell-capture graph can trigger.
|
|
154
|
+
* All three are TRANSIENT: retrying the same request seconds later succeeds.
|
|
155
|
+
*
|
|
156
|
+
* Before this, the read-through fired ONCE and mapped any non-2xx to a hard
|
|
157
|
+
* MISS, so a Prerender+ppr route's FIRST request lost the race and served
|
|
158
|
+
* axis-1 (x-rango-shell: MISS) while a later poll HIT — the build-time
|
|
159
|
+
* first-request-HIT contract held only after a warm-up. A fast machine loses
|
|
160
|
+
* the race deterministically (the document request beats the boot infra);
|
|
161
|
+
* slow CI arrives after it settled, hence "0-flake on CI".
|
|
162
|
+
*
|
|
163
|
+
* Fix: the endpoint tags the transient not-ready branches with
|
|
164
|
+
* `x-rango-shell-dev: NOT-READY` (HTTP 503), and the read-through re-polls
|
|
165
|
+
* ONLY that signal — a bounded await of readiness, not a blind retry. A
|
|
166
|
+
* genuine negative (route not prerenderable, capture refused: 404) stays an
|
|
167
|
+
* immediate MISS, so a non-baked route never stalls the foreground. The window
|
|
168
|
+
* is capped by DEV_SHELL_READINESS_DEADLINE_MS; the boot infra normally
|
|
169
|
+
* settles well inside the first poll.
|
|
170
|
+
*/
|
|
171
|
+
const DEV_SHELL_NOT_READY = "NOT-READY";
|
|
172
|
+
const DEV_SHELL_READINESS_DEADLINE_MS = 10_000;
|
|
173
|
+
const DEV_SHELL_READINESS_POLL_MS = 150;
|
|
174
|
+
|
|
149
175
|
/**
|
|
150
176
|
* Timed out like the dev prerender store fetch (see #697): inside a workerd
|
|
151
177
|
* waitUntil an unsettled fetch pends forever instead of rejecting; on timeout
|
|
@@ -192,14 +218,30 @@ async function fetchDevShellEntry(
|
|
|
192
218
|
if (dev.captureTimeout !== undefined) {
|
|
193
219
|
params.set("captureTimeout", String(dev.captureTimeout));
|
|
194
220
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
221
|
+
const shellUrl = `${devUrl}/__rsc_shell?${params}`;
|
|
222
|
+
// Await the endpoint's boot readiness (issue #719): re-poll ONLY the
|
|
223
|
+
// NOT-READY signal, capped by DEV_SHELL_READINESS_DEADLINE_MS. Every other
|
|
224
|
+
// outcome — a served entry, a genuine negative (404), or a network fault —
|
|
225
|
+
// resolves on the first attempt, so a non-baked route never stalls here.
|
|
226
|
+
const readinessDeadline = Date.now() + DEV_SHELL_READINESS_DEADLINE_MS;
|
|
227
|
+
for (;;) {
|
|
228
|
+
try {
|
|
229
|
+
const res = await fetch(shellUrl, {
|
|
230
|
+
signal: AbortSignal.timeout(devShellFetchTimeoutMs(dev.captureTimeout)),
|
|
231
|
+
});
|
|
232
|
+
if (
|
|
233
|
+
res.status === 503 &&
|
|
234
|
+
res.headers.get("x-rango-shell-dev") === DEV_SHELL_NOT_READY &&
|
|
235
|
+
Date.now() < readinessDeadline
|
|
236
|
+
) {
|
|
237
|
+
await new Promise((r) => setTimeout(r, DEV_SHELL_READINESS_POLL_MS));
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (!res.ok) return undefined;
|
|
241
|
+
return (await res.json()) as BuildShellEntry;
|
|
242
|
+
} catch {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
203
245
|
}
|
|
204
246
|
}
|
|
205
247
|
|
package/src/rsc/shell-capture.ts
CHANGED
|
@@ -53,9 +53,10 @@ import {
|
|
|
53
53
|
getRecordingStore,
|
|
54
54
|
} from "../cache/shell-snapshot.js";
|
|
55
55
|
import type { HandlerContext } from "./handler-context.js";
|
|
56
|
-
import type {
|
|
56
|
+
import type { SSRModule } from "./types.js";
|
|
57
57
|
import { buildFullPayload } from "./full-payload.js";
|
|
58
58
|
import { resolveDeferredHandleValues } from "../handles/deferred-resolution.js";
|
|
59
|
+
import { RSC_FLIGHT_ONLY_PHASES, renderRscFlightStage } from "./helpers.js";
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
62
|
* Task-quantized quiesce: the number of consecutive macrotask hops with zero new
|
|
@@ -1131,11 +1132,21 @@ async function attemptCapture(
|
|
|
1131
1132
|
derivedCtx,
|
|
1132
1133
|
freshHandleStore,
|
|
1133
1134
|
);
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
ctx
|
|
1135
|
+
const flightStage = renderRscFlightStage(
|
|
1136
|
+
{
|
|
1137
|
+
ctx,
|
|
1138
|
+
request,
|
|
1139
|
+
env,
|
|
1140
|
+
url,
|
|
1141
|
+
payload,
|
|
1142
|
+
tracking: {
|
|
1143
|
+
mode: "full",
|
|
1144
|
+
routeKey: derivedCtx._routeName,
|
|
1145
|
+
phases: RSC_FLIGHT_ONLY_PHASES,
|
|
1146
|
+
},
|
|
1137
1147
|
},
|
|
1138
|
-
|
|
1148
|
+
performance.now(),
|
|
1149
|
+
);
|
|
1139
1150
|
|
|
1140
1151
|
// Pass the descriptor with its STATIC ppr.tags unchanged. The shell's own
|
|
1141
1152
|
// render-recorded tags are snapshotted at the putShell WRITE BARRIER inside
|
|
@@ -1145,7 +1156,7 @@ async function attemptCapture(
|
|
|
1145
1156
|
// shell-tag snapshot must sit behind the quiesce gate (issue #676).
|
|
1146
1157
|
return captureAndStoreShell(
|
|
1147
1158
|
ssrModule,
|
|
1148
|
-
|
|
1159
|
+
flightStage.stream,
|
|
1149
1160
|
freshHandleStore,
|
|
1150
1161
|
derivedCtx,
|
|
1151
1162
|
descriptor,
|