@rangojs/router 0.0.0-experimental.d20dd405 → 0.0.0-experimental.dacec167

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 (212) hide show
  1. package/README.md +8 -8
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +914 -485
  5. package/package.json +55 -11
  6. package/skills/bundle-analysis/SKILL.md +159 -0
  7. package/skills/cache-guide/SKILL.md +220 -30
  8. package/skills/caching/SKILL.md +116 -8
  9. package/skills/composability/SKILL.md +27 -2
  10. package/skills/document-cache/SKILL.md +78 -55
  11. package/skills/handler-use/SKILL.md +1 -1
  12. package/skills/hooks/SKILL.md +196 -21
  13. package/skills/host-router/SKILL.md +45 -20
  14. package/skills/intercept/SKILL.md +1 -4
  15. package/skills/layout/SKILL.md +4 -7
  16. package/skills/links/SKILL.md +22 -10
  17. package/skills/loader/SKILL.md +149 -6
  18. package/skills/middleware/SKILL.md +13 -9
  19. package/skills/migrate-nextjs/SKILL.md +1 -1
  20. package/skills/mime-routes/SKILL.md +27 -0
  21. package/skills/observability/SKILL.md +137 -0
  22. package/skills/parallel/SKILL.md +3 -6
  23. package/skills/prerender/SKILL.md +14 -33
  24. package/skills/rango/SKILL.md +242 -26
  25. package/skills/react-compiler/SKILL.md +168 -0
  26. package/skills/response-routes/SKILL.md +58 -9
  27. package/skills/route/SKILL.md +9 -4
  28. package/skills/router-setup/SKILL.md +3 -3
  29. package/skills/server-actions/SKILL.md +53 -41
  30. package/skills/testing/SKILL.md +778 -0
  31. package/skills/typesafety/SKILL.md +310 -26
  32. package/skills/use-cache/SKILL.md +34 -5
  33. package/skills/view-transitions/SKILL.md +85 -3
  34. package/src/__augment-tests__/augment.ts +81 -0
  35. package/src/__augment-tests__/augmented.check.ts +117 -0
  36. package/src/browser/action-coordinator.ts +53 -36
  37. package/src/browser/event-controller.ts +42 -66
  38. package/src/browser/history-state.ts +21 -0
  39. package/src/browser/index.ts +3 -3
  40. package/src/browser/navigation-bridge.ts +9 -67
  41. package/src/browser/navigation-client.ts +12 -15
  42. package/src/browser/navigation-store.ts +7 -8
  43. package/src/browser/navigation-transaction.ts +10 -28
  44. package/src/browser/partial-update.ts +8 -16
  45. package/src/browser/react/NavigationProvider.tsx +55 -65
  46. package/src/browser/react/location-state-shared.ts +175 -4
  47. package/src/browser/react/location-state.ts +39 -13
  48. package/src/browser/react/use-handle.ts +17 -9
  49. package/src/browser/react/use-params.ts +3 -4
  50. package/src/browser/react/use-reverse.ts +19 -12
  51. package/src/browser/react/use-router.ts +14 -1
  52. package/src/browser/response-adapter.ts +25 -0
  53. package/src/browser/rsc-router.tsx +30 -16
  54. package/src/browser/scroll-restoration.ts +30 -25
  55. package/src/browser/segment-structure-assert.ts +2 -2
  56. package/src/browser/server-action-bridge.ts +23 -30
  57. package/src/browser/types.ts +2 -0
  58. package/src/build/collect-fallback-refs.ts +107 -0
  59. package/src/build/generate-manifest.ts +60 -35
  60. package/src/build/generate-route-types.ts +2 -0
  61. package/src/build/index.ts +2 -0
  62. package/src/build/route-types/codegen.ts +4 -4
  63. package/src/build/route-types/include-resolution.ts +1 -1
  64. package/src/build/route-types/per-module-writer.ts +7 -4
  65. package/src/build/route-types/router-processing.ts +55 -14
  66. package/src/build/route-types/scan-filter.ts +1 -1
  67. package/src/build/route-types/source-scan.ts +118 -0
  68. package/src/build/runtime-discovery.ts +9 -20
  69. package/src/cache/cache-scope.ts +28 -42
  70. package/src/cache/cf/cf-cache-store.ts +49 -6
  71. package/src/client.tsx +5 -7
  72. package/src/context-var.ts +5 -5
  73. package/src/decode-loader-results.ts +36 -0
  74. package/src/errors.ts +30 -1
  75. package/src/handle.ts +26 -13
  76. package/src/host/index.ts +2 -2
  77. package/src/host/router.ts +129 -57
  78. package/src/host/types.ts +31 -2
  79. package/src/host/utils.ts +1 -1
  80. package/src/href-client.ts +136 -19
  81. package/src/index.rsc.ts +6 -4
  82. package/src/index.ts +13 -6
  83. package/src/loader-store.ts +500 -0
  84. package/src/loader.rsc.ts +21 -6
  85. package/src/loader.ts +3 -10
  86. package/src/missing-id-error.ts +68 -0
  87. package/src/prerender.ts +4 -4
  88. package/src/response-utils.ts +9 -0
  89. package/src/reverse.ts +16 -13
  90. package/src/route-content-wrapper.tsx +6 -28
  91. package/src/route-definition/dsl-helpers.ts +238 -263
  92. package/src/route-definition/helper-factories.ts +29 -139
  93. package/src/route-definition/helpers-types.ts +37 -14
  94. package/src/route-definition/use-item-types.ts +32 -0
  95. package/src/route-types.ts +19 -41
  96. package/src/router/basename.ts +14 -0
  97. package/src/router/content-negotiation.ts +15 -2
  98. package/src/router/error-handling.ts +1 -1
  99. package/src/router/intercept-resolution.ts +4 -18
  100. package/src/router/lazy-includes.ts +2 -2
  101. package/src/router/loader-resolution.ts +16 -2
  102. package/src/router/match-handlers.ts +62 -20
  103. package/src/router/match-middleware/cache-lookup.ts +44 -91
  104. package/src/router/match-middleware/cache-store.ts +3 -2
  105. package/src/router/match-result.ts +32 -30
  106. package/src/router/metrics.ts +1 -1
  107. package/src/router/middleware-types.ts +1 -1
  108. package/src/router/middleware.ts +46 -78
  109. package/src/router/prerender-match.ts +1 -1
  110. package/src/router/preview-match.ts +3 -1
  111. package/src/router/request-classification.ts +4 -28
  112. package/src/router/revalidation.ts +43 -1
  113. package/src/router/router-interfaces.ts +45 -28
  114. package/src/router/router-options.ts +40 -1
  115. package/src/router/router-registry.ts +2 -5
  116. package/src/router/segment-resolution/fresh.ts +19 -6
  117. package/src/router/segment-resolution/revalidation.ts +19 -6
  118. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  119. package/src/router/telemetry.ts +99 -0
  120. package/src/router/types.ts +8 -0
  121. package/src/router.ts +37 -21
  122. package/src/rsc/handler-context.ts +2 -2
  123. package/src/rsc/handler.ts +20 -65
  124. package/src/rsc/helpers.ts +22 -2
  125. package/src/rsc/index.ts +1 -1
  126. package/src/rsc/origin-guard.ts +28 -10
  127. package/src/rsc/response-route-handler.ts +32 -52
  128. package/src/rsc/rsc-rendering.ts +27 -53
  129. package/src/rsc/runtime-warnings.ts +9 -10
  130. package/src/rsc/server-action.ts +13 -37
  131. package/src/rsc/ssr-setup.ts +16 -0
  132. package/src/rsc/types.ts +2 -2
  133. package/src/search-params.ts +4 -4
  134. package/src/segment-system.tsx +64 -49
  135. package/src/serialize.ts +243 -0
  136. package/src/server/context.ts +118 -51
  137. package/src/server/cookie-store.ts +28 -4
  138. package/src/server/request-context.ts +10 -0
  139. package/src/static-handler.ts +1 -1
  140. package/src/testing/cache-status.ts +166 -0
  141. package/src/testing/collect-handle.ts +63 -0
  142. package/src/testing/dispatch.ts +440 -0
  143. package/src/testing/dom.entry.ts +22 -0
  144. package/src/testing/e2e/fixture.ts +154 -0
  145. package/src/testing/e2e/index.ts +149 -0
  146. package/src/testing/e2e/matchers.ts +51 -0
  147. package/src/testing/e2e/page-helpers.ts +272 -0
  148. package/src/testing/e2e/parity.ts +306 -0
  149. package/src/testing/e2e/server.ts +183 -0
  150. package/src/testing/flight-matchers.ts +104 -0
  151. package/src/testing/flight-runtime.d.ts +57 -0
  152. package/src/testing/flight-tree.ts +320 -0
  153. package/src/testing/flight.entry.ts +39 -0
  154. package/src/testing/flight.ts +197 -0
  155. package/src/testing/generated-routes.ts +223 -0
  156. package/src/testing/index.ts +106 -0
  157. package/src/testing/internal/context.ts +331 -0
  158. package/src/testing/internal/flight-client-globals.ts +30 -0
  159. package/src/testing/render-route.tsx +565 -0
  160. package/src/testing/run-loader.ts +341 -0
  161. package/src/testing/run-middleware.ts +188 -0
  162. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  163. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  164. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  165. package/src/testing/vitest-stubs/version.ts +5 -0
  166. package/src/testing/vitest.ts +270 -0
  167. package/src/types/global-namespace.ts +39 -26
  168. package/src/types/handler-context.ts +56 -11
  169. package/src/types/index.ts +1 -0
  170. package/src/types/segments.ts +18 -1
  171. package/src/urls/include-helper.ts +10 -53
  172. package/src/urls/index.ts +0 -3
  173. package/src/urls/path-helper-types.ts +11 -3
  174. package/src/urls/path-helper.ts +17 -52
  175. package/src/urls/pattern-types.ts +36 -19
  176. package/src/urls/response-types.ts +20 -19
  177. package/src/urls/type-extraction.ts +26 -116
  178. package/src/urls/urls-function.ts +1 -5
  179. package/src/use-loader.tsx +413 -42
  180. package/src/vite/debug.ts +1 -0
  181. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  182. package/src/vite/discovery/discover-routers.ts +70 -48
  183. package/src/vite/discovery/discovery-errors.ts +194 -0
  184. package/src/vite/discovery/prerender-collection.ts +19 -25
  185. package/src/vite/discovery/route-types-writer.ts +40 -84
  186. package/src/vite/discovery/state.ts +33 -0
  187. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  188. package/src/vite/index.ts +2 -0
  189. package/src/vite/plugin-types.ts +67 -0
  190. package/src/vite/plugins/cjs-to-esm.ts +3 -7
  191. package/src/vite/plugins/client-ref-hashing.ts +12 -1
  192. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
  193. package/src/vite/plugins/expose-action-id.ts +2 -2
  194. package/src/vite/plugins/expose-id-utils.ts +12 -8
  195. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  196. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  197. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  198. package/src/vite/plugins/expose-internal-ids.ts +47 -67
  199. package/src/vite/plugins/performance-tracks.ts +12 -16
  200. package/src/vite/plugins/use-cache-transform.ts +13 -11
  201. package/src/vite/plugins/version-injector.ts +2 -12
  202. package/src/vite/plugins/version-plugin.ts +59 -2
  203. package/src/vite/plugins/virtual-entries.ts +2 -2
  204. package/src/vite/rango.ts +67 -15
  205. package/src/vite/router-discovery.ts +208 -63
  206. package/src/vite/utils/ast-handler-extract.ts +15 -15
  207. package/src/vite/utils/bundle-analysis.ts +4 -2
  208. package/src/vite/utils/client-chunks.ts +190 -0
  209. package/src/vite/utils/forward-user-plugins.ts +193 -0
  210. package/src/vite/utils/manifest-utils.ts +21 -5
  211. package/src/vite/utils/shared-utils.ts +107 -26
  212. package/src/browser/action-response-classifier.ts +0 -99
@@ -91,6 +91,20 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
91
91
  ]);
92
92
  ```
93
93
 
94
+ > **Client refresh `key` vs. server `cache({ key })` vs. `revalidate()`.** Three
95
+ > different "what refreshes" knobs that are easy to confuse:
96
+ >
97
+ > - `useLoader(Loader, { key })` / `useFetchLoader(Loader, { key })` — a
98
+ > **client** refresh identity. It groups which mounted reads of one loader
99
+ > refresh together when one calls `load()`. It never touches the server
100
+ > request. For refreshing **different** loaders together, tag them with
101
+ > `{ refreshGroup }` (one name or several) and call `useRefreshLoaders()(name)`
102
+ > (plain GET only). See the hooks skill ("Scoping refetch with a `key`" and
103
+ > "Refreshing multiple loaders together").
104
+ > - `cache({ key })` — a **server** cache identity (storage hit/miss/ttl/swr).
105
+ > - `revalidate()` — which **server** segments/loaders recompute during
106
+ > navigation and action refreshes.
107
+
94
108
  DSL loaders are the **live data layer** — they resolve fresh on every
95
109
  request, even when the route is inside a `cache()` boundary. The router
96
110
  excludes them from the segment cache at storage time and re-resolves them
@@ -185,7 +199,7 @@ export const ProductLoader = createLoader(async (ctx) => {
185
199
  // Request headers
186
200
  const auth = ctx.request.headers.get("Authorization");
187
201
 
188
- // Variables set by middleware (from RSCRouter.Vars augmentation)
202
+ // Variables set by middleware (from Rango.Vars augmentation)
189
203
  const user = ctx.get("user");
190
204
 
191
205
  // Type-checked URLs for payloads. `.name` resolves within the current
@@ -244,15 +258,23 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
244
258
  revalidate(() => false), // Never revalidate
245
259
  ]),
246
260
 
247
- // Loader that revalidates after cart actions
261
+ // Loader that revalidates after cart actions (defer otherwise — keeps the
262
+ // permissive loader defaults for navigation and other actions intact)
248
263
  loader(CartLoader, () => [
249
- revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
264
+ revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
250
265
  ]),
251
266
  ]);
252
267
  ```
253
268
 
254
269
  ### `revalidate()` return shapes
255
270
 
271
+ > **Scope: `revalidate()` is a partial-render concern, not a cache concern.**
272
+ > It decides whether a segment (here, a loader) re-runs and streams to the
273
+ > client on a navigation or action — never whether a cached value is stale. The
274
+ > cache decides hit/miss/ttl/swr independently and never reads `revalidate()`.
275
+ > Caching a loader is a separate, opt-in step (`loader(Fn, () => [cache({...})])`).
276
+ > See `/cache-guide` → "Two axes" and `/rango` → "The shape of rango".
277
+
256
278
  A `revalidate(fn)` callback can return one of four shapes. The chain
257
279
  processes revalidators in order; each call's return controls how the
258
280
  chain continues:
@@ -282,6 +304,58 @@ revalidate(() => null); // explicit defer
282
304
  If every revalidator on a segment defers, the segment-type default
283
305
  (e.g. params-changed for routes, `false` for parallels) is used.
284
306
 
307
+ #### `|| undefined` (defer) vs `?? false` (hard) — pick deliberately
308
+
309
+ A boolean return — including `false` — is a **hard** decision: it short-circuits
310
+ the chain and overrides the segment default. `undefined` **defers** to the
311
+ running suggestion / segment default. They are not interchangeable:
312
+
313
+ ```typescript
314
+ // Defer: "revalidate on match, otherwise let the default/downstream decide."
315
+ revalidate(({ actionId }) => actionId?.includes("Cart") || undefined);
316
+
317
+ // Hard: "revalidate ONLY on match, suppress everything else."
318
+ revalidate(({ actionId }) => actionId?.includes("Cart") ?? false);
319
+ ```
320
+
321
+ This matters most for loaders, whose defaults are permissive: a loader defaults
322
+ to revalidating on **any** action (`POST`) and on **param/search changes**
323
+ during navigation. So `?? false` on a loader silently suppresses both — the
324
+ loader will not refetch when you navigate to a different `:id`. Use
325
+ `|| undefined` when you want to _add_ a revalidation signal on top of the
326
+ sensible defaults, and reserve `?? false` for the rare case where you genuinely
327
+ want the loader to refetch on nothing but your matched action.
328
+
329
+ When **composing multiple revalidators** on one segment (see below), defer is
330
+ mandatory: the first hard `?? false` ends the chain and the later contracts
331
+ never run.
332
+
333
+ #### Matching actions: `ctx.isAction()`
334
+
335
+ To revalidate after specific server actions, match them by **reference** with
336
+ `ctx.isAction()` rather than hand-written `actionId` substrings. A rename or
337
+ moved file then becomes a type error instead of silently failing to match:
338
+
339
+ ```typescript
340
+ import { addToCart, removeFromCart } from "../actions/cart";
341
+ import * as CartActions from "../actions/cart";
342
+
343
+ loader(CartLoader, () => [
344
+ revalidate((ctx) => ctx.isAction(addToCart) || undefined), // one action
345
+ ]);
346
+ revalidate((ctx) => ctx.isAction(addToCart, removeFromCart) || undefined); // several
347
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined); // any action in the module
348
+ ```
349
+
350
+ `isAction()` is a method on the revalidate predicate's **context argument** —
351
+ there is no standalone `isAction` import; you always reach it through the callback
352
+ parameter (`revalidate((ctx) => ctx.isAction(...))`). It returns a raw boolean, so
353
+ pair it with `|| undefined` for the usual "revalidate on match, else defer"
354
+ intent. It returns `false` on plain navigation and on non-matches, and resolves
355
+ the reference the same way the router derives `actionId` (`$id` in production,
356
+ `$$id` in dev), so it matches in both modes. The raw `actionId` string stays
357
+ available on the same context as an escape hatch.
358
+
285
359
  ### Revalidation Contracts for Loader Dependencies
286
360
 
287
361
  If a loader reads `ctx.get()` data produced by an outer handler/layout, share
@@ -289,8 +363,12 @@ the same named revalidation contract across producer and consumer segments.
289
363
 
290
364
  ```typescript
291
365
  // revalidation-contracts.ts
292
- export const revalidateAccountScope = ({ actionId }) =>
293
- actionId?.includes("src/actions/account.ts#") ?? false;
366
+ import * as AccountActions from "./actions/account";
367
+
368
+ // Match by reference with ctx.isAction() (rename-safe), and defer (|| undefined)
369
+ // so these contracts compose — a hard `false` would short-circuit the rest.
370
+ export const revalidateAccountScope = (ctx) =>
371
+ ctx.isAction(AccountActions) || undefined;
294
372
 
295
373
  layout(AccountLayout, () => [
296
374
  revalidate(revalidateAccountScope), // producer reruns
@@ -333,6 +411,64 @@ follows the same rule: at build time, loaders are skipped entirely (there is no
333
411
  real request context), and at runtime the worker resolves them fresh against
334
412
  the live database.
335
413
 
414
+ ### Parallel and streaming — latency overlaps first paint
415
+
416
+ Loaders do not block the page. As the render pass begins — the pass that route
417
+ middleware wraps, so loaders run right after middleware, not in a later
418
+ phase — every matched loader is kicked off **concurrently** (their promises start in the
419
+ same tick), and each result is **streamed** to the client as its own RSC Flight
420
+ chunk rather than awaited up front. Pair a loader with `loading()` (or a
421
+ client `<Suspense>`) and the shell paints immediately while the data streams in.
422
+
423
+ This is why **"cached UI still pays full data latency" is the wrong intuition**:
424
+ on a `cache()` hit the UI segments stream instantly from cache while the live
425
+ loaders resolve fresh **in parallel** — data latency _overlaps_ first paint
426
+ instead of being added on top of it. (Without a `loading()` / `<Suspense>`
427
+ boundary a parallel loader blocks its parent, so add one to keep the overlap.)
428
+
429
+ If you come from a framework where the loader is a blocking step that runs
430
+ before the response is built, this is the shift to internalize: here the
431
+ response starts streaming first and loader data fills in.
432
+
433
+ ### See it: `debugPerformance`
434
+
435
+ Turn on the per-request performance timeline early — it is the fastest way to
436
+ confirm loaders overlap rather than serialize, and to find the real bottleneck
437
+ locally instead of guessing:
438
+
439
+ ```typescript
440
+ const router = createRouter({ document: Document, debugPerformance: true });
441
+ ```
442
+
443
+ Or enable it per-request from middleware (e.g. only when `?debug` is present) by
444
+ calling `ctx.debugPerformance()` **before** `await next()`. Each HTML request
445
+ then prints a shared-axis waterfall (and emits a `Server-Timing` header):
446
+
447
+ ```
448
+ [RSC Perf] GET /product/widget (24.53ms)
449
+ start dur span timeline
450
+ 0.08ms 3.20ms route-matching |#####...................................|
451
+ 3.40ms 8.70ms ssr-render-html |.....##############.....................|
452
+ 3.42ms 11.90ms loader:…#ProductLoader |.....###################................|
453
+ 3.45ms 11.40ms loader:…#ReviewsLoader |.....##################.................|
454
+ 0.00ms 24.53ms handler:total |########################################|
455
+ ```
456
+
457
+ How to read it:
458
+
459
+ - **Humans:** scan the `#` bars on the shared axis. Bars that start at the same
460
+ offset and run side by side are executing **in parallel** — loaders should
461
+ overlap `ssr-render-html` / `render:total`, not sit alone to the right of
462
+ everything. A lone `loader:*` bar past the render bar is serialized latency to
463
+ chase. `handler:total` is the whole request; `render:total` is the render pass.
464
+ - **LLMs / programmatic:** read each row as `{ start, dur, label }`. A loader
465
+ overlaps paint when its `[start, start+dur]` interval intersects
466
+ `render:total` / `ssr-render-html`. Flag a regression when a `loader:*`
467
+ interval is **disjoint from and starts after** `render:total`, or when its
468
+ `dur` approaches `handler:total` — that loader is on the critical path instead
469
+ of overlapping it. Two `loader:*` rows with near-equal `start` confirm
470
+ parallel execution.
471
+
336
472
  ### Opting a Loader into Caching
337
473
 
338
474
  To cache a specific loader's data, attach a `cache()` child:
@@ -606,6 +742,13 @@ export const FileUploadLoader = createLoader(async (ctx) => {
606
742
 
607
743
  Client usage — see `/hooks useFetchLoader` for the full client-side pattern.
608
744
 
745
+ > **Refetch sharing**: when the loader is registered on the route via
746
+ > `loader()`, a plain `load()` call (no `params`, no `body`) broadcasts
747
+ > the new value to every component reading the same loader id —
748
+ > `useLoader` reads in layouts, pages, and parallel slots all converge.
749
+ > Calls with `params` or a non-GET method stay local to the call site.
750
+ > See `/hooks` → "Shared refetch behavior" for the full contract.
751
+
609
752
  ## Complete Example
610
753
 
611
754
  ```typescript
@@ -641,7 +784,7 @@ export const CartLoader = createLoader(async (ctx) => {
641
784
  export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
642
785
  layout(<ShopLayout />, () => [
643
786
  loader(CartLoader, () => [
644
- revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
787
+ revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
645
788
  ]),
646
789
 
647
790
  path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
@@ -10,9 +10,6 @@ Middleware runs before/after route handlers using the onion model.
10
10
 
11
11
  ## Execution Model
12
12
 
13
- Canonical semantics reference:
14
- [docs/execution-model.md](../../docs/internal/execution-model.md)
15
-
16
13
  There are two levels of middleware with different execution scopes:
17
14
 
18
15
  ### Global middleware (`router.use()`)
@@ -36,15 +33,22 @@ Registered inside `urls()` callback. Wraps **rendering only** -- it does NOT wra
36
33
 
37
34
  ```
38
35
  Request flow (with action):
39
- global mw -> action executes -> route mw -> layout -> handler -> loaders
36
+ global mw -> action executes -> route mw -> render pass
40
37
 
41
38
  Request flow (no action):
42
- global mw -> route mw -> layout -> handler -> loaders
39
+ global mw -> route mw -> render pass
43
40
 
44
41
  Progressive enhancement (no-JS form POST):
45
42
  global mw -> action executes -> route mw -> full page re-render
46
43
  ```
47
44
 
45
+ The **render pass** resolves handler, layouts, parallels, and loaders together —
46
+ it is not a handler-then-loaders sequence. Handler-first ordering is guaranteed
47
+ only between a route handler and its child/orphan layouts and parallels (so
48
+ `ctx.set` is visible); loaders run **concurrently** and stream their results, so
49
+ their latency overlaps rendering rather than blocking it. See `/loader` →
50
+ "Parallel and streaming".
51
+
48
52
  The contract is: **route middleware wraps rendering regardless of transport** (JS-enabled RSC stream or no-JS HTML). During PE re-render, route middleware observes action-set state (cookies, context variables) the same way it does during JS-enabled post-action revalidation.
49
53
 
50
54
  Revalidation is still partial. Route middleware wraps the render pass that
@@ -64,7 +68,7 @@ and consumer segments, even when middleware is present in the chain.
64
68
 
65
69
  ```typescript
66
70
  export const revalidateCartData = ({ actionId }) =>
67
- actionId?.includes("src/actions/cart.ts#") ?? false;
71
+ actionId?.includes("src/actions/cart.ts#") || undefined;
68
72
 
69
73
  layout(CartLayout, () => [
70
74
  middleware(cartRenderMiddleware),
@@ -192,7 +196,7 @@ export const myMiddleware: Middleware = async (ctx, next) => {
192
196
  ctx.env.DB; // D1Database
193
197
  ctx.env.KV; // KVNamespace
194
198
 
195
- // Set variables for downstream handlers (typed via RSCRouter.Vars)
199
+ // Set variables for downstream handlers (typed via Rango.Vars)
196
200
  ctx.set("user", { id: "123", name: "John" });
197
201
 
198
202
  // Continue to next middleware/handler
@@ -233,8 +237,8 @@ const Dashboard: Handler<"dashboard"> = (ctx) => {
233
237
  ```
234
238
 
235
239
  This works alongside `ctx.get("key")` / `ctx.set("key", value)` (global typing
236
- via RSCRouter.Vars augmentation). Use `createVar` for route-local or feature-scoped
237
- data; use RSCRouter.Vars for app-wide middleware state.
240
+ via Rango.Vars augmentation). Use `createVar` for route-local or feature-scoped
241
+ data; use Rango.Vars for app-wide middleware state.
238
242
 
239
243
  ## Redirect with State in Middleware
240
244
 
@@ -302,7 +302,7 @@ re-rendering. This is about the segment tree, not cache invalidation:
302
302
  ```typescript
303
303
  // Re-run this layout when a blog action fires
304
304
  layout(BlogLayout, () => [
305
- revalidate(({ actionId }) => actionId?.includes("updateBlog") ?? false),
305
+ revalidate(({ actionId }) => actionId?.includes("updateBlog") || undefined),
306
306
  path("/blog/:slug", BlogPost, { name: "blogPost" }),
307
307
  ]);
308
308
 
@@ -108,6 +108,33 @@ path.text("/api/data", () => "plain text version", { name: "dataText" }),
108
108
  Without an RSC primary, there is no `text/html` candidate — the Accept header
109
109
  picks among the response-type candidates directly.
110
110
 
111
+ ## Type Safety For Negotiated Paths
112
+
113
+ `router.named-routes.gen.ts` validates route names, params, search, `href()`, and
114
+ the `Rango.Path` type, but it does not carry response payload metadata. For MIME or
115
+ response payload types, use one of these surfaces:
116
+
117
+ - `RouteResponse<typeof patterns, "routeName">` for a specific response variant
118
+ by route name. This is the clearest option when several MIME variants share
119
+ one URL pattern.
120
+ - `Rango.PathResponse<"/products/:id">` (ambient, no import) for global lookup by URL pattern or concrete path after the app
121
+ registers `typeof router.routeMap`:
122
+
123
+ ```typescript
124
+ // router.tsx
125
+ export const router = createRouter({ document: Document }).routes(urlpatterns);
126
+
127
+ declare global {
128
+ namespace Rango {
129
+ interface RegisteredRoutes extends typeof router.routeMap {}
130
+ }
131
+ }
132
+ ```
133
+
134
+ `RegisteredRoutes` is what exposes the richer routeMap entries containing
135
+ response payload metadata. Without it, URL-pattern response lookup has paths but
136
+ no payloads, so response types resolve to `ResponseEnvelope<never>`.
137
+
111
138
  ## How It Works
112
139
 
113
140
  1. **Build time**: `buildRouteTrie()` calls `mergeLeaves()` when multiple routes share a pattern.
@@ -0,0 +1,137 @@
1
+ ---
2
+ name: observability
3
+ description: Debug Rango request performance with debugPerformance, Server-Timing, structured telemetry, and tracing
4
+ argument-hint:
5
+ ---
6
+
7
+ # Observability
8
+
9
+ Use this when you need to understand request latency, cache decisions,
10
+ revalidation behavior, loader overlap, or production traces.
11
+
12
+ Rango exposes two complementary observability surfaces:
13
+
14
+ 1. **Performance timeline** (`debugPerformance`) — per-request waterfall for
15
+ local or targeted debugging. It prints to the console and emits
16
+ `Server-Timing`.
17
+ 2. **Structured telemetry** (`telemetry`) — lifecycle events sent to a pluggable
18
+ sink for production monitoring, OpenTelemetry, or custom metrics.
19
+
20
+ The essentials are below. The exported `TelemetryEvent` union type
21
+ (`import type { TelemetryEvent } from "@rangojs/router"`) is the full event
22
+ contract — every event kind and its fields are typed there.
23
+
24
+ ## Performance timeline
25
+
26
+ Enable globally while debugging:
27
+
28
+ ```typescript
29
+ import { createRouter } from "@rangojs/router";
30
+
31
+ const router = createRouter({
32
+ document: Document,
33
+ urls: urlpatterns,
34
+ debugPerformance: true,
35
+ });
36
+ ```
37
+
38
+ Or enable for selected requests from middleware:
39
+
40
+ ```typescript
41
+ middleware(async (ctx, next) => {
42
+ if (ctx.url.searchParams.has("debug")) {
43
+ ctx.debugPerformance();
44
+ }
45
+ await next();
46
+ });
47
+ ```
48
+
49
+ Call `ctx.debugPerformance()` before `await next()`. The request then prints a
50
+ shared-axis waterfall and adds a `Server-Timing` header.
51
+
52
+ Read the timeline as intervals:
53
+
54
+ - `handler:total` is the whole router request.
55
+ - `render:total` / `ssr-render-html` show the render pass.
56
+ - `loader:*` rows should overlap render work. If a loader starts only after the
57
+ render bar, it is serialized latency.
58
+ - Cache, route matching, middleware pre/post, RSC serialization, and SSR phases
59
+ appear as separate spans, so the slow phase is visible without guessing.
60
+
61
+ ## Structured telemetry
62
+
63
+ Use telemetry when you want durable production events rather than a one-request
64
+ debug waterfall.
65
+
66
+ ```typescript
67
+ import { createRouter, createConsoleSink } from "@rangojs/router";
68
+
69
+ const router = createRouter({
70
+ document: Document,
71
+ urls: urlpatterns,
72
+ telemetry: createConsoleSink(),
73
+ });
74
+ ```
75
+
76
+ For OpenTelemetry:
77
+
78
+ ```typescript
79
+ import { createRouter, createOTelSink } from "@rangojs/router";
80
+ import { trace } from "@opentelemetry/api";
81
+
82
+ const router = createRouter({
83
+ document: Document,
84
+ urls: urlpatterns,
85
+ telemetry: createOTelSink(trace.getTracer("my-app")),
86
+ });
87
+ ```
88
+
89
+ Custom sinks implement `emit(event)`:
90
+
91
+ ```typescript
92
+ import { createRouter } from "@rangojs/router";
93
+
94
+ const router = createRouter({
95
+ document: Document,
96
+ urls: urlpatterns,
97
+ telemetry: {
98
+ emit(event) {
99
+ myMetrics.record(event);
100
+ },
101
+ },
102
+ });
103
+ ```
104
+
105
+ Events include `request.start/end/error`, `loader.start/end/error`,
106
+ `handler.error`, `cache.decision`, and `revalidation.decision`.
107
+
108
+ ## Debugging revalidation and stale data
109
+
110
+ When stale UI or unexpected partial renders are the question, use all three
111
+ layers together:
112
+
113
+ ```typescript
114
+ import { createConsoleSink, createRouter } from "@rangojs/router";
115
+
116
+ const router = createRouter({
117
+ document: Document,
118
+ urls: urlpatterns,
119
+ debugPerformance: true,
120
+ telemetry: createConsoleSink(),
121
+ });
122
+ ```
123
+
124
+ Then inspect:
125
+
126
+ - `revalidation.decision` telemetry to see which segment re-ran or skipped.
127
+ - cache spans / `cache.decision` events to see hit, miss, stale, and background
128
+ revalidation behavior.
129
+ - loader spans to confirm live loaders overlap the render rather than blocking
130
+ first paint.
131
+ - the `Server-Timing` header to compare local logs with browser-network timing.
132
+
133
+ ## Zero-overhead defaults
134
+
135
+ `debugPerformance` is off by default, and `telemetry` emits nothing unless a sink
136
+ is configured. Per-request `ctx.debugPerformance()` lets you turn on the
137
+ waterfall only for the route, user, or query param you are investigating.
@@ -8,9 +8,6 @@ argument-hint: [@slot-name]
8
8
 
9
9
  Parallel routes render multiple components simultaneously in named slots.
10
10
 
11
- Canonical semantics reference:
12
- [docs/execution-model.md](../../docs/internal/execution-model.md)
13
-
14
11
  ## Basic Parallel Routes
15
12
 
16
13
  ```typescript
@@ -340,7 +337,7 @@ parallel(
340
337
  () => [
341
338
  loader(CartLoader),
342
339
  // Revalidate when cart actions occur
343
- revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
340
+ revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
344
341
  ]
345
342
  )
346
343
  ```
@@ -364,7 +361,7 @@ the parallel consumer:
364
361
  ```typescript
365
362
  // revalidation-contracts.ts
366
363
  export const revalidateCartData = ({ actionId }) =>
367
- actionId?.includes("src/actions/cart.ts#") ?? false;
364
+ actionId?.includes("src/actions/cart.ts#") || undefined;
368
365
 
369
366
  layout(CartLayout, () => [
370
367
  revalidate(revalidateCartData), // producer reruns
@@ -482,7 +479,7 @@ export const shopPatterns = urls(({
482
479
  () => [
483
480
  loader(CartLoader),
484
481
  loading(<CartSkeleton />),
485
- revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
482
+ revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
486
483
  ]
487
484
  ),
488
485
 
@@ -11,9 +11,6 @@ deserialization path, same segment system. The worker handles every request --
11
11
  there are NO static .html or .rsc files served from assets. The worker reads
12
12
  pre-computed Flight payloads instead of executing handler code.
13
13
 
14
- Canonical semantics reference:
15
- [docs/execution-model.md](../../docs/internal/execution-model.md)
16
-
17
14
  ## API: Prerender
18
15
 
19
16
  ### Static Route (no params)
@@ -361,16 +358,16 @@ Both error types propagate to the router's `onError` callback with phase
361
358
  The build produces per-URL timing logs:
362
359
 
363
360
  ```
364
- [rsc-router] Pre-rendering 12 URL(s) (concurrency: 4)...
365
- [rsc-router] OK /articles/hello (42ms)
366
- [rsc-router] PASS /articles/remote-only (5ms) - live fallback
367
- [rsc-router] SKIP /articles/draft-post (3ms) - Article is a draft
368
- [rsc-router] Pre-render complete: 11 done, 1 skipped (1204ms total)
369
-
370
- [rsc-router] Rendering 3 static handler(s)...
371
- [rsc-router] OK DocsLayout (28ms)
372
- [rsc-router] SKIP TocSidebar (1ms) - Not ready
373
- [rsc-router] Static render complete: 2 done, 1 skipped (120ms total)
361
+ [rango] Pre-rendering 12 URL(s) (concurrency: 4)...
362
+ [rango] OK /articles/hello (42ms)
363
+ [rango] PASS /articles/remote-only (5ms) - live fallback
364
+ [rango] SKIP /articles/draft-post (3ms) - Article is a draft
365
+ [rango] Pre-render complete: 11 done, 1 skipped (1204ms total)
366
+
367
+ [rango] Rendering 3 static handler(s)...
368
+ [rango] OK DocsLayout (28ms)
369
+ [rango] SKIP TocSidebar (1ms) - Not ready
370
+ [rango] Static render complete: 2 done, 1 skipped (120ms total)
374
371
  ```
375
372
 
376
373
  A `FAIL` line is logged per-URL when a handler throws a non-Skip error. The
@@ -466,9 +463,9 @@ export const Product = Passthrough(ProductDef, async (ctx) => {
466
463
  Passthrough entries are logged distinctly:
467
464
 
468
465
  ```
469
- [rsc-router] OK /blog/a (42ms)
470
- [rsc-router] PASS /blog/b (3ms) - live fallback
471
- [rsc-router] OK /blog/c (38ms)
466
+ [rango] OK /blog/a (42ms)
467
+ [rango] PASS /blog/b (3ms) - live fallback
468
+ [rango] OK /blog/c (38ms)
472
469
  ```
473
470
 
474
471
  ## Edge Cases and Constraints
@@ -640,16 +637,7 @@ At runtime, the cache-lookup middleware uses these flags:
640
637
 
641
638
  ## Contributor Checklist
642
639
 
643
- Before changing prerender behavior, read these docs and run these tests.
644
-
645
- ### Docs to re-read
646
-
647
- - [Prerender API design](../../docs/prerender-api-design.md) -- canonical
648
- architecture: build-time flow, runtime flow, storage, Passthrough, intercept
649
- - [Execution model](../../docs/internal/execution-model.md) -- handler-first
650
- ordering, middleware scope, context visibility rules
651
- - [Semantic change checklist](../../docs/internal/semantic-change-checklist.md)
652
- -- gate for any change to execution semantics
640
+ Before changing prerender behavior, run these tests.
653
641
 
654
642
  ### Tests to run
655
643
 
@@ -676,10 +664,3 @@ pnpm --filter @rangojs/router exec playwright test handler-first
676
664
  dev/build-only and do not need a production counterpart.
677
665
  - Behavioral assertions (rendered content, loader freshness, Passthrough
678
666
  fallback, intercept variant selection) must work in the production build.
679
-
680
- ## Maintenance References
681
-
682
- - [Stability next steps plan](../../docs/internal/stability-next-steps-plan.md)
683
- -- completed parity and cleanup pass (reference for decisions made)
684
- - [Test quality baseline](../../docs/internal/test-quality-baseline.md) --
685
- measured test inventory, sleep debt, production coverage gaps