@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26

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 (225) hide show
  1. package/README.md +294 -28
  2. package/dist/bin/rango.js +355 -47
  3. package/dist/vite/index.js +1658 -1239
  4. package/package.json +3 -3
  5. package/skills/cache-guide/SKILL.md +9 -5
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +40 -29
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/intercept/SKILL.md +79 -0
  11. package/skills/layout/SKILL.md +62 -2
  12. package/skills/loader/SKILL.md +229 -15
  13. package/skills/middleware/SKILL.md +109 -30
  14. package/skills/parallel/SKILL.md +57 -2
  15. package/skills/prerender/SKILL.md +189 -19
  16. package/skills/rango/SKILL.md +1 -2
  17. package/skills/response-routes/SKILL.md +3 -3
  18. package/skills/route/SKILL.md +44 -3
  19. package/skills/router-setup/SKILL.md +80 -3
  20. package/skills/theme/SKILL.md +5 -4
  21. package/skills/typesafety/SKILL.md +59 -16
  22. package/skills/use-cache/SKILL.md +16 -2
  23. package/src/__internal.ts +1 -1
  24. package/src/bin/rango.ts +56 -19
  25. package/src/browser/action-coordinator.ts +97 -0
  26. package/src/browser/event-controller.ts +29 -48
  27. package/src/browser/history-state.ts +80 -0
  28. package/src/browser/intercept-utils.ts +1 -1
  29. package/src/browser/link-interceptor.ts +19 -3
  30. package/src/browser/merge-segment-loaders.ts +9 -2
  31. package/src/browser/navigation-bridge.ts +66 -443
  32. package/src/browser/navigation-client.ts +34 -62
  33. package/src/browser/navigation-store.ts +4 -33
  34. package/src/browser/navigation-transaction.ts +295 -0
  35. package/src/browser/partial-update.ts +103 -151
  36. package/src/browser/prefetch/cache.ts +67 -0
  37. package/src/browser/prefetch/fetch.ts +137 -0
  38. package/src/browser/prefetch/observer.ts +65 -0
  39. package/src/browser/prefetch/policy.ts +42 -0
  40. package/src/browser/prefetch/queue.ts +88 -0
  41. package/src/browser/rango-state.ts +112 -0
  42. package/src/browser/react/Link.tsx +154 -44
  43. package/src/browser/react/NavigationProvider.tsx +32 -0
  44. package/src/browser/react/context.ts +6 -0
  45. package/src/browser/react/filter-segment-order.ts +11 -0
  46. package/src/browser/react/index.ts +2 -6
  47. package/src/browser/react/location-state-shared.ts +29 -11
  48. package/src/browser/react/location-state.ts +6 -4
  49. package/src/browser/react/nonce-context.ts +23 -0
  50. package/src/browser/react/shallow-equal.ts +27 -0
  51. package/src/browser/react/use-action.ts +23 -45
  52. package/src/browser/react/use-client-cache.ts +5 -3
  53. package/src/browser/react/use-handle.ts +21 -64
  54. package/src/browser/react/use-navigation.ts +7 -32
  55. package/src/browser/react/use-params.ts +5 -34
  56. package/src/browser/react/use-pathname.ts +2 -3
  57. package/src/browser/react/use-router.ts +3 -6
  58. package/src/browser/react/use-search-params.ts +2 -1
  59. package/src/browser/react/use-segments.ts +75 -114
  60. package/src/browser/response-adapter.ts +73 -0
  61. package/src/browser/rsc-router.tsx +46 -22
  62. package/src/browser/scroll-restoration.ts +10 -7
  63. package/src/browser/server-action-bridge.ts +458 -405
  64. package/src/browser/types.ts +21 -35
  65. package/src/browser/validate-redirect-origin.ts +29 -0
  66. package/src/build/generate-manifest.ts +38 -13
  67. package/src/build/generate-route-types.ts +4 -0
  68. package/src/build/index.ts +1 -0
  69. package/src/build/route-trie.ts +19 -3
  70. package/src/build/route-types/codegen.ts +13 -4
  71. package/src/build/route-types/include-resolution.ts +13 -0
  72. package/src/build/route-types/per-module-writer.ts +15 -3
  73. package/src/build/route-types/router-processing.ts +170 -18
  74. package/src/build/runtime-discovery.ts +13 -1
  75. package/src/cache/background-task.ts +34 -0
  76. package/src/cache/cache-key-utils.ts +44 -0
  77. package/src/cache/cache-policy.ts +125 -0
  78. package/src/cache/cache-runtime.ts +136 -123
  79. package/src/cache/cache-scope.ts +76 -83
  80. package/src/cache/cf/cf-cache-store.ts +12 -7
  81. package/src/cache/document-cache.ts +93 -69
  82. package/src/cache/handle-capture.ts +81 -0
  83. package/src/cache/index.ts +0 -15
  84. package/src/cache/memory-segment-store.ts +43 -69
  85. package/src/cache/profile-registry.ts +43 -8
  86. package/src/cache/read-through-swr.ts +134 -0
  87. package/src/cache/segment-codec.ts +140 -117
  88. package/src/cache/taint.ts +30 -3
  89. package/src/cache/types.ts +1 -115
  90. package/src/client.rsc.tsx +0 -1
  91. package/src/client.tsx +53 -76
  92. package/src/errors.ts +6 -1
  93. package/src/handle.ts +1 -1
  94. package/src/handles/MetaTags.tsx +5 -2
  95. package/src/host/cookie-handler.ts +8 -3
  96. package/src/host/index.ts +0 -3
  97. package/src/host/router.ts +14 -1
  98. package/src/href-client.ts +3 -1
  99. package/src/index.rsc.ts +53 -10
  100. package/src/index.ts +73 -43
  101. package/src/loader.rsc.ts +12 -4
  102. package/src/loader.ts +8 -0
  103. package/src/prerender/store.ts +60 -18
  104. package/src/prerender.ts +76 -18
  105. package/src/reverse.ts +11 -7
  106. package/src/root-error-boundary.tsx +30 -26
  107. package/src/route-definition/dsl-helpers.ts +9 -6
  108. package/src/route-definition/index.ts +0 -3
  109. package/src/route-definition/redirect.ts +15 -3
  110. package/src/route-map-builder.ts +38 -2
  111. package/src/route-name.ts +53 -0
  112. package/src/route-types.ts +7 -0
  113. package/src/router/content-negotiation.ts +1 -1
  114. package/src/router/debug-manifest.ts +16 -3
  115. package/src/router/handler-context.ts +96 -17
  116. package/src/router/intercept-resolution.ts +6 -4
  117. package/src/router/lazy-includes.ts +4 -0
  118. package/src/router/loader-resolution.ts +6 -11
  119. package/src/router/logging.ts +100 -3
  120. package/src/router/manifest.ts +32 -3
  121. package/src/router/match-api.ts +62 -54
  122. package/src/router/match-context.ts +3 -0
  123. package/src/router/match-handlers.ts +185 -11
  124. package/src/router/match-middleware/background-revalidation.ts +65 -85
  125. package/src/router/match-middleware/cache-lookup.ts +78 -10
  126. package/src/router/match-middleware/cache-store.ts +2 -0
  127. package/src/router/match-pipelines.ts +8 -43
  128. package/src/router/match-result.ts +0 -9
  129. package/src/router/metrics.ts +233 -13
  130. package/src/router/middleware-types.ts +34 -39
  131. package/src/router/middleware.ts +290 -130
  132. package/src/router/pattern-matching.ts +61 -10
  133. package/src/router/prerender-match.ts +36 -6
  134. package/src/router/preview-match.ts +7 -1
  135. package/src/router/revalidation.ts +61 -2
  136. package/src/router/router-context.ts +15 -0
  137. package/src/router/router-interfaces.ts +158 -40
  138. package/src/router/router-options.ts +223 -1
  139. package/src/router/router-registry.ts +5 -2
  140. package/src/router/segment-resolution/fresh.ts +165 -242
  141. package/src/router/segment-resolution/helpers.ts +263 -0
  142. package/src/router/segment-resolution/loader-cache.ts +102 -98
  143. package/src/router/segment-resolution/revalidation.ts +394 -272
  144. package/src/router/segment-resolution/static-store.ts +2 -2
  145. package/src/router/segment-resolution.ts +1 -3
  146. package/src/router/segment-wrappers.ts +3 -0
  147. package/src/router/telemetry-otel.ts +299 -0
  148. package/src/router/telemetry.ts +300 -0
  149. package/src/router/timeout.ts +148 -0
  150. package/src/router/trie-matching.ts +20 -2
  151. package/src/router/types.ts +7 -1
  152. package/src/router.ts +203 -18
  153. package/src/rsc/handler-context.ts +13 -2
  154. package/src/rsc/handler.ts +489 -438
  155. package/src/rsc/helpers.ts +125 -5
  156. package/src/rsc/index.ts +0 -20
  157. package/src/rsc/loader-fetch.ts +84 -42
  158. package/src/rsc/manifest-init.ts +3 -2
  159. package/src/rsc/origin-guard.ts +141 -0
  160. package/src/rsc/progressive-enhancement.ts +245 -19
  161. package/src/rsc/response-route-handler.ts +347 -0
  162. package/src/rsc/rsc-rendering.ts +47 -43
  163. package/src/rsc/runtime-warnings.ts +42 -0
  164. package/src/rsc/server-action.ts +166 -66
  165. package/src/rsc/ssr-setup.ts +128 -0
  166. package/src/rsc/types.ts +20 -2
  167. package/src/search-params.ts +38 -23
  168. package/src/server/context.ts +61 -7
  169. package/src/server/cookie-store.ts +190 -0
  170. package/src/server/fetchable-loader-store.ts +11 -6
  171. package/src/server/handle-store.ts +84 -12
  172. package/src/server/loader-registry.ts +11 -46
  173. package/src/server/request-context.ts +275 -49
  174. package/src/server.ts +6 -0
  175. package/src/ssr/index.tsx +67 -28
  176. package/src/static-handler.ts +7 -0
  177. package/src/theme/ThemeProvider.tsx +6 -1
  178. package/src/theme/index.ts +4 -18
  179. package/src/theme/theme-context.ts +1 -28
  180. package/src/theme/theme-script.ts +2 -1
  181. package/src/types/cache-types.ts +6 -1
  182. package/src/types/error-types.ts +3 -0
  183. package/src/types/global-namespace.ts +22 -0
  184. package/src/types/handler-context.ts +103 -16
  185. package/src/types/index.ts +1 -1
  186. package/src/types/loader-types.ts +9 -6
  187. package/src/types/route-config.ts +17 -26
  188. package/src/types/route-entry.ts +28 -0
  189. package/src/types/segments.ts +0 -5
  190. package/src/urls/include-helper.ts +49 -8
  191. package/src/urls/index.ts +1 -0
  192. package/src/urls/path-helper-types.ts +30 -12
  193. package/src/urls/path-helper.ts +17 -2
  194. package/src/urls/pattern-types.ts +21 -1
  195. package/src/urls/response-types.ts +29 -7
  196. package/src/urls/type-extraction.ts +23 -15
  197. package/src/use-loader.tsx +27 -9
  198. package/src/vite/discovery/bundle-postprocess.ts +32 -52
  199. package/src/vite/discovery/discover-routers.ts +52 -26
  200. package/src/vite/discovery/prerender-collection.ts +58 -41
  201. package/src/vite/discovery/route-types-writer.ts +7 -7
  202. package/src/vite/discovery/state.ts +7 -7
  203. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  204. package/src/vite/index.ts +10 -51
  205. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  206. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  207. package/src/vite/plugins/expose-internal-ids.ts +4 -3
  208. package/src/vite/plugins/refresh-cmd.ts +65 -0
  209. package/src/vite/plugins/use-cache-transform.ts +91 -3
  210. package/src/vite/plugins/version-plugin.ts +188 -18
  211. package/src/vite/rango.ts +61 -36
  212. package/src/vite/router-discovery.ts +173 -100
  213. package/src/vite/utils/prerender-utils.ts +81 -0
  214. package/src/vite/utils/shared-utils.ts +19 -9
  215. package/skills/testing/SKILL.md +0 -226
  216. package/src/browser/lru-cache.ts +0 -61
  217. package/src/browser/react/prefetch.ts +0 -27
  218. package/src/browser/request-controller.ts +0 -164
  219. package/src/cache/memory-store.ts +0 -253
  220. package/src/href-context.ts +0 -33
  221. package/src/route-definition/route-function.ts +0 -119
  222. package/src/router.gen.ts +0 -6
  223. package/src/static-handler.gen.ts +0 -5
  224. package/src/urls.gen.ts +0 -8
  225. /package/{CLAUDE.md → AGENTS.md} +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
+ }
@@ -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 {
@@ -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";
@@ -24,7 +27,10 @@ import {
24
27
  type MetricsStore,
25
28
  } from "./server/context";
26
29
  import { createHandleStore, type HandleStore } from "./server/handle-store.js";
27
- import { getRequestContext } from "./server/request-context.js";
30
+ import {
31
+ getRequestContext,
32
+ _getRequestContext,
33
+ } from "./server/request-context.js";
28
34
  import type {
29
35
  ErrorPhase,
30
36
  HandlerContext,
@@ -65,12 +71,14 @@ import {
65
71
  extractStaticPrefix,
66
72
  traverseBack,
67
73
  } from "./router/pattern-matching.js";
74
+ import { resolveSink, safeEmit, getRequestId } from "./router/telemetry.js";
68
75
  import { evaluateRevalidation } from "./router/revalidation.js";
69
76
  import {
70
77
  type RouterContext,
71
78
  runWithRouterContext,
72
79
  } from "./router/router-context.js";
73
80
  import { resolveThemeConfig } from "./theme/constants.js";
81
+ import { resolveTimeouts } from "./router/timeout.js";
74
82
 
75
83
  // Extracted content negotiation utilities
76
84
  import { flattenNamedRoutes } from "./router/content-negotiation.js";
@@ -87,6 +95,7 @@ import type {
87
95
  } from "./router/router-options.js";
88
96
  import type {
89
97
  RSCRouter,
98
+ RSCRouterInternal,
90
99
  RouterRequestInput,
91
100
  } from "./router/router-interfaces.js";
92
101
 
@@ -107,11 +116,16 @@ export { RSC_ROUTER_BRAND, RouterRegistry } from "./router/router-registry.js";
107
116
  export type {
108
117
  RSCRouterOptions,
109
118
  RootLayoutProps,
119
+ SSRStreamMode,
120
+ SSROptions,
121
+ ResolveStreamingContext,
110
122
  } from "./router/router-options.js";
111
123
  export type {
112
124
  RSCRouter,
125
+ RSCRouterInternal,
113
126
  RouterRequestInput,
114
127
  } from "./router/router-interfaces.js";
128
+ export { toInternal } from "./router/router-interfaces.js";
115
129
 
116
130
  export function createRouter<TEnv = any>(
117
131
  options: RSCRouterOptions<TEnv> = {},
@@ -133,14 +147,25 @@ export function createRouter<TEnv = any>(
133
147
  $$sourceFile: injectedSourceFile,
134
148
  nonce,
135
149
  version,
150
+ prefetchCacheControl: prefetchCacheControlOption,
136
151
  warmup: warmupOption,
137
152
  allowDebugManifest: allowDebugManifestOption = false,
153
+ telemetry: telemetrySink,
154
+ ssr: ssrOption,
155
+ timeout: timeoutShorthand,
156
+ timeouts: timeoutsOption,
157
+ onTimeout,
158
+ originCheck: originCheckOption,
138
159
  } = options;
139
160
 
140
- // Set cache profiles for "use cache" directive
141
- if (cacheProfilesOption) {
142
- setCacheProfiles(cacheProfilesOption);
143
- }
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);
144
169
 
145
170
  // Source file: prefer Vite-injected path (zero cost), fall back to
146
171
  // stack trace parsing for non-Vite environments (e.g. tests).
@@ -175,6 +200,12 @@ export function createRouter<TEnv = any>(
175
200
  const routerId =
176
201
  userProvidedId ?? injectedId ?? `router_${nextRouterAutoId()}`;
177
202
 
203
+ // Resolve prefetch cache control (default: 'private, max-age=300')
204
+ const prefetchCacheControl =
205
+ prefetchCacheControlOption !== undefined
206
+ ? prefetchCacheControlOption
207
+ : "private, max-age=300";
208
+
178
209
  // Resolve warmup enabled flag (default: true)
179
210
  const warmupEnabled = warmupOption !== false;
180
211
 
@@ -183,15 +214,29 @@ export function createRouter<TEnv = any>(
183
214
  ? resolveThemeConfig(themeOption)
184
215
  : null;
185
216
 
217
+ // Resolve timeout config (merge shorthand + structured)
218
+ const resolvedTimeouts = resolveTimeouts(timeoutShorthand, timeoutsOption);
219
+
186
220
  /**
187
221
  * Wrapper for invokeOnError that binds the router's onError callback.
188
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).
189
227
  */
190
228
  function callOnError(
191
229
  error: unknown,
192
230
  phase: ErrorPhase,
193
231
  context: Parameters<typeof invokeOnError<TEnv>>[3],
194
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
+ }
195
240
  invokeOnError(onError, error, phase, context, "Router");
196
241
  }
197
242
 
@@ -281,6 +326,11 @@ export function createRouter<TEnv = any>(
281
326
  const mergedRouteMap: Record<string, string> =
282
327
  flattenNamedRoutes(staticRouteNames);
283
328
 
329
+ // Track names that came from the static seed so we can silently overwrite
330
+ // them during routes() registration. The gen file may be stale during HMR,
331
+ // so conflicts between seeded and runtime-registered values are expected.
332
+ const seededNames = new Set(Object.keys(mergedRouteMap));
333
+
284
334
  // Lazy precomputed entries lookup: rebuilt when per-router data arrives.
285
335
  // In production multi-router setups, per-router data is loaded lazily via
286
336
  // ensureRouterManifest(). At createRouter() time the data isn't available yet,
@@ -307,8 +357,18 @@ export function createRouter<TEnv = any>(
307
357
  return precomputedByPrefix;
308
358
  }
309
359
 
310
- // Wrapper to pass debugPerformance to external createMetricsStore
311
- const getMetricsStore = () => createMetricsStore(debugPerformance);
360
+ // Wrapper to pass debugPerformance to external createMetricsStore.
361
+ // Also checks per-request flag set by ctx.debugPerformance() in middleware.
362
+ const getMetricsStore = () => {
363
+ const reqCtx = _getRequestContext();
364
+ const enabled = debugPerformance || !!reqCtx?._debugPerformance;
365
+ if (!enabled) return undefined;
366
+ if (!reqCtx) {
367
+ return createMetricsStore(true);
368
+ }
369
+ reqCtx._metricsStore ??= createMetricsStore(true);
370
+ return reqCtx._metricsStore;
371
+ };
312
372
 
313
373
  // Wrapper to pass defaults to error/notFound boundary finders
314
374
  const findNearestErrorBoundary = (entry: EntryData | null) =>
@@ -319,17 +379,46 @@ export function createRouter<TEnv = any>(
319
379
 
320
380
  // Helper to get handleStore from request context
321
381
  const getHandleStore = (): HandleStore | undefined => {
322
- return getRequestContext()?._handleStore;
382
+ return _getRequestContext()?._handleStore;
323
383
  };
324
384
 
325
- // Track a pending handler promise (non-blocking)
326
- const trackHandler = <T>(promise: Promise<T>): Promise<T> => {
385
+ // Track a pending handler promise (non-blocking).
386
+ // Attaches a side-effect .catch() to report streaming handler errors to onError
387
+ // without altering the rejection chain (React's streaming error boundary still handles it).
388
+ const trackHandler = <T>(
389
+ promise: Promise<T>,
390
+ errorContext?: {
391
+ segmentId?: string;
392
+ segmentType?: string;
393
+ },
394
+ ): Promise<T> => {
327
395
  const store = getHandleStore();
328
- return store ? store.track(promise) : promise;
396
+ const tracked = store ? store.track(promise) : promise;
397
+
398
+ // Report streaming handler errors to onError as a side-effect.
399
+ // The rejection still propagates to the RSC stream for client error boundaries.
400
+ // Captures request context eagerly (closure) so the catch handler has full context.
401
+ const reqCtx = _getRequestContext();
402
+ if (reqCtx && onError) {
403
+ tracked.catch((error) => {
404
+ callOnError(error, "handler", {
405
+ request: reqCtx.request,
406
+ url: reqCtx.url,
407
+ routeKey: reqCtx._routeName,
408
+ params: reqCtx.params as Record<string, string>,
409
+ env: reqCtx.env as TEnv,
410
+ segmentId: errorContext?.segmentId,
411
+ segmentType: errorContext?.segmentType as any,
412
+ handledByBoundary: true,
413
+ });
414
+ });
415
+ }
416
+
417
+ return tracked;
329
418
  };
330
419
 
331
420
  // Wrapper for wrapLoaderWithErrorHandling that uses router's error boundary finder
332
- // Includes onError callback for loader error notification
421
+ // Includes onError callback for loader error notification and telemetry emission.
333
422
  function wrapLoaderPromise<T>(
334
423
  promise: Promise<T>,
335
424
  entry: EntryData,
@@ -345,7 +434,25 @@ export function createRouter<TEnv = any>(
345
434
  requestStartTime?: number;
346
435
  },
347
436
  ): Promise<LoaderDataResult<T>> {
348
- return wrapLoaderWithErrorHandling(
437
+ const loaderStart = telemetrySink ? performance.now() : 0;
438
+ const loaderRequestId = telemetrySink
439
+ ? errorContext?.request
440
+ ? getRequestId(errorContext.request)
441
+ : undefined
442
+ : undefined;
443
+ if (telemetrySink) {
444
+ const loaderName = segmentId.split(".").pop() || "unknown";
445
+ safeEmit(telemetry, {
446
+ type: "loader.start",
447
+ timestamp: loaderStart,
448
+ requestId: loaderRequestId,
449
+ segmentId,
450
+ loaderName,
451
+ pathname,
452
+ });
453
+ }
454
+
455
+ const result = wrapLoaderWithErrorHandling(
349
456
  promise,
350
457
  entry,
351
458
  segmentId,
@@ -368,9 +475,42 @@ export function createRouter<TEnv = any>(
368
475
  handledByBoundary: ctx.handledByBoundary,
369
476
  requestStartTime: errorContext.requestStartTime,
370
477
  });
478
+ if (telemetrySink) {
479
+ const errorObj =
480
+ error instanceof Error ? error : new Error(String(error));
481
+ safeEmit(telemetry, {
482
+ type: "loader.error",
483
+ timestamp: performance.now(),
484
+ requestId: loaderRequestId,
485
+ segmentId: ctx.segmentId,
486
+ loaderName: ctx.loaderName,
487
+ pathname,
488
+ error: errorObj,
489
+ handledByBoundary: ctx.handledByBoundary,
490
+ });
491
+ }
371
492
  }
372
493
  : undefined,
373
494
  );
495
+
496
+ // Emit loader.end after the promise settles (fire-and-forget)
497
+ if (telemetrySink) {
498
+ const loaderName = segmentId.split(".").pop() || "unknown";
499
+ result.then((r) => {
500
+ safeEmit(telemetry, {
501
+ type: "loader.end",
502
+ timestamp: performance.now(),
503
+ requestId: loaderRequestId,
504
+ segmentId,
505
+ loaderName,
506
+ pathname,
507
+ durationMs: performance.now() - loaderStart,
508
+ ok: r.ok,
509
+ });
510
+ });
511
+ }
512
+
513
+ return result;
374
514
  }
375
515
 
376
516
  // Dependencies object for extracted segment resolution functions.
@@ -450,6 +590,7 @@ export function createRouter<TEnv = any>(
450
590
  resolveLoadersOnlyWithRevalidation,
451
591
  resolveInterceptLoadersOnly,
452
592
  resolveLoadersOnly,
593
+ telemetry: telemetrySink,
453
594
  };
454
595
  }
455
596
 
@@ -465,8 +606,15 @@ export function createRouter<TEnv = any>(
465
606
  pathname: string,
466
607
  params: Record<string, string>,
467
608
  buildVars?: Record<string, any>,
609
+ isPassthroughRoute?: boolean,
468
610
  ) {
469
- return _matchForPrerender(pathname, params, prerenderDeps, buildVars);
611
+ return _matchForPrerender(
612
+ pathname,
613
+ params,
614
+ prerenderDeps,
615
+ buildVars,
616
+ isPassthroughRoute,
617
+ );
470
618
  }
471
619
 
472
620
  async function renderStaticSegment(
@@ -490,6 +638,7 @@ export function createRouter<TEnv = any>(
490
638
  defaultErrorBoundary,
491
639
  findMatch,
492
640
  findInterceptForRoute,
641
+ telemetry: telemetrySink,
493
642
  });
494
643
 
495
644
  const { match, matchPartial, matchError, previewMatch } = matchHandlers;
@@ -499,7 +648,7 @@ export function createRouter<TEnv = any>(
499
648
  * The type system tracks accumulated routes through the builder chain
500
649
  * Initial TRoutes is {} (empty) to avoid poisoning accumulated types with Record<string, string>
501
650
  */
502
- const router: RSCRouter<TEnv, {}> = {
651
+ const router: RSCRouterInternal<TEnv, {}> = {
503
652
  __brand: RSC_ROUTER_BRAND,
504
653
  id: routerId,
505
654
 
@@ -550,6 +699,7 @@ export function createRouter<TEnv = any>(
550
699
  parent: syntheticMapRoot,
551
700
  counters: {},
552
701
  mountIndex: currentMountIndex,
702
+ cacheProfiles: resolvedCacheProfiles,
553
703
  },
554
704
  () => {
555
705
  handlerResult = urlPatterns.handler() as AllUseItems[];
@@ -564,10 +714,15 @@ export function createRouter<TEnv = any>(
564
714
 
565
715
  // Collect route keys that have prerender handlers (for non-trie match path)
566
716
  let prerenderRouteKeys: Set<string> | undefined;
717
+ let passthroughRouteKeys: Set<string> | undefined;
567
718
  for (const [name, entry] of manifest.entries()) {
568
719
  if (entry.type === "route" && entry.isPrerender) {
569
720
  if (!prerenderRouteKeys) prerenderRouteKeys = new Set();
570
721
  prerenderRouteKeys.add(name);
722
+ if (entry.prerenderDef?.options?.passthrough === true) {
723
+ if (!passthroughRouteKeys) passthroughRouteKeys = new Set();
724
+ passthroughRouteKeys.add(name);
725
+ }
571
726
  }
572
727
  }
573
728
 
@@ -590,7 +745,9 @@ export function createRouter<TEnv = any>(
590
745
  trailingSlash: trailingSlashConfig,
591
746
  handler: urlPatterns.handler,
592
747
  mountIndex: currentMountIndex,
748
+ cacheProfiles: resolvedCacheProfiles,
593
749
  ...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
750
+ ...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
594
751
  });
595
752
  }
596
753
  } else {
@@ -607,21 +764,30 @@ export function createRouter<TEnv = any>(
607
764
  trailingSlash: trailingSlashConfig,
608
765
  handler: urlPatterns.handler,
609
766
  mountIndex: currentMountIndex,
767
+ cacheProfiles: resolvedCacheProfiles,
610
768
  ...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
769
+ ...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
611
770
  });
612
771
  }
613
772
 
614
773
  // Build route map from registered patterns
615
774
  for (const [name, pattern] of routePatterns.entries()) {
616
- // Runtime validation: warn if key already exists with different pattern
775
+ // Runtime validation: warn if key already exists with different pattern.
776
+ // Skip warning for entries that came from the static seed — the gen file
777
+ // can be stale during HMR, so runtime registration is authoritative.
617
778
  const existingPattern = mergedRouteMap[name];
618
- if (existingPattern !== undefined && existingPattern !== pattern) {
779
+ if (
780
+ existingPattern !== undefined &&
781
+ existingPattern !== pattern &&
782
+ !seededNames.has(name)
783
+ ) {
619
784
  console.warn(
620
785
  `[@rangojs/router] Route name conflict: "${name}" already maps to "${existingPattern}", ` +
621
786
  `overwriting with "${pattern}". Use unique route names to avoid this.`,
622
787
  );
623
788
  }
624
789
  mergedRouteMap[name] = pattern;
790
+ seededNames.delete(name);
625
791
  }
626
792
 
627
793
  // Detect lazy includes in handler result and create placeholder entries
@@ -710,12 +876,31 @@ export function createRouter<TEnv = any>(
710
876
  // Expose resolved theme configuration for NavigationProvider and MetaTags
711
877
  themeConfig: resolvedThemeConfig,
712
878
 
879
+ // Expose resolved cache profiles for per-request resolution
880
+ cacheProfiles: resolvedCacheProfiles,
881
+
882
+ // Expose prefetch cache control for RSC handler
883
+ prefetchCacheControl,
884
+
713
885
  // Expose warmup enabled flag for handler and client
714
886
  warmupEnabled,
715
887
 
888
+ // Expose router-wide performance debugging for request-level metrics setup
889
+ debugPerformance,
890
+
716
891
  // Expose debug manifest flag for handler
717
892
  allowDebugManifest: allowDebugManifestOption,
718
893
 
894
+ // Expose origin check configuration for handler (default: enabled)
895
+ originCheck: originCheckOption ?? true,
896
+
897
+ // Expose SSR configuration for handler
898
+ ssr: ssrOption,
899
+
900
+ // Expose resolved timeouts for RSC handler
901
+ timeouts: resolvedTimeouts,
902
+ onTimeout,
903
+
719
904
  // Expose global middleware for RSC handler
720
905
  middleware: globalMiddleware,
721
906
 
@@ -6,13 +6,14 @@
6
6
  * RSC rendering) so they can be standalone modules without closure coupling.
7
7
  */
8
8
 
9
- import type { RSCRouter } from "../router.js";
9
+ 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
- router: RSCRouter<TEnv, any>;
16
+ router: RSCRouterInternal<TEnv, any>;
16
17
  version: string;
17
18
  renderToReadableStream: RSCDependencies["renderToReadableStream"];
18
19
  decodeReply: RSCDependencies["decodeReply"];
@@ -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
  }