@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1b930379

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 (84) hide show
  1. package/README.md +46 -12
  2. package/dist/bin/rango.js +109 -15
  3. package/dist/vite/index.js +323 -121
  4. package/package.json +15 -16
  5. package/skills/breadcrumbs/SKILL.md +250 -0
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +33 -31
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/loader/SKILL.md +55 -15
  11. package/skills/prerender/SKILL.md +2 -2
  12. package/skills/rango/SKILL.md +0 -1
  13. package/skills/route/SKILL.md +3 -4
  14. package/skills/router-setup/SKILL.md +8 -3
  15. package/skills/typesafety/SKILL.md +25 -23
  16. package/src/__internal.ts +92 -0
  17. package/src/bin/rango.ts +18 -0
  18. package/src/browser/link-interceptor.ts +4 -0
  19. package/src/browser/navigation-bridge.ts +95 -5
  20. package/src/browser/navigation-client.ts +97 -72
  21. package/src/browser/prefetch/cache.ts +112 -25
  22. package/src/browser/prefetch/fetch.ts +28 -30
  23. package/src/browser/prefetch/policy.ts +6 -0
  24. package/src/browser/react/Link.tsx +19 -7
  25. package/src/browser/rsc-router.tsx +11 -2
  26. package/src/browser/server-action-bridge.ts +448 -432
  27. package/src/browser/types.ts +24 -0
  28. package/src/build/generate-route-types.ts +2 -0
  29. package/src/build/route-trie.ts +19 -3
  30. package/src/build/route-types/router-processing.ts +125 -15
  31. package/src/client.rsc.tsx +2 -1
  32. package/src/client.tsx +1 -46
  33. package/src/handles/breadcrumbs.ts +66 -0
  34. package/src/handles/index.ts +1 -0
  35. package/src/host/index.ts +0 -3
  36. package/src/index.rsc.ts +5 -36
  37. package/src/index.ts +32 -66
  38. package/src/prerender/store.ts +56 -15
  39. package/src/route-definition/index.ts +0 -3
  40. package/src/router/handler-context.ts +30 -3
  41. package/src/router/loader-resolution.ts +1 -1
  42. package/src/router/match-api.ts +1 -1
  43. package/src/router/match-result.ts +0 -9
  44. package/src/router/metrics.ts +233 -13
  45. package/src/router/middleware-types.ts +53 -10
  46. package/src/router/middleware.ts +170 -81
  47. package/src/router/pattern-matching.ts +20 -5
  48. package/src/router/prerender-match.ts +4 -0
  49. package/src/router/revalidation.ts +27 -7
  50. package/src/router/router-interfaces.ts +14 -1
  51. package/src/router/router-options.ts +13 -8
  52. package/src/router/segment-resolution/fresh.ts +18 -0
  53. package/src/router/segment-resolution/helpers.ts +1 -1
  54. package/src/router/segment-resolution/revalidation.ts +22 -9
  55. package/src/router/trie-matching.ts +20 -2
  56. package/src/router.ts +29 -9
  57. package/src/rsc/handler.ts +106 -11
  58. package/src/rsc/index.ts +0 -20
  59. package/src/rsc/progressive-enhancement.ts +21 -8
  60. package/src/rsc/rsc-rendering.ts +30 -43
  61. package/src/rsc/server-action.ts +14 -10
  62. package/src/rsc/ssr-setup.ts +128 -0
  63. package/src/rsc/types.ts +2 -0
  64. package/src/search-params.ts +16 -13
  65. package/src/server/context.ts +8 -2
  66. package/src/server/request-context.ts +38 -16
  67. package/src/server.ts +6 -0
  68. package/src/theme/index.ts +4 -13
  69. package/src/types/handler-context.ts +12 -16
  70. package/src/types/route-config.ts +17 -8
  71. package/src/types/segments.ts +0 -5
  72. package/src/vite/discovery/bundle-postprocess.ts +31 -56
  73. package/src/vite/discovery/discover-routers.ts +18 -4
  74. package/src/vite/discovery/prerender-collection.ts +34 -14
  75. package/src/vite/discovery/state.ts +4 -7
  76. package/src/vite/index.ts +4 -3
  77. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  78. package/src/vite/plugins/refresh-cmd.ts +65 -0
  79. package/src/vite/rango.ts +11 -0
  80. package/src/vite/router-discovery.ts +16 -0
  81. package/src/vite/utils/prerender-utils.ts +60 -0
  82. package/skills/testing/SKILL.md +0 -226
  83. package/src/route-definition/route-function.ts +0 -119
  84. /package/{CLAUDE.md → AGENTS.md} +0 -0
@@ -24,6 +24,7 @@ import {
24
24
  } from "./helpers.js";
25
25
  import { getRouterContext } from "../router-context.js";
26
26
  import { resolveSink, safeEmit } from "../telemetry.js";
27
+ import { track } from "../../server/context.js";
27
28
 
28
29
  // ---------------------------------------------------------------------------
29
30
  // Streamed handler telemetry
@@ -178,7 +179,9 @@ export async function resolveSegment<TEnv>(
178
179
  (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
179
180
  entry.shortCode;
180
181
 
182
+ const doneLayoutHandler = track(`handler:${entry.id}`, 2);
181
183
  const component = await resolveLayoutComponent(entry, context);
184
+ doneLayoutHandler();
182
185
 
183
186
  segments.push({
184
187
  id: entry.shortCode,
@@ -241,9 +244,11 @@ export async function resolveSegment<TEnv>(
241
244
  entry.shortCode,
242
245
  );
243
246
  if (component === undefined) {
247
+ const doneRouteHandler = track(`handler:${entry.id}`, 2);
244
248
  if (entry.loading) {
245
249
  const result = handleHandlerResult(entry.handler(context));
246
250
  if (result instanceof Promise) {
251
+ result.finally(doneRouteHandler).catch(() => {});
247
252
  const tracked = deps.trackHandler(result, {
248
253
  segmentId: entry.shortCode,
249
254
  segmentType: entry.type,
@@ -258,10 +263,12 @@ export async function resolveSegment<TEnv>(
258
263
  );
259
264
  component = tracked;
260
265
  } else {
266
+ doneRouteHandler();
261
267
  component = result;
262
268
  }
263
269
  } else {
264
270
  component = handleHandlerResult(await entry.handler(context));
271
+ doneRouteHandler();
265
272
  }
266
273
  }
267
274
 
@@ -343,7 +350,9 @@ export async function resolveOrphanLayout<TEnv>(
343
350
 
344
351
  // Handler-first: orphan layout handler executes before its parallels
345
352
  // so that ctx.set() values are visible to parallel children.
353
+ const doneOrphanHandler = track(`handler:${orphan.id}`, 2);
346
354
  const component = await resolveLayoutComponent(orphan, context);
355
+ doneOrphanHandler();
347
356
 
348
357
  segments.push({
349
358
  id: orphan.shortCode,
@@ -410,12 +419,17 @@ export async function resolveParallelEntry<TEnv>(
410
419
  );
411
420
 
412
421
  if (component === undefined) {
422
+ const doneParallelHandler = track(
423
+ `handler:${parallelEntry.id}.${slot}`,
424
+ 2,
425
+ );
413
426
  const hasLoadingFallback =
414
427
  parallelEntry.loading !== undefined && parallelEntry.loading !== false;
415
428
  if (hasLoadingFallback) {
416
429
  const result =
417
430
  typeof handler === "function" ? handler(context) : handler;
418
431
  if (result instanceof Promise) {
432
+ result.finally(doneParallelHandler).catch(() => {});
419
433
  const tracked = deps.trackHandler(result, {
420
434
  segmentId: `${parentShortCode}.${slot}`,
421
435
  segmentType: "parallel",
@@ -430,11 +444,13 @@ export async function resolveParallelEntry<TEnv>(
430
444
  );
431
445
  component = tracked as ReactNode;
432
446
  } else {
447
+ doneParallelHandler();
433
448
  component = result as ReactNode;
434
449
  }
435
450
  } else {
436
451
  component =
437
452
  typeof handler === "function" ? await handler(context) : handler;
453
+ doneParallelHandler();
438
454
  }
439
455
  }
440
456
 
@@ -499,6 +515,7 @@ export async function resolveAllSegments<TEnv>(
499
515
  } catch {}
500
516
 
501
517
  for (const entry of entries) {
518
+ const doneEntry = track(`segment:${entry.id}`, 1);
502
519
  const resolvedSegments = await resolveWithErrorBoundary(
503
520
  entry,
504
521
  params,
@@ -518,6 +535,7 @@ export async function resolveAllSegments<TEnv>(
518
535
  { request: safeRequest, url: context.url, routeKey, telemetry },
519
536
  context.pathname,
520
537
  );
538
+ doneEntry();
521
539
  // Deduplicate by segment ID. include() scopes can produce entries that
522
540
  // resolve the same shared layout/loader segment. Duplicates in the segment
523
541
  // array propagate to the client's matched[] and change the React tree depth.
@@ -174,7 +174,7 @@ export function catchSegmentError<TEnv>(
174
174
  const setResponseStatus = (status: number) => {
175
175
  const reqCtx = getRequestContext();
176
176
  if (reqCtx) {
177
- reqCtx.setStatus(status);
177
+ reqCtx._setStatus(status);
178
178
  }
179
179
  };
180
180
 
@@ -37,6 +37,7 @@ import {
37
37
  } from "./helpers.js";
38
38
  import { getRouterContext } from "../router-context.js";
39
39
  import { resolveSink, safeEmit } from "../telemetry.js";
40
+ import { track } from "../../server/context.js";
40
41
 
41
42
  // ---------------------------------------------------------------------------
42
43
  // Telemetry helpers
@@ -621,20 +622,29 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
621
622
  return shouldRevalidate;
622
623
  },
623
624
  async () => {
625
+ const doneHandler = track(`handler:${entry.id}`, 2);
624
626
  (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
625
627
  entry.shortCode;
626
628
  if (entry.type === "layout" || entry.type === "cache") {
627
- return resolveLayoutComponent(entry, context);
629
+ const layoutComponent = await resolveLayoutComponent(entry, context);
630
+ doneHandler();
631
+ return layoutComponent;
628
632
  }
629
633
  const staticComponent = await tryStaticHandler(entry, entry.shortCode);
630
- if (staticComponent !== undefined) return staticComponent;
634
+ if (staticComponent !== undefined) {
635
+ doneHandler();
636
+ return staticComponent;
637
+ }
631
638
  const routeEntry = entry as Extract<EntryData, { type: "route" }>;
632
639
  if (!routeEntry.loading) {
633
- return handleHandlerResult(await routeEntry.handler(context));
640
+ const result = handleHandlerResult(await routeEntry.handler(context));
641
+ doneHandler();
642
+ return result;
634
643
  }
635
644
  if (!actionContext) {
636
645
  const result = handleHandlerResult(routeEntry.handler(context));
637
646
  if (result instanceof Promise) {
647
+ result.finally(doneHandler).catch(() => {});
638
648
  const tracked = deps.trackHandler(result, {
639
649
  segmentId: entry.shortCode,
640
650
  segmentType: entry.type,
@@ -649,15 +659,18 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
649
659
  );
650
660
  return { content: tracked };
651
661
  }
662
+ doneHandler();
652
663
  return { content: result };
653
664
  }
654
665
  debugLog("segment.action", "resolving action route with awaited value", {
655
666
  entryId: entry.id,
656
667
  });
668
+ const actionResult = handleHandlerResult(
669
+ await routeEntry.handler(context),
670
+ );
671
+ doneHandler();
657
672
  return {
658
- content: Promise.resolve(
659
- handleHandlerResult(await routeEntry.handler(context)),
660
- ),
673
+ content: Promise.resolve(actionResult),
661
674
  };
662
675
  },
663
676
  () => null,
@@ -1178,6 +1191,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1178
1191
  }
1179
1192
 
1180
1193
  const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
1194
+ const doneEntry = track(`segment:${entry.id}`, 1);
1181
1195
  const resolved = await resolveWithErrorBoundary(
1182
1196
  nonParallelEntry,
1183
1197
  params,
@@ -1199,11 +1213,10 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1199
1213
  ),
1200
1214
  (seg) => ({ segments: [seg], matchedIds: [seg.id] }),
1201
1215
  deps,
1202
- telemetry
1203
- ? { request, url: context.url, routeKey, isPartial: true, telemetry }
1204
- : undefined,
1216
+ { request, url: context.url, routeKey, isPartial: true, telemetry },
1205
1217
  pathname,
1206
1218
  );
1219
+ doneEntry();
1207
1220
 
1208
1221
  // Deduplicate segments and matchedIds by ID, matching resolveAllSegments.
1209
1222
  // include() scopes can produce entries that resolve the same shared
@@ -114,7 +114,25 @@ function walkTrie(
114
114
  if (result) return result;
115
115
  }
116
116
 
117
- // Priority 2: Param match
117
+ // Priority 2: Suffix-param match (e.g., :productId.html)
118
+ if (node.xp) {
119
+ for (const suffix in node.xp) {
120
+ if (segment.endsWith(suffix) && segment.length > suffix.length) {
121
+ const paramValue = segment.slice(0, -suffix.length);
122
+ paramValues.push(paramValue);
123
+ const result = walkTrie(
124
+ node.xp[suffix].c,
125
+ segments,
126
+ index + 1,
127
+ paramValues,
128
+ );
129
+ paramValues.pop();
130
+ if (result) return result;
131
+ }
132
+ }
133
+ }
134
+
135
+ // Priority 3: Param match
118
136
  if (node.p) {
119
137
  paramValues.push(segment);
120
138
  const result = walkTrie(node.p.c, segments, index + 1, paramValues);
@@ -122,7 +140,7 @@ function walkTrie(
122
140
  if (result) return result;
123
141
  }
124
142
 
125
- // Priority 3: Wildcard match (consumes rest)
143
+ // Priority 4: Wildcard match (consumes rest)
126
144
  if (node.w) {
127
145
  const rest = joinRemainingSegments(segments, index);
128
146
  return {
package/src/router.ts CHANGED
@@ -147,7 +147,7 @@ export function createRouter<TEnv = any>(
147
147
  $$sourceFile: injectedSourceFile,
148
148
  nonce,
149
149
  version,
150
- prefetchCacheControl: prefetchCacheControlOption,
150
+ prefetchCacheTTL: prefetchCacheTTLOption,
151
151
  warmup: warmupOption,
152
152
  allowDebugManifest: allowDebugManifestOption = false,
153
153
  telemetry: telemetrySink,
@@ -200,11 +200,17 @@ export function createRouter<TEnv = any>(
200
200
  const routerId =
201
201
  userProvidedId ?? injectedId ?? `router_${nextRouterAutoId()}`;
202
202
 
203
- // Resolve prefetch cache control (default: 'private, max-age=300')
204
- const prefetchCacheControl =
205
- prefetchCacheControlOption !== undefined
206
- ? prefetchCacheControlOption
207
- : "private, max-age=300";
203
+ // Resolve prefetch cache TTL (default: 300 seconds / 5 minutes)
204
+ // Clamp to a non-negative integer for valid Cache-Control max-age.
205
+ const rawTTL =
206
+ prefetchCacheTTLOption !== undefined ? prefetchCacheTTLOption : 300;
207
+ const prefetchCacheTTLSeconds =
208
+ rawTTL === false ? 0 : Math.max(0, Math.floor(rawTTL));
209
+ const prefetchCacheTTL = prefetchCacheTTLSeconds * 1000;
210
+ const prefetchCacheControl: string | false =
211
+ prefetchCacheTTLSeconds === 0
212
+ ? false
213
+ : `private, max-age=${prefetchCacheTTLSeconds}`;
208
214
 
209
215
  // Resolve warmup enabled flag (default: true)
210
216
  const warmupEnabled = warmupOption !== false;
@@ -357,8 +363,18 @@ export function createRouter<TEnv = any>(
357
363
  return precomputedByPrefix;
358
364
  }
359
365
 
360
- // Wrapper to pass debugPerformance to external createMetricsStore
361
- const getMetricsStore = () => createMetricsStore(debugPerformance);
366
+ // Wrapper to pass debugPerformance to external createMetricsStore.
367
+ // Also checks per-request flag set by ctx.debugPerformance() in middleware.
368
+ const getMetricsStore = () => {
369
+ const reqCtx = _getRequestContext();
370
+ const enabled = debugPerformance || !!reqCtx?._debugPerformance;
371
+ if (!enabled) return undefined;
372
+ if (!reqCtx) {
373
+ return createMetricsStore(true);
374
+ }
375
+ reqCtx._metricsStore ??= createMetricsStore(true);
376
+ return reqCtx._metricsStore;
377
+ };
362
378
 
363
379
  // Wrapper to pass defaults to error/notFound boundary finders
364
380
  const findNearestErrorBoundary = (entry: EntryData | null) =>
@@ -869,12 +885,16 @@ export function createRouter<TEnv = any>(
869
885
  // Expose resolved cache profiles for per-request resolution
870
886
  cacheProfiles: resolvedCacheProfiles,
871
887
 
872
- // Expose prefetch cache control for RSC handler
888
+ // Expose prefetch cache settings
873
889
  prefetchCacheControl,
890
+ prefetchCacheTTL,
874
891
 
875
892
  // Expose warmup enabled flag for handler and client
876
893
  warmupEnabled,
877
894
 
895
+ // Expose router-wide performance debugging for request-level metrics setup
896
+ debugPerformance,
897
+
878
898
  // Expose debug manifest flag for handler
879
899
  allowDebugManifest: allowDebugManifestOption,
880
900
 
@@ -18,7 +18,12 @@ import {
18
18
  } from "../server/request-context.js";
19
19
  import * as rscDeps from "@vitejs/plugin-rsc/rsc";
20
20
 
21
- import type { RscPayload, CreateRSCHandlerOptions } from "./types.js";
21
+ import type {
22
+ RscPayload,
23
+ CreateRSCHandlerOptions,
24
+ LoadSSRModule,
25
+ SSRModule,
26
+ } from "./types.js";
22
27
  import {
23
28
  createResponseWithMergedHeaders,
24
29
  finalizeResponse,
@@ -66,6 +71,17 @@ import {
66
71
  createDefaultTimeoutResponse,
67
72
  type TimeoutPhase,
68
73
  } from "../router/timeout.js";
74
+ import {
75
+ createMetricsStore,
76
+ appendMetric,
77
+ buildMetricsTiming,
78
+ } from "../router/metrics.js";
79
+ import {
80
+ startSSRSetup,
81
+ getSSRSetup,
82
+ mayNeedSSR,
83
+ SSR_SETUP_VAR,
84
+ } from "./ssr-setup.js";
69
85
 
70
86
  /**
71
87
  * Create an RSC request handler.
@@ -117,10 +133,22 @@ export function createRSCHandler<
117
133
  decodeFormState,
118
134
  } = deps;
119
135
 
120
- // Use provided loadSSRModule or default to vite RSC module loader
121
- const loadSSRModule =
136
+ // Use provided loadSSRModule or default to vite RSC module loader.
137
+ // In production the SSR module is stable across requests, so memoize
138
+ // the dynamic import to avoid repeated module resolution overhead.
139
+ // In dev mode Vite may hot-reload the module, so skip memoization.
140
+ const rawLoadSSRModule: LoadSSRModule =
122
141
  options.loadSSRModule ??
123
142
  (() => import.meta.viteRsc.loadModule("ssr", "index"));
143
+ let _ssrModulePromise: Promise<SSRModule> | undefined;
144
+ const loadSSRModule: LoadSSRModule =
145
+ process.env.NODE_ENV === "production"
146
+ ? () =>
147
+ (_ssrModulePromise ??= rawLoadSSRModule().catch((err) => {
148
+ _ssrModulePromise = undefined;
149
+ throw err;
150
+ }))
151
+ : rawLoadSSRModule;
124
152
 
125
153
  /**
126
154
  * Per-request error reporter that deduplicates via the ALS request context.
@@ -268,6 +296,11 @@ export function createRSCHandler<
268
296
  input: RouterRequestInput<TEnv> = {},
269
297
  ): Promise<Response> {
270
298
  const handlerStart = performance.now();
299
+ // Create the metrics store at handler start so handler:total has startTime=0
300
+ // and all metrics are relative to the request entry point.
301
+ const earlyMetricsStore = router.debugPerformance
302
+ ? createMetricsStore(true, handlerStart)
303
+ : undefined;
271
304
 
272
305
  const { env = {} as TEnv, vars: initialVars, ctx: executionCtx } = input;
273
306
 
@@ -381,6 +414,10 @@ export function createRSCHandler<
381
414
  executionContext: executionCtx,
382
415
  themeConfig: router.themeConfig,
383
416
  });
417
+ if (earlyMetricsStore) {
418
+ requestContext._debugPerformance = true;
419
+ requestContext._metricsStore = earlyMetricsStore;
420
+ }
384
421
  // Wire background error reporting so "use cache" and other subsystems
385
422
  // can surface non-fatal errors through the router's onError callback.
386
423
  requestContext._reportBackgroundError = (
@@ -422,6 +459,7 @@ export function createRSCHandler<
422
459
  };
423
460
 
424
461
  // Execute middleware chain if any, otherwise call core handler directly
462
+ let response: Response;
425
463
  if (matchedMiddleware.length > 0) {
426
464
  const mwResponse = await executeMiddleware(
427
465
  matchedMiddleware,
@@ -440,13 +478,52 @@ export function createRSCHandler<
440
478
  mwResponse,
441
479
  createRedirectFlightResponse,
442
480
  );
443
- if (intercepted) return intercepted;
481
+ response = intercepted ?? finalizeResponse(mwResponse);
482
+ } else {
483
+ response = finalizeResponse(mwResponse);
444
484
  }
485
+ } else {
486
+ response = await coreHandler();
487
+ }
445
488
 
446
- return finalizeResponse(mwResponse);
489
+ // Finalize metrics after all middleware (including post-next work)
490
+ // has completed so :post spans are captured in the timeline.
491
+ // Handler timing parts are always emitted (even without debug metrics)
492
+ // so non-debug requests still get bootstrap Server-Timing entries.
493
+ const handlerTimingArr: string[] = variables.__handlerTiming || [];
494
+ // Preserve any existing Server-Timing set by response routes or middleware
495
+ const existingTiming = response.headers.get("Server-Timing");
496
+ const timingParts = existingTiming
497
+ ? [existingTiming, ...handlerTimingArr]
498
+ : [...handlerTimingArr];
499
+
500
+ const metricsStore = requestContext._metricsStore;
501
+ if (metricsStore) {
502
+ // When the store was created at handler start (earlyMetricsStore),
503
+ // handler:total covers the full request. When ctx.debugPerformance()
504
+ // created the store mid-request, use its requestStart to avoid a
505
+ // negative startTime offset.
506
+ const totalStart = earlyMetricsStore
507
+ ? handlerStart
508
+ : metricsStore.requestStart;
509
+ appendMetric(
510
+ metricsStore,
511
+ "handler:total",
512
+ totalStart,
513
+ performance.now() - totalStart,
514
+ );
515
+ const metricsTiming = buildMetricsTiming(
516
+ request.method,
517
+ url.pathname,
518
+ metricsStore,
519
+ );
520
+ if (metricsTiming) timingParts.push(metricsTiming);
447
521
  }
448
522
 
449
- return coreHandler();
523
+ const fullTiming = timingParts.join(", ");
524
+ if (fullTiming) response.headers.set("Server-Timing", fullTiming);
525
+
526
+ return response;
450
527
  });
451
528
  };
452
529
 
@@ -490,6 +567,21 @@ export function createRSCHandler<
490
567
  return responseOutcome.result;
491
568
  }
492
569
 
570
+ // Kick off SSR module loading + stream mode resolution in parallel with
571
+ // segment resolution. Placed after the response-route short-circuit so
572
+ // response/mime routes never pay for SSR work.
573
+ if (mayNeedSSR(request, url)) {
574
+ variables[SSR_SETUP_VAR] = startSSRSetup(
575
+ handlerCtx,
576
+ request,
577
+ env,
578
+ url,
579
+ router.debugPerformance
580
+ ? () => requireRequestContext()._metricsStore
581
+ : undefined,
582
+ );
583
+ }
584
+
493
585
  const routeReverse = createReverseFunction(getRequiredRouteMap());
494
586
 
495
587
  const isAction =
@@ -964,11 +1056,14 @@ export function createRSCHandler<
964
1056
  });
965
1057
  }
966
1058
 
967
- // Delegate to SSR for HTML response
968
- const [ssrModule, streamMode] = await Promise.all([
969
- loadSSRModule(),
970
- handlerCtx.resolveStreamMode(request, env, url),
971
- ]);
1059
+ // Delegate to SSR for HTML response (reuse early setup if available)
1060
+ const [ssrModule, streamMode] = await getSSRSetup(
1061
+ handlerCtx,
1062
+ request,
1063
+ env,
1064
+ url,
1065
+ requireRequestContext()._metricsStore,
1066
+ );
972
1067
  const htmlStream = await ssrModule.renderHTML(rscStream, {
973
1068
  nonce,
974
1069
  streamMode,
package/src/rsc/index.ts CHANGED
@@ -29,28 +29,8 @@ export type {
29
29
  NonceProvider,
30
30
  } from "./types.js";
31
31
 
32
- // Re-export HandleStore types for consumers who need custom handling
33
- export {
34
- createHandleStore,
35
- type HandleStore,
36
- type HandleData,
37
- } from "../server/handle-store.js";
38
-
39
32
  // Re-export request context utilities for server-side access to env/request/params
40
33
  export {
41
34
  getRequestContext,
42
35
  requireRequestContext,
43
- setRequestContextParams,
44
36
  } from "../server/request-context.js";
45
-
46
- // Re-export cache store types and implementations
47
- export type {
48
- SegmentCacheStore,
49
- CachedEntryData,
50
- CachedEntryResult,
51
- SegmentCacheProvider,
52
- SegmentHandleData,
53
- } from "../cache/types.js";
54
-
55
- export { MemorySegmentCacheStore } from "../cache/memory-segment-store.js";
56
- export { CFCacheStore, type CFCacheStoreOptions } from "../cache/cf/index.js";
@@ -10,6 +10,7 @@ import {
10
10
  requireRequestContext,
11
11
  setRequestContextParams,
12
12
  } from "../server/request-context.js";
13
+ import { getSSRSetup } from "./ssr-setup.js";
13
14
  import type { MiddlewareFn } from "../router/middleware.js";
14
15
  import { executeMiddleware } from "../router/middleware.js";
15
16
  import type { RscPayload, ReactFormState } from "./types.js";
@@ -257,10 +258,16 @@ export async function handleProgressiveEnhancement<TEnv>(
257
258
  };
258
259
 
259
260
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
260
- const [ssrModule, streamMode] = await Promise.all([
261
- ctx.loadSSRModule(),
262
- ctx.resolveStreamMode(request, env, url),
263
- ]);
261
+ // metricsStore=undefined is safe: the handler already stashed the early
262
+ // SSR setup promise on request variables, so getSSRSetup returns it
263
+ // without falling back to a fresh startSSRSetup.
264
+ const [ssrModule, streamMode] = await getSSRSetup(
265
+ ctx,
266
+ request,
267
+ env,
268
+ url,
269
+ undefined,
270
+ );
264
271
  const htmlStream = await ssrModule.renderHTML(rscStream, {
265
272
  formState: reactFormState,
266
273
  nonce,
@@ -350,10 +357,16 @@ async function renderPeErrorBoundary<TEnv>(
350
357
  };
351
358
 
352
359
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
353
- const [ssrModule, streamMode] = await Promise.all([
354
- ctx.loadSSRModule(),
355
- ctx.resolveStreamMode(request, env, url),
356
- ]);
360
+ // metricsStore=undefined is safe: the handler already stashed the early
361
+ // SSR setup promise on request variables, so getSSRSetup returns it
362
+ // without falling back to a fresh startSSRSetup.
363
+ const [ssrModule, streamMode] = await getSSRSetup(
364
+ ctx,
365
+ request,
366
+ env,
367
+ url,
368
+ undefined,
369
+ );
357
370
  const htmlStream = await ssrModule.renderHTML(rscStream, {
358
371
  nonce,
359
372
  streamMode,