@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.
Files changed (65) hide show
  1. package/README.md +5 -1
  2. package/dist/types/browser/event-controller.d.ts +6 -0
  3. package/dist/types/browser/prefetch/cache.d.ts +31 -2
  4. package/dist/types/cache/cf/cf-cache-constants.d.ts +8 -1
  5. package/dist/types/cache/cf/cf-cache-store.d.ts +15 -1
  6. package/dist/types/cache/cf/cf-cache-types.d.ts +1 -1
  7. package/dist/types/cache/cf/cf-kv-utils.d.ts +21 -0
  8. package/dist/types/handles/is-thenable.d.ts +2 -4
  9. package/dist/types/route-definition/helpers-types.d.ts +5 -4
  10. package/dist/types/rsc/helpers.d.ts +3 -0
  11. package/dist/types/rsc/render-pipeline.d.ts +9 -0
  12. package/dist/types/rsc/routine-plan.d.ts +124 -0
  13. package/dist/types/rsc/shell-capture-constants.d.ts +17 -0
  14. package/dist/types/server/request-context.d.ts +4 -1
  15. package/dist/types/static-handler.d.ts +15 -1
  16. package/dist/types/urls/path-helper-types.d.ts +6 -7
  17. package/dist/types/vite/encryption-key.d.ts +2 -0
  18. package/dist/types/vite/plugins/expose-internal-ids.d.ts +10 -0
  19. package/dist/types/vite/plugins/server-ref-hashing.d.ts +24 -0
  20. package/dist/types/vite/plugins/server-reference-pattern.d.ts +1 -0
  21. package/dist/types/vite/utils/shared-utils.d.ts +12 -0
  22. package/dist/vite/index.js +154 -57
  23. package/package.json +3 -3
  24. package/skills/caching/SKILL.md +1 -1
  25. package/skills/mime-routes/SKILL.md +3 -1
  26. package/skills/parallel/SKILL.md +1 -1
  27. package/skills/response-routes/SKILL.md +4 -2
  28. package/skills/typesafety/generated-files-and-cli.md +16 -9
  29. package/skills/typesafety/route-types.md +5 -1
  30. package/skills/use-cache/SKILL.md +47 -0
  31. package/src/browser/event-controller.ts +40 -15
  32. package/src/browser/partial-update.ts +26 -11
  33. package/src/browser/prefetch/cache.ts +53 -9
  34. package/src/browser/prefetch/fetch.ts +83 -2
  35. package/src/browser/react/NavigationProvider.tsx +7 -0
  36. package/src/cache/cache-runtime.ts +89 -15
  37. package/src/cache/cf/cf-cache-constants.ts +8 -1
  38. package/src/cache/cf/cf-cache-store.ts +41 -64
  39. package/src/cache/cf/cf-cache-types.ts +1 -1
  40. package/src/cache/cf/cf-kv-utils.ts +38 -0
  41. package/src/cache/segment-codec.ts +21 -5
  42. package/src/handles/is-thenable.ts +2 -4
  43. package/src/route-definition/helpers-types.ts +5 -3
  44. package/src/router/match-middleware/cache-lookup.ts +24 -15
  45. package/src/rsc/handler.ts +26 -5
  46. package/src/rsc/helpers.ts +13 -0
  47. package/src/rsc/progressive-enhancement.ts +247 -70
  48. package/src/rsc/render-pipeline.ts +68 -24
  49. package/src/rsc/routine-plan.ts +359 -0
  50. package/src/rsc/rsc-rendering.ts +555 -302
  51. package/src/rsc/server-action.ts +180 -71
  52. package/src/rsc/shell-capture-constants.ts +18 -0
  53. package/src/rsc/shell-capture.ts +92 -21
  54. package/src/server/request-context.ts +6 -0
  55. package/src/ssr/ssr-root.tsx +1 -0
  56. package/src/static-handler.ts +18 -2
  57. package/src/urls/path-helper-types.ts +9 -10
  58. package/src/vite/encryption-key.ts +29 -0
  59. package/src/vite/plugins/expose-action-id.ts +2 -2
  60. package/src/vite/plugins/expose-internal-ids.ts +46 -0
  61. package/src/vite/plugins/server-ref-hashing.ts +74 -0
  62. package/src/vite/plugins/server-reference-pattern.ts +10 -0
  63. package/src/vite/rango.ts +9 -0
  64. package/src/vite/router-discovery.ts +21 -2
  65. 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 { renderRscResponse } from "./render-pipeline.js";
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
- const match = await ctx.router.match(renderRequest, { env });
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
- payload,
338
- init: {
339
- // boundarylessErrorStatus is set only when the action threw and no error
340
- // boundary matched; it makes the re-render carry 500 like the JS path.
341
- // The redirect branch above returns before this, so a redirect re-render
342
- // keeps its 308 and is never overridden.
343
- ...(boundarylessErrorStatus !== undefined
344
- ? { status: boundarylessErrorStatus }
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 = expectCommandResult(
204
- yield {
205
- type: "flight",
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 = expectCommandResult(
217
- yield {
218
- type: "html",
219
- prepare: () => options.html!(flight),
220
- },
221
- "html",
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 expectCommandResult(
228
- yield {
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,