@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19

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 (177) hide show
  1. package/README.md +46 -8
  2. package/dist/bin/rango.js +105 -18
  3. package/dist/vite/index.js +227 -93
  4. package/package.json +15 -14
  5. package/skills/hooks/SKILL.md +1 -1
  6. package/skills/intercept/SKILL.md +79 -0
  7. package/skills/layout/SKILL.md +62 -2
  8. package/skills/loader/SKILL.md +94 -1
  9. package/skills/middleware/SKILL.md +81 -0
  10. package/skills/parallel/SKILL.md +57 -2
  11. package/skills/prerender/SKILL.md +187 -17
  12. package/skills/route/SKILL.md +42 -1
  13. package/skills/router-setup/SKILL.md +77 -0
  14. package/src/__internal.ts +1 -1
  15. package/src/bin/rango.ts +38 -19
  16. package/src/browser/action-coordinator.ts +97 -0
  17. package/src/browser/event-controller.ts +25 -27
  18. package/src/browser/history-state.ts +80 -0
  19. package/src/browser/intercept-utils.ts +1 -1
  20. package/src/browser/link-interceptor.ts +0 -3
  21. package/src/browser/merge-segment-loaders.ts +9 -2
  22. package/src/browser/navigation-bridge.ts +46 -13
  23. package/src/browser/navigation-client.ts +32 -61
  24. package/src/browser/navigation-store.ts +1 -31
  25. package/src/browser/navigation-transaction.ts +46 -207
  26. package/src/browser/partial-update.ts +102 -150
  27. package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
  28. package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
  29. package/src/browser/prefetch/policy.ts +42 -0
  30. package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
  31. package/src/browser/react/Link.tsx +28 -23
  32. package/src/browser/react/NavigationProvider.tsx +9 -1
  33. package/src/browser/react/index.ts +2 -6
  34. package/src/browser/react/location-state-shared.ts +1 -1
  35. package/src/browser/react/location-state.ts +2 -0
  36. package/src/browser/react/nonce-context.ts +23 -0
  37. package/src/browser/react/use-action.ts +9 -1
  38. package/src/browser/react/use-handle.ts +3 -25
  39. package/src/browser/react/use-params.ts +2 -4
  40. package/src/browser/react/use-pathname.ts +2 -3
  41. package/src/browser/react/use-router.ts +1 -1
  42. package/src/browser/react/use-search-params.ts +2 -1
  43. package/src/browser/react/use-segments.ts +7 -60
  44. package/src/browser/response-adapter.ts +73 -0
  45. package/src/browser/rsc-router.tsx +29 -23
  46. package/src/browser/scroll-restoration.ts +10 -7
  47. package/src/browser/server-action-bridge.ts +115 -96
  48. package/src/browser/types.ts +1 -31
  49. package/src/browser/validate-redirect-origin.ts +29 -0
  50. package/src/build/generate-manifest.ts +5 -0
  51. package/src/build/generate-route-types.ts +2 -0
  52. package/src/build/route-types/codegen.ts +13 -4
  53. package/src/build/route-types/include-resolution.ts +13 -0
  54. package/src/build/route-types/per-module-writer.ts +15 -3
  55. package/src/build/route-types/router-processing.ts +45 -3
  56. package/src/build/runtime-discovery.ts +13 -1
  57. package/src/cache/background-task.ts +34 -0
  58. package/src/cache/cache-key-utils.ts +44 -0
  59. package/src/cache/cache-policy.ts +125 -0
  60. package/src/cache/cache-runtime.ts +132 -96
  61. package/src/cache/cache-scope.ts +71 -73
  62. package/src/cache/cf/cf-cache-store.ts +9 -4
  63. package/src/cache/document-cache.ts +72 -47
  64. package/src/cache/handle-capture.ts +81 -0
  65. package/src/cache/memory-segment-store.ts +18 -7
  66. package/src/cache/profile-registry.ts +43 -8
  67. package/src/cache/read-through-swr.ts +134 -0
  68. package/src/cache/segment-codec.ts +101 -112
  69. package/src/cache/taint.ts +26 -0
  70. package/src/client.tsx +53 -30
  71. package/src/errors.ts +6 -1
  72. package/src/handle.ts +1 -1
  73. package/src/handles/MetaTags.tsx +5 -2
  74. package/src/host/cookie-handler.ts +8 -3
  75. package/src/host/router.ts +14 -1
  76. package/src/href-client.ts +3 -1
  77. package/src/index.rsc.ts +33 -1
  78. package/src/index.ts +27 -0
  79. package/src/loader.rsc.ts +12 -4
  80. package/src/loader.ts +8 -0
  81. package/src/prerender/store.ts +4 -3
  82. package/src/prerender.ts +76 -18
  83. package/src/reverse.ts +11 -7
  84. package/src/root-error-boundary.tsx +30 -26
  85. package/src/route-definition/dsl-helpers.ts +9 -6
  86. package/src/route-definition/redirect.ts +15 -3
  87. package/src/route-map-builder.ts +38 -2
  88. package/src/route-name.ts +53 -0
  89. package/src/route-types.ts +7 -0
  90. package/src/router/content-negotiation.ts +1 -1
  91. package/src/router/debug-manifest.ts +16 -3
  92. package/src/router/handler-context.ts +94 -15
  93. package/src/router/intercept-resolution.ts +6 -4
  94. package/src/router/lazy-includes.ts +4 -0
  95. package/src/router/loader-resolution.ts +1 -0
  96. package/src/router/logging.ts +100 -3
  97. package/src/router/manifest.ts +32 -3
  98. package/src/router/match-api.ts +61 -7
  99. package/src/router/match-context.ts +3 -0
  100. package/src/router/match-handlers.ts +185 -11
  101. package/src/router/match-middleware/background-revalidation.ts +65 -85
  102. package/src/router/match-middleware/cache-lookup.ts +69 -4
  103. package/src/router/match-middleware/cache-store.ts +2 -0
  104. package/src/router/match-pipelines.ts +8 -43
  105. package/src/router/middleware-types.ts +7 -0
  106. package/src/router/middleware.ts +93 -8
  107. package/src/router/pattern-matching.ts +41 -5
  108. package/src/router/prerender-match.ts +34 -6
  109. package/src/router/preview-match.ts +7 -1
  110. package/src/router/revalidation.ts +61 -2
  111. package/src/router/router-context.ts +15 -0
  112. package/src/router/router-interfaces.ts +34 -0
  113. package/src/router/router-options.ts +200 -0
  114. package/src/router/segment-resolution/fresh.ts +123 -30
  115. package/src/router/segment-resolution/helpers.ts +19 -0
  116. package/src/router/segment-resolution/loader-cache.ts +37 -146
  117. package/src/router/segment-resolution/revalidation.ts +358 -94
  118. package/src/router/segment-wrappers.ts +3 -0
  119. package/src/router/telemetry-otel.ts +299 -0
  120. package/src/router/telemetry.ts +300 -0
  121. package/src/router/timeout.ts +148 -0
  122. package/src/router/types.ts +7 -1
  123. package/src/router.ts +155 -11
  124. package/src/rsc/handler-context.ts +11 -0
  125. package/src/rsc/handler.ts +380 -88
  126. package/src/rsc/helpers.ts +25 -16
  127. package/src/rsc/loader-fetch.ts +84 -42
  128. package/src/rsc/origin-guard.ts +141 -0
  129. package/src/rsc/progressive-enhancement.ts +232 -19
  130. package/src/rsc/response-route-handler.ts +37 -26
  131. package/src/rsc/rsc-rendering.ts +12 -5
  132. package/src/rsc/runtime-warnings.ts +42 -0
  133. package/src/rsc/server-action.ts +134 -58
  134. package/src/rsc/types.ts +8 -0
  135. package/src/search-params.ts +22 -10
  136. package/src/server/context.ts +53 -5
  137. package/src/server/fetchable-loader-store.ts +11 -6
  138. package/src/server/handle-store.ts +66 -9
  139. package/src/server/loader-registry.ts +11 -46
  140. package/src/server/request-context.ts +90 -9
  141. package/src/ssr/index.tsx +63 -27
  142. package/src/static-handler.ts +7 -0
  143. package/src/theme/ThemeProvider.tsx +6 -1
  144. package/src/theme/index.ts +1 -6
  145. package/src/theme/theme-context.ts +1 -28
  146. package/src/theme/theme-script.ts +2 -1
  147. package/src/types/cache-types.ts +5 -0
  148. package/src/types/error-types.ts +3 -0
  149. package/src/types/global-namespace.ts +9 -0
  150. package/src/types/handler-context.ts +35 -13
  151. package/src/types/loader-types.ts +7 -0
  152. package/src/types/route-entry.ts +28 -0
  153. package/src/urls/include-helper.ts +49 -8
  154. package/src/urls/index.ts +1 -0
  155. package/src/urls/path-helper-types.ts +30 -12
  156. package/src/urls/path-helper.ts +17 -2
  157. package/src/urls/pattern-types.ts +21 -1
  158. package/src/urls/response-types.ts +27 -2
  159. package/src/urls/type-extraction.ts +23 -15
  160. package/src/use-loader.tsx +12 -4
  161. package/src/vite/discovery/bundle-postprocess.ts +12 -7
  162. package/src/vite/discovery/discover-routers.ts +30 -18
  163. package/src/vite/discovery/prerender-collection.ts +24 -27
  164. package/src/vite/discovery/route-types-writer.ts +7 -7
  165. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  166. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  167. package/src/vite/plugins/use-cache-transform.ts +91 -3
  168. package/src/vite/rango.ts +3 -3
  169. package/src/vite/router-discovery.ts +99 -36
  170. package/src/vite/utils/prerender-utils.ts +21 -0
  171. package/src/vite/utils/shared-utils.ts +3 -1
  172. package/src/browser/request-controller.ts +0 -164
  173. package/src/href-context.ts +0 -33
  174. package/src/router.gen.ts +0 -6
  175. package/src/static-handler.gen.ts +0 -5
  176. package/src/urls.gen.ts +0 -8
  177. /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Router Timeout
3
+ *
4
+ * Types, resolution logic, and helpers for request-level timeouts.
5
+ * Timeouts wrap action execution and render-start phases with
6
+ * a Promise.race mechanism, returning 504 on expiry.
7
+ */
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Public types
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export interface RouterTimeouts {
14
+ /** Timeout for server action execution (ms). */
15
+ actionMs?: number;
16
+ /** Timeout for initial render/response production (ms). */
17
+ renderStartMs?: number;
18
+ /** Timeout for idle streaming after render starts (ms). Reserved for PR 2. */
19
+ streamIdleMs?: number;
20
+ }
21
+
22
+ export type TimeoutPhase = "action" | "render-start" | "stream-idle";
23
+
24
+ export interface TimeoutContext<TEnv = any> {
25
+ phase: TimeoutPhase;
26
+ request: Request;
27
+ url: URL;
28
+ env: TEnv;
29
+ routeKey?: string;
30
+ actionId?: string;
31
+ durationMs: number;
32
+ }
33
+
34
+ export type OnTimeoutCallback<TEnv = any> = (
35
+ ctx: TimeoutContext<TEnv>,
36
+ ) => Response | Promise<Response>;
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Internal resolved form
40
+ // ---------------------------------------------------------------------------
41
+
42
+ export interface ResolvedTimeouts {
43
+ actionMs: number | undefined;
44
+ renderStartMs: number | undefined;
45
+ streamIdleMs: number | undefined;
46
+ }
47
+
48
+ /**
49
+ * Merge the `timeout` shorthand with the structured `timeouts` object.
50
+ *
51
+ * - `timeout` applies to `actionMs` and `renderStartMs` (NOT `streamIdleMs`).
52
+ * - Explicit `timeouts.*` values override the shorthand.
53
+ * - Returns `undefined` for any phase that has no configured value.
54
+ */
55
+ export function resolveTimeouts(
56
+ timeout?: number,
57
+ timeouts?: RouterTimeouts,
58
+ ): ResolvedTimeouts {
59
+ return {
60
+ actionMs: timeouts?.actionMs ?? timeout ?? undefined,
61
+ renderStartMs: timeouts?.renderStartMs ?? timeout ?? undefined,
62
+ streamIdleMs: timeouts?.streamIdleMs ?? undefined,
63
+ };
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Error class
68
+ // ---------------------------------------------------------------------------
69
+
70
+ export class RouterTimeoutError extends Error {
71
+ override name = "RouterTimeoutError" as const;
72
+ phase: TimeoutPhase;
73
+ durationMs: number;
74
+
75
+ constructor(phase: TimeoutPhase, durationMs: number) {
76
+ super(
77
+ `Request timed out during ${phase} after ${Math.round(durationMs)}ms`,
78
+ );
79
+ this.phase = phase;
80
+ this.durationMs = durationMs;
81
+ }
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Race helper
86
+ // ---------------------------------------------------------------------------
87
+
88
+ type TimeoutResult<T> =
89
+ | { result: T; timedOut: false }
90
+ | { timedOut: true; durationMs: number };
91
+
92
+ /**
93
+ * Race an operation against a deadline.
94
+ *
95
+ * Returns a discriminated union so callers handle the timeout case
96
+ * without try/catch. Non-timeout errors from the operation re-throw.
97
+ *
98
+ * When `timeoutMs` is `undefined` or `<= 0`, the operation runs
99
+ * without any deadline (pass-through).
100
+ */
101
+ export async function withTimeout<T>(
102
+ operation: Promise<T>,
103
+ timeoutMs: number | undefined,
104
+ phase: TimeoutPhase,
105
+ ): Promise<TimeoutResult<T>> {
106
+ if (timeoutMs == null || timeoutMs <= 0) {
107
+ return { result: await operation, timedOut: false };
108
+ }
109
+
110
+ const start = performance.now();
111
+ let timer: ReturnType<typeof setTimeout>;
112
+
113
+ const timeoutPromise = new Promise<never>((_, reject) => {
114
+ timer = setTimeout(() => {
115
+ reject(new RouterTimeoutError(phase, performance.now() - start));
116
+ }, timeoutMs);
117
+ });
118
+
119
+ try {
120
+ const result = await Promise.race([operation, timeoutPromise]);
121
+ clearTimeout(timer!);
122
+ return { result, timedOut: false };
123
+ } catch (error) {
124
+ clearTimeout(timer!);
125
+ if (error instanceof RouterTimeoutError) {
126
+ return { timedOut: true, durationMs: error.durationMs };
127
+ }
128
+ throw error;
129
+ }
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Default response
134
+ // ---------------------------------------------------------------------------
135
+
136
+ /**
137
+ * Create the default 504 response for a timed-out request.
138
+ * Includes `X-Rango-Timeout-Phase` header for observability.
139
+ */
140
+ export function createDefaultTimeoutResponse(phase: TimeoutPhase): Response {
141
+ return new Response("Request timed out", {
142
+ status: 504,
143
+ headers: {
144
+ "Content-Type": "text/plain;charset=utf-8",
145
+ "X-Rango-Timeout-Phase": phase,
146
+ },
147
+ });
148
+ }
@@ -83,7 +83,13 @@ export interface SegmentResolutionDeps<TEnv = any> {
83
83
  requestStartTime?: number;
84
84
  },
85
85
  ) => Promise<LoaderDataResult<T>>;
86
- trackHandler: <T>(promise: Promise<T>) => Promise<T>;
86
+ trackHandler: <T>(
87
+ promise: Promise<T>,
88
+ errorContext?: {
89
+ segmentId?: string;
90
+ segmentType?: string;
91
+ },
92
+ ) => Promise<T>;
87
93
  findNearestErrorBoundary: (
88
94
  entry: EntryData | null,
89
95
  ) => ReactNode | ErrorBoundaryHandler | null;
package/src/router.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { type ReactNode } from "react";
2
2
  import { createCacheScope } from "./cache/cache-scope.js";
3
- import { setCacheProfiles } from "./cache/profile-registry.js";
3
+ import {
4
+ setCacheProfiles,
5
+ resolveCacheProfiles,
6
+ } from "./cache/profile-registry.js";
4
7
  import { isCachedFunction } from "./cache/taint.js";
5
8
  import { assertClientComponent } from "./component-utils.js";
6
9
  import { DefaultDocument } from "./components/DefaultDocument.js";
@@ -68,12 +71,14 @@ import {
68
71
  extractStaticPrefix,
69
72
  traverseBack,
70
73
  } from "./router/pattern-matching.js";
74
+ import { resolveSink, safeEmit, getRequestId } from "./router/telemetry.js";
71
75
  import { evaluateRevalidation } from "./router/revalidation.js";
72
76
  import {
73
77
  type RouterContext,
74
78
  runWithRouterContext,
75
79
  } from "./router/router-context.js";
76
80
  import { resolveThemeConfig } from "./theme/constants.js";
81
+ import { resolveTimeouts } from "./router/timeout.js";
77
82
 
78
83
  // Extracted content negotiation utilities
79
84
  import { flattenNamedRoutes } from "./router/content-negotiation.js";
@@ -111,6 +116,9 @@ export { RSC_ROUTER_BRAND, RouterRegistry } from "./router/router-registry.js";
111
116
  export type {
112
117
  RSCRouterOptions,
113
118
  RootLayoutProps,
119
+ SSRStreamMode,
120
+ SSROptions,
121
+ ResolveStreamingContext,
114
122
  } from "./router/router-options.js";
115
123
  export type {
116
124
  RSCRouter,
@@ -142,12 +150,22 @@ export function createRouter<TEnv = any>(
142
150
  prefetchCacheControl: prefetchCacheControlOption,
143
151
  warmup: warmupOption,
144
152
  allowDebugManifest: allowDebugManifestOption = false,
153
+ telemetry: telemetrySink,
154
+ ssr: ssrOption,
155
+ timeout: timeoutShorthand,
156
+ timeouts: timeoutsOption,
157
+ onTimeout,
158
+ originCheck: originCheckOption,
145
159
  } = options;
146
160
 
147
- // Set cache profiles for "use cache" directive
148
- if (cacheProfilesOption) {
149
- setCacheProfiles(cacheProfilesOption);
150
- }
161
+ // Resolve telemetry sink (no-op when not configured)
162
+ const telemetry = resolveSink(telemetrySink);
163
+
164
+ // Resolve cache profiles: merge user config with guaranteed default profile.
165
+ // This resolved map is both stored on the router (for per-request context)
166
+ // and written to the global registry (for DSL-time cache("profileName")).
167
+ const resolvedCacheProfiles = resolveCacheProfiles(cacheProfilesOption);
168
+ setCacheProfiles(resolvedCacheProfiles);
151
169
 
152
170
  // Source file: prefer Vite-injected path (zero cost), fall back to
153
171
  // stack trace parsing for non-Vite environments (e.g. tests).
@@ -196,15 +214,29 @@ export function createRouter<TEnv = any>(
196
214
  ? resolveThemeConfig(themeOption)
197
215
  : null;
198
216
 
217
+ // Resolve timeout config (merge shorthand + structured)
218
+ const resolvedTimeouts = resolveTimeouts(timeoutShorthand, timeoutsOption);
219
+
199
220
  /**
200
221
  * Wrapper for invokeOnError that binds the router's onError callback.
201
222
  * Uses the shared utility from router/error-handling.ts for consistent behavior.
223
+ *
224
+ * Deduplicates via per-request WeakSet stored on the ALS request context.
225
+ * A closure-level WeakSet would silently swallow errors if the same object
226
+ * instance is thrown across separate requests (e.g. a singleton error).
202
227
  */
203
228
  function callOnError(
204
229
  error: unknown,
205
230
  phase: ErrorPhase,
206
231
  context: Parameters<typeof invokeOnError<TEnv>>[3],
207
232
  ): void {
233
+ if (error != null && typeof error === "object") {
234
+ const reportedErrors = _getRequestContext()?._reportedErrors;
235
+ if (reportedErrors) {
236
+ if (reportedErrors.has(error)) return;
237
+ reportedErrors.add(error);
238
+ }
239
+ }
208
240
  invokeOnError(onError, error, phase, context, "Router");
209
241
  }
210
242
 
@@ -340,14 +372,43 @@ export function createRouter<TEnv = any>(
340
372
  return _getRequestContext()?._handleStore;
341
373
  };
342
374
 
343
- // Track a pending handler promise (non-blocking)
344
- const trackHandler = <T>(promise: Promise<T>): Promise<T> => {
375
+ // Track a pending handler promise (non-blocking).
376
+ // Attaches a side-effect .catch() to report streaming handler errors to onError
377
+ // without altering the rejection chain (React's streaming error boundary still handles it).
378
+ const trackHandler = <T>(
379
+ promise: Promise<T>,
380
+ errorContext?: {
381
+ segmentId?: string;
382
+ segmentType?: string;
383
+ },
384
+ ): Promise<T> => {
345
385
  const store = getHandleStore();
346
- return store ? store.track(promise) : promise;
386
+ const tracked = store ? store.track(promise) : promise;
387
+
388
+ // Report streaming handler errors to onError as a side-effect.
389
+ // The rejection still propagates to the RSC stream for client error boundaries.
390
+ // Captures request context eagerly (closure) so the catch handler has full context.
391
+ const reqCtx = _getRequestContext();
392
+ if (reqCtx && onError) {
393
+ tracked.catch((error) => {
394
+ callOnError(error, "handler", {
395
+ request: reqCtx.request,
396
+ url: reqCtx.url,
397
+ routeKey: reqCtx._routeName,
398
+ params: reqCtx.params as Record<string, string>,
399
+ env: reqCtx.env as TEnv,
400
+ segmentId: errorContext?.segmentId,
401
+ segmentType: errorContext?.segmentType as any,
402
+ handledByBoundary: true,
403
+ });
404
+ });
405
+ }
406
+
407
+ return tracked;
347
408
  };
348
409
 
349
410
  // Wrapper for wrapLoaderWithErrorHandling that uses router's error boundary finder
350
- // Includes onError callback for loader error notification
411
+ // Includes onError callback for loader error notification and telemetry emission.
351
412
  function wrapLoaderPromise<T>(
352
413
  promise: Promise<T>,
353
414
  entry: EntryData,
@@ -363,7 +424,25 @@ export function createRouter<TEnv = any>(
363
424
  requestStartTime?: number;
364
425
  },
365
426
  ): Promise<LoaderDataResult<T>> {
366
- return wrapLoaderWithErrorHandling(
427
+ const loaderStart = telemetrySink ? performance.now() : 0;
428
+ const loaderRequestId = telemetrySink
429
+ ? errorContext?.request
430
+ ? getRequestId(errorContext.request)
431
+ : undefined
432
+ : undefined;
433
+ if (telemetrySink) {
434
+ const loaderName = segmentId.split(".").pop() || "unknown";
435
+ safeEmit(telemetry, {
436
+ type: "loader.start",
437
+ timestamp: loaderStart,
438
+ requestId: loaderRequestId,
439
+ segmentId,
440
+ loaderName,
441
+ pathname,
442
+ });
443
+ }
444
+
445
+ const result = wrapLoaderWithErrorHandling(
367
446
  promise,
368
447
  entry,
369
448
  segmentId,
@@ -386,9 +465,42 @@ export function createRouter<TEnv = any>(
386
465
  handledByBoundary: ctx.handledByBoundary,
387
466
  requestStartTime: errorContext.requestStartTime,
388
467
  });
468
+ if (telemetrySink) {
469
+ const errorObj =
470
+ error instanceof Error ? error : new Error(String(error));
471
+ safeEmit(telemetry, {
472
+ type: "loader.error",
473
+ timestamp: performance.now(),
474
+ requestId: loaderRequestId,
475
+ segmentId: ctx.segmentId,
476
+ loaderName: ctx.loaderName,
477
+ pathname,
478
+ error: errorObj,
479
+ handledByBoundary: ctx.handledByBoundary,
480
+ });
481
+ }
389
482
  }
390
483
  : undefined,
391
484
  );
485
+
486
+ // Emit loader.end after the promise settles (fire-and-forget)
487
+ if (telemetrySink) {
488
+ const loaderName = segmentId.split(".").pop() || "unknown";
489
+ result.then((r) => {
490
+ safeEmit(telemetry, {
491
+ type: "loader.end",
492
+ timestamp: performance.now(),
493
+ requestId: loaderRequestId,
494
+ segmentId,
495
+ loaderName,
496
+ pathname,
497
+ durationMs: performance.now() - loaderStart,
498
+ ok: r.ok,
499
+ });
500
+ });
501
+ }
502
+
503
+ return result;
392
504
  }
393
505
 
394
506
  // Dependencies object for extracted segment resolution functions.
@@ -468,6 +580,7 @@ export function createRouter<TEnv = any>(
468
580
  resolveLoadersOnlyWithRevalidation,
469
581
  resolveInterceptLoadersOnly,
470
582
  resolveLoadersOnly,
583
+ telemetry: telemetrySink,
471
584
  };
472
585
  }
473
586
 
@@ -483,8 +596,15 @@ export function createRouter<TEnv = any>(
483
596
  pathname: string,
484
597
  params: Record<string, string>,
485
598
  buildVars?: Record<string, any>,
599
+ isPassthroughRoute?: boolean,
486
600
  ) {
487
- return _matchForPrerender(pathname, params, prerenderDeps, buildVars);
601
+ return _matchForPrerender(
602
+ pathname,
603
+ params,
604
+ prerenderDeps,
605
+ buildVars,
606
+ isPassthroughRoute,
607
+ );
488
608
  }
489
609
 
490
610
  async function renderStaticSegment(
@@ -508,6 +628,7 @@ export function createRouter<TEnv = any>(
508
628
  defaultErrorBoundary,
509
629
  findMatch,
510
630
  findInterceptForRoute,
631
+ telemetry: telemetrySink,
511
632
  });
512
633
 
513
634
  const { match, matchPartial, matchError, previewMatch } = matchHandlers;
@@ -568,6 +689,7 @@ export function createRouter<TEnv = any>(
568
689
  parent: syntheticMapRoot,
569
690
  counters: {},
570
691
  mountIndex: currentMountIndex,
692
+ cacheProfiles: resolvedCacheProfiles,
571
693
  },
572
694
  () => {
573
695
  handlerResult = urlPatterns.handler() as AllUseItems[];
@@ -582,10 +704,15 @@ export function createRouter<TEnv = any>(
582
704
 
583
705
  // Collect route keys that have prerender handlers (for non-trie match path)
584
706
  let prerenderRouteKeys: Set<string> | undefined;
707
+ let passthroughRouteKeys: Set<string> | undefined;
585
708
  for (const [name, entry] of manifest.entries()) {
586
709
  if (entry.type === "route" && entry.isPrerender) {
587
710
  if (!prerenderRouteKeys) prerenderRouteKeys = new Set();
588
711
  prerenderRouteKeys.add(name);
712
+ if (entry.prerenderDef?.options?.passthrough === true) {
713
+ if (!passthroughRouteKeys) passthroughRouteKeys = new Set();
714
+ passthroughRouteKeys.add(name);
715
+ }
589
716
  }
590
717
  }
591
718
 
@@ -608,7 +735,9 @@ export function createRouter<TEnv = any>(
608
735
  trailingSlash: trailingSlashConfig,
609
736
  handler: urlPatterns.handler,
610
737
  mountIndex: currentMountIndex,
738
+ cacheProfiles: resolvedCacheProfiles,
611
739
  ...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
740
+ ...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
612
741
  });
613
742
  }
614
743
  } else {
@@ -625,7 +754,9 @@ export function createRouter<TEnv = any>(
625
754
  trailingSlash: trailingSlashConfig,
626
755
  handler: urlPatterns.handler,
627
756
  mountIndex: currentMountIndex,
757
+ cacheProfiles: resolvedCacheProfiles,
628
758
  ...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
759
+ ...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
629
760
  });
630
761
  }
631
762
 
@@ -735,6 +866,9 @@ export function createRouter<TEnv = any>(
735
866
  // Expose resolved theme configuration for NavigationProvider and MetaTags
736
867
  themeConfig: resolvedThemeConfig,
737
868
 
869
+ // Expose resolved cache profiles for per-request resolution
870
+ cacheProfiles: resolvedCacheProfiles,
871
+
738
872
  // Expose prefetch cache control for RSC handler
739
873
  prefetchCacheControl,
740
874
 
@@ -744,6 +878,16 @@ export function createRouter<TEnv = any>(
744
878
  // Expose debug manifest flag for handler
745
879
  allowDebugManifest: allowDebugManifestOption,
746
880
 
881
+ // Expose origin check configuration for handler (default: enabled)
882
+ originCheck: originCheckOption ?? true,
883
+
884
+ // Expose SSR configuration for handler
885
+ ssr: ssrOption,
886
+
887
+ // Expose resolved timeouts for RSC handler
888
+ timeouts: resolvedTimeouts,
889
+ onTimeout,
890
+
747
891
  // Expose global middleware for RSC handler
748
892
  middleware: globalMiddleware,
749
893
 
@@ -10,6 +10,7 @@ import type { RSCRouterInternal } from "../router/router-interfaces.js";
10
10
  import type { ErrorPhase } from "../types.js";
11
11
  import type { InvokeOnErrorContext } from "../router/error-handling.js";
12
12
  import type { RSCDependencies, LoadSSRModule } from "./types.js";
13
+ import type { SSRStreamMode } from "../router/router-options.js";
13
14
 
14
15
  export interface HandlerContext<TEnv = unknown> {
15
16
  router: RSCRouterInternal<TEnv, any>;
@@ -31,4 +32,14 @@ export interface HandlerContext<TEnv = unknown> {
31
32
  redirectUrl: string,
32
33
  locationState?: Record<string, unknown>,
33
34
  ) => Response;
35
+
36
+ /**
37
+ * Resolve the SSR stream mode for a given request.
38
+ * Returns "stream" when no resolveStreaming callback is configured.
39
+ */
40
+ resolveStreamMode: (
41
+ request: Request,
42
+ env: TEnv,
43
+ url: URL,
44
+ ) => Promise<SSRStreamMode>;
34
45
  }