@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
|
@@ -22,12 +22,23 @@ import {
|
|
|
22
22
|
finalizeResponse,
|
|
23
23
|
buildRouteMiddlewareEntries,
|
|
24
24
|
} from "./helpers.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
createRenderStageTraceBridge,
|
|
27
|
+
renderRscResponse,
|
|
28
|
+
} from "./render-pipeline.js";
|
|
29
|
+
import {
|
|
30
|
+
createRoutineTrace,
|
|
31
|
+
runRoutine,
|
|
32
|
+
step,
|
|
33
|
+
type RoutinePlan,
|
|
34
|
+
type RoutineTrace,
|
|
35
|
+
} from "./routine-plan.js";
|
|
26
36
|
import type { HandlerContext } from "./handler-context.js";
|
|
27
37
|
import {
|
|
28
38
|
extractRedirectResponse,
|
|
29
39
|
warnNonRedirectPeResponse,
|
|
30
40
|
} from "./runtime-warnings.js";
|
|
41
|
+
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
31
42
|
|
|
32
43
|
export interface PeRouteMiddlewareInfo {
|
|
33
44
|
routeMiddleware?: Array<{
|
|
@@ -61,6 +72,40 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
61
72
|
return null;
|
|
62
73
|
}
|
|
63
74
|
|
|
75
|
+
// Flow trace: shared by every plan this request drives (error-boundary
|
|
76
|
+
// renders and the re-render), so a PE request prints ONE tree. A non-PE form
|
|
77
|
+
// POST (no $ACTION fields) drives no plan and stays silent.
|
|
78
|
+
const trace = INTERNAL_RANGO_DEBUG ? createRoutineTrace("pe") : undefined;
|
|
79
|
+
try {
|
|
80
|
+
return await handleProgressiveEnhancementInner(
|
|
81
|
+
ctx,
|
|
82
|
+
request,
|
|
83
|
+
env,
|
|
84
|
+
url,
|
|
85
|
+
handleStore,
|
|
86
|
+
nonce,
|
|
87
|
+
routeMwInfo,
|
|
88
|
+
trace,
|
|
89
|
+
);
|
|
90
|
+
} finally {
|
|
91
|
+
if (trace && trace.entries.length > 0) {
|
|
92
|
+
console.log(
|
|
93
|
+
`[routine] ${request.method} ${url.pathname} (${trace.name})\n${trace.format()}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function handleProgressiveEnhancementInner<TEnv>(
|
|
100
|
+
ctx: HandlerContext<TEnv>,
|
|
101
|
+
request: Request,
|
|
102
|
+
env: TEnv,
|
|
103
|
+
url: URL,
|
|
104
|
+
handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
|
|
105
|
+
nonce: string | undefined,
|
|
106
|
+
routeMwInfo: PeRouteMiddlewareInfo | undefined,
|
|
107
|
+
trace: RoutineTrace | undefined,
|
|
108
|
+
): Promise<Response | null> {
|
|
64
109
|
// Clone the request to read FormData without consuming it.
|
|
65
110
|
// Wrap in try-catch so malformed POST bodies are reported as action
|
|
66
111
|
// errors, not routing errors from the outer catch in handler.ts.
|
|
@@ -77,6 +122,7 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
77
122
|
error,
|
|
78
123
|
handleStore,
|
|
79
124
|
nonce,
|
|
125
|
+
trace,
|
|
80
126
|
);
|
|
81
127
|
if (errorHtml) {
|
|
82
128
|
ctx.callOnError(error, "action", {
|
|
@@ -154,6 +200,7 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
154
200
|
error,
|
|
155
201
|
handleStore,
|
|
156
202
|
nonce,
|
|
203
|
+
trace,
|
|
157
204
|
useActionStateId,
|
|
158
205
|
true, // an action ran and threw
|
|
159
206
|
);
|
|
@@ -208,6 +255,7 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
208
255
|
error,
|
|
209
256
|
handleStore,
|
|
210
257
|
nonce,
|
|
258
|
+
trace,
|
|
211
259
|
directActionId,
|
|
212
260
|
true, // an action ran and threw
|
|
213
261
|
);
|
|
@@ -287,79 +335,20 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
287
335
|
peReqCtx._gateActionResult = actionResult;
|
|
288
336
|
peReqCtx._gateFormData = formData;
|
|
289
337
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (match.redirect) {
|
|
293
|
-
return createResponseWithMergedHeaders(null, {
|
|
294
|
-
status: 308,
|
|
295
|
-
headers: { Location: match.redirect },
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const payload: RscPayload = {
|
|
300
|
-
metadata: {
|
|
301
|
-
pathname: url.pathname,
|
|
302
|
-
routerId: ctx.router.id,
|
|
303
|
-
basename: ctx.router.basename,
|
|
304
|
-
segments: gateTransitions(
|
|
305
|
-
match.segments,
|
|
306
|
-
getRequestContext(),
|
|
307
|
-
ctx.router.onError,
|
|
308
|
-
),
|
|
309
|
-
matched: match.matched,
|
|
310
|
-
diff: match.diff,
|
|
311
|
-
resolvedIds: match.resolvedIds,
|
|
312
|
-
params: match.params,
|
|
313
|
-
isPartial: false,
|
|
314
|
-
rootLayout: ctx.router.rootLayout,
|
|
315
|
-
// PE full render: resolve deferred handle values server-side.
|
|
316
|
-
handles: resolvedHandleStream(handleStore),
|
|
317
|
-
version: ctx.version,
|
|
318
|
-
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
319
|
-
themeConfig: ctx.router.themeConfig,
|
|
320
|
-
warmupEnabled: ctx.router.warmupEnabled,
|
|
321
|
-
strictMode: ctx.router.strictMode,
|
|
322
|
-
initialTheme: getRequestContext().theme,
|
|
323
|
-
},
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
const stageTracking = {
|
|
327
|
-
mode: "progressive-enhancement" as const,
|
|
328
|
-
routeKey: getRequestContext()._routeName,
|
|
329
|
-
actionId: directActionId ?? undefined,
|
|
330
|
-
};
|
|
331
|
-
return renderRscResponse(
|
|
332
|
-
{
|
|
338
|
+
return runRoutine(
|
|
339
|
+
peRenderPlan({
|
|
333
340
|
ctx,
|
|
334
341
|
request,
|
|
335
342
|
env,
|
|
336
343
|
url,
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
: {}),
|
|
346
|
-
headers: { "content-type": "text/html;charset=utf-8" },
|
|
347
|
-
},
|
|
348
|
-
tracking: stageTracking,
|
|
349
|
-
},
|
|
350
|
-
{
|
|
351
|
-
// metricsStore=undefined is safe: the handler already stashed the early
|
|
352
|
-
// SSR setup promise, so this reuses it instead of starting setup again.
|
|
353
|
-
// reactFormState travels through the SSR option, not RscPayload.
|
|
354
|
-
html: createSsrHtmlStage({
|
|
355
|
-
ctx,
|
|
356
|
-
request,
|
|
357
|
-
env,
|
|
358
|
-
url,
|
|
359
|
-
metricsStore: undefined,
|
|
360
|
-
render: { formState: reactFormState, nonce },
|
|
361
|
-
}),
|
|
362
|
-
},
|
|
344
|
+
renderRequest,
|
|
345
|
+
handleStore,
|
|
346
|
+
nonce,
|
|
347
|
+
reactFormState,
|
|
348
|
+
boundarylessErrorStatus,
|
|
349
|
+
directActionId,
|
|
350
|
+
}),
|
|
351
|
+
{ trace, owner: getRequestContext() },
|
|
363
352
|
);
|
|
364
353
|
};
|
|
365
354
|
|
|
@@ -382,6 +371,142 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
382
371
|
return renderPage();
|
|
383
372
|
}
|
|
384
373
|
|
|
374
|
+
interface PeRenderInput<TEnv> {
|
|
375
|
+
ctx: HandlerContext<TEnv>;
|
|
376
|
+
request: Request;
|
|
377
|
+
env: TEnv;
|
|
378
|
+
url: URL;
|
|
379
|
+
renderRequest: Request;
|
|
380
|
+
handleStore: ReturnType<typeof getRequestContext>["_handleStore"];
|
|
381
|
+
nonce: string | undefined;
|
|
382
|
+
reactFormState: ReactFormState | null;
|
|
383
|
+
boundarylessErrorStatus: number | undefined;
|
|
384
|
+
directActionId: string | null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** PE re-render: match the bodyless GET mirror, then render full HTML. */
|
|
388
|
+
function* peRenderPlan<TEnv>(
|
|
389
|
+
input: PeRenderInput<TEnv>,
|
|
390
|
+
): RoutinePlan<Response> {
|
|
391
|
+
const { ctx, env, renderRequest } = input;
|
|
392
|
+
|
|
393
|
+
const match = yield* step("match", () =>
|
|
394
|
+
ctx.router.match(renderRequest, { env }),
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
if (match.redirect) {
|
|
398
|
+
return createResponseWithMergedHeaders(null, {
|
|
399
|
+
status: 308,
|
|
400
|
+
headers: { Location: match.redirect },
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return yield* step("render", () => renderPeResponse(input, match));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Build the PE full-document payload and render it through the stage driver. */
|
|
408
|
+
function renderPeResponse<TEnv>(
|
|
409
|
+
input: PeRenderInput<TEnv>,
|
|
410
|
+
match: Awaited<ReturnType<HandlerContext<TEnv>["router"]["match"]>>,
|
|
411
|
+
): Promise<Response> {
|
|
412
|
+
const {
|
|
413
|
+
ctx,
|
|
414
|
+
request,
|
|
415
|
+
env,
|
|
416
|
+
url,
|
|
417
|
+
handleStore,
|
|
418
|
+
nonce,
|
|
419
|
+
reactFormState,
|
|
420
|
+
boundarylessErrorStatus,
|
|
421
|
+
directActionId,
|
|
422
|
+
} = input;
|
|
423
|
+
|
|
424
|
+
const payload: RscPayload = {
|
|
425
|
+
metadata: {
|
|
426
|
+
pathname: url.pathname,
|
|
427
|
+
routerId: ctx.router.id,
|
|
428
|
+
basename: ctx.router.basename,
|
|
429
|
+
segments: gateTransitions(
|
|
430
|
+
match.segments,
|
|
431
|
+
getRequestContext(),
|
|
432
|
+
ctx.router.onError,
|
|
433
|
+
),
|
|
434
|
+
matched: match.matched,
|
|
435
|
+
diff: match.diff,
|
|
436
|
+
resolvedIds: match.resolvedIds,
|
|
437
|
+
params: match.params,
|
|
438
|
+
isPartial: false,
|
|
439
|
+
rootLayout: ctx.router.rootLayout,
|
|
440
|
+
// PE full render: resolve deferred handle values server-side.
|
|
441
|
+
handles: resolvedHandleStream(handleStore),
|
|
442
|
+
version: ctx.version,
|
|
443
|
+
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
444
|
+
themeConfig: ctx.router.themeConfig,
|
|
445
|
+
warmupEnabled: ctx.router.warmupEnabled,
|
|
446
|
+
strictMode: ctx.router.strictMode,
|
|
447
|
+
initialTheme: getRequestContext().theme,
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const trace = getRequestContext()._activeRoutine;
|
|
452
|
+
|
|
453
|
+
const stageTracking = {
|
|
454
|
+
mode: "progressive-enhancement" as const,
|
|
455
|
+
routeKey: getRequestContext()._routeName,
|
|
456
|
+
actionId: directActionId ?? undefined,
|
|
457
|
+
onEvent: trace && createRenderStageTraceBridge(trace),
|
|
458
|
+
};
|
|
459
|
+
return renderRscResponse(
|
|
460
|
+
{
|
|
461
|
+
ctx,
|
|
462
|
+
request,
|
|
463
|
+
env,
|
|
464
|
+
url,
|
|
465
|
+
payload,
|
|
466
|
+
init: {
|
|
467
|
+
// boundarylessErrorStatus is set only when the action threw and no error
|
|
468
|
+
// boundary matched; it makes the re-render carry 500 like the JS path.
|
|
469
|
+
// The redirect branch in peRenderPlan returns before this, so a redirect
|
|
470
|
+
// re-render keeps its 308 and is never overridden.
|
|
471
|
+
...(boundarylessErrorStatus !== undefined
|
|
472
|
+
? { status: boundarylessErrorStatus }
|
|
473
|
+
: {}),
|
|
474
|
+
headers: { "content-type": "text/html;charset=utf-8" },
|
|
475
|
+
},
|
|
476
|
+
tracking: stageTracking,
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
// metricsStore=undefined is safe: the handler already stashed the early
|
|
480
|
+
// SSR setup promise, so this reuses it instead of starting setup again.
|
|
481
|
+
// reactFormState travels through the SSR option, not RscPayload.
|
|
482
|
+
html: createSsrHtmlStage({
|
|
483
|
+
ctx,
|
|
484
|
+
request,
|
|
485
|
+
env,
|
|
486
|
+
url,
|
|
487
|
+
metricsStore: undefined,
|
|
488
|
+
render: { formState: reactFormState, nonce },
|
|
489
|
+
}),
|
|
490
|
+
},
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
interface PeErrorBoundaryInput<TEnv> {
|
|
495
|
+
ctx: HandlerContext<TEnv>;
|
|
496
|
+
request: Request;
|
|
497
|
+
env: TEnv;
|
|
498
|
+
url: URL;
|
|
499
|
+
error: unknown;
|
|
500
|
+
handleStore: ReturnType<typeof getRequestContext>["_handleStore"];
|
|
501
|
+
nonce: string | undefined;
|
|
502
|
+
actionId: string | null | undefined;
|
|
503
|
+
actionRan: boolean;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
type PeErrorMatch<TEnv> = NonNullable<
|
|
507
|
+
Awaited<ReturnType<HandlerContext<TEnv>["router"]["matchError"]>>
|
|
508
|
+
>;
|
|
509
|
+
|
|
385
510
|
/**
|
|
386
511
|
* Attempt to render an error boundary as full HTML for the PE path.
|
|
387
512
|
* Returns null if no error boundary is found (caller falls through to
|
|
@@ -395,6 +520,7 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
395
520
|
error: unknown,
|
|
396
521
|
handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
|
|
397
522
|
nonce: string | undefined,
|
|
523
|
+
trace: RoutineTrace | undefined,
|
|
398
524
|
actionId?: string | null,
|
|
399
525
|
// True when an action actually ran and threw (vs a malformed form body, where
|
|
400
526
|
// no action executed). Drives _inActionRevalidation for JS/PE parity — it must
|
|
@@ -402,6 +528,44 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
402
528
|
// and throw with no $$id (actionId === undefined) yet still be an action error.
|
|
403
529
|
actionRan = false,
|
|
404
530
|
): Promise<Response | null> {
|
|
531
|
+
return runRoutine(
|
|
532
|
+
peErrorBoundaryPlan({
|
|
533
|
+
ctx,
|
|
534
|
+
request,
|
|
535
|
+
env,
|
|
536
|
+
url,
|
|
537
|
+
error,
|
|
538
|
+
handleStore,
|
|
539
|
+
nonce,
|
|
540
|
+
actionId,
|
|
541
|
+
actionRan,
|
|
542
|
+
}),
|
|
543
|
+
{ trace, owner: getRequestContext() },
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** PE error boundary: match a boundary for the thrown error, then render it. */
|
|
548
|
+
function* peErrorBoundaryPlan<TEnv>(
|
|
549
|
+
input: PeErrorBoundaryInput<TEnv>,
|
|
550
|
+
): RoutinePlan<Response | null> {
|
|
551
|
+
const boundary = yield* step("match:error", () =>
|
|
552
|
+
matchPeErrorBoundary(input),
|
|
553
|
+
);
|
|
554
|
+
if (!boundary) return null;
|
|
555
|
+
return yield* step("render", () => renderPeErrorResponse(input, boundary));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Match an error boundary for the PE path, owning the reporting protocol:
|
|
560
|
+
* a matchError failure reports the ORIGINAL error as unhandled and rethrows
|
|
561
|
+
* the match failure; a miss returns null; a hit reports handled and stamps
|
|
562
|
+
* params + action gate context before the render.
|
|
563
|
+
*/
|
|
564
|
+
async function matchPeErrorBoundary<TEnv>(
|
|
565
|
+
input: PeErrorBoundaryInput<TEnv>,
|
|
566
|
+
): Promise<PeErrorMatch<TEnv> | null> {
|
|
567
|
+
const { ctx, request, env, url, error, actionId, actionRan } = input;
|
|
568
|
+
|
|
405
569
|
// JS/PE parity for an action-triggered error re-render: a stale
|
|
406
570
|
// `foregroundOnAction` cache entry inside the error boundary must foreground
|
|
407
571
|
// too, exactly as the JS path (revalidateAfterAction sets this unconditionally
|
|
@@ -448,6 +612,16 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
448
612
|
peErrCtx._gateActionUrl = new URL(url);
|
|
449
613
|
}
|
|
450
614
|
|
|
615
|
+
return errorResult;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** Render the matched PE error boundary as a full HTML document. */
|
|
619
|
+
function renderPeErrorResponse<TEnv>(
|
|
620
|
+
input: PeErrorBoundaryInput<TEnv>,
|
|
621
|
+
errorResult: PeErrorMatch<TEnv>,
|
|
622
|
+
): Promise<Response> {
|
|
623
|
+
const { ctx, request, env, url, handleStore, nonce, actionId } = input;
|
|
624
|
+
|
|
451
625
|
const payload: RscPayload = {
|
|
452
626
|
metadata: {
|
|
453
627
|
pathname: url.pathname,
|
|
@@ -476,10 +650,13 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
476
650
|
},
|
|
477
651
|
};
|
|
478
652
|
|
|
653
|
+
const trace = getRequestContext()._activeRoutine;
|
|
654
|
+
|
|
479
655
|
const stageTracking = {
|
|
480
656
|
mode: "progressive-enhancement-error" as const,
|
|
481
657
|
routeKey: getRequestContext()._routeName,
|
|
482
658
|
actionId: actionId ?? undefined,
|
|
659
|
+
onEvent: trace && createRenderStageTraceBridge(trace),
|
|
483
660
|
};
|
|
484
661
|
return renderRscResponse(
|
|
485
662
|
{
|
|
@@ -7,6 +7,7 @@ import type { RenderMode, RenderPhase } from "../router/timeout.js";
|
|
|
7
7
|
import type { HandlerContext } from "./handler-context.js";
|
|
8
8
|
import { createResponseWithMergedHeaders } from "./helpers.js";
|
|
9
9
|
import type { RscPayload } from "./types.js";
|
|
10
|
+
import type { RoutineTrace, RoutineTraceEntry } from "./routine-plan.js";
|
|
10
11
|
|
|
11
12
|
// Canonical unions live in router/timeout.ts (the dependency-free leaf); these
|
|
12
13
|
// aliases keep the render-pipeline names stable for existing importers.
|
|
@@ -191,6 +192,34 @@ function expectCommandResult(
|
|
|
191
192
|
return result.value;
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Step helpers: each yields one command and returns its narrowed result, so a
|
|
197
|
+
* plan body reads imperatively while the yielded value stays the plain command
|
|
198
|
+
* the driver interprets (yield-before-execute, exact identity — see
|
|
199
|
+
* docs/design/render-stage-driver.md).
|
|
200
|
+
*/
|
|
201
|
+
function* flightStep<TEnv>(
|
|
202
|
+
execute: () => RscFlightStage,
|
|
203
|
+
recordSerializeMetric: boolean,
|
|
204
|
+
): Generator<RscRenderCommand<TEnv>, RscFlightStage, RscRenderCommandResult> {
|
|
205
|
+
return expectCommandResult(
|
|
206
|
+
yield { type: "flight", execute, recordSerializeMetric },
|
|
207
|
+
"flight",
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function* htmlStep<TEnv>(
|
|
212
|
+
prepare: () => RscPreparedHtmlRender | Promise<RscPreparedHtmlRender>,
|
|
213
|
+
): Generator<RscRenderCommand<TEnv>, RscHtmlResult, RscRenderCommandResult> {
|
|
214
|
+
return expectCommandResult(yield { type: "html", prepare }, "html");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function* responseStep<TEnv>(
|
|
218
|
+
execute: () => Response,
|
|
219
|
+
): Generator<RscRenderCommand<TEnv>, Response, RscRenderCommandResult> {
|
|
220
|
+
return expectCommandResult(yield { type: "response", execute }, "response");
|
|
221
|
+
}
|
|
222
|
+
|
|
194
223
|
/**
|
|
195
224
|
* Describe the foreground response-construction work without executing it.
|
|
196
225
|
* Each command is yielded before its work starts; the driver owns execution,
|
|
@@ -200,36 +229,24 @@ export function* createRscRenderPlan<TEnv>(
|
|
|
200
229
|
input: RscRenderInput<TEnv>,
|
|
201
230
|
options: RscRenderOptions = {},
|
|
202
231
|
): RscRenderPlan<TEnv> {
|
|
203
|
-
const flight =
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
recordSerializeMetric: input.recordSerializeMetric ?? true,
|
|
207
|
-
execute: () => createFlightStage(input, input.init),
|
|
208
|
-
},
|
|
209
|
-
"flight",
|
|
232
|
+
const flight = yield* flightStep<TEnv>(
|
|
233
|
+
() => createFlightStage(input, input.init),
|
|
234
|
+
input.recordSerializeMetric ?? true,
|
|
210
235
|
);
|
|
211
236
|
|
|
212
|
-
let body: BodyInit | null = flight.stream;
|
|
213
|
-
let init = input.init;
|
|
214
|
-
|
|
215
237
|
if (options.html) {
|
|
216
|
-
const html =
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
238
|
+
const html = options.html;
|
|
239
|
+
const page = yield* htmlStep<TEnv>(() => html(flight));
|
|
240
|
+
return yield* responseStep<TEnv>(() =>
|
|
241
|
+
createResponseWithMergedHeaders(page.body, {
|
|
242
|
+
...input.init,
|
|
243
|
+
...page.init,
|
|
244
|
+
}),
|
|
222
245
|
);
|
|
223
|
-
body = html.body;
|
|
224
|
-
init = { ...init, ...html.init };
|
|
225
246
|
}
|
|
226
247
|
|
|
227
|
-
return
|
|
228
|
-
|
|
229
|
-
type: "response",
|
|
230
|
-
execute: () => createResponseWithMergedHeaders(body, init),
|
|
231
|
-
},
|
|
232
|
-
"response",
|
|
248
|
+
return yield* responseStep<TEnv>(() =>
|
|
249
|
+
createResponseWithMergedHeaders(flight.stream, input.init),
|
|
233
250
|
);
|
|
234
251
|
}
|
|
235
252
|
|
|
@@ -546,6 +563,33 @@ export function renderRscFlightStage<TEnv>(
|
|
|
546
563
|
}
|
|
547
564
|
}
|
|
548
565
|
|
|
566
|
+
/**
|
|
567
|
+
* Feed this driver's stage events (flight/html/response) into a routine flow
|
|
568
|
+
* trace as child entries of the currently running plan step. Stages are
|
|
569
|
+
* strictly sequential, so one `current` slot suffices. Depth is captured at
|
|
570
|
+
* creation: the bridge is built while the caller's render step executes, so
|
|
571
|
+
* children sit one level under it.
|
|
572
|
+
*/
|
|
573
|
+
export function createRenderStageTraceBridge(
|
|
574
|
+
trace: RoutineTrace,
|
|
575
|
+
): (event: RscRenderStageEvent) => void {
|
|
576
|
+
const depth = trace.currentDepth() + 1;
|
|
577
|
+
let current: RoutineTraceEntry | undefined;
|
|
578
|
+
return (event) => {
|
|
579
|
+
if (event.type === "stage:start") {
|
|
580
|
+
current = trace.begin(event.context.phase, "step", depth);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (!current) return;
|
|
584
|
+
if (event.type === "stage:complete") {
|
|
585
|
+
trace.end(current);
|
|
586
|
+
} else {
|
|
587
|
+
trace.fail(current, event.error);
|
|
588
|
+
}
|
|
589
|
+
current = undefined;
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
549
593
|
export function createRscStageDebugSink(
|
|
550
594
|
log: (message: string, details?: Record<string, unknown>) => void = (
|
|
551
595
|
message,
|