@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
@@ -15,6 +15,7 @@ import {
15
15
  import ts from "typescript";
16
16
  import { generateRouteTypesSource } from "./codegen.js";
17
17
  import type { ScanFilter } from "./scan-filter.js";
18
+ import { firstCodeMatchIndex } from "./source-scan.js";
18
19
  import {
19
20
  resolveImportedVariable,
20
21
  resolveImportPath,
@@ -38,6 +39,8 @@ function countPublicRouteEntries(source: string): number {
38
39
  }
39
40
 
40
41
  const ROUTER_CALL_PATTERN = /\bcreateRouter\s*[<(]/;
42
+ // Global variant for the code-region scan (firstCodeMatchIndex sets lastIndex).
43
+ const ROUTER_CALL_PATTERN_G = /\bcreateRouter\s*[<(]/g;
41
44
 
42
45
  function isRoutableSourceFile(name: string): boolean {
43
46
  return (
@@ -61,7 +64,7 @@ function findRouterFilesRecursive(
61
64
  entries = readdirSync(dir, { withFileTypes: true });
62
65
  } catch (err) {
63
66
  console.warn(
64
- `[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`,
67
+ `[rango] Failed to scan directory ${dir}: ${(err as Error).message}`,
65
68
  );
66
69
  return;
67
70
  }
@@ -90,7 +93,17 @@ function findRouterFilesRecursive(
90
93
 
91
94
  try {
92
95
  const source = readFileSync(fullPath, "utf-8");
93
- if (ROUTER_CALL_PATTERN.test(source)) {
96
+ // Fast path: most files contain no `createRouter(` at all, so the cheap
97
+ // raw regex short-circuits before the code-region scan. Only a file that
98
+ // mentions the token (real call OR a comment/string mention) is rescanned
99
+ // over code regions — allocation-free, never building a stripped copy —
100
+ // so a mention inside a comment or string is not mistaken for a real
101
+ // router file (which previously triggered a spurious "Multiple routers
102
+ // found" error).
103
+ if (
104
+ ROUTER_CALL_PATTERN.test(source) &&
105
+ firstCodeMatchIndex(source, ROUTER_CALL_PATTERN_G) >= 0
106
+ ) {
94
107
  routerFilesInDir.push(fullPath);
95
108
  }
96
109
  } catch {
@@ -142,7 +155,7 @@ export function findNestedRouterConflict(
142
155
 
143
156
  export function formatNestedRouterConflictError(
144
157
  conflict: { ancestor: string; nested: string },
145
- prefix = "[rsc-router]",
158
+ prefix = "[rango]",
146
159
  ): string {
147
160
  return (
148
161
  `${prefix} Nested router roots are not supported.\n` +
@@ -339,6 +352,36 @@ function applyBasenameToRoutes(
339
352
  return { routes: prefixed, searchSchemas: result.searchSchemas };
340
353
  }
341
354
 
355
+ // Filesystem path of the generated route-types file for a router source file.
356
+ // Native separators — matches the self-gen-tracking Map key the watcher compares.
357
+ export function genFileTsPath(sourceFile: string): string {
358
+ const base = pathBasename(sourceFile).replace(/\.(tsx?|jsx?)$/, "");
359
+ return join(dirname(sourceFile), `${base}.named-routes.gen.ts`);
360
+ }
361
+
362
+ // Search schemas for the gen file: prefer the runtime manifest's; when it omits
363
+ // them (some module-runner flows) fall back to static parsing filtered to the
364
+ // public route-name set. Returns the runtime value unchanged otherwise.
365
+ export function resolveSearchSchemas(
366
+ publicRouteNames: string[],
367
+ runtimeSchemas: Record<string, Record<string, string>> | undefined,
368
+ sourceFile: string,
369
+ ): Record<string, Record<string, string>> | undefined {
370
+ if (runtimeSchemas && Object.keys(runtimeSchemas).length > 0) {
371
+ return runtimeSchemas;
372
+ }
373
+ const staticParsed = buildCombinedRouteMapForRouterFile(sourceFile);
374
+ if (Object.keys(staticParsed.searchSchemas).length === 0) {
375
+ return runtimeSchemas;
376
+ }
377
+ const filtered: Record<string, Record<string, string>> = {};
378
+ for (const name of publicRouteNames) {
379
+ const schema = staticParsed.searchSchemas[name];
380
+ if (schema) filtered[name] = schema;
381
+ }
382
+ return Object.keys(filtered).length > 0 ? filtered : runtimeSchemas;
383
+ }
384
+
342
385
  /**
343
386
  * Resolve routes and search schemas from a router source file by following the
344
387
  * variable passed to `.routes(...)` or `urls: ...` in createRouter options,
@@ -519,6 +562,73 @@ export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
519
562
  return result;
520
563
  }
521
564
 
565
+ const HOST_ROUTER_CALL_PATTERN = /\bcreateHostRouter\s*[<(]/;
566
+ const HOST_ROUTER_CALL_PATTERN_G = /\bcreateHostRouter\s*[<(]/g;
567
+
568
+ function findHostRouterFilesRecursive(
569
+ dir: string,
570
+ filter: ScanFilter | undefined,
571
+ results: string[],
572
+ ): void {
573
+ let entries;
574
+ try {
575
+ entries = readdirSync(dir, { withFileTypes: true });
576
+ } catch (err) {
577
+ console.warn(
578
+ `[rango] Failed to scan directory ${dir}: ${(err as Error).message}`,
579
+ );
580
+ return;
581
+ }
582
+
583
+ for (const entry of entries) {
584
+ const fullPath = join(dir, entry.name);
585
+ if (entry.isDirectory()) {
586
+ if (
587
+ entry.name === "node_modules" ||
588
+ entry.name === "dist" ||
589
+ entry.name === "coverage" ||
590
+ entry.name === "__tests__" ||
591
+ entry.name === "__mocks__" ||
592
+ entry.name.startsWith(".")
593
+ )
594
+ continue;
595
+ // Unlike createRouter scanning, do NOT stop at router roots: the host entry
596
+ // lives above the sub-app router directories, so descend the whole tree.
597
+ findHostRouterFilesRecursive(fullPath, filter, results);
598
+ continue;
599
+ }
600
+
601
+ if (!isRoutableSourceFile(entry.name)) continue;
602
+ if (filter && !filter(fullPath)) continue;
603
+
604
+ try {
605
+ const source = readFileSync(fullPath, "utf-8");
606
+ if (
607
+ HOST_ROUTER_CALL_PATTERN.test(source) &&
608
+ firstCodeMatchIndex(source, HOST_ROUTER_CALL_PATTERN_G) >= 0
609
+ ) {
610
+ results.push(fullPath);
611
+ }
612
+ } catch {
613
+ continue;
614
+ }
615
+ }
616
+ }
617
+
618
+ /**
619
+ * Scan for files containing createHostRouter() and return their paths. Unlike
620
+ * findRouterFiles, this does NOT stop at the first router-root directory -- a host
621
+ * entry typically sits above the sub-app router roots, so the whole tree is scanned.
622
+ */
623
+ export function findHostRouterFiles(
624
+ root: string,
625
+ filter?: ScanFilter,
626
+ ): string[] {
627
+ const result: string[] = [];
628
+ findHostRouterFilesRecursive(root, filter, result);
629
+ return result;
630
+ }
631
+
522
632
  /**
523
633
  * Write named-routes.gen.ts files from static source parsing.
524
634
  * Dev-only: provides initial .gen.ts files for IDE types before runtime
@@ -528,7 +638,10 @@ export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
528
638
  export function writeCombinedRouteTypes(
529
639
  root: string,
530
640
  knownRouterFiles?: string[],
531
- opts?: { preserveIfLarger?: boolean },
641
+ opts?: {
642
+ preserveIfLarger?: boolean;
643
+ onWrite?: (outPath: string, content: string) => void;
644
+ },
532
645
  ): void {
533
646
  // Delete old combined named-routes.gen.ts if it exists (stale from older versions)
534
647
  try {
@@ -536,7 +649,7 @@ export function writeCombinedRouteTypes(
536
649
  if (existsSync(oldCombinedPath)) {
537
650
  unlinkSync(oldCombinedPath);
538
651
  console.log(
539
- `[rsc-router] Removed stale combined route types: ${oldCombinedPath}`,
652
+ `[rango] Removed stale combined route types: ${oldCombinedPath}`,
540
653
  );
541
654
  }
542
655
  } catch {}
@@ -566,24 +679,15 @@ export function writeCombinedRouteTypes(
566
679
  if (!extractUrlsFromRouter(routerSource)) continue;
567
680
  }
568
681
 
569
- const routerBasename = pathBasename(routerFilePath).replace(
570
- /\.(tsx?|jsx?)$/,
571
- "",
572
- );
573
- const outPath = join(
574
- dirname(routerFilePath),
575
- `${routerBasename}.named-routes.gen.ts`,
576
- );
682
+ const outPath = genFileTsPath(routerFilePath);
577
683
  const existing = existsSync(outPath)
578
684
  ? readFileSync(outPath, "utf-8")
579
685
  : null;
580
686
 
581
- // When the static parser can't extract routes (e.g. callback-style urls()),
582
- // write an empty placeholder so the build-time transform's injected import
583
- // resolves. Runtime discovery will overwrite this with the real routes.
584
687
  if (Object.keys(result.routes).length === 0) {
585
688
  if (!existing) {
586
689
  const emptySource = generateRouteTypesSource({});
690
+ opts?.onWrite?.(outPath, emptySource);
587
691
  writeFileSync(outPath, emptySource);
588
692
  }
589
693
  continue;
@@ -595,11 +699,6 @@ export function writeCombinedRouteTypes(
595
699
  hasSearchSchemas ? result.searchSchemas : undefined,
596
700
  );
597
701
  if (existing !== source) {
598
- // On initial dev startup, don't overwrite a file from runtime discovery
599
- // (which has all dynamic routes) with a smaller set from the static
600
- // parser. The static parser can't see routes generated by Array.from()
601
- // or other dynamic code. During HMR (file watcher), always write so
602
- // newly added routes appear immediately.
603
702
  if (opts?.preserveIfLarger && existing) {
604
703
  const existingCount = countPublicRouteEntries(existing);
605
704
  const newCount = Object.keys(result.routes).filter(
@@ -609,9 +708,10 @@ export function writeCombinedRouteTypes(
609
708
  continue;
610
709
  }
611
710
  }
711
+ opts?.onWrite?.(outPath, source);
612
712
  writeFileSync(outPath, source);
613
713
  console.log(
614
- `[rsc-router] Generated route types (${Object.keys(result.routes).length} routes) -> ${outPath}`,
714
+ `[rango] Generated route types (${Object.keys(result.routes).length} routes) -> ${outPath}`,
615
715
  );
616
716
  }
617
717
  }
@@ -54,7 +54,7 @@ export function findTsFiles(dir: string, filter?: ScanFilter): string[] {
54
54
  entries = readdirSync(dir, { withFileTypes: true });
55
55
  } catch (err) {
56
56
  console.warn(
57
- `[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`,
57
+ `[rango] Failed to scan directory ${dir}: ${(err as Error).message}`,
58
58
  );
59
59
  return results;
60
60
  }
@@ -0,0 +1,118 @@
1
+ // Allocation-light, linear-time source scanning for the build-time scanners.
2
+ //
3
+ // The router-file scanner, the HMR relevance check, and the unsupported-shape
4
+ // warning all need to know whether a token like `createRouter(` / `createLoader(`
5
+ // appears in REAL code versus inside a comment or string literal. Rather than
6
+ // build a full comment/string-stripped copy of the source (which on a large
7
+ // file allocates an O(n) string plus, naively, a per-char array), these helpers
8
+ // run the regex over the whole source ONCE (the engine sweeps left-to-right,
9
+ // O(n)) and classify each match's offset with a forward, O(1)-memory cursor that
10
+ // advances monotonically across the source.
11
+ //
12
+ // Time: O(n) — one native regex sweep plus one forward classification pass.
13
+ // Memory: O(1) for the boolean check; O(#matches) for the index list. No
14
+ // stripped copy and no per-char array are ever materialized.
15
+ //
16
+ // Pragmatic scanner, not a full tokenizer: regex literals are not special-cased
17
+ // (a target token inside one is implausible) and template interpolations are
18
+ // treated as opaque string content. One intentional consequence: a token whose
19
+ // match would only complete by treating an interleaved comment as whitespace
20
+ // (e.g. `createRouter /* x */ (`) is not detected — real calls never interleave
21
+ // a comment between the callee and its arguments.
22
+
23
+ // JS line terminators end a `//` comment: LF, CR, LS (U+2028), PS (U+2029).
24
+ function isLineTerminator(ch: string): boolean {
25
+ const c = ch.charCodeAt(0);
26
+ // LF, CR, LS (U+2028), PS (U+2029)
27
+ return c === 10 || c === 13 || c === 0x2028 || c === 0x2029;
28
+ }
29
+
30
+ /**
31
+ * Build a classifier that answers "is offset `q` in code (not a comment or
32
+ * string)?" for STRICTLY INCREASING `q`. The internal cursor only moves forward,
33
+ * so a full left-to-right sequence of queries costs O(n) total with O(1) memory.
34
+ */
35
+ function makeCodeClassifier(code: string): (q: number) => boolean {
36
+ const n = code.length;
37
+ let i = 0; // forward cursor: everything before `i` is already classified
38
+ let skipStart = -1; // last detected comment/string region (cache)
39
+ let skipEnd = -1;
40
+
41
+ return (q: number): boolean => {
42
+ if (q >= skipStart && q < skipEnd) return false; // q in the cached region
43
+ while (i < n && i <= q) {
44
+ const c = code[i];
45
+ const d = i + 1 < n ? code[i + 1] : "";
46
+ let end = -1;
47
+ if (c === "/" && d === "/") {
48
+ let j = i + 2;
49
+ while (j < n && !isLineTerminator(code[j])) j++;
50
+ end = j;
51
+ } else if (c === "/" && d === "*") {
52
+ let j = i + 2;
53
+ while (j < n && !(code[j] === "*" && code[j + 1] === "/")) j++;
54
+ end = Math.min(n, j + 2);
55
+ } else if (c === '"' || c === "'" || c === "`") {
56
+ let j = i + 1;
57
+ while (j < n) {
58
+ if (code[j] === "\\") {
59
+ j += 2;
60
+ continue;
61
+ }
62
+ if (code[j] === c) {
63
+ j++;
64
+ break;
65
+ }
66
+ j++;
67
+ }
68
+ end = j;
69
+ }
70
+ if (end >= 0) {
71
+ // Comment/string region [i, end). `q >= i` here (loop condition).
72
+ if (q < end) {
73
+ skipStart = i;
74
+ skipEnd = end;
75
+ return false;
76
+ }
77
+ i = end;
78
+ } else {
79
+ i++;
80
+ }
81
+ }
82
+ return true; // reached q in code mode
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Index of the first match of `pattern` that occurs in code (not in a comment
88
+ * or string), or -1. `pattern` MUST be a global (`/g`) regex. Single native
89
+ * regex sweep with early-exit; O(1) extra memory.
90
+ */
91
+ export function firstCodeMatchIndex(code: string, pattern: RegExp): number {
92
+ const inCode = makeCodeClassifier(code);
93
+ pattern.lastIndex = 0;
94
+ let m: RegExpExecArray | null;
95
+ while ((m = pattern.exec(code)) !== null) {
96
+ if (inCode(m.index)) return m.index;
97
+ if (pattern.lastIndex <= m.index) pattern.lastIndex = m.index + 1;
98
+ }
99
+ return -1;
100
+ }
101
+
102
+ /**
103
+ * Byte offsets of every match of `pattern` that occurs in code (not in a
104
+ * comment or string). `pattern` MUST be a global (`/g`) regex. Each offset is
105
+ * the match start — the same byte offset a raw `pattern.exec` reports. O(n)
106
+ * time, O(#matches) memory.
107
+ */
108
+ export function codeMatchIndices(code: string, pattern: RegExp): number[] {
109
+ const inCode = makeCodeClassifier(code);
110
+ const indices: number[] = [];
111
+ pattern.lastIndex = 0;
112
+ let m: RegExpExecArray | null;
113
+ while ((m = pattern.exec(code)) !== null) {
114
+ if (inCode(m.index)) indices.push(m.index);
115
+ if (pattern.lastIndex <= m.index) pattern.lastIndex = m.index + 1;
116
+ }
117
+ return indices;
118
+ }
@@ -1,8 +1,9 @@
1
- import { dirname, join, basename, resolve } from "node:path";
1
+ import { resolve } from "node:path";
2
2
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import {
4
4
  generateRouteTypesSource,
5
- buildCombinedRouteMapForRouterFile,
5
+ genFileTsPath,
6
+ resolveSearchSchemas,
6
7
  } from "./generate-route-types.ts";
7
8
  import { isAutoGeneratedRouteName } from "../route-name.js";
8
9
 
@@ -175,25 +176,13 @@ export async function discoverAndWriteRouteTypes(
175
176
  );
176
177
  }
177
178
 
178
- // Search schema fallback: runtime manifest may omit search schema metadata
179
- // in some module-runner flows. Fall back to static source parsing.
180
- if (!routeSearchSchemas || Object.keys(routeSearchSchemas).length === 0) {
181
- const staticParsed = buildCombinedRouteMapForRouterFile(sourceFile);
182
- if (Object.keys(staticParsed.searchSchemas).length > 0) {
183
- const filtered: Record<string, Record<string, string>> = {};
184
- for (const name of Object.keys(routeManifest)) {
185
- const schema = staticParsed.searchSchemas[name];
186
- if (schema) filtered[name] = schema;
187
- }
188
- if (Object.keys(filtered).length > 0) {
189
- routeSearchSchemas = filtered;
190
- }
191
- }
192
- }
179
+ routeSearchSchemas = resolveSearchSchemas(
180
+ Object.keys(routeManifest),
181
+ routeSearchSchemas,
182
+ sourceFile,
183
+ );
193
184
 
194
- const routerDir = dirname(sourceFile);
195
- const routerBasename = basename(sourceFile).replace(/\.(tsx?|jsx?)$/, "");
196
- const outPath = join(routerDir, `${routerBasename}.named-routes.gen.ts`);
185
+ const outPath = genFileTsPath(sourceFile);
197
186
 
198
187
  const source = generateRouteTypesSource(
199
188
  routeManifest,
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Cache error reporting.
3
+ *
4
+ * Caches are best-effort: a read failure degrades to a miss (render fresh) and a
5
+ * write failure degrades to a no-op - they MUST NOT throw up and fail the
6
+ * request. But the failure must still be LOUD: it is logged to the console (so
7
+ * it is visible even in a background waitUntil task or when no hook is wired)
8
+ * AND routed through the router's onError callback (via the request context's
9
+ * deduped _reportBackgroundError) so consumers can observe cache degradation in
10
+ * their own telemetry.
11
+ *
12
+ * The one deliberate exception is the invalidation WRITE verb (updateTag ->
13
+ * store.invalidateTags): a failed durable marker write is rejected so an awaited
14
+ * updateTag() surfaces it (read-your-own-writes honesty). That is not a data
15
+ * read/write and does not go through this helper's swallow-and-degrade path.
16
+ */
17
+
18
+ import { _getRequestContext } from "../server/request-context.js";
19
+
20
+ /**
21
+ * Minimal shape of a request context for error reporting. Passed explicitly by
22
+ * background tasks (waitUntil) where the ALS context is already gone, so the
23
+ * error can still reach the router's onError. Structural to avoid importing the
24
+ * full RequestContext type (request-context.ts imports CacheErrorCategory from
25
+ * here - a mutual type-only reference).
26
+ */
27
+ export interface CacheErrorReporter {
28
+ _reportBackgroundError?: (
29
+ error: unknown,
30
+ category: CacheErrorCategory,
31
+ ) => void;
32
+ }
33
+
34
+ export type CacheErrorCategory =
35
+ /** A read failed (transient infra: KV/Cache API error). Degrade to a miss. */
36
+ | "cache-read"
37
+ /** A write failed. Degrade to a no-op (entry simply not cached). */
38
+ | "cache-write"
39
+ /** A delete/eviction failed. Best-effort. */
40
+ | "cache-delete"
41
+ /**
42
+ * A STORED entry could not be parsed/deserialized (partial KV read, truncated
43
+ * Cache API body, malformed envelope/RSC payload). The entry is faulty and is
44
+ * evicted so subsequent reads do not keep failing on it. Distinct from
45
+ * cache-read so consumers can tell corruption from a transient outage.
46
+ */
47
+ | "cache-corrupt"
48
+ /** A tag-invalidation side effect failed (e.g. the eager CDN purge hook). */
49
+ | "cache-invalidate"
50
+ /**
51
+ * A background stale-while-revalidate refresh failed (the `"use cache"`
52
+ * read-through path). The stale value was already served; the refresh that
53
+ * would have replaced it errored.
54
+ */
55
+ | "stale-revalidation";
56
+
57
+ /**
58
+ * Report a non-fatal cache error loudly without failing the request: always logs
59
+ * (label + error) and, when a request context is available, routes the error
60
+ * through the router's onError callback. Never throws.
61
+ *
62
+ * `ctx` is for callers running in a detached background task (waitUntil), where
63
+ * the ALS request context is already gone (so `_getRequestContext()` is null):
64
+ * they capture the context up front and pass it here so onError still fires.
65
+ * Foreground callers omit it and fall back to the ALS context.
66
+ */
67
+ export function reportCacheError(
68
+ error: unknown,
69
+ category: CacheErrorCategory,
70
+ label: string,
71
+ ctx?: CacheErrorReporter,
72
+ ): void {
73
+ console.error(`${label}:`, error);
74
+ try {
75
+ const target = ctx ?? _getRequestContext();
76
+ target?._reportBackgroundError?.(error, category);
77
+ } catch {
78
+ // Reporting must never itself break the cache path.
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Run a best-effort async cache task (typically scheduled via waitUntil), catching
84
+ * any rejection and routing it through reportCacheError so background cache work
85
+ * (non-blocking L1 writes, KV persistence, L1 promotion) reports failures via
86
+ * onError instead of throwing or silently swallowing. Never rejects.
87
+ *
88
+ * Pass `ctx` when the task runs detached (the ALS context is gone) and the
89
+ * failure should still reach onError; omit it to fall back to the ALS context.
90
+ *
91
+ * @example this.waitUntil(() => reportingAsync(() => cache.put(req, res), "cache-write", "[CFCacheStore] L1 write"))
92
+ */
93
+ export async function reportingAsync(
94
+ task: () => Promise<unknown>,
95
+ category: CacheErrorCategory,
96
+ label: string,
97
+ ctx?: CacheErrorReporter,
98
+ ): Promise<void> {
99
+ try {
100
+ await task();
101
+ } catch (error) {
102
+ reportCacheError(error, category, label, ctx);
103
+ }
104
+ }
@@ -9,6 +9,7 @@
9
9
  import type { CacheDefaults, SegmentCacheStore } from "./types.js";
10
10
  import { _getRequestContext } from "../server/request-context.js";
11
11
  import type { RequestContext } from "../server/request-context.js";
12
+ import { normalizeTags } from "./cache-tag.js";
12
13
 
13
14
  /**
14
15
  * Default TTL for route-level cache() DSL and loader cache.
@@ -68,24 +69,6 @@ export function computeExpiration(
68
69
  return { staleAt, expiresAt };
69
70
  }
70
71
 
71
- // ============================================================================
72
- // Cache Key Resolution
73
- // ============================================================================
74
-
75
- /**
76
- * Resolve cache key using the 3-tier priority:
77
- * 1. keyFn (full override from route/loader cache options)
78
- * 2. store.keyGenerator (modifies default key)
79
- * 3. defaultKey (used when neither keyFn nor keyGenerator is provided)
80
- *
81
- * Errors from keyFn and store.keyGenerator propagate to the caller.
82
- * Cache identity is correctness-critical: if explicit key logic throws,
83
- * silently remapping to a different key could cause cache collisions or
84
- * serve stale/wrong data. Callers must handle the error or let it surface.
85
- *
86
- * Uses _getRequestContext (non-throwing) so that calls outside ALS
87
- * (e.g. build-time) gracefully fall back to defaultKey.
88
- */
89
72
  export async function resolveCacheKey(
90
73
  keyFn: ((ctx: RequestContext) => string | Promise<string>) | undefined,
91
74
  store: SegmentCacheStore | null,
@@ -94,32 +77,89 @@ export async function resolveCacheKey(
94
77
  ): Promise<string> {
95
78
  const requestCtx = _getRequestContext();
96
79
 
97
- // Priority 1: Route/loader-level key function (full override)
98
80
  if (keyFn && requestCtx) {
99
81
  return await keyFn(requestCtx);
100
82
  }
101
83
 
102
- // Priority 2: Store-level keyGenerator (modifies default key)
103
84
  if (store?.keyGenerator && requestCtx) {
104
85
  return await store.keyGenerator(requestCtx, defaultKey);
105
86
  }
106
87
 
107
- // Priority 3: Default key (no custom key logic provided)
108
88
  return defaultKey;
109
89
  }
110
90
 
111
- // ============================================================================
112
- // Cache Store Resolution
113
- // ============================================================================
91
+ export function resolveTagsOption<TEnv>(
92
+ tags: string[] | ((ctx: RequestContext<TEnv>) => string[]) | undefined,
93
+ ctx: RequestContext<TEnv> | undefined,
94
+ label: string,
95
+ ): string[] | undefined {
96
+ if (!tags) return undefined;
97
+ if (typeof tags === "function") {
98
+ if (!ctx) {
99
+ console.warn(
100
+ `[${label}] Dynamic tags function present but no request context; ` +
101
+ `caching without tags (this entry will not be tag-invalidatable).`,
102
+ );
103
+ return undefined;
104
+ }
105
+ try {
106
+ return normalizeTagList(tags(ctx));
107
+ } catch (error) {
108
+ console.error(
109
+ `[${label}] Tags function failed, caching without tags:`,
110
+ error,
111
+ );
112
+ return undefined;
113
+ }
114
+ }
115
+ return normalizeTagList(tags);
116
+ }
114
117
 
115
118
  /**
116
- * Resolve cache store from the 2-tier priority:
117
- * 1. Explicit store from cache options
118
- * 2. App-level store from request context
119
+ * Normalize a resolved tags array so the WRITE path matches the invalidate path:
120
+ * updateTag()/revalidateTag()/cacheTag() all drop empty/whitespace-only tags via
121
+ * normalizeTag(). Without this, an empty tag attached at write time would enter
122
+ * the store index but could never be invalidated (the verbs normalize it away),
123
+ * and on CFCacheStore would also cost a wasted KV marker read per request.
124
+ * Returns undefined when nothing usable remains, keeping the entry header-free.
119
125
  */
126
+ function normalizeTagList(tags: string[]): string[] | undefined {
127
+ const out = normalizeTags(tags);
128
+ return out.length > 0 ? out : undefined;
129
+ }
130
+
120
131
  export function resolveCacheStore(
121
132
  explicitStore: SegmentCacheStore | undefined,
122
133
  ): SegmentCacheStore | null {
123
- if (explicitStore) return explicitStore;
134
+ if (explicitStore) {
135
+ registerExplicitTaggedStore(explicitStore);
136
+ return explicitStore;
137
+ }
124
138
  return _getRequestContext()?._cacheStore ?? null;
125
139
  }
140
+
141
+ /**
142
+ * Upper bound on the per-handler explicit-store registry. A module-singleton
143
+ * store (the recommended pattern) dedupes to a single entry and never approaches
144
+ * this. The cap bounds the niche case of an explicit store constructed PER
145
+ * request/boundary (e.g. a ctx-bound CFCacheStore, which must take a per-request
146
+ * ctx): without it the registry - which intentionally persists across requests so
147
+ * a server action's updateTag() can reach stores a prior render registered -
148
+ * would accumulate one dead instance per request and fan invalidation out to
149
+ * finished execution contexts. LRU-touch on re-resolution keeps a live, re-used
150
+ * store from being evicted by that churn.
151
+ */
152
+ const EXPLICIT_STORE_REGISTRY_CAP = 64;
153
+
154
+ function registerExplicitTaggedStore(store: SegmentCacheStore): void {
155
+ const set = _getRequestContext()?._explicitTaggedStores;
156
+ if (!set) return;
157
+ // LRU touch: move an already-present store to the most-recent position (Set
158
+ // preserves insertion order) so a store re-resolved every request stays live.
159
+ set.delete(store);
160
+ set.add(store);
161
+ if (set.size > EXPLICIT_STORE_REGISTRY_CAP) {
162
+ const oldest = set.values().next().value;
163
+ if (oldest !== undefined) set.delete(oldest);
164
+ }
165
+ }