@rangojs/router 0.0.0-experimental.98 → 0.0.0-experimental.98914650

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 (355) hide show
  1. package/README.md +24 -9
  2. package/dist/bin/rango.js +157 -63
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +1584 -639
  5. package/package.json +60 -11
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +60 -0
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +222 -30
  10. package/skills/caching/SKILL.md +263 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +235 -28
  16. package/skills/host-router/SKILL.md +122 -22
  17. package/skills/intercept/SKILL.md +29 -5
  18. package/skills/layout/SKILL.md +13 -9
  19. package/skills/links/SKILL.md +173 -17
  20. package/skills/loader/SKILL.md +170 -23
  21. package/skills/middleware/SKILL.md +16 -10
  22. package/skills/migrate-nextjs/SKILL.md +38 -16
  23. package/skills/mime-routes/SKILL.md +27 -0
  24. package/skills/observability/SKILL.md +137 -0
  25. package/skills/parallel/SKILL.md +11 -7
  26. package/skills/prerender/SKILL.md +14 -33
  27. package/skills/rango/SKILL.md +250 -26
  28. package/skills/react-compiler/SKILL.md +168 -0
  29. package/skills/response-routes/SKILL.md +114 -47
  30. package/skills/route/SKILL.md +22 -5
  31. package/skills/router-setup/SKILL.md +3 -3
  32. package/skills/server-actions/SKILL.md +78 -42
  33. package/skills/tailwind/SKILL.md +27 -3
  34. package/skills/testing/SKILL.md +129 -0
  35. package/skills/testing/bindings.md +89 -0
  36. package/skills/testing/cache-prerender.md +124 -0
  37. package/skills/testing/client-components.md +122 -0
  38. package/skills/testing/e2e-parity.md +125 -0
  39. package/skills/testing/flight.md +92 -0
  40. package/skills/testing/handles.md +129 -0
  41. package/skills/testing/loader.md +128 -0
  42. package/skills/testing/middleware.md +99 -0
  43. package/skills/testing/render-handler.md +121 -0
  44. package/skills/testing/response-routes.md +95 -0
  45. package/skills/testing/reverse-and-types.md +84 -0
  46. package/skills/testing/server-actions.md +107 -0
  47. package/skills/testing/server-tree.md +128 -0
  48. package/skills/testing/setup.md +120 -0
  49. package/skills/typesafety/SKILL.md +310 -26
  50. package/skills/use-cache/SKILL.md +36 -5
  51. package/skills/vercel/SKILL.md +107 -0
  52. package/skills/view-transitions/SKILL.md +294 -0
  53. package/src/__augment-tests__/augment.ts +81 -0
  54. package/src/__augment-tests__/augmented.check.ts +116 -0
  55. package/src/__internal.ts +0 -65
  56. package/src/browser/action-coordinator.ts +53 -36
  57. package/src/browser/action-fence.ts +47 -0
  58. package/src/browser/app-shell.ts +14 -27
  59. package/src/browser/cookie-name.ts +140 -0
  60. package/src/browser/event-controller.ts +37 -143
  61. package/src/browser/history-state.ts +21 -0
  62. package/src/browser/index.ts +3 -3
  63. package/src/browser/invalidate-client-cache.ts +52 -0
  64. package/src/browser/navigation-bridge.ts +30 -59
  65. package/src/browser/navigation-client.ts +96 -84
  66. package/src/browser/navigation-store-handle.ts +38 -0
  67. package/src/browser/navigation-store.ts +32 -82
  68. package/src/browser/navigation-transaction.ts +9 -59
  69. package/src/browser/partial-update.ts +60 -127
  70. package/src/browser/prefetch/cache.ts +82 -72
  71. package/src/browser/prefetch/fetch.ts +108 -33
  72. package/src/browser/prefetch/queue.ts +6 -3
  73. package/src/browser/rango-state.ts +157 -115
  74. package/src/browser/react/Link.tsx +0 -2
  75. package/src/browser/react/NavigationProvider.tsx +41 -48
  76. package/src/browser/react/ScrollRestoration.tsx +10 -6
  77. package/src/browser/react/filter-segment-order.ts +0 -2
  78. package/src/browser/react/index.ts +0 -48
  79. package/src/browser/react/location-state-shared.ts +166 -8
  80. package/src/browser/react/location-state.ts +39 -14
  81. package/src/browser/react/use-action.ts +6 -15
  82. package/src/browser/react/use-handle.ts +17 -14
  83. package/src/browser/react/use-link-status.ts +0 -4
  84. package/src/browser/react/use-navigation.ts +0 -3
  85. package/src/browser/react/use-params.ts +3 -6
  86. package/src/browser/react/use-reverse.ts +106 -0
  87. package/src/browser/react/use-router.ts +20 -5
  88. package/src/browser/react/use-search-params.ts +0 -5
  89. package/src/browser/react/use-segments.ts +0 -13
  90. package/src/browser/response-adapter.ts +52 -1
  91. package/src/browser/rsc-router.tsx +70 -34
  92. package/src/browser/scroll-restoration.ts +22 -14
  93. package/src/browser/segment-structure-assert.ts +2 -2
  94. package/src/browser/server-action-bridge.ts +168 -44
  95. package/src/browser/types.ts +36 -21
  96. package/src/browser/validate-redirect-origin.ts +43 -16
  97. package/src/build/collect-fallback-refs.ts +107 -0
  98. package/src/build/generate-manifest.ts +60 -35
  99. package/src/build/generate-route-types.ts +3 -0
  100. package/src/build/index.ts +8 -2
  101. package/src/build/prefix-tree-utils.ts +123 -0
  102. package/src/build/route-trie.ts +89 -11
  103. package/src/build/route-types/codegen.ts +4 -4
  104. package/src/build/route-types/include-resolution.ts +1 -1
  105. package/src/build/route-types/param-extraction.ts +6 -3
  106. package/src/build/route-types/per-module-writer.ts +7 -4
  107. package/src/build/route-types/router-processing.ts +122 -22
  108. package/src/build/route-types/scan-filter.ts +1 -1
  109. package/src/build/route-types/source-scan.ts +118 -0
  110. package/src/build/runtime-discovery.ts +9 -20
  111. package/src/cache/cache-error.ts +104 -0
  112. package/src/cache/cache-policy.ts +68 -28
  113. package/src/cache/cache-runtime.ts +134 -32
  114. package/src/cache/cache-scope.ts +100 -74
  115. package/src/cache/cache-tag.ts +98 -0
  116. package/src/cache/cf/cf-cache-store.ts +2255 -238
  117. package/src/cache/cf/index.ts +6 -16
  118. package/src/cache/document-cache.ts +61 -20
  119. package/src/cache/handle-snapshot.ts +63 -0
  120. package/src/cache/index.ts +22 -20
  121. package/src/cache/memory-segment-store.ts +136 -37
  122. package/src/cache/profile-registry.ts +6 -30
  123. package/src/cache/read-through-swr.ts +41 -11
  124. package/src/cache/segment-codec.ts +0 -16
  125. package/src/cache/tag-invalidation.ts +230 -0
  126. package/src/cache/types.ts +33 -100
  127. package/src/cache/vercel/index.ts +11 -0
  128. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  129. package/src/client.rsc.tsx +6 -21
  130. package/src/client.tsx +25 -61
  131. package/src/component-utils.ts +19 -0
  132. package/src/context-var.ts +17 -5
  133. package/src/decode-loader-results.ts +36 -0
  134. package/src/defer.ts +196 -0
  135. package/src/deps/ssr.ts +0 -1
  136. package/src/errors.ts +30 -4
  137. package/src/handle.ts +31 -23
  138. package/src/handles/MetaTags.tsx +0 -14
  139. package/src/handles/breadcrumbs.ts +16 -5
  140. package/src/handles/meta.ts +0 -39
  141. package/src/host/cookie-handler.ts +0 -36
  142. package/src/host/errors.ts +0 -24
  143. package/src/host/index.ts +8 -2
  144. package/src/host/pattern-matcher.ts +7 -50
  145. package/src/host/router.ts +107 -99
  146. package/src/host/testing.ts +40 -27
  147. package/src/host/types.ts +37 -4
  148. package/src/host/utils.ts +1 -1
  149. package/src/href-client.ts +137 -22
  150. package/src/index.rsc.ts +63 -9
  151. package/src/index.ts +64 -9
  152. package/src/internal-debug.ts +2 -4
  153. package/src/loader-store.ts +500 -0
  154. package/src/loader.rsc.ts +20 -13
  155. package/src/loader.ts +12 -11
  156. package/src/missing-id-error.ts +68 -0
  157. package/src/network-error-thrower.tsx +1 -6
  158. package/src/outlet-provider.tsx +1 -5
  159. package/src/prerender/param-hash.ts +10 -11
  160. package/src/prerender/store.ts +32 -37
  161. package/src/prerender.ts +61 -6
  162. package/src/redirect-origin.ts +100 -0
  163. package/src/response-utils.ts +9 -0
  164. package/src/reverse.ts +65 -41
  165. package/src/root-error-boundary.tsx +1 -19
  166. package/src/route-content-wrapper.tsx +7 -72
  167. package/src/route-definition/dsl-helpers.ts +244 -281
  168. package/src/route-definition/helper-factories.ts +29 -139
  169. package/src/route-definition/helpers-types.ts +40 -17
  170. package/src/route-definition/redirect.ts +43 -9
  171. package/src/route-definition/resolve-handler-use.ts +6 -0
  172. package/src/route-definition/use-item-types.ts +32 -0
  173. package/src/route-map-builder.ts +0 -16
  174. package/src/route-types.ts +19 -41
  175. package/src/router/basename.ts +14 -0
  176. package/src/router/content-negotiation.ts +15 -15
  177. package/src/router/error-handling.ts +13 -17
  178. package/src/router/find-match.ts +44 -23
  179. package/src/router/handler-context.ts +4 -42
  180. package/src/router/intercept-resolution.ts +14 -19
  181. package/src/router/lazy-includes.ts +9 -46
  182. package/src/router/loader-resolution.ts +91 -46
  183. package/src/router/logging.ts +0 -6
  184. package/src/router/manifest.ts +18 -29
  185. package/src/router/match-api.ts +0 -20
  186. package/src/router/match-context.ts +0 -22
  187. package/src/router/match-handlers.ts +57 -58
  188. package/src/router/match-middleware/background-revalidation.ts +0 -7
  189. package/src/router/match-middleware/cache-lookup.ts +150 -271
  190. package/src/router/match-middleware/cache-store.ts +3 -33
  191. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  192. package/src/router/match-middleware/segment-resolution.ts +0 -22
  193. package/src/router/match-pipelines.ts +1 -42
  194. package/src/router/match-result.ts +31 -80
  195. package/src/router/metrics.ts +0 -34
  196. package/src/router/middleware-types.ts +0 -116
  197. package/src/router/middleware.ts +118 -133
  198. package/src/router/navigation-snapshot.ts +0 -51
  199. package/src/router/params-util.ts +23 -0
  200. package/src/router/pattern-matching.ts +20 -58
  201. package/src/router/prerender-match.ts +99 -63
  202. package/src/router/preview-match.ts +3 -1
  203. package/src/router/request-classification.ts +28 -62
  204. package/src/router/revalidation.ts +50 -56
  205. package/src/router/route-snapshot.ts +0 -1
  206. package/src/router/router-context.ts +0 -27
  207. package/src/router/router-interfaces.ts +68 -35
  208. package/src/router/router-options.ts +55 -1
  209. package/src/router/router-registry.ts +2 -5
  210. package/src/router/segment-resolution/fresh.ts +44 -63
  211. package/src/router/segment-resolution/helpers.ts +34 -0
  212. package/src/router/segment-resolution/loader-cache.ts +40 -37
  213. package/src/router/segment-resolution/revalidation.ts +203 -285
  214. package/src/router/segment-resolution/static-store.ts +19 -5
  215. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  216. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  217. package/src/router/segment-resolution.ts +4 -1
  218. package/src/router/segment-wrappers.ts +0 -3
  219. package/src/router/state-cookie-name.ts +33 -0
  220. package/src/router/substitute-pattern-params.ts +56 -0
  221. package/src/router/telemetry-otel.ts +0 -20
  222. package/src/router/telemetry.ts +96 -19
  223. package/src/router/timeout.ts +0 -20
  224. package/src/router/trie-matching.ts +87 -47
  225. package/src/router/types.ts +9 -63
  226. package/src/router/url-params.ts +0 -5
  227. package/src/router.ts +80 -41
  228. package/src/rsc/handler-context.ts +3 -2
  229. package/src/rsc/handler.ts +83 -78
  230. package/src/rsc/helpers.ts +93 -5
  231. package/src/rsc/index.ts +1 -1
  232. package/src/rsc/json-route-result.ts +38 -0
  233. package/src/rsc/manifest-init.ts +28 -41
  234. package/src/rsc/origin-guard.ts +39 -25
  235. package/src/rsc/progressive-enhancement.ts +12 -1
  236. package/src/rsc/redirect-guard.ts +99 -0
  237. package/src/rsc/response-error.ts +79 -12
  238. package/src/rsc/response-route-handler.ts +76 -62
  239. package/src/rsc/rsc-rendering.ts +41 -60
  240. package/src/rsc/runtime-warnings.ts +23 -10
  241. package/src/rsc/server-action.ts +62 -67
  242. package/src/rsc/ssr-setup.ts +16 -0
  243. package/src/rsc/types.ts +10 -5
  244. package/src/runtime-env.ts +18 -0
  245. package/src/search-params.ts +4 -20
  246. package/src/segment-loader-promise.ts +14 -2
  247. package/src/segment-system.tsx +199 -142
  248. package/src/serialize.ts +243 -0
  249. package/src/server/context.ts +150 -51
  250. package/src/server/cookie-store.ts +80 -5
  251. package/src/server/handle-store.ts +7 -24
  252. package/src/server/loader-registry.ts +5 -24
  253. package/src/server/request-context.ts +165 -87
  254. package/src/ssr/index.tsx +14 -14
  255. package/src/static-handler.ts +10 -13
  256. package/src/testing/cache-status.ts +162 -0
  257. package/src/testing/collect-handle.ts +40 -0
  258. package/src/testing/dispatch.ts +618 -0
  259. package/src/testing/dom.entry.ts +22 -0
  260. package/src/testing/e2e/fixture.ts +188 -0
  261. package/src/testing/e2e/index.ts +128 -0
  262. package/src/testing/e2e/matchers.ts +35 -0
  263. package/src/testing/e2e/page-helpers.ts +272 -0
  264. package/src/testing/e2e/parity.ts +387 -0
  265. package/src/testing/e2e/server.ts +195 -0
  266. package/src/testing/flight-matchers.ts +97 -0
  267. package/src/testing/flight-normalize.ts +11 -0
  268. package/src/testing/flight-runtime.d.ts +57 -0
  269. package/src/testing/flight-tree.ts +682 -0
  270. package/src/testing/flight.entry.ts +52 -0
  271. package/src/testing/flight.ts +232 -0
  272. package/src/testing/generated-routes.ts +183 -0
  273. package/src/testing/index.ts +99 -0
  274. package/src/testing/internal/context.ts +348 -0
  275. package/src/testing/internal/flight-client-globals.ts +30 -0
  276. package/src/testing/internal/seed-vars.ts +54 -0
  277. package/src/testing/render-handler.ts +330 -0
  278. package/src/testing/render-route.tsx +566 -0
  279. package/src/testing/run-loader.ts +378 -0
  280. package/src/testing/run-middleware.ts +205 -0
  281. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  282. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  283. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  284. package/src/testing/vitest-stubs/version.ts +5 -0
  285. package/src/testing/vitest.ts +305 -0
  286. package/src/theme/ThemeProvider.tsx +0 -52
  287. package/src/theme/ThemeScript.tsx +0 -6
  288. package/src/theme/constants.ts +0 -12
  289. package/src/theme/index.ts +0 -7
  290. package/src/theme/theme-context.ts +1 -5
  291. package/src/theme/theme-script.ts +0 -14
  292. package/src/theme/use-theme.ts +0 -3
  293. package/src/types/boundaries.ts +0 -35
  294. package/src/types/cache-types.ts +13 -4
  295. package/src/types/error-types.ts +30 -90
  296. package/src/types/global-namespace.ts +54 -41
  297. package/src/types/handler-context.ts +97 -22
  298. package/src/types/index.ts +1 -10
  299. package/src/types/loader-types.ts +6 -3
  300. package/src/types/request-scope.ts +0 -19
  301. package/src/types/route-config.ts +6 -50
  302. package/src/types/route-entry.ts +0 -6
  303. package/src/types/segments.ts +18 -14
  304. package/src/urls/include-helper.ts +9 -56
  305. package/src/urls/index.ts +1 -11
  306. package/src/urls/path-helper-types.ts +19 -5
  307. package/src/urls/path-helper.ts +17 -106
  308. package/src/urls/pattern-types.ts +36 -19
  309. package/src/urls/response-types.ts +20 -19
  310. package/src/urls/type-extraction.ts +58 -139
  311. package/src/urls/urls-function.ts +1 -18
  312. package/src/use-loader.tsx +292 -107
  313. package/src/vite/debug.ts +1 -0
  314. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  315. package/src/vite/discovery/discover-routers.ts +95 -82
  316. package/src/vite/discovery/discovery-errors.ts +194 -0
  317. package/src/vite/discovery/prerender-collection.ts +26 -34
  318. package/src/vite/discovery/route-types-writer.ts +40 -84
  319. package/src/vite/discovery/state.ts +39 -1
  320. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  321. package/src/vite/index.ts +4 -0
  322. package/src/vite/plugin-types.ts +185 -10
  323. package/src/vite/plugins/cjs-to-esm.ts +3 -18
  324. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  325. package/src/vite/plugins/client-ref-hashing.ts +12 -11
  326. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  327. package/src/vite/plugins/expose-action-id.ts +4 -75
  328. package/src/vite/plugins/expose-id-utils.ts +3 -54
  329. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  330. package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
  331. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  332. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  333. package/src/vite/plugins/expose-internal-ids.ts +57 -67
  334. package/src/vite/plugins/performance-tracks.ts +9 -16
  335. package/src/vite/plugins/refresh-cmd.ts +1 -1
  336. package/src/vite/plugins/use-cache-transform.ts +26 -49
  337. package/src/vite/plugins/vercel-output.ts +258 -0
  338. package/src/vite/plugins/version-injector.ts +2 -32
  339. package/src/vite/plugins/version-plugin.ts +32 -23
  340. package/src/vite/plugins/virtual-entries.ts +35 -17
  341. package/src/vite/rango.ts +148 -115
  342. package/src/vite/router-discovery.ts +220 -68
  343. package/src/vite/utils/ast-handler-extract.ts +15 -31
  344. package/src/vite/utils/bundle-analysis.ts +10 -15
  345. package/src/vite/utils/client-chunks.ts +184 -0
  346. package/src/vite/utils/forward-user-plugins.ts +171 -0
  347. package/src/vite/utils/manifest-utils.ts +4 -59
  348. package/src/vite/utils/package-resolution.ts +1 -73
  349. package/src/vite/utils/prerender-utils.ts +0 -35
  350. package/src/vite/utils/shared-utils.ts +95 -43
  351. package/src/browser/action-response-classifier.ts +0 -99
  352. package/src/browser/react/use-client-cache.ts +0 -58
  353. package/src/browser/shallow.ts +0 -40
  354. package/src/handles/index.ts +0 -7
  355. package/src/router/middleware-cookies.ts +0 -55
@@ -19,12 +19,17 @@ import type {
19
19
  ErrorBoundaryFallbackProps,
20
20
  ErrorInfo,
21
21
  } from "../types";
22
- import type { LoaderRevalidationResult, ActionContext } from "./types";
23
22
  import { isHandle, collectHandleData, type Handle } from "../handle.js";
23
+ import { withDefer } from "../defer.js";
24
24
  import { buildHandleSnapshot } from "../server/handle-store.js";
25
25
  import { getFetchableLoader } from "../server/fetchable-loader-store.js";
26
26
  import { _getRequestContext } from "../server/request-context.js";
27
- import { isInsideLoaderScope } from "../server/context.js";
27
+ import {
28
+ isInsideLoaderScope,
29
+ runInsideLoaderBodyScope,
30
+ isInsidePushCallbackScope,
31
+ runInsidePushCallbackScope,
32
+ } from "../server/context.js";
28
33
  import { debugLog } from "./logging.js";
29
34
 
30
35
  /**
@@ -66,7 +71,9 @@ export function wrapLoaderWithErrorHandling<T>(
66
71
  ) => ErrorInfo,
67
72
  onError?: LoaderErrorCallback,
68
73
  ): Promise<LoaderDataResult<T>> {
69
- // Extract loader name from segmentId (format: "M1L0D0.loaderName")
74
+ // Extract the trailing token from segmentId (format: "<shortCode>D<i>.<loaderId>").
75
+ // The token is the loader's $$id (hash#export in prod, pathfrag#export in dev),
76
+ // not a clean display name.
70
77
  const loaderName = segmentId.split(".").pop() || "unknown";
71
78
 
72
79
  return Promise.resolve(promise)
@@ -287,6 +294,12 @@ function createLoaderExecutor<TEnv>(
287
294
  );
288
295
  }
289
296
  const segmentOrder = reqCtx._renderBarrierSegmentOrder ?? [];
297
+ // The complete snapshot is cached at barrier resolution for
298
+ // non-streaming trees, and by rendered() after handleStore.settled for
299
+ // streaming trees (where the eager snapshot would have been incomplete
300
+ // because loading() handlers were still in flight). Either way it is
301
+ // present by the time a loader reads a handle; the fresh build is only
302
+ // a defensive fallback.
290
303
  const snapshot =
291
304
  reqCtx._renderBarrierHandleSnapshot ??
292
305
  buildHandleSnapshot(reqCtx._handleStore, segmentOrder);
@@ -308,15 +321,7 @@ function createLoaderExecutor<TEnv>(
308
321
  );
309
322
  }
310
323
 
311
- // Guard: reject streaming trees
312
324
  const reqCtx = reqCtxRef ?? _getRequestContext();
313
- if (reqCtx?._treeHasStreaming) {
314
- throw new Error(
315
- `ctx.rendered() is not supported when the matched route tree uses loading(). ` +
316
- `Streaming handlers may not have settled when rendered() resolves. ` +
317
- `Remove loading() from the route tree or restructure to avoid rendered().`,
318
- );
319
- }
320
325
 
321
326
  if (renderedPromise) return renderedPromise;
322
327
 
@@ -327,7 +332,10 @@ function createLoaderExecutor<TEnv>(
327
332
  }
328
333
 
329
334
  // Bidirectional deadlock check: if a handler already started
330
- // awaiting this loader, calling rendered() would deadlock.
335
+ // awaiting this loader, calling rendered() would deadlock. This is the
336
+ // real cycle guard (it holds for both streaming and non-streaming): the
337
+ // handler blocks segment resolution, which blocks the barrier, which
338
+ // blocks this loader.
331
339
  if (reqCtx._handlerLoaderDeps?.has(currentLoaderId)) {
332
340
  throw new Error(
333
341
  `Deadlock: loader "${currentLoaderId}" called ctx.rendered() but a handler ` +
@@ -345,7 +353,29 @@ function createLoaderExecutor<TEnv>(
345
353
  }
346
354
  reqCtx._renderBarrierWaiters.add(currentLoaderId);
347
355
 
348
- renderedPromise = reqCtx._renderBarrier.then(() => {
356
+ // Streaming trees (loading()): the barrier resolves once the segment
357
+ // tree is resolved, but loading() handlers stream behind Suspense and
358
+ // their handle pushes are still in flight then. Their async execution
359
+ // IS tracked in the handle store (trackHandler -> store.track), so after
360
+ // the barrier we seal (no further handlers register once the tree is
361
+ // resolved) and wait for settled — every tracked handler, streaming
362
+ // included, has finished pushing. The loader's own segment streams in
363
+ // after, so this does not block the shell; the deadlock guard above
364
+ // keeps a handler from depending on this loader.
365
+ const streaming = reqCtx._treeHasStreaming === true;
366
+ renderedPromise = reqCtx._renderBarrier.then(async () => {
367
+ if (streaming) {
368
+ reqCtx._handleStore.seal();
369
+ await reqCtx._handleStore.settled;
370
+ // The eager snapshot was intentionally left unbuilt for streaming
371
+ // (it would have been incomplete). Build the complete one once, now
372
+ // that the store has settled, so every ctx.use(handle) reads the
373
+ // cached snapshot instead of rebuilding it per call.
374
+ reqCtx._renderBarrierHandleSnapshot ??= buildHandleSnapshot(
375
+ reqCtx._handleStore,
376
+ reqCtx._renderBarrierSegmentOrder ?? [],
377
+ );
378
+ }
349
379
  renderedResolved = true;
350
380
  });
351
381
  return renderedPromise;
@@ -353,8 +383,19 @@ function createLoaderExecutor<TEnv>(
353
383
  };
354
384
 
355
385
  const doneLoader = track(`loader:${loader.$$id}`, 2);
386
+ // Run the loader body inside loader scope so request-scoped reads
387
+ // (cookies()/headers() and non-cacheable ctx.get) are exempt from the
388
+ // cache-purity guards: loaders always run fresh, so their reads never leak
389
+ // into a cached segment. DSL loaders are already wrapped by fresh.ts; this
390
+ // also covers handler-invoked loaders (ctx.use(Loader) from a handler),
391
+ // which otherwise execute in the caller's cache scope and would wrongly
392
+ // throw. rendered() gating uses the captured isDslLoader (above), so this
393
+ // does not grant rendered() to handler-invoked loaders. Uses a body-only
394
+ // scope, so isInsideLoaderScope() / barrier / deadlock gating is unchanged.
356
395
  const promise = Promise.resolve(
357
- loaderFn(loaderCtx as LoaderContext<any, TEnv>),
396
+ runInsideLoaderBodyScope(() =>
397
+ loaderFn(loaderCtx as LoaderContext<any, TEnv>),
398
+ ),
358
399
  ).finally(() => {
359
400
  pendingLoaders.delete(loader.$$id);
360
401
  doneLoader();
@@ -390,12 +431,6 @@ export function setupLoaderAccess<TEnv>(
390
431
 
391
432
  const useLoader = createLoaderExecutor(ctx, loaderPromises);
392
433
 
393
- // Track whether we're inside a handle push callback. Loaders started
394
- // from push callbacks (e.g. push(async () => ctx.use(Loader))) do NOT
395
- // block segment resolution, so they must not be registered as handler
396
- // dependencies for deadlock detection.
397
- let insideHandlePush = false;
398
-
399
434
  ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
400
435
  if (isHandle(item)) {
401
436
  const handle = item;
@@ -410,35 +445,40 @@ export function setupLoaderAccess<TEnv>(
410
445
  );
411
446
  }
412
447
 
413
- return (
414
- dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
415
- ) => {
416
- if (!store) return;
417
-
418
- if (typeof dataOrFn === "function") {
419
- // Mark scope so ctx.use(loader) calls inside the callback
420
- // are not registered as handler-to-loader deps.
421
- insideHandlePush = true;
422
- try {
423
- const result = (dataOrFn as () => Promise<unknown>)();
448
+ return withDefer(
449
+ (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
450
+ if (!store) return;
451
+
452
+ if (typeof dataOrFn === "function") {
453
+ // Run the callback inside the push-callback scope so ctx.use(loader)
454
+ // calls it makes including after its own awaits, for an async
455
+ // callback — are not registered as handler-to-loader deps and do not
456
+ // trip the deadlock guard. A pushed promise value is not tracked by
457
+ // handleStore.settled and does not block segment resolution, so it
458
+ // cannot form a rendered() deadlock. The ALS scope (not a plain
459
+ // boolean) is what survives the callback's awaits.
460
+ const result = runInsidePushCallbackScope(() =>
461
+ (dataOrFn as () => Promise<unknown>)(),
462
+ );
424
463
  store.push(handle.$$id, segmentId, result);
425
- } finally {
426
- insideHandlePush = false;
464
+ return;
427
465
  }
428
- return;
429
- }
430
466
 
431
- store.push(handle.$$id, segmentId, dataOrFn);
432
- };
467
+ store.push(handle.$$id, segmentId, dataOrFn);
468
+ },
469
+ );
433
470
  }
434
471
 
435
472
  // Deadlock guard and handler-to-loader dependency tracking.
436
473
  // Skip when inside a DSL loader scope (resolveLoaderData also calls
437
474
  // ctx.use() but that's DSL-to-DSL, not handler-to-loader) or when
438
475
  // inside a handle push callback (push callbacks don't block segment
439
- // resolution so they can't cause rendered() deadlocks).
476
+ // resolution so they can't cause rendered() deadlocks). The push-callback
477
+ // check is an ALS scope so it also exempts an ASYNC callback's continuation
478
+ // after its first await — relevant on streaming trees, where the guard
479
+ // state now stays live until handleStore.settled.
440
480
  const loader = item as LoaderDefinition<any, any>;
441
- if (!isInsideLoaderScope() && !insideHandlePush) {
481
+ if (!isInsideLoaderScope() && !isInsidePushCallbackScope()) {
442
482
  const reqCtx = reqCtxRef ?? _getRequestContext();
443
483
  if (reqCtx) {
444
484
  // Direction 1: handler awaits loader that already called rendered()
@@ -452,13 +492,18 @@ export function setupLoaderAccess<TEnv>(
452
492
  `Move the data dependency to a loader-to-loader pattern instead.`,
453
493
  );
454
494
  }
455
- // Direction 2: track dep so rendered() can detect the deadlock
456
- // if the loader calls it later. Skip when the barrier has already
457
- // resolved no deadlock is possible (rendered() resolves immediately).
458
- // _renderBarrierSegmentOrder is undefined before resolution, string[]
459
- // after. This also prevents false positives from handle push callbacks
460
- // that resume after their first await (post-barrier-resolution).
461
- if (reqCtx._renderBarrierSegmentOrder === undefined) {
495
+ // Direction 2: track dep so rendered() can detect the deadlock if the
496
+ // loader calls it later. Skip once the guard window is CLOSED — for a
497
+ // non-streaming tree that is when the barrier resolves (rendered()
498
+ // resolves immediately), and for a streaming tree it is when
499
+ // handleStore.settled completes (rendered() keeps waiting until then, so
500
+ // a loading() handler resuming after the barrier can still form a
501
+ // cycle). Using the explicit guard-closed flag rather than
502
+ // _renderBarrierSegmentOrder keeps tracking live across the streaming
503
+ // settle wait. (Handle push callbacks are already excluded above via
504
+ // isInsidePushCallbackScope(), so they cannot produce false positives
505
+ // here.)
506
+ if (!reqCtx._renderBarrierGuardClosed) {
462
507
  if (!reqCtx._handlerLoaderDeps) reqCtx._handlerLoaderDeps = new Set();
463
508
  reqCtx._handlerLoaderDeps.add(loader.$$id);
464
509
  }
@@ -1,8 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
3
3
 
4
- // -- Revalidation trace types --
5
-
6
4
  export interface RevalidationTraceEntry {
7
5
  segmentId: string;
8
6
  segmentType: string;
@@ -36,8 +34,6 @@ export interface RevalidationTrace {
36
34
  entries: RevalidationTraceEntry[];
37
35
  }
38
36
 
39
- // -- Log context --
40
-
41
37
  interface RouterLogContext {
42
38
  requestId: string;
43
39
  transactionId: string;
@@ -195,8 +191,6 @@ export function debugWarn(
195
191
  console.warn(`${prefix} ${message}`);
196
192
  }
197
193
 
198
- // -- Revalidation trace helpers --
199
-
200
194
  export function isTraceActive(): boolean {
201
195
  if (!INTERNAL_RANGO_DEBUG) return false;
202
196
  const ctx = routerLogContext.getStore();
@@ -1,9 +1,3 @@
1
- /**
2
- * Router Manifest Loading
3
- *
4
- * Handles lazy loading and validation of route manifests.
5
- */
6
-
7
1
  import { invariant, RouteNotFoundError } from "../errors";
8
2
  import { createRouteHelpers } from "../route-definition";
9
3
  import {
@@ -14,6 +8,7 @@ import {
14
8
  type MetricsStore,
15
9
  } from "../server/context";
16
10
  import MapRootLayout from "../server/root-layout";
11
+ import { joinPrefix } from "./pattern-matching.js";
17
12
  import type { RouteEntry } from "../types";
18
13
  import type { UrlPatterns } from "../urls";
19
14
  import { VERSION } from "@rangojs/router:version";
@@ -23,20 +18,19 @@ import { VERSION } from "@rangojs/router:version";
23
18
  // stable references), so the resulting EntryData tree can be safely cached and reused
24
19
  // across requests within the same isolate.
25
20
  //
26
- // Cache is keyed by (VERSION, mountIndex, routeKey, isSSR). VERSION comes from the
21
+ // Cache is keyed by (VERSION, routerId, mountIndex, routeKey, isSSR). routeKey is
22
+ // REQUIRED in the key: loadManifest() runs the handler with forRoute=routeKey, and
23
+ // path-helper.ts prunes (skips registering) every route except forRoute, so the
24
+ // resulting Store.manifest is pruned to the requested route — NOT the full include.
25
+ // Dropping routeKey would make a sibling route miss and overwrite this entry with its
26
+ // own pruned manifest, so alternating sibling requests would thrash (re-run the
27
+ // handler every time). Running the include handler once per isolate instead of once
28
+ // per route is possible but needs an unpruned manifest cache with prune-on-read — see
29
+ // LP1 in docs/internal/matching-and-lazy-discovery.md. VERSION comes from the
27
30
  // @rangojs/router:version virtual module which Vite invalidates on RSC module HMR.
28
31
  // When VERSION changes, this module re-evaluates and the cache is recreated empty.
29
- // Including VERSION in the key is additional defense against stale entries.
30
32
  const manifestModuleCache = new Map<string, Map<string, EntryData>>();
31
33
 
32
- /**
33
- * Load manifest from route entry with AsyncLocalStorage context
34
- * Handles lazy imports, unwrapping, and validation
35
- *
36
- * Results are cached at module level after first execution. Subsequent calls
37
- * for the same (routeKey, isSSR) within the same isolate return cached data
38
- * without re-executing the DSL handler.
39
- */
40
34
  /**
41
35
  * Clear the module-level manifest cache.
42
36
  * Called on HMR to ensure stale handler references are discarded.
@@ -65,9 +59,11 @@ export async function loadManifest(
65
59
 
66
60
  const mountIndex = entry.mountIndex;
67
61
 
68
- // Check module-level cache (persists across requests within same isolate)
62
+ // Check module-level cache (persists across requests within same isolate).
69
63
  // Include routerId so multi-router setups (host routing) don't share cached
70
64
  // EntryData across routers with overlapping mountIndex + routeKey combinations.
65
+ // routeKey is in the key because loadManifest() builds a manifest pruned to
66
+ // forRoute=routeKey (see path-helper.ts) — see the cache comment above.
71
67
  const cacheKey = `${VERSION}:${entry.routerId ?? ""}:${mountIndex ?? ""}:${routeKey}:${isSSR ? 1 : 0}`;
72
68
  const cached = manifestModuleCache.get(cacheKey);
73
69
  if (cached) {
@@ -88,20 +84,15 @@ export async function loadManifest(
88
84
  const storeSetupStart = performance.now();
89
85
  const Store = getContext().getOrCreateStore(routeKey);
90
86
 
91
- // Set mount index in store for unique shortCode prefixes
92
87
  Store.mountIndex = mountIndex;
93
-
94
- // Set isSSR flag so loading() can check if we're in SSR
95
88
  Store.isSSR = isSSR;
96
89
 
97
- // Attach metrics store to context if provided
98
90
  if (metricsStore) {
99
91
  Store.metrics = metricsStore;
100
92
  }
101
93
 
102
94
  pushMetric?.("manifest:store-setup", storeSetupStart);
103
95
 
104
- // Clear manifest before rebuilding to prevent stale entry mutations
105
96
  const clearStart = performance.now();
106
97
  Store.manifest.clear();
107
98
  pushMetric?.("manifest:clear", clearStart);
@@ -176,7 +167,10 @@ export async function loadManifest(
176
167
  if (entry.lazy && entry.lazyPatterns) {
177
168
  const lazyPatterns = entry.lazyPatterns as UrlPatterns<any>;
178
169
  const includePrefix = (entry as any)._lazyPrefix || "";
179
- const fullPrefix = (lazyContext?.urlPrefix || "") + includePrefix;
170
+ // Slash-collapsing join so a trailing-slash parent prefix does not
171
+ // bake a double slash into the registered route patterns (must match
172
+ // the same join in evaluateLazyEntry / the build-time runWithPrefixes).
173
+ const fullPrefix = joinPrefix(lazyContext?.urlPrefix, includePrefix);
180
174
 
181
175
  if (fullPrefix || lazyContext?.namePrefix) {
182
176
  return runWithPrefixes(fullPrefix, lazyContext?.namePrefix, () =>
@@ -186,20 +180,16 @@ export async function loadManifest(
186
180
  return lazyPatterns.handler();
187
181
  }
188
182
 
189
- // Wrap handler execution in root layout so routes get correct parent
190
- // This ensures all routes are registered with the layout as their parent
191
183
  let promiseResult: Promise<any> | null = null;
192
184
  const wrappedItems = helpers.layout(MapRootLayout, () => {
193
185
  const result = entry.handler();
194
186
  if (result instanceof Promise) {
195
- // Lazy handler detected - capture promise for async handling
196
187
  promiseResult = result;
197
- return []; // Return empty, we'll discard this wrapped result
188
+ return [];
198
189
  }
199
190
  return result;
200
191
  });
201
192
 
202
- // Handle lazy (Promise-based) handlers
203
193
  if (promiseResult !== null) {
204
194
  const load = await (promiseResult as Promise<any>);
205
195
  if (
@@ -233,7 +223,6 @@ export async function loadManifest(
233
223
  );
234
224
  }
235
225
 
236
- // Inline handler - routes were registered with correct parent inside layout
237
226
  return [wrappedItems].flat(3);
238
227
  },
239
228
  );
@@ -1,10 +1,3 @@
1
- /**
2
- * Match API
3
- *
4
- * Extracted from createRouter closure. Contains match context creation functions
5
- * and the matchError function for error boundary resolution.
6
- */
7
-
8
1
  import { CacheScope, createCacheScope } from "../cache/cache-scope.js";
9
2
  import { RouteNotFoundError } from "../errors";
10
3
  import {
@@ -59,8 +52,6 @@ export async function createMatchContextForFull<TEnv>(
59
52
 
60
53
  const metricsStore = deps.getMetricsStore();
61
54
 
62
- // Full renders always resolve fresh with isSSR: true because loadManifest
63
- // keys its cache on isSSR and stamps Store.isSSR for downstream behavior.
64
55
  const result = await resolveRoute<TEnv>(pathname, {
65
56
  findMatch: (p) => deps.findMatch(p, metricsStore),
66
57
  metricsStore,
@@ -84,12 +75,10 @@ export async function createMatchContextForFull<TEnv>(
84
75
 
85
76
  const { matched } = snapshot;
86
77
 
87
- // Backward compat: downstream middleware reads matched.pt
88
78
  if (snapshot.isPassthrough) {
89
79
  matched.pt = true;
90
80
  }
91
81
 
92
- // Clean URL without internal _rsc* params for userland access
93
82
  const cleanUrl = stripInternalParams(url);
94
83
 
95
84
  const handlerContext = createHandlerContext(
@@ -231,7 +220,6 @@ export async function createMatchContextForPartial<TEnv>(
231
220
  matched.pt = true;
232
221
  }
233
222
 
234
- // Navigation state (prev + intercept-source findMatch calls)
235
223
  const nav = resolveNavigation(request, url, matched.routeKey, {
236
224
  findMatch: deps.findMatch,
237
225
  });
@@ -239,10 +227,6 @@ export async function createMatchContextForPartial<TEnv>(
239
227
  return null;
240
228
  }
241
229
 
242
- // Push route-matching metric. On the fresh path this covers all findMatch
243
- // calls (current + prev + intercept-source). On the reuse path, current-route
244
- // findMatch was already timed during classification, so this only covers
245
- // the nav lookups (prev + intercept-source).
246
230
  if (metricsStore) {
247
231
  const isReuse = !!classifiedRoute;
248
232
  metricsStore.metrics.push({
@@ -259,7 +243,6 @@ export async function createMatchContextForPartial<TEnv>(
259
243
  });
260
244
  }
261
245
 
262
- // Clean URL without internal _rsc* params for userland access
263
246
  const cleanUrl = stripInternalParams(url);
264
247
 
265
248
  const handlerContext = createHandlerContext(
@@ -304,9 +287,6 @@ export async function createMatchContextForPartial<TEnv>(
304
287
  });
305
288
  }
306
289
 
307
- // Store previous route key on the request context for revalidation
308
- // fromRouteName. Uses effectiveFromMatch so intercept-source navigations
309
- // see the intercept origin route, not the plain previous URL route.
310
290
  setRequestContextPrevRouteKey(nav.effectiveFromMatch?.routeKey);
311
291
 
312
292
  const interceptSelectorContext: InterceptSelectorContext = {
@@ -242,25 +242,3 @@ export function createPipelineState(): MatchPipelineState {
242
242
  slots: {},
243
243
  };
244
244
  }
245
-
246
- /**
247
- * Input parameters for createMatchContext
248
- */
249
- export interface CreateMatchContextInput<TEnv = any> {
250
- request: Request;
251
- env: TEnv;
252
- actionContext?: ActionContext;
253
- }
254
-
255
- /**
256
- * Result from createMatchContext - either a context or null (fall back to full match)
257
- */
258
- export type CreateMatchContextResult<TEnv = any> =
259
- | { type: "context"; ctx: MatchContext<TEnv> }
260
- | { type: "fallback"; reason: string }
261
- | { type: "error"; error: Error };
262
-
263
- // Note: createMatchContext() will be implemented in Step J10 when we wire everything together.
264
- // It requires access to RouterContext (findMatch, loadManifest, etc.) which are closure
265
- // functions from createRouter(). The implementation will live in router.ts initially
266
- // and call getRouterContext() to access these dependencies.
@@ -33,10 +33,13 @@ import type { ErrorBoundaryHandler, NotFoundBoundaryHandler } from "../types";
33
33
  import type { MiddlewareFn } from "./middleware.js";
34
34
  import {
35
35
  type TelemetrySink,
36
+ type CacheSegmentSignal,
36
37
  safeEmit,
37
38
  resolveSink,
38
39
  getRequestId,
40
+ buildCacheSignalSegments,
39
41
  } from "./telemetry.js";
42
+ import { _getRequestContext } from "../server/request-context.js";
40
43
 
41
44
  export interface MatchHandlerDeps<TEnv = any> {
42
45
  buildRouterContext: () => RouterContext<TEnv>;
@@ -51,6 +54,12 @@ export interface MatchHandlerDeps<TEnv = any> {
51
54
  isAction: boolean,
52
55
  ) => { intercept: InterceptEntry; entry: EntryData } | null;
53
56
  telemetry?: TelemetrySink;
57
+ /**
58
+ * DEVELOPMENT/TEST ONLY gate for the X-Rango-Cache debug header. When true,
59
+ * match/matchPartial stash a coarse route-level cache signal on the request
60
+ * context for the response-finalization path to emit. Default off.
61
+ */
62
+ cacheSignalEnabled?: boolean;
54
63
  }
55
64
 
56
65
  export interface MatchHandlers<TEnv = any> {
@@ -80,20 +89,6 @@ export interface MatchHandlers<TEnv = any> {
80
89
  negotiated?: boolean;
81
90
  manifestEntry?: EntryData;
82
91
  } | null>;
83
- createMatchContextForFull: (
84
- request: Request,
85
- env: TEnv,
86
- ) => Promise<MatchContext<TEnv> | { type: "redirect"; redirectUrl: string }>;
87
- createMatchContextForPartial: (
88
- request: Request,
89
- env: TEnv,
90
- actionContext?: {
91
- actionId?: string;
92
- actionUrl?: URL;
93
- actionResult?: any;
94
- formData?: FormData;
95
- },
96
- ) => Promise<MatchContext<TEnv> | null>;
97
92
  }
98
93
 
99
94
  /**
@@ -113,6 +108,20 @@ export function createMatchHandlers<TEnv = any>(
113
108
  } = deps;
114
109
  const hasTelemetry = !!deps.telemetry;
115
110
  const telemetry = resolveSink(deps.telemetry);
111
+ const cacheSignalEnabled = !!deps.cacheSignalEnabled;
112
+ const buildSignal = (
113
+ routeKey: string,
114
+ state: {
115
+ cacheHit: boolean;
116
+ cacheSource?: "runtime" | "prerender";
117
+ shouldRevalidate?: boolean;
118
+ },
119
+ ): CacheSegmentSignal[] => buildCacheSignalSegments(routeKey, state);
120
+ const recordSignalIfEnabled = (segments: CacheSegmentSignal[]): void => {
121
+ if (!cacheSignalEnabled) return;
122
+ const reqCtx = _getRequestContext();
123
+ if (reqCtx) reqCtx._cacheSignal = segments;
124
+ };
116
125
 
117
126
  async function createMatchContextForFull(
118
127
  request: Request,
@@ -145,15 +154,6 @@ export function createMatchHandlers<TEnv = any>(
145
154
  );
146
155
  }
147
156
 
148
- /**
149
- * Match request and return segments (document/SSR requests)
150
- *
151
- * Uses generator middleware pipeline for clean separation of concerns:
152
- * - cache-lookup: Check cache first
153
- * - segment-resolution: Resolve segments on cache miss
154
- * - cache-store: Store results in cache
155
- * - background-revalidation: SWR revalidation
156
- */
157
157
  async function match(request: Request, env: TEnv): Promise<MatchResult> {
158
158
  const requestId = hasTelemetry ? getRequestId(request) : undefined;
159
159
  return runWithRouterLogContext({ request, transaction: "match" }, () => {
@@ -177,7 +177,6 @@ export function createMatchHandlers<TEnv = any>(
177
177
 
178
178
  const result = await createMatchContextForFull(request, env);
179
179
 
180
- // Handle redirect case
181
180
  if ("type" in result && result.type === "redirect") {
182
181
  if (hasTelemetry) {
183
182
  safeEmit(telemetry, {
@@ -208,17 +207,24 @@ export function createMatchHandlers<TEnv = any>(
208
207
  const state = createPipelineState();
209
208
  const pipeline = createMatchPartialPipeline(ctx, state);
210
209
  const matchResult = await collectMatchResult(pipeline, ctx, state);
210
+ if (hasTelemetry || cacheSignalEnabled) {
211
+ const signalSegments = buildSignal(ctx.routeKey, state);
212
+ recordSignalIfEnabled(signalSegments);
213
+ if (hasTelemetry) {
214
+ safeEmit(telemetry, {
215
+ type: "cache.decision",
216
+ timestamp: performance.now(),
217
+ requestId,
218
+ pathname,
219
+ routeKey: ctx.routeKey,
220
+ hit: state.cacheHit,
221
+ shouldRevalidate: !!state.shouldRevalidate,
222
+ source: state.cacheSource,
223
+ segments: signalSegments,
224
+ });
225
+ }
226
+ }
211
227
  if (hasTelemetry) {
212
- safeEmit(telemetry, {
213
- type: "cache.decision",
214
- timestamp: performance.now(),
215
- requestId,
216
- pathname,
217
- routeKey: ctx.routeKey,
218
- hit: state.cacheHit,
219
- shouldRevalidate: !!state.shouldRevalidate,
220
- source: state.cacheSource,
221
- });
222
228
  safeEmit(telemetry, {
223
229
  type: "request.end",
224
230
  timestamp: performance.now(),
@@ -249,7 +255,6 @@ export function createMatchHandlers<TEnv = any>(
249
255
  });
250
256
  }
251
257
  if (error instanceof Response) throw error;
252
- // Report unhandled errors during full match pipeline
253
258
  callOnError(error, "routing", {
254
259
  request,
255
260
  url: ctx.url,
@@ -284,16 +289,6 @@ export function createMatchHandlers<TEnv = any>(
284
289
  );
285
290
  }
286
291
 
287
- /**
288
- * Match partial request with revalidation
289
- *
290
- * Uses generator middleware pipeline for clean separation of concerns:
291
- * - cache-lookup: Check cache first
292
- * - segment-resolution: Resolve segments on cache miss
293
- * - intercept-resolution: Handle intercept routes
294
- * - cache-store: Store results in cache
295
- * - background-revalidation: SWR revalidation
296
- */
297
292
  async function matchPartial(
298
293
  request: Request,
299
294
  context: TEnv,
@@ -363,17 +358,24 @@ export function createMatchHandlers<TEnv = any>(
363
358
  state,
364
359
  );
365
360
  flushRevalidationTrace();
361
+ if (hasTelemetry || cacheSignalEnabled) {
362
+ const signalSegments = buildSignal(ctx.routeKey, state);
363
+ recordSignalIfEnabled(signalSegments);
364
+ if (hasTelemetry) {
365
+ safeEmit(telemetry, {
366
+ type: "cache.decision",
367
+ timestamp: performance.now(),
368
+ requestId: partialRequestId,
369
+ pathname,
370
+ routeKey: ctx.routeKey,
371
+ hit: state.cacheHit,
372
+ shouldRevalidate: !!state.shouldRevalidate,
373
+ source: state.cacheSource,
374
+ segments: signalSegments,
375
+ });
376
+ }
377
+ }
366
378
  if (hasTelemetry) {
367
- safeEmit(telemetry, {
368
- type: "cache.decision",
369
- timestamp: performance.now(),
370
- requestId: partialRequestId,
371
- pathname,
372
- routeKey: ctx.routeKey,
373
- hit: state.cacheHit,
374
- shouldRevalidate: !!state.shouldRevalidate,
375
- source: state.cacheSource,
376
- });
377
379
  safeEmit(telemetry, {
378
380
  type: "request.end",
379
381
  timestamp: performance.now(),
@@ -406,7 +408,6 @@ export function createMatchHandlers<TEnv = any>(
406
408
  });
407
409
  }
408
410
  if (error instanceof Response) throw error;
409
- // Report unhandled errors during partial match pipeline
410
411
  callOnError(error, actionContext ? "action" : "revalidation", {
411
412
  request,
412
413
  url: ctx.url,
@@ -435,7 +436,5 @@ export function createMatchHandlers<TEnv = any>(
435
436
  matchPartial: matchPartial,
436
437
  matchError: matchError,
437
438
  previewMatch: previewMatch,
438
- createMatchContextForFull: createMatchContextForFull,
439
- createMatchContextForPartial: createMatchContextForPartial,
440
439
  };
441
440
  }