@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
|
@@ -206,6 +206,11 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
206
206
|
pathname,
|
|
207
207
|
searchParams: new URLSearchParams(),
|
|
208
208
|
_variables: variables,
|
|
209
|
+
build: true,
|
|
210
|
+
// Inert here: the prerender-collect / static-render pass has no live
|
|
211
|
+
// PPR-shell decision to gate; dynamic() reaches the shell axis only on a
|
|
212
|
+
// live request or a shell capture.
|
|
213
|
+
dynamic: () => {},
|
|
209
214
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
210
215
|
set: ((keyOrVar: any, value: any) => {
|
|
211
216
|
contextSet(variables, keyOrVar, value);
|
|
@@ -476,6 +481,8 @@ export async function renderStaticSegment<TEnv = any>(
|
|
|
476
481
|
pathname: "/",
|
|
477
482
|
searchParams: syntheticUrl.searchParams,
|
|
478
483
|
_variables: {},
|
|
484
|
+
build: true,
|
|
485
|
+
dynamic: () => {},
|
|
479
486
|
get: () => undefined as any,
|
|
480
487
|
set: () => {},
|
|
481
488
|
params: {},
|
|
@@ -366,12 +366,6 @@ export interface RangoInternal<
|
|
|
366
366
|
*/
|
|
367
367
|
readonly telemetry?: TelemetrySink;
|
|
368
368
|
|
|
369
|
-
/**
|
|
370
|
-
* Whether ?__debug_manifest is allowed in production.
|
|
371
|
-
* Always enabled in development.
|
|
372
|
-
*/
|
|
373
|
-
readonly allowDebugManifest: boolean;
|
|
374
|
-
|
|
375
369
|
/**
|
|
376
370
|
* Resolved timeout configuration (merged from shorthand + structured).
|
|
377
371
|
*/
|
|
@@ -126,14 +126,6 @@ export interface RangoOptions<TEnv = any> {
|
|
|
126
126
|
*/
|
|
127
127
|
debugPerformance?: boolean;
|
|
128
128
|
|
|
129
|
-
/**
|
|
130
|
-
* Allow the `?__debug_manifest` query parameter to return route manifest data as JSON.
|
|
131
|
-
* In development mode this is always enabled regardless of this setting.
|
|
132
|
-
* Defaults to false. Set to true to enable in production.
|
|
133
|
-
* @internal
|
|
134
|
-
*/
|
|
135
|
-
allowDebugManifest?: boolean;
|
|
136
|
-
|
|
137
129
|
/**
|
|
138
130
|
* DEVELOPMENT/TEST ONLY. Emit an `X-Rango-Cache` response header describing
|
|
139
131
|
* the cache status of the matched route, for use by testing primitives such
|
package/src/router.ts
CHANGED
|
@@ -158,7 +158,6 @@ export function createRouter<TEnv = any>(
|
|
|
158
158
|
prefetchConcurrency: prefetchConcurrencyOption,
|
|
159
159
|
stateCookiePrefix: stateCookiePrefixOption,
|
|
160
160
|
warmup: warmupOption,
|
|
161
|
-
allowDebugManifest: allowDebugManifestOption = false,
|
|
162
161
|
telemetry: telemetrySink,
|
|
163
162
|
tracing: tracingOption,
|
|
164
163
|
ssr: ssrOption,
|
|
@@ -1021,9 +1020,6 @@ export function createRouter<TEnv = any>(
|
|
|
1021
1020
|
// undefined when unconfigured and call sites gate on truthiness.
|
|
1022
1021
|
telemetry: telemetrySink,
|
|
1023
1022
|
|
|
1024
|
-
// Expose debug manifest flag for handler
|
|
1025
|
-
allowDebugManifest: allowDebugManifestOption,
|
|
1026
|
-
|
|
1027
1023
|
// Expose origin check configuration for handler (default: enabled)
|
|
1028
1024
|
originCheck: originCheckOption ?? true,
|
|
1029
1025
|
|
package/src/rsc/handler.ts
CHANGED
|
@@ -25,10 +25,14 @@ import type {
|
|
|
25
25
|
SSRModule,
|
|
26
26
|
} from "./types.js";
|
|
27
27
|
import {
|
|
28
|
+
RSC_FLIGHT_HTML_PHASES,
|
|
29
|
+
RSC_FLIGHT_ONLY_PHASES,
|
|
28
30
|
createResponseWithMergedHeaders,
|
|
29
31
|
finalizeResponse,
|
|
30
32
|
interceptRedirectForPartial,
|
|
31
33
|
buildRouteMiddlewareEntries,
|
|
34
|
+
observeRscHtmlStage,
|
|
35
|
+
renderRscFlightStage,
|
|
32
36
|
} from "./helpers.js";
|
|
33
37
|
import { guardOutgoingRedirect } from "./redirect-guard.js";
|
|
34
38
|
import { resolvedHandleStream } from "../handles/deferred-resolution.js";
|
|
@@ -52,8 +56,6 @@ import {
|
|
|
52
56
|
import { contextSet } from "../context-var.js";
|
|
53
57
|
import {
|
|
54
58
|
hasCachedManifest,
|
|
55
|
-
getRouteTrie,
|
|
56
|
-
getPrecomputedEntries,
|
|
57
59
|
waitForManifestReady,
|
|
58
60
|
getRouterManifest,
|
|
59
61
|
getRouterTrie,
|
|
@@ -301,17 +303,26 @@ export function createRSCHandler<
|
|
|
301
303
|
...(locationState && { locationState }),
|
|
302
304
|
},
|
|
303
305
|
};
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
306
|
+
const reqCtx = _getRequestContext<TEnv>();
|
|
307
|
+
const rscStream = reqCtx
|
|
308
|
+
? renderRscFlightStage(
|
|
309
|
+
{
|
|
310
|
+
ctx: { renderToReadableStream, callOnError },
|
|
311
|
+
request: reqCtx.request,
|
|
312
|
+
url: reqCtx.url,
|
|
313
|
+
env: reqCtx.env,
|
|
314
|
+
payload: redirectPayload,
|
|
315
|
+
tracking: {
|
|
316
|
+
mode: reqCtx.url.searchParams.has("_rsc_action")
|
|
317
|
+
? "action-revalidation"
|
|
318
|
+
: "partial",
|
|
319
|
+
routeKey: reqCtx._routeName,
|
|
320
|
+
phases: RSC_FLIGHT_ONLY_PHASES,
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
performance.now(),
|
|
324
|
+
).stream
|
|
325
|
+
: renderToReadableStream<RscPayload>(redirectPayload);
|
|
315
326
|
return createResponseWithMergedHeaders(rscStream, {
|
|
316
327
|
status: 200,
|
|
317
328
|
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
@@ -632,35 +643,6 @@ export function createRSCHandler<
|
|
|
632
643
|
): Promise<Response> {
|
|
633
644
|
const handlerTiming: string[] = variables.__handlerTiming || [];
|
|
634
645
|
|
|
635
|
-
// Debug manifest endpoint: handled before classification since it
|
|
636
|
-
// doesn't need a route match and needs trie access from the closure.
|
|
637
|
-
const isDev = process.env.NODE_ENV !== "production";
|
|
638
|
-
if (
|
|
639
|
-
url.searchParams.has("__debug_manifest") &&
|
|
640
|
-
(isDev || router.allowDebugManifest)
|
|
641
|
-
) {
|
|
642
|
-
const trie = getRouterTrie(router.id) ?? getRouteTrie();
|
|
643
|
-
const routeManifest = getRequiredRouteMap();
|
|
644
|
-
const { extractAncestryFromTrie } =
|
|
645
|
-
await import("../build/route-trie.js");
|
|
646
|
-
return new Response(
|
|
647
|
-
JSON.stringify(
|
|
648
|
-
{
|
|
649
|
-
routerId: router.id,
|
|
650
|
-
routeManifest,
|
|
651
|
-
routeAncestry: trie ? extractAncestryFromTrie(trie) : {},
|
|
652
|
-
routeTrie: trie,
|
|
653
|
-
precomputedEntries: getPrecomputedEntries(),
|
|
654
|
-
},
|
|
655
|
-
null,
|
|
656
|
-
2,
|
|
657
|
-
),
|
|
658
|
-
{
|
|
659
|
-
headers: { "Content-Type": "application/json" },
|
|
660
|
-
},
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
646
|
// ---- 1. Classify ----
|
|
665
647
|
// classifyRequest may throw RouteNotFoundError for unknown routes.
|
|
666
648
|
// In that case, fall through to a full-render plan so the pipeline
|
|
@@ -1171,13 +1153,31 @@ export function createRSCHandler<
|
|
|
1171
1153
|
},
|
|
1172
1154
|
};
|
|
1173
1155
|
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1176
|
-
|
|
1156
|
+
const isNotFoundFlightResponse = isRscRequest(
|
|
1157
|
+
request,
|
|
1158
|
+
url,
|
|
1159
|
+
isPartial,
|
|
1160
|
+
);
|
|
1161
|
+
const notFoundStageTracking = {
|
|
1162
|
+
mode: isPartial ? ("partial" as const) : ("full" as const),
|
|
1163
|
+
routeKey,
|
|
1164
|
+
phases: isNotFoundFlightResponse
|
|
1165
|
+
? RSC_FLIGHT_ONLY_PHASES
|
|
1166
|
+
: RSC_FLIGHT_HTML_PHASES,
|
|
1167
|
+
};
|
|
1168
|
+
const rscStream = renderRscFlightStage(
|
|
1169
|
+
{
|
|
1170
|
+
ctx: { renderToReadableStream, callOnError },
|
|
1171
|
+
request,
|
|
1172
|
+
url,
|
|
1173
|
+
env,
|
|
1174
|
+
payload,
|
|
1175
|
+
tracking: notFoundStageTracking,
|
|
1177
1176
|
},
|
|
1178
|
-
|
|
1177
|
+
performance.now(),
|
|
1178
|
+
).stream;
|
|
1179
1179
|
|
|
1180
|
-
if (
|
|
1180
|
+
if (isNotFoundFlightResponse) {
|
|
1181
1181
|
return createResponseWithMergedHeaders(rscStream, {
|
|
1182
1182
|
status: 404,
|
|
1183
1183
|
headers: {
|
|
@@ -1196,10 +1196,14 @@ export function createRSCHandler<
|
|
|
1196
1196
|
url,
|
|
1197
1197
|
getRequestContext()._metricsStore,
|
|
1198
1198
|
);
|
|
1199
|
-
const htmlStream = await
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1199
|
+
const htmlStream = await observeRscHtmlStage(
|
|
1200
|
+
{ url, tracking: notFoundStageTracking },
|
|
1201
|
+
() =>
|
|
1202
|
+
ssrModule.renderHTML(rscStream, {
|
|
1203
|
+
nonce,
|
|
1204
|
+
streamMode,
|
|
1205
|
+
}),
|
|
1206
|
+
);
|
|
1203
1207
|
|
|
1204
1208
|
return createResponseWithMergedHeaders(htmlStream, {
|
|
1205
1209
|
status: 404,
|
package/src/rsc/helpers.ts
CHANGED
|
@@ -18,7 +18,468 @@ import {
|
|
|
18
18
|
} from "../redirect-origin.js";
|
|
19
19
|
import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
|
|
20
20
|
import { formatCacheSignalHeader } from "../router/telemetry.js";
|
|
21
|
+
import { appendMetric } from "../router/metrics.js";
|
|
21
22
|
import type { RscPayload } from "./types.js";
|
|
23
|
+
import type { HandlerContext } from "./handler-context.js";
|
|
24
|
+
|
|
25
|
+
export interface RscRenderStageInput<TEnv> {
|
|
26
|
+
ctx: Pick<HandlerContext<TEnv>, "renderToReadableStream" | "callOnError">;
|
|
27
|
+
request: Request;
|
|
28
|
+
url: URL;
|
|
29
|
+
env: TEnv;
|
|
30
|
+
payload: RscPayload;
|
|
31
|
+
init: ResponseInit;
|
|
32
|
+
temporaryReferences?: unknown;
|
|
33
|
+
recordSerializeMetric?: boolean;
|
|
34
|
+
tracking?: RscRenderStageTracking;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type RscRenderMode =
|
|
38
|
+
| "unknown"
|
|
39
|
+
| "full"
|
|
40
|
+
| "partial"
|
|
41
|
+
| "action-revalidation"
|
|
42
|
+
| "progressive-enhancement"
|
|
43
|
+
| "progressive-enhancement-error";
|
|
44
|
+
|
|
45
|
+
export type RscRenderPhase = "payload" | "flight" | "html" | "response";
|
|
46
|
+
|
|
47
|
+
export const RSC_RENDER_FLIGHT_RESPONSE_PHASES: readonly RscRenderPhase[] = [
|
|
48
|
+
"payload",
|
|
49
|
+
"flight",
|
|
50
|
+
"response",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export const RSC_RENDER_HTML_RESPONSE_PHASES: readonly RscRenderPhase[] = [
|
|
54
|
+
"payload",
|
|
55
|
+
"flight",
|
|
56
|
+
"html",
|
|
57
|
+
"response",
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export const RSC_FLIGHT_ONLY_PHASES: readonly RscRenderPhase[] = ["flight"];
|
|
61
|
+
|
|
62
|
+
export const RSC_FLIGHT_HTML_PHASES: readonly RscRenderPhase[] = [
|
|
63
|
+
"flight",
|
|
64
|
+
"html",
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
export interface RscRenderStageProgress {
|
|
68
|
+
completed: number;
|
|
69
|
+
total: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface RscRenderStageContext {
|
|
73
|
+
mode: RscRenderMode;
|
|
74
|
+
phase: RscRenderPhase;
|
|
75
|
+
pathname: string;
|
|
76
|
+
progress: RscRenderStageProgress;
|
|
77
|
+
startedAt: number;
|
|
78
|
+
phaseStartedAt: number;
|
|
79
|
+
routeKey?: string;
|
|
80
|
+
actionId?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type RscRenderStageEvent =
|
|
84
|
+
| {
|
|
85
|
+
type: "stage:yield";
|
|
86
|
+
context: RscRenderStageContext;
|
|
87
|
+
}
|
|
88
|
+
| {
|
|
89
|
+
type: "stage:start";
|
|
90
|
+
context: RscRenderStageContext;
|
|
91
|
+
}
|
|
92
|
+
| {
|
|
93
|
+
type: "stage:complete";
|
|
94
|
+
context: RscRenderStageContext;
|
|
95
|
+
durationMs: number;
|
|
96
|
+
}
|
|
97
|
+
| {
|
|
98
|
+
type: "stage:error";
|
|
99
|
+
context: RscRenderStageContext;
|
|
100
|
+
durationMs: number;
|
|
101
|
+
error: unknown;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export interface RscRenderStageTracking {
|
|
105
|
+
mode?: RscRenderMode;
|
|
106
|
+
routeKey?: string;
|
|
107
|
+
actionId?: string;
|
|
108
|
+
phases?: readonly RscRenderPhase[];
|
|
109
|
+
totalStages?: number;
|
|
110
|
+
onEvent?: (event: RscRenderStageEvent) => void;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface RscFlightStageInput<TEnv> {
|
|
114
|
+
ctx: Pick<HandlerContext<TEnv>, "renderToReadableStream" | "callOnError">;
|
|
115
|
+
request: Request;
|
|
116
|
+
url: URL;
|
|
117
|
+
env: TEnv;
|
|
118
|
+
payload: RscPayload;
|
|
119
|
+
temporaryReferences?: unknown;
|
|
120
|
+
recordSerializeMetric?: boolean;
|
|
121
|
+
tracking?: RscRenderStageTracking;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface RscHtmlStageInput {
|
|
125
|
+
url: URL;
|
|
126
|
+
tracking?: RscRenderStageTracking;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type RscRenderStage =
|
|
130
|
+
| {
|
|
131
|
+
type: "payload";
|
|
132
|
+
payload: RscPayload;
|
|
133
|
+
init: ResponseInit;
|
|
134
|
+
context: RscRenderStageContext;
|
|
135
|
+
}
|
|
136
|
+
| {
|
|
137
|
+
type: "flight";
|
|
138
|
+
payload: RscPayload;
|
|
139
|
+
stream: ReadableStream<Uint8Array>;
|
|
140
|
+
init: ResponseInit;
|
|
141
|
+
durationMs: number;
|
|
142
|
+
context: RscRenderStageContext;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export type RscFlightStage = Extract<RscRenderStage, { type: "flight" }>;
|
|
146
|
+
|
|
147
|
+
export interface RscRenderStageControl {
|
|
148
|
+
payload?: RscPayload;
|
|
149
|
+
init?: ResponseInit;
|
|
150
|
+
body?: BodyInit | null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface RscFlightStageResult {
|
|
154
|
+
stage: RscFlightStage;
|
|
155
|
+
control: RscRenderStageControl | undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
type RscRenderStageSink = NonNullable<RscRenderStageTracking["onEvent"]>;
|
|
159
|
+
|
|
160
|
+
function rscStagePhases(
|
|
161
|
+
tracking: RscRenderStageTracking | undefined,
|
|
162
|
+
): readonly RscRenderPhase[] {
|
|
163
|
+
const phases = tracking?.phases;
|
|
164
|
+
if (phases && phases.length > 0) return phases;
|
|
165
|
+
const total = tracking?.totalStages;
|
|
166
|
+
if (total === 1) return RSC_FLIGHT_ONLY_PHASES;
|
|
167
|
+
if (total === 2) return RSC_FLIGHT_HTML_PHASES;
|
|
168
|
+
if (total === 4) return RSC_RENDER_HTML_RESPONSE_PHASES;
|
|
169
|
+
return RSC_RENDER_FLIGHT_RESPONSE_PHASES;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function createRscStageContext(
|
|
173
|
+
input: { url: URL; tracking?: RscRenderStageTracking },
|
|
174
|
+
phase: RscRenderPhase,
|
|
175
|
+
startedAt: number,
|
|
176
|
+
phaseStartedAt: number,
|
|
177
|
+
): RscRenderStageContext {
|
|
178
|
+
const phases = rscStagePhases(input.tracking);
|
|
179
|
+
const phaseIndex = phases.indexOf(phase);
|
|
180
|
+
const total = phases.length;
|
|
181
|
+
const completed = phaseIndex >= 0 ? phaseIndex + 1 : total;
|
|
182
|
+
return {
|
|
183
|
+
mode: input.tracking?.mode ?? "unknown",
|
|
184
|
+
phase,
|
|
185
|
+
pathname: input.url.pathname,
|
|
186
|
+
progress: { completed, total },
|
|
187
|
+
startedAt,
|
|
188
|
+
phaseStartedAt,
|
|
189
|
+
...(input.tracking?.routeKey && { routeKey: input.tracking.routeKey }),
|
|
190
|
+
...(input.tracking?.actionId && { actionId: input.tracking.actionId }),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getRscStageSink(input: {
|
|
195
|
+
tracking?: RscRenderStageTracking;
|
|
196
|
+
}): RscRenderStageSink | undefined {
|
|
197
|
+
return input.tracking?.onEvent;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function emitRscStageEvent(
|
|
201
|
+
sink: RscRenderStageSink | undefined,
|
|
202
|
+
createEvent: () => RscRenderStageEvent,
|
|
203
|
+
): void {
|
|
204
|
+
if (!sink) return;
|
|
205
|
+
try {
|
|
206
|
+
sink(createEvent());
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (process.env.NODE_ENV !== "production") {
|
|
209
|
+
console.error("[RSC] render stage event sink failed:", error);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function renderRscFlightStage<TEnv>(
|
|
215
|
+
input: RscFlightStageInput<TEnv>,
|
|
216
|
+
startedAt: number,
|
|
217
|
+
init: ResponseInit = {},
|
|
218
|
+
): RscFlightStage {
|
|
219
|
+
const phaseStartedAt = performance.now();
|
|
220
|
+
const context = createRscStageContext(
|
|
221
|
+
input,
|
|
222
|
+
"flight",
|
|
223
|
+
startedAt,
|
|
224
|
+
phaseStartedAt,
|
|
225
|
+
);
|
|
226
|
+
const sink = getRscStageSink(input);
|
|
227
|
+
emitRscStageEvent(sink, () => ({ type: "stage:start", context }));
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const stream = input.ctx.renderToReadableStream<RscPayload>(input.payload, {
|
|
231
|
+
temporaryReferences: input.temporaryReferences,
|
|
232
|
+
onError: (error: unknown) => {
|
|
233
|
+
input.ctx.callOnError(error, "rendering", {
|
|
234
|
+
request: input.request,
|
|
235
|
+
url: input.url,
|
|
236
|
+
env: input.env,
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
const durationMs = performance.now() - phaseStartedAt;
|
|
241
|
+
if (input.recordSerializeMetric === true) {
|
|
242
|
+
appendMetric(
|
|
243
|
+
_getRequestContext()?._metricsStore,
|
|
244
|
+
"rsc-serialize",
|
|
245
|
+
phaseStartedAt,
|
|
246
|
+
durationMs,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
emitRscStageEvent(sink, () => ({
|
|
250
|
+
type: "stage:complete",
|
|
251
|
+
context,
|
|
252
|
+
durationMs,
|
|
253
|
+
}));
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
type: "flight" as const,
|
|
257
|
+
payload: input.payload,
|
|
258
|
+
stream,
|
|
259
|
+
init,
|
|
260
|
+
durationMs,
|
|
261
|
+
context,
|
|
262
|
+
};
|
|
263
|
+
} catch (error) {
|
|
264
|
+
emitRscStageEvent(sink, () => ({
|
|
265
|
+
type: "stage:error",
|
|
266
|
+
context,
|
|
267
|
+
durationMs: performance.now() - phaseStartedAt,
|
|
268
|
+
error,
|
|
269
|
+
}));
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function* createRscFlightStages<TEnv>(
|
|
275
|
+
input: RscRenderStageInput<TEnv>,
|
|
276
|
+
payload: RscPayload,
|
|
277
|
+
init: ResponseInit,
|
|
278
|
+
startedAt: number,
|
|
279
|
+
): AsyncGenerator<
|
|
280
|
+
RscFlightStage,
|
|
281
|
+
RscFlightStageResult,
|
|
282
|
+
RscRenderStageControl | undefined
|
|
283
|
+
> {
|
|
284
|
+
const stage = renderRscFlightStage(
|
|
285
|
+
{
|
|
286
|
+
ctx: input.ctx,
|
|
287
|
+
request: input.request,
|
|
288
|
+
url: input.url,
|
|
289
|
+
env: input.env,
|
|
290
|
+
payload,
|
|
291
|
+
temporaryReferences: input.temporaryReferences,
|
|
292
|
+
recordSerializeMetric: input.recordSerializeMetric ?? true,
|
|
293
|
+
tracking: input.tracking,
|
|
294
|
+
},
|
|
295
|
+
startedAt,
|
|
296
|
+
init,
|
|
297
|
+
);
|
|
298
|
+
const sink = getRscStageSink(input);
|
|
299
|
+
emitRscStageEvent(sink, () => ({
|
|
300
|
+
type: "stage:yield",
|
|
301
|
+
context: stage.context,
|
|
302
|
+
}));
|
|
303
|
+
const control = yield stage;
|
|
304
|
+
return { stage, control };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function observeRscHtmlStage<T>(
|
|
308
|
+
input: RscHtmlStageInput,
|
|
309
|
+
fn: () => Promise<T>,
|
|
310
|
+
): Promise<T> {
|
|
311
|
+
const startedAt = performance.now();
|
|
312
|
+
const sink = getRscStageSink(input);
|
|
313
|
+
const context = sink
|
|
314
|
+
? createRscStageContext(input, "html", startedAt, startedAt)
|
|
315
|
+
: undefined;
|
|
316
|
+
emitRscStageEvent(sink, () => ({
|
|
317
|
+
type: "stage:start",
|
|
318
|
+
context: context!,
|
|
319
|
+
}));
|
|
320
|
+
try {
|
|
321
|
+
const result = await fn();
|
|
322
|
+
emitRscStageEvent(sink, () => ({
|
|
323
|
+
type: "stage:complete",
|
|
324
|
+
context: context!,
|
|
325
|
+
durationMs: performance.now() - startedAt,
|
|
326
|
+
}));
|
|
327
|
+
return result;
|
|
328
|
+
} catch (error) {
|
|
329
|
+
emitRscStageEvent(sink, () => ({
|
|
330
|
+
type: "stage:error",
|
|
331
|
+
context: context!,
|
|
332
|
+
durationMs: performance.now() - startedAt,
|
|
333
|
+
error,
|
|
334
|
+
}));
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function createRscStageDebugSink(
|
|
340
|
+
log: (message: string, details?: Record<string, unknown>) => void = (
|
|
341
|
+
message,
|
|
342
|
+
details,
|
|
343
|
+
) => console.debug(message, details),
|
|
344
|
+
): (event: RscRenderStageEvent) => void {
|
|
345
|
+
return (event) => {
|
|
346
|
+
const { context } = event;
|
|
347
|
+
log(`[RSC][stage] ${event.type} ${context.phase}`, {
|
|
348
|
+
mode: context.mode,
|
|
349
|
+
pathname: context.pathname,
|
|
350
|
+
routeKey: context.routeKey,
|
|
351
|
+
actionId: context.actionId,
|
|
352
|
+
progress: `${context.progress.completed}/${context.progress.total}`,
|
|
353
|
+
...("durationMs" in event && { durationMs: event.durationMs }),
|
|
354
|
+
...("error" in event && { error: event.error }),
|
|
355
|
+
});
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Stage the common RSC render flow as a resumable async generator:
|
|
361
|
+
* payload inspection/mutation -> Flight stream creation -> response finalization.
|
|
362
|
+
* HTML callers pause at the Flight stage, render SSR from that stream, then
|
|
363
|
+
* resume with the HTML body/init so header merging still happens once.
|
|
364
|
+
*/
|
|
365
|
+
export async function* createRscRenderStages<TEnv>(
|
|
366
|
+
input: RscRenderStageInput<TEnv>,
|
|
367
|
+
): AsyncGenerator<RscRenderStage, Response, RscRenderStageControl | undefined> {
|
|
368
|
+
const startedAt = performance.now();
|
|
369
|
+
let payload = input.payload;
|
|
370
|
+
let init = input.init;
|
|
371
|
+
|
|
372
|
+
const payloadContext = createRscStageContext(
|
|
373
|
+
input,
|
|
374
|
+
"payload",
|
|
375
|
+
startedAt,
|
|
376
|
+
startedAt,
|
|
377
|
+
);
|
|
378
|
+
const sink = getRscStageSink(input);
|
|
379
|
+
emitRscStageEvent(sink, () => ({
|
|
380
|
+
type: "stage:yield",
|
|
381
|
+
context: payloadContext,
|
|
382
|
+
}));
|
|
383
|
+
const payloadControl = yield {
|
|
384
|
+
type: "payload" as const,
|
|
385
|
+
payload,
|
|
386
|
+
init,
|
|
387
|
+
context: payloadContext,
|
|
388
|
+
};
|
|
389
|
+
payload = payloadControl?.payload ?? payload;
|
|
390
|
+
init = payloadControl?.init ?? init;
|
|
391
|
+
|
|
392
|
+
const flightResult = yield* createRscFlightStages(
|
|
393
|
+
input,
|
|
394
|
+
payload,
|
|
395
|
+
init,
|
|
396
|
+
startedAt,
|
|
397
|
+
);
|
|
398
|
+
const flightControl = flightResult.control;
|
|
399
|
+
|
|
400
|
+
const body =
|
|
401
|
+
flightControl && "body" in flightControl
|
|
402
|
+
? flightControl.body!
|
|
403
|
+
: flightResult.stage.stream;
|
|
404
|
+
|
|
405
|
+
const phaseStartedAt = performance.now();
|
|
406
|
+
const responseContext = sink
|
|
407
|
+
? createRscStageContext(input, "response", startedAt, phaseStartedAt)
|
|
408
|
+
: undefined;
|
|
409
|
+
emitRscStageEvent(sink, () => ({
|
|
410
|
+
type: "stage:start",
|
|
411
|
+
context: responseContext!,
|
|
412
|
+
}));
|
|
413
|
+
try {
|
|
414
|
+
const response = createResponseWithMergedHeaders(body, {
|
|
415
|
+
...init,
|
|
416
|
+
...flightControl?.init,
|
|
417
|
+
});
|
|
418
|
+
emitRscStageEvent(sink, () => ({
|
|
419
|
+
type: "stage:complete",
|
|
420
|
+
context: responseContext!,
|
|
421
|
+
durationMs: performance.now() - phaseStartedAt,
|
|
422
|
+
}));
|
|
423
|
+
return response;
|
|
424
|
+
} catch (error) {
|
|
425
|
+
emitRscStageEvent(sink, () => ({
|
|
426
|
+
type: "stage:error",
|
|
427
|
+
context: responseContext!,
|
|
428
|
+
durationMs: performance.now() - phaseStartedAt,
|
|
429
|
+
error,
|
|
430
|
+
}));
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export async function runRscRenderStages(
|
|
436
|
+
stages: AsyncGenerator<
|
|
437
|
+
RscRenderStage,
|
|
438
|
+
Response,
|
|
439
|
+
RscRenderStageControl | undefined
|
|
440
|
+
>,
|
|
441
|
+
): Promise<Response> {
|
|
442
|
+
for (;;) {
|
|
443
|
+
const step = await stages.next();
|
|
444
|
+
if (step.done) return step.value;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export async function readRscFlightStage(
|
|
449
|
+
stages: AsyncGenerator<
|
|
450
|
+
RscRenderStage,
|
|
451
|
+
Response,
|
|
452
|
+
RscRenderStageControl | undefined
|
|
453
|
+
>,
|
|
454
|
+
): Promise<RscFlightStage> {
|
|
455
|
+
const payloadStage = await stages.next();
|
|
456
|
+
if (payloadStage.done || payloadStage.value.type !== "payload") {
|
|
457
|
+
throw new Error("[RSC] render stage pipeline skipped payload stage");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const flightStage = await stages.next();
|
|
461
|
+
if (flightStage.done || flightStage.value.type !== "flight") {
|
|
462
|
+
throw new Error("[RSC] render stage pipeline skipped Flight stream");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return flightStage.value;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export async function finishRscRenderStages(
|
|
469
|
+
stages: AsyncGenerator<
|
|
470
|
+
RscRenderStage,
|
|
471
|
+
Response,
|
|
472
|
+
RscRenderStageControl | undefined
|
|
473
|
+
>,
|
|
474
|
+
control?: RscRenderStageControl,
|
|
475
|
+
): Promise<Response> {
|
|
476
|
+
const responseStage = await stages.next(control);
|
|
477
|
+
if (!responseStage.done) {
|
|
478
|
+
throw new Error("[RSC] render stage pipeline did not finish response");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return responseStage.value;
|
|
482
|
+
}
|
|
22
483
|
|
|
23
484
|
/**
|
|
24
485
|
* DEVELOPMENT/TEST ONLY. When the debug cache signal gate is on,
|