@rangojs/router 0.0.0-experimental.13 → 0.0.0-experimental.13221847

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 (298) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1531 -212
  4. package/dist/vite/index.js +3995 -2489
  5. package/package.json +57 -52
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +85 -23
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +6 -4
  13. package/skills/hooks/SKILL.md +328 -70
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +62 -15
  18. package/skills/loader/SKILL.md +368 -42
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +14 -10
  21. package/skills/parallel/SKILL.md +137 -1
  22. package/skills/prerender/SKILL.md +366 -28
  23. package/skills/rango/SKILL.md +85 -21
  24. package/skills/response-routes/SKILL.md +136 -83
  25. package/skills/route/SKILL.md +195 -21
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/theme/SKILL.md +9 -8
  28. package/skills/typesafety/SKILL.md +240 -102
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +102 -4
  31. package/src/bin/rango.ts +312 -15
  32. package/src/browser/action-coordinator.ts +97 -0
  33. package/src/browser/action-response-classifier.ts +99 -0
  34. package/src/browser/event-controller.ts +92 -64
  35. package/src/browser/history-state.ts +80 -0
  36. package/src/browser/intercept-utils.ts +52 -0
  37. package/src/browser/link-interceptor.ts +24 -4
  38. package/src/browser/logging.ts +11 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +266 -558
  41. package/src/browser/navigation-client.ts +132 -75
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +297 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +303 -309
  46. package/src/browser/prefetch/cache.ts +206 -0
  47. package/src/browser/prefetch/fetch.ts +144 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +48 -0
  50. package/src/browser/prefetch/queue.ts +128 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +190 -70
  53. package/src/browser/react/NavigationProvider.tsx +78 -11
  54. package/src/browser/react/context.ts +6 -0
  55. package/src/browser/react/filter-segment-order.ts +11 -0
  56. package/src/browser/react/index.ts +12 -12
  57. package/src/browser/react/location-state-shared.ts +95 -53
  58. package/src/browser/react/location-state.ts +60 -15
  59. package/src/browser/react/mount-context.ts +6 -1
  60. package/src/browser/react/nonce-context.ts +23 -0
  61. package/src/browser/react/shallow-equal.ts +27 -0
  62. package/src/browser/react/use-action.ts +29 -51
  63. package/src/browser/react/use-client-cache.ts +5 -3
  64. package/src/browser/react/use-handle.ts +29 -70
  65. package/src/browser/react/use-link-status.ts +6 -5
  66. package/src/browser/react/use-navigation.ts +22 -63
  67. package/src/browser/react/use-params.ts +65 -0
  68. package/src/browser/react/use-pathname.ts +47 -0
  69. package/src/browser/react/use-router.ts +63 -0
  70. package/src/browser/react/use-search-params.ts +56 -0
  71. package/src/browser/react/use-segments.ts +80 -97
  72. package/src/browser/response-adapter.ts +73 -0
  73. package/src/browser/rsc-router.tsx +188 -57
  74. package/src/browser/scroll-restoration.ts +117 -44
  75. package/src/browser/segment-reconciler.ts +221 -0
  76. package/src/browser/segment-structure-assert.ts +16 -0
  77. package/src/browser/server-action-bridge.ts +488 -606
  78. package/src/browser/shallow.ts +6 -1
  79. package/src/browser/types.ts +116 -47
  80. package/src/browser/validate-redirect-origin.ts +29 -0
  81. package/src/build/generate-manifest.ts +63 -21
  82. package/src/build/generate-route-types.ts +36 -1038
  83. package/src/build/index.ts +2 -5
  84. package/src/build/route-trie.ts +38 -12
  85. package/src/build/route-types/ast-helpers.ts +25 -0
  86. package/src/build/route-types/ast-route-extraction.ts +98 -0
  87. package/src/build/route-types/codegen.ts +102 -0
  88. package/src/build/route-types/include-resolution.ts +411 -0
  89. package/src/build/route-types/param-extraction.ts +48 -0
  90. package/src/build/route-types/per-module-writer.ts +128 -0
  91. package/src/build/route-types/router-processing.ts +479 -0
  92. package/src/build/route-types/scan-filter.ts +78 -0
  93. package/src/build/runtime-discovery.ts +231 -0
  94. package/src/cache/background-task.ts +34 -0
  95. package/src/cache/cache-key-utils.ts +44 -0
  96. package/src/cache/cache-policy.ts +125 -0
  97. package/src/cache/cache-runtime.ts +342 -0
  98. package/src/cache/cache-scope.ts +122 -303
  99. package/src/cache/cf/cf-cache-store.ts +571 -17
  100. package/src/cache/cf/index.ts +13 -3
  101. package/src/cache/document-cache.ts +116 -77
  102. package/src/cache/handle-capture.ts +81 -0
  103. package/src/cache/handle-snapshot.ts +41 -0
  104. package/src/cache/index.ts +1 -15
  105. package/src/cache/memory-segment-store.ts +191 -13
  106. package/src/cache/profile-registry.ts +73 -0
  107. package/src/cache/read-through-swr.ts +134 -0
  108. package/src/cache/segment-codec.ts +256 -0
  109. package/src/cache/taint.ts +98 -0
  110. package/src/cache/types.ts +72 -122
  111. package/src/client.rsc.tsx +3 -1
  112. package/src/client.tsx +84 -126
  113. package/src/component-utils.ts +4 -4
  114. package/src/components/DefaultDocument.tsx +5 -1
  115. package/src/context-var.ts +86 -0
  116. package/src/debug.ts +19 -9
  117. package/src/errors.ts +77 -7
  118. package/src/handle.ts +12 -7
  119. package/src/handles/MetaTags.tsx +73 -20
  120. package/src/handles/breadcrumbs.ts +66 -0
  121. package/src/handles/index.ts +1 -0
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +21 -15
  124. package/src/host/errors.ts +8 -8
  125. package/src/host/index.ts +4 -7
  126. package/src/host/pattern-matcher.ts +27 -27
  127. package/src/host/router.ts +61 -39
  128. package/src/host/testing.ts +8 -8
  129. package/src/host/types.ts +15 -7
  130. package/src/host/utils.ts +1 -1
  131. package/src/href-client.ts +65 -45
  132. package/src/index.rsc.ts +104 -40
  133. package/src/index.ts +122 -67
  134. package/src/internal-debug.ts +9 -3
  135. package/src/loader.rsc.ts +18 -93
  136. package/src/loader.ts +26 -9
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +4 -2
  140. package/src/prerender/store.ts +121 -17
  141. package/src/prerender.ts +325 -20
  142. package/src/reverse.ts +144 -124
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +7 -4
  145. package/src/route-definition/dsl-helpers.ts +959 -0
  146. package/src/route-definition/helper-factories.ts +200 -0
  147. package/src/route-definition/helpers-types.ts +430 -0
  148. package/src/route-definition/index.ts +52 -0
  149. package/src/route-definition/redirect.ts +93 -0
  150. package/src/route-definition.ts +1 -1450
  151. package/src/route-map-builder.ts +87 -133
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +41 -6
  154. package/src/router/content-negotiation.ts +116 -0
  155. package/src/router/debug-manifest.ts +72 -0
  156. package/src/router/error-handling.ts +9 -9
  157. package/src/router/find-match.ts +160 -0
  158. package/src/router/handler-context.ts +324 -116
  159. package/src/router/intercept-resolution.ts +11 -4
  160. package/src/router/lazy-includes.ts +237 -0
  161. package/src/router/loader-resolution.ts +179 -133
  162. package/src/router/logging.ts +112 -6
  163. package/src/router/manifest.ts +58 -19
  164. package/src/router/match-api.ts +89 -88
  165. package/src/router/match-context.ts +4 -2
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +86 -89
  168. package/src/router/match-middleware/cache-lookup.ts +295 -49
  169. package/src/router/match-middleware/cache-store.ts +56 -13
  170. package/src/router/match-middleware/intercept-resolution.ts +45 -22
  171. package/src/router/match-middleware/segment-resolution.ts +20 -9
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +44 -21
  174. package/src/router/metrics.ts +240 -15
  175. package/src/router/middleware-cookies.ts +55 -0
  176. package/src/router/middleware-types.ts +222 -0
  177. package/src/router/middleware.ts +327 -369
  178. package/src/router/pattern-matching.ts +169 -31
  179. package/src/router/prerender-match.ts +402 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +105 -14
  182. package/src/router/router-context.ts +40 -21
  183. package/src/router/router-interfaces.ts +452 -0
  184. package/src/router/router-options.ts +592 -0
  185. package/src/router/router-registry.ts +24 -0
  186. package/src/router/segment-resolution/fresh.ts +677 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +199 -0
  189. package/src/router/segment-resolution/revalidation.ts +1296 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -1354
  192. package/src/router/segment-wrappers.ts +291 -0
  193. package/src/router/telemetry-otel.ts +299 -0
  194. package/src/router/telemetry.ts +300 -0
  195. package/src/router/timeout.ts +148 -0
  196. package/src/router/trie-matching.ts +96 -29
  197. package/src/router/types.ts +15 -9
  198. package/src/router.ts +642 -2366
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +639 -1027
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +0 -20
  203. package/src/rsc/loader-fetch.ts +209 -0
  204. package/src/rsc/manifest-init.ts +86 -0
  205. package/src/rsc/nonce.ts +14 -0
  206. package/src/rsc/origin-guard.ts +141 -0
  207. package/src/rsc/progressive-enhancement.ts +379 -0
  208. package/src/rsc/response-error.ts +37 -0
  209. package/src/rsc/response-route-handler.ts +347 -0
  210. package/src/rsc/rsc-rendering.ts +237 -0
  211. package/src/rsc/runtime-warnings.ts +42 -0
  212. package/src/rsc/server-action.ts +348 -0
  213. package/src/rsc/ssr-setup.ts +128 -0
  214. package/src/rsc/types.ts +38 -11
  215. package/src/search-params.ts +66 -54
  216. package/src/segment-system.tsx +165 -17
  217. package/src/server/context.ts +237 -54
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +11 -6
  220. package/src/server/handle-store.ts +94 -15
  221. package/src/server/loader-registry.ts +15 -56
  222. package/src/server/request-context.ts +438 -71
  223. package/src/server.ts +26 -164
  224. package/src/ssr/index.tsx +101 -31
  225. package/src/static-handler.ts +22 -4
  226. package/src/theme/ThemeProvider.tsx +21 -15
  227. package/src/theme/ThemeScript.tsx +5 -5
  228. package/src/theme/constants.ts +5 -2
  229. package/src/theme/index.ts +4 -14
  230. package/src/theme/theme-context.ts +4 -30
  231. package/src/theme/theme-script.ts +21 -18
  232. package/src/types/boundaries.ts +158 -0
  233. package/src/types/cache-types.ts +198 -0
  234. package/src/types/error-types.ts +192 -0
  235. package/src/types/global-namespace.ts +100 -0
  236. package/src/types/handler-context.ts +773 -0
  237. package/src/types/index.ts +88 -0
  238. package/src/types/loader-types.ts +183 -0
  239. package/src/types/route-config.ts +170 -0
  240. package/src/types/route-entry.ts +109 -0
  241. package/src/types/segments.ts +150 -0
  242. package/src/types.ts +1 -1795
  243. package/src/urls/include-helper.ts +197 -0
  244. package/src/urls/index.ts +53 -0
  245. package/src/urls/path-helper-types.ts +339 -0
  246. package/src/urls/path-helper.ts +329 -0
  247. package/src/urls/pattern-types.ts +95 -0
  248. package/src/urls/response-types.ts +106 -0
  249. package/src/urls/type-extraction.ts +372 -0
  250. package/src/urls/urls-function.ts +98 -0
  251. package/src/urls.ts +1 -1323
  252. package/src/use-loader.tsx +85 -77
  253. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  254. package/src/vite/discovery/discover-routers.ts +344 -0
  255. package/src/vite/discovery/prerender-collection.ts +385 -0
  256. package/src/vite/discovery/route-types-writer.ts +258 -0
  257. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  258. package/src/vite/discovery/state.ts +108 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -2259
  261. package/src/vite/plugin-types.ts +48 -0
  262. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  263. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  265. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -47
  266. package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +8 -43
  267. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  268. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  269. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  270. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  271. package/src/vite/plugins/expose-ids/types.ts +45 -0
  272. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  273. package/src/vite/plugins/refresh-cmd.ts +65 -0
  274. package/src/vite/plugins/use-cache-transform.ts +323 -0
  275. package/src/vite/plugins/version-injector.ts +83 -0
  276. package/src/vite/plugins/version-plugin.ts +266 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +445 -0
  280. package/src/vite/router-discovery.ts +777 -0
  281. package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +181 -9
  282. package/src/vite/utils/banner.ts +36 -0
  283. package/src/vite/utils/bundle-analysis.ts +137 -0
  284. package/src/vite/utils/manifest-utils.ts +70 -0
  285. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  286. package/src/vite/utils/prerender-utils.ts +189 -0
  287. package/src/vite/utils/shared-utils.ts +169 -0
  288. package/CLAUDE.md +0 -43
  289. package/dist/vite/index.named-routes.gen.ts +0 -103
  290. package/src/browser/lru-cache.ts +0 -69
  291. package/src/browser/request-controller.ts +0 -164
  292. package/src/cache/memory-store.ts +0 -253
  293. package/src/href-context.ts +0 -33
  294. package/src/router.gen.ts +0 -6
  295. package/src/static-handler.gen.ts +0 -5
  296. package/src/urls.gen.ts +0 -8
  297. package/src/vite/expose-internal-ids.ts +0 -1167
  298. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Router Telemetry Sink
3
+ *
4
+ * Internal event model for structured lifecycle events.
5
+ * The sink is optional and zero-cost when not configured.
6
+ *
7
+ * Emit points:
8
+ * - request.start / request.end (match-handlers.ts)
9
+ * - request.error (match-handlers.ts catch blocks)
10
+ * - request.origin-rejected (rsc/handler.ts origin guard)
11
+ * - loader.start / loader.end / loader.error (loader-resolution.ts)
12
+ * - handler.error (trackHandler catch, segment-resolution/helpers.ts)
13
+ * - cache.decision (cache-lookup middleware)
14
+ * - revalidation.decision (revalidation evaluation)
15
+ */
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Event types
19
+ // ---------------------------------------------------------------------------
20
+
21
+ interface BaseEvent {
22
+ /** Monotonic timestamp from performance.now() */
23
+ timestamp: number;
24
+ /** Request ID (from header or generated) */
25
+ requestId?: string;
26
+ }
27
+
28
+ export interface RequestStartEvent extends BaseEvent {
29
+ type: "request.start";
30
+ method: string;
31
+ pathname: string;
32
+ /** "match" for full document requests, "matchPartial" for navigation */
33
+ transaction: "match" | "matchPartial";
34
+ isPartial: boolean;
35
+ }
36
+
37
+ export interface RequestEndEvent extends BaseEvent {
38
+ type: "request.end";
39
+ method: string;
40
+ pathname: string;
41
+ transaction: "match" | "matchPartial";
42
+ durationMs: number;
43
+ segmentCount: number;
44
+ cacheHit: boolean;
45
+ }
46
+
47
+ export interface RequestErrorEvent extends BaseEvent {
48
+ type: "request.error";
49
+ method: string;
50
+ pathname: string;
51
+ transaction: "match" | "matchPartial";
52
+ error: Error;
53
+ phase: string;
54
+ durationMs: number;
55
+ }
56
+
57
+ export interface LoaderStartEvent extends BaseEvent {
58
+ type: "loader.start";
59
+ segmentId: string;
60
+ loaderName: string;
61
+ pathname: string;
62
+ }
63
+
64
+ export interface LoaderEndEvent extends BaseEvent {
65
+ type: "loader.end";
66
+ segmentId: string;
67
+ loaderName: string;
68
+ pathname: string;
69
+ durationMs: number;
70
+ ok: boolean;
71
+ }
72
+
73
+ export interface LoaderErrorEvent extends BaseEvent {
74
+ type: "loader.error";
75
+ segmentId: string;
76
+ loaderName: string;
77
+ pathname: string;
78
+ error: Error;
79
+ handledByBoundary: boolean;
80
+ }
81
+
82
+ export interface HandlerErrorEvent extends BaseEvent {
83
+ type: "handler.error";
84
+ segmentId?: string;
85
+ segmentType?: string;
86
+ error: Error;
87
+ handledByBoundary: boolean;
88
+ pathname?: string;
89
+ routeKey?: string;
90
+ params?: Record<string, string>;
91
+ }
92
+
93
+ export interface CacheDecisionEvent extends BaseEvent {
94
+ type: "cache.decision";
95
+ pathname: string;
96
+ routeKey: string;
97
+ hit: boolean;
98
+ /** Whether stale-while-revalidate was triggered */
99
+ shouldRevalidate: boolean;
100
+ source?: "runtime" | "prerender";
101
+ }
102
+
103
+ export interface RevalidationDecisionEvent extends BaseEvent {
104
+ type: "revalidation.decision";
105
+ segmentId: string;
106
+ pathname: string;
107
+ routeKey: string;
108
+ shouldRevalidate: boolean;
109
+ }
110
+
111
+ export interface RequestTimeoutEvent extends BaseEvent {
112
+ type: "request.timeout";
113
+ phase: import("./timeout.js").TimeoutPhase;
114
+ pathname: string;
115
+ routeKey?: string;
116
+ actionId?: string;
117
+ durationMs: number;
118
+ customHandler: boolean;
119
+ }
120
+
121
+ export interface OriginCheckRejectedEvent extends BaseEvent {
122
+ type: "request.origin-rejected";
123
+ method: string;
124
+ pathname: string;
125
+ phase: import("../rsc/origin-guard.js").OriginCheckPhase;
126
+ origin: string | null;
127
+ host: string | null;
128
+ }
129
+
130
+ export type TelemetryEvent =
131
+ | RequestStartEvent
132
+ | RequestEndEvent
133
+ | RequestErrorEvent
134
+ | LoaderStartEvent
135
+ | LoaderEndEvent
136
+ | LoaderErrorEvent
137
+ | HandlerErrorEvent
138
+ | CacheDecisionEvent
139
+ | RevalidationDecisionEvent
140
+ | RequestTimeoutEvent
141
+ | OriginCheckRejectedEvent;
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Sink interface
145
+ // ---------------------------------------------------------------------------
146
+
147
+ /**
148
+ * Telemetry sink receives structured lifecycle events from the router.
149
+ * Implement this interface to integrate with any observability backend.
150
+ *
151
+ * All methods are fire-and-forget — exceptions are caught and logged.
152
+ */
153
+ export interface TelemetrySink {
154
+ emit(event: TelemetryEvent): void;
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // No-op singleton (zero-cost disabled state)
159
+ // ---------------------------------------------------------------------------
160
+
161
+ const noopSink: TelemetrySink = {
162
+ emit() {},
163
+ };
164
+
165
+ /**
166
+ * Returns the configured sink, or the no-op singleton.
167
+ * Call sites use this so they don't need null checks.
168
+ */
169
+ export function resolveSink(sink: TelemetrySink | undefined): TelemetrySink {
170
+ return sink ?? noopSink;
171
+ }
172
+
173
+ /**
174
+ * Safe emit — catches any error thrown by the sink to prevent
175
+ * telemetry failures from affecting request handling.
176
+ */
177
+ export function safeEmit(sink: TelemetrySink, event: TelemetryEvent): void {
178
+ try {
179
+ sink.emit(event);
180
+ } catch (e) {
181
+ // Telemetry must never break request handling
182
+ if (process.env.NODE_ENV !== "production") {
183
+ console.error("[Router.telemetry] Sink error:", e);
184
+ }
185
+ }
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Request ID extraction (for span correlation)
190
+ // ---------------------------------------------------------------------------
191
+
192
+ // Per-request memoization so the same Request object always maps to the
193
+ // same ID. WeakMap allows GC when the Request is no longer referenced.
194
+ const requestIds = new WeakMap<Request, string>();
195
+ let telemetryRequestCounter = 0;
196
+
197
+ /**
198
+ * Get or create a request ID for telemetry correlation.
199
+ * Checks standard headers first (x-rsc-router-request-id, x-request-id,
200
+ * cf-ray), then generates an internal ID when none is present.
201
+ * Generated IDs use format "t-{base36}" to distinguish from header values.
202
+ */
203
+ export function getRequestId(request: Request): string {
204
+ const existing = requestIds.get(request);
205
+ if (existing) return existing;
206
+
207
+ const candidate =
208
+ request.headers.get("x-rsc-router-request-id") ??
209
+ request.headers.get("x-request-id") ??
210
+ request.headers.get("cf-ray");
211
+
212
+ let id: string;
213
+ if (candidate) {
214
+ const trimmed = candidate.trim();
215
+ id =
216
+ trimmed.length > 0
217
+ ? trimmed
218
+ : `t-${(++telemetryRequestCounter).toString(36)}`;
219
+ } else {
220
+ id = `t-${(++telemetryRequestCounter).toString(36)}`;
221
+ }
222
+
223
+ requestIds.set(request, id);
224
+ return id;
225
+ }
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // Console sink (built-in, replaces ad-hoc console.log debug traces)
229
+ // ---------------------------------------------------------------------------
230
+
231
+ /**
232
+ * Built-in console sink that logs events in a structured format.
233
+ * Designed as the default sink for development / debugging.
234
+ */
235
+ export function createConsoleSink(): TelemetrySink {
236
+ return {
237
+ emit(event: TelemetryEvent): void {
238
+ switch (event.type) {
239
+ case "request.start":
240
+ console.log(
241
+ `[telemetry] ${event.type} ${event.method} ${event.pathname} (${event.transaction})`,
242
+ );
243
+ break;
244
+ case "request.end":
245
+ console.log(
246
+ `[telemetry] ${event.type} ${event.method} ${event.pathname} ${event.durationMs.toFixed(1)}ms segments=${event.segmentCount} cache=${event.cacheHit}`,
247
+ );
248
+ break;
249
+ case "request.error":
250
+ console.log(
251
+ `[telemetry] ${event.type} ${event.method} ${event.pathname} phase=${event.phase} ${event.durationMs.toFixed(1)}ms`,
252
+ event.error.message,
253
+ );
254
+ break;
255
+ case "loader.start":
256
+ console.log(
257
+ `[telemetry] ${event.type} ${event.loaderName} (${event.segmentId})`,
258
+ );
259
+ break;
260
+ case "loader.end":
261
+ console.log(
262
+ `[telemetry] ${event.type} ${event.loaderName} ${event.durationMs.toFixed(1)}ms ok=${event.ok}`,
263
+ );
264
+ break;
265
+ case "loader.error":
266
+ console.log(
267
+ `[telemetry] ${event.type} ${event.loaderName} boundary=${event.handledByBoundary}`,
268
+ event.error.message,
269
+ );
270
+ break;
271
+ case "handler.error":
272
+ console.log(
273
+ `[telemetry] ${event.type} segment=${event.segmentId ?? "unknown"} boundary=${event.handledByBoundary}${event.pathname ? ` ${event.pathname}` : ""}`,
274
+ event.error.message,
275
+ );
276
+ break;
277
+ case "cache.decision":
278
+ console.log(
279
+ `[telemetry] ${event.type} ${event.pathname} hit=${event.hit} swr=${event.shouldRevalidate}${event.source ? ` source=${event.source}` : ""}`,
280
+ );
281
+ break;
282
+ case "revalidation.decision":
283
+ console.log(
284
+ `[telemetry] ${event.type} ${event.segmentId} revalidate=${event.shouldRevalidate}`,
285
+ );
286
+ break;
287
+ case "request.timeout":
288
+ console.log(
289
+ `[telemetry] ${event.type} phase=${event.phase} ${event.pathname} ${event.durationMs.toFixed(1)}ms custom=${event.customHandler}`,
290
+ );
291
+ break;
292
+ case "request.origin-rejected":
293
+ console.log(
294
+ `[telemetry] ${event.type} ${event.method} ${event.pathname} phase=${event.phase} origin=${event.origin ?? "none"} host=${event.host ?? "none"}`,
295
+ );
296
+ break;
297
+ }
298
+ },
299
+ };
300
+ }
@@ -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
+ }
@@ -43,13 +43,22 @@ export function tryTrieMatch(
43
43
  if (!trie) return null;
44
44
 
45
45
  // Split pathname into segments, filtering empty strings from leading/trailing slashes
46
- const pathnameHasTrailingSlash = pathname.length > 1 && pathname.endsWith("/");
47
- const normalizedPath = pathnameHasTrailingSlash ? pathname.slice(0, -1) : pathname;
46
+ const pathnameHasTrailingSlash =
47
+ pathname.length > 1 && pathname.endsWith("/");
48
+ const normalizedPath = pathnameHasTrailingSlash
49
+ ? pathname.slice(0, -1)
50
+ : pathname;
48
51
 
49
52
  // Handle root path
50
53
  if (normalizedPath === "" || normalizedPath === "/") {
51
54
  if (trie.r) {
52
- return validateAndBuild(trie.r, {}, pathname, pathnameHasTrailingSlash);
55
+ return validateAndBuild(
56
+ trie.r,
57
+ [],
58
+ undefined,
59
+ pathname,
60
+ pathnameHasTrailingSlash,
61
+ );
53
62
  }
54
63
  return null;
55
64
  }
@@ -58,9 +67,15 @@ export function tryTrieMatch(
58
67
  const segments = normalizedPath.slice(1).split("/");
59
68
 
60
69
  // Try exact match with normalized path (no trailing slash)
61
- const result = walkTrie(trie, segments, 0, {});
70
+ const result = walkTrie(trie, segments, 0, []);
62
71
  if (result) {
63
- return validateAndBuild(result.leaf, result.params, pathname, pathnameHasTrailingSlash);
72
+ return validateAndBuild(
73
+ result.leaf,
74
+ result.paramValues,
75
+ result.wildcardValue,
76
+ pathname,
77
+ pathnameHasTrailingSlash,
78
+ );
64
79
  }
65
80
 
66
81
  return null;
@@ -68,7 +83,8 @@ export function tryTrieMatch(
68
83
 
69
84
  interface WalkResult {
70
85
  leaf: TrieLeaf;
71
- params: Record<string, string>;
86
+ paramValues: string[];
87
+ wildcardValue?: string;
72
88
  }
73
89
 
74
90
  /**
@@ -79,57 +95,101 @@ function walkTrie(
79
95
  node: TrieNode,
80
96
  segments: string[],
81
97
  index: number,
82
- params: Record<string, string>,
98
+ paramValues: string[],
83
99
  ): WalkResult | null {
84
100
  // All segments consumed: check for terminal
85
101
  if (index === segments.length) {
86
102
  if (node.r) {
87
- return { leaf: node.r, params };
103
+ return { leaf: node.r, paramValues: [...paramValues] };
88
104
  }
89
105
  return null;
90
106
  }
91
107
 
92
108
  const segment = segments[index];
109
+ const staticChild = node.s?.[segment];
93
110
 
94
111
  // Priority 1: Static match
95
- if (node.s?.[segment]) {
96
- const result = walkTrie(node.s[segment], segments, index + 1, params);
112
+ if (staticChild) {
113
+ const result = walkTrie(staticChild, segments, index + 1, paramValues);
97
114
  if (result) return result;
98
115
  }
99
116
 
100
- // 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
101
136
  if (node.p) {
102
- const result = walkTrie(node.p.c, segments, index + 1, {
103
- ...params,
104
- [node.p.n]: segment,
105
- });
137
+ paramValues.push(segment);
138
+ const result = walkTrie(node.p.c, segments, index + 1, paramValues);
139
+ paramValues.pop();
106
140
  if (result) return result;
107
141
  }
108
142
 
109
- // Priority 3: Wildcard match (consumes rest)
143
+ // Priority 4: Wildcard match (consumes rest)
110
144
  if (node.w) {
111
- const rest = segments.slice(index).join("/");
145
+ const rest = joinRemainingSegments(segments, index);
112
146
  return {
113
147
  leaf: node.w,
114
- params: { ...params, [node.w.pn]: rest },
148
+ paramValues: [...paramValues],
149
+ wildcardValue: rest,
115
150
  };
116
151
  }
117
152
 
118
153
  return null;
119
154
  }
120
155
 
156
+ function joinRemainingSegments(segments: string[], start: number): string {
157
+ if (start >= segments.length) return "";
158
+
159
+ let rest = segments[start]!;
160
+ for (let i = start + 1; i < segments.length; i++) {
161
+ rest += "/" + segments[i]!;
162
+ }
163
+ return rest;
164
+ }
165
+
121
166
  /**
122
167
  * Post-match: validate constraints and handle trailing slash logic.
123
168
  */
124
169
  function validateAndBuild(
125
170
  leaf: TrieLeaf,
126
- params: Record<string, string>,
171
+ paramValues: string[],
172
+ wildcardValue: string | undefined,
127
173
  originalPathname: string,
128
174
  pathnameHasTrailingSlash: boolean,
129
175
  ): TrieMatchResult | null {
176
+ // Build named params by zipping leaf.pa with positional paramValues
177
+ const params: Record<string, string> = {};
178
+ if (leaf.pa) {
179
+ for (let i = 0; i < leaf.pa.length && i < paramValues.length; i++) {
180
+ params[leaf.pa[i]] = paramValues[i];
181
+ }
182
+ }
183
+
184
+ // Add wildcard param (wildcard leaves have pn from TrieNode.w type)
185
+ if (wildcardValue !== undefined && "pn" in leaf) {
186
+ params[(leaf as TrieLeaf & { pn: string }).pn] = wildcardValue;
187
+ }
188
+
130
189
  // Validate constraints
131
190
  if (leaf.cv) {
132
- for (const [paramName, allowed] of Object.entries(leaf.cv)) {
191
+ for (const paramName in leaf.cv) {
192
+ const allowed = leaf.cv[paramName]!;
133
193
  const value = params[paramName];
134
194
  if (value !== undefined && value !== "" && !allowed.includes(value)) {
135
195
  return null;
@@ -150,23 +210,30 @@ function validateAndBuild(
150
210
  const tsMode = leaf.ts as "never" | "always" | "ignore" | undefined;
151
211
  let redirectTo: string | undefined;
152
212
 
153
- if (tsMode === "always" && !pathnameHasTrailingSlash && originalPathname !== "/") {
213
+ if (
214
+ tsMode === "always" &&
215
+ !pathnameHasTrailingSlash &&
216
+ originalPathname !== "/"
217
+ ) {
154
218
  redirectTo = originalPathname + "/";
155
219
  } else if (tsMode === "never" && pathnameHasTrailingSlash) {
156
220
  redirectTo = originalPathname.slice(0, -1);
157
221
  }
158
222
 
159
- return {
223
+ const result: TrieMatchResult = {
160
224
  routeKey: leaf.n,
161
225
  sp: leaf.sp,
162
226
  params,
163
- optionalParams: leaf.op,
164
227
  ancestry: leaf.a,
165
- ...(redirectTo ? { redirectTo } : {}),
166
- ...(leaf.pr ? { pr: true } : {}),
167
- ...(leaf.pt ? { pt: true } : {}),
168
- ...(leaf.rt ? { responseType: leaf.rt } : {}),
169
- ...(leaf.nv ? { negotiateVariants: leaf.nv } : {}),
170
- ...(leaf.rf ? { rscFirst: true } : {}),
171
228
  };
229
+
230
+ if (leaf.op) result.optionalParams = leaf.op;
231
+ if (redirectTo) result.redirectTo = redirectTo;
232
+ if (leaf.pr) result.pr = true;
233
+ if (leaf.pt) result.pt = true;
234
+ if (leaf.rt) result.responseType = leaf.rt;
235
+ if (leaf.nv) result.negotiateVariants = leaf.nv;
236
+ if (leaf.rf) result.rscFirst = true;
237
+
238
+ return result;
172
239
  }
@@ -5,7 +5,11 @@
5
5
  */
6
6
 
7
7
  import type { ReactNode } from "react";
8
- import type { EntryData, InterceptEntry, InterceptSelectorContext } from "../server/context";
8
+ import type {
9
+ EntryData,
10
+ InterceptEntry,
11
+ InterceptSelectorContext,
12
+ } from "../server/context";
9
13
  import type {
10
14
  ErrorInfo,
11
15
  ErrorPhase,
@@ -52,10 +56,10 @@ export type ActionContext = {
52
56
  */
53
57
  export interface RouterDependencies<TEnv> {
54
58
  findNearestErrorBoundary: (
55
- entry: EntryData | null
59
+ entry: EntryData | null,
56
60
  ) => ReactNode | ErrorBoundaryHandler | null;
57
61
  findNearestNotFoundBoundary: (
58
- entry: EntryData | null
62
+ entry: EntryData | null,
59
63
  ) => ReactNode | NotFoundBoundaryHandler | null;
60
64
  }
61
65
 
@@ -79,18 +83,20 @@ export interface SegmentResolutionDeps<TEnv = any> {
79
83
  requestStartTime?: number;
80
84
  },
81
85
  ) => Promise<LoaderDataResult<T>>;
82
- 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>;
83
93
  findNearestErrorBoundary: (
84
94
  entry: EntryData | null,
85
95
  ) => ReactNode | ErrorBoundaryHandler | null;
86
96
  findNearestNotFoundBoundary: (
87
97
  entry: EntryData | null,
88
98
  ) => ReactNode | NotFoundBoundaryHandler | null;
89
- callOnError: (
90
- error: unknown,
91
- phase: ErrorPhase,
92
- context: any,
93
- ) => void;
99
+ callOnError: (error: unknown, phase: ErrorPhase, context: any) => void;
94
100
  }
95
101
 
96
102
  /**