@rangojs/router 0.0.0-experimental.79 → 0.0.0-experimental.7c7e4327

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 (440) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +301 -797
  3. package/dist/bin/rango.js +603 -145
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3750 -1160
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +96 -24
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +85 -6
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +228 -33
  12. package/skills/caching/SKILL.md +336 -19
  13. package/skills/catalog.json +271 -0
  14. package/skills/comparison/SKILL.md +50 -0
  15. package/skills/comparison/agents/openai.yaml +4 -0
  16. package/skills/comparison/references/framework-comparison.md +837 -0
  17. package/skills/composability/SKILL.md +110 -4
  18. package/skills/css/SKILL.md +76 -0
  19. package/skills/debug-manifest/SKILL.md +5 -3
  20. package/skills/defer-hydration/SKILL.md +235 -0
  21. package/skills/document-cache/SKILL.md +87 -56
  22. package/skills/fonts/SKILL.md +1 -1
  23. package/skills/handler-use/SKILL.md +12 -10
  24. package/skills/hooks/SKILL.md +73 -691
  25. package/skills/hooks/data.md +273 -0
  26. package/skills/hooks/handle-and-actions.md +103 -0
  27. package/skills/hooks/navigation.md +110 -0
  28. package/skills/hooks/outlets.md +41 -0
  29. package/skills/hooks/state.md +228 -0
  30. package/skills/hooks/urls.md +135 -0
  31. package/skills/host-router/SKILL.md +129 -27
  32. package/skills/i18n/SKILL.md +276 -0
  33. package/skills/intercept/SKILL.md +75 -19
  34. package/skills/layout/SKILL.md +40 -19
  35. package/skills/links/SKILL.md +247 -17
  36. package/skills/loader/SKILL.md +248 -10
  37. package/skills/middleware/SKILL.md +25 -13
  38. package/skills/migrate-nextjs/SKILL.md +205 -20
  39. package/skills/migrate-react-router/SKILL.md +59 -670
  40. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  41. package/skills/migrate-react-router/component-migration.md +196 -0
  42. package/skills/migrate-react-router/data-and-actions.md +225 -0
  43. package/skills/migrate-react-router/route-mapping.md +271 -0
  44. package/skills/mime-routes/SKILL.md +29 -2
  45. package/skills/observability/SKILL.md +202 -0
  46. package/skills/parallel/SKILL.md +40 -10
  47. package/skills/ppr/SKILL.md +616 -0
  48. package/skills/prerender/SKILL.md +72 -60
  49. package/skills/rango/SKILL.md +318 -26
  50. package/skills/react-compiler/SKILL.md +168 -0
  51. package/skills/response-routes/SKILL.md +138 -49
  52. package/skills/route/SKILL.md +117 -9
  53. package/skills/router-setup/SKILL.md +44 -9
  54. package/skills/scripts/SKILL.md +179 -0
  55. package/skills/server-actions/SKILL.md +776 -0
  56. package/skills/shell-manifest/SKILL.md +185 -0
  57. package/skills/streams-and-websockets/SKILL.md +283 -0
  58. package/skills/tailwind/SKILL.md +28 -4
  59. package/skills/testing/SKILL.md +130 -0
  60. package/skills/testing/bindings.md +103 -0
  61. package/skills/testing/cache-prerender.md +127 -0
  62. package/skills/testing/client-components.md +124 -0
  63. package/skills/testing/e2e-parity.md +125 -0
  64. package/skills/testing/flight.md +91 -0
  65. package/skills/testing/handles.md +131 -0
  66. package/skills/testing/loader.md +128 -0
  67. package/skills/testing/middleware.md +99 -0
  68. package/skills/testing/render-handler.md +122 -0
  69. package/skills/testing/response-routes.md +95 -0
  70. package/skills/testing/reverse-and-types.md +85 -0
  71. package/skills/testing/server-actions.md +107 -0
  72. package/skills/testing/server-tree.md +128 -0
  73. package/skills/testing/setup.md +123 -0
  74. package/skills/theme/SKILL.md +1 -1
  75. package/skills/typesafety/SKILL.md +45 -626
  76. package/skills/typesafety/env-and-bindings.md +254 -0
  77. package/skills/typesafety/generated-files-and-cli.md +335 -0
  78. package/skills/typesafety/params-and-search.md +153 -0
  79. package/skills/typesafety/route-types.md +209 -0
  80. package/skills/use-cache/SKILL.md +74 -15
  81. package/skills/vercel/SKILL.md +128 -0
  82. package/skills/view-transitions/SKILL.md +337 -0
  83. package/src/__augment-tests__/augment.ts +81 -0
  84. package/src/__augment-tests__/augmented.check.ts +116 -0
  85. package/src/__internal.ts +0 -65
  86. package/src/browser/action-coordinator.ts +53 -36
  87. package/src/browser/action-fence.ts +47 -0
  88. package/src/browser/app-shell.ts +39 -0
  89. package/src/browser/connection-warmup.ts +134 -0
  90. package/src/browser/cookie-name.ts +140 -0
  91. package/src/browser/event-controller.ts +252 -158
  92. package/src/browser/history-state.ts +21 -0
  93. package/src/browser/index.ts +3 -3
  94. package/src/browser/invalidate-client-cache.ts +52 -0
  95. package/src/browser/logging.ts +28 -0
  96. package/src/browser/merge-segment-loaders.ts +6 -4
  97. package/src/browser/navigation-bridge.ts +94 -25
  98. package/src/browser/navigation-client.ts +144 -79
  99. package/src/browser/navigation-store-handle.ts +38 -0
  100. package/src/browser/navigation-store.ts +161 -73
  101. package/src/browser/navigation-transaction.ts +9 -59
  102. package/src/browser/network-error-handler.ts +34 -7
  103. package/src/browser/partial-update.ts +183 -144
  104. package/src/browser/prefetch/cache.ts +242 -77
  105. package/src/browser/prefetch/fetch.ts +325 -69
  106. package/src/browser/prefetch/queue.ts +61 -12
  107. package/src/browser/rango-state.ts +158 -76
  108. package/src/browser/react/Link.tsx +58 -20
  109. package/src/browser/react/NavigationProvider.tsx +202 -120
  110. package/src/browser/react/ScrollRestoration.tsx +10 -6
  111. package/src/browser/react/filter-segment-order.ts +66 -7
  112. package/src/browser/react/index.ts +0 -48
  113. package/src/browser/react/location-state-shared.ts +178 -8
  114. package/src/browser/react/location-state.ts +39 -14
  115. package/src/browser/react/use-action.ts +6 -15
  116. package/src/browser/react/use-handle.ts +17 -14
  117. package/src/browser/react/use-href.tsx +8 -1
  118. package/src/browser/react/use-link-status.ts +33 -8
  119. package/src/browser/react/use-navigation.ts +32 -7
  120. package/src/browser/react/use-params.ts +20 -10
  121. package/src/browser/react/use-reverse.ts +106 -0
  122. package/src/browser/react/use-router.ts +25 -3
  123. package/src/browser/react/use-search-params.ts +0 -5
  124. package/src/browser/react/use-segments.ts +11 -21
  125. package/src/browser/response-adapter.ts +99 -8
  126. package/src/browser/rsc-router.tsx +145 -28
  127. package/src/browser/scroll-restoration.ts +37 -22
  128. package/src/browser/segment-reconciler.ts +31 -21
  129. package/src/browser/segment-structure-assert.ts +2 -2
  130. package/src/browser/server-action-bridge.ts +236 -65
  131. package/src/browser/types.ts +102 -9
  132. package/src/browser/validate-redirect-origin.ts +43 -16
  133. package/src/build/collect-fallback-refs.ts +107 -0
  134. package/src/build/generate-manifest.ts +203 -154
  135. package/src/build/generate-route-types.ts +3 -1
  136. package/src/build/index.ts +11 -3
  137. package/src/build/prefix-tree-utils.ts +123 -0
  138. package/src/build/route-trie.ts +152 -21
  139. package/src/build/route-types/ast-route-extraction.ts +15 -8
  140. package/src/build/route-types/codegen.ts +16 -5
  141. package/src/build/route-types/include-resolution.ts +456 -62
  142. package/src/build/route-types/param-extraction.ts +6 -3
  143. package/src/build/route-types/per-module-writer.ts +22 -6
  144. package/src/build/route-types/router-processing.ts +128 -51
  145. package/src/build/route-types/scan-filter.ts +1 -1
  146. package/src/build/route-types/source-scan.ts +216 -0
  147. package/src/build/runtime-discovery.ts +13 -21
  148. package/src/cache/cache-error.ts +104 -0
  149. package/src/cache/cache-key-utils.ts +58 -13
  150. package/src/cache/cache-policy.ts +108 -34
  151. package/src/cache/cache-runtime.ts +421 -58
  152. package/src/cache/cache-scope.ts +187 -96
  153. package/src/cache/cache-tag.ts +149 -0
  154. package/src/cache/cf/cf-base64.ts +33 -0
  155. package/src/cache/cf/cf-cache-constants.ts +127 -0
  156. package/src/cache/cf/cf-cache-store.ts +2202 -372
  157. package/src/cache/cf/cf-cache-types.ts +349 -0
  158. package/src/cache/cf/cf-kv-utils.ts +46 -0
  159. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  160. package/src/cache/cf/index.ts +6 -16
  161. package/src/cache/document-cache.ts +126 -41
  162. package/src/cache/handle-snapshot.ts +70 -0
  163. package/src/cache/index.ts +23 -20
  164. package/src/cache/memory-segment-store.ts +243 -37
  165. package/src/cache/profile-registry.ts +46 -31
  166. package/src/cache/read-through-swr.ts +56 -12
  167. package/src/cache/segment-codec.ts +13 -21
  168. package/src/cache/shell-snapshot.ts +417 -0
  169. package/src/cache/tag-invalidation.ts +230 -0
  170. package/src/cache/types.ts +180 -99
  171. package/src/cache/vercel/index.ts +11 -0
  172. package/src/cache/vercel/vercel-cache-store.ts +1127 -0
  173. package/src/client.rsc.tsx +41 -21
  174. package/src/client.tsx +33 -61
  175. package/src/cloudflare/index.ts +11 -0
  176. package/src/cloudflare/tracing.ts +108 -0
  177. package/src/component-utils.ts +19 -0
  178. package/src/components/DefaultDocument.tsx +8 -2
  179. package/src/context-var.ts +18 -6
  180. package/src/decode-loader-results.ts +52 -0
  181. package/src/defer.ts +185 -0
  182. package/src/deps/ssr.ts +0 -1
  183. package/src/encode-kv.ts +49 -0
  184. package/src/errors.ts +30 -4
  185. package/src/escape-script.ts +52 -0
  186. package/src/handle.ts +67 -37
  187. package/src/handles/MetaTags.tsx +24 -53
  188. package/src/handles/Scripts.tsx +183 -0
  189. package/src/handles/breadcrumbs.ts +35 -8
  190. package/src/handles/deferred-resolution.ts +127 -0
  191. package/src/handles/is-thenable.ts +18 -0
  192. package/src/handles/meta.ts +14 -40
  193. package/src/handles/script.ts +244 -0
  194. package/src/host/cookie-handler.ts +9 -60
  195. package/src/host/errors.ts +13 -22
  196. package/src/host/index.ts +9 -2
  197. package/src/host/pattern-matcher.ts +23 -52
  198. package/src/host/router.ts +107 -99
  199. package/src/host/testing.ts +40 -27
  200. package/src/host/types.ts +37 -4
  201. package/src/host/utils.ts +1 -1
  202. package/src/href-client.ts +137 -22
  203. package/src/index.rsc.ts +97 -12
  204. package/src/index.ts +98 -14
  205. package/src/internal-debug.ts +11 -10
  206. package/src/loader-store.ts +500 -0
  207. package/src/loader.rsc.ts +20 -13
  208. package/src/loader.ts +12 -11
  209. package/src/missing-id-error.ts +68 -0
  210. package/src/outlet-context.ts +1 -1
  211. package/src/outlet-provider.tsx +1 -5
  212. package/src/prerender/param-hash.ts +16 -16
  213. package/src/prerender/store.ts +32 -37
  214. package/src/prerender.ts +78 -10
  215. package/src/redirect-origin.ts +114 -0
  216. package/src/regex-escape.ts +8 -0
  217. package/src/render-error-thrower.tsx +20 -0
  218. package/src/response-utils.ts +62 -0
  219. package/src/reverse.ts +65 -39
  220. package/src/root-error-boundary.tsx +1 -19
  221. package/src/route-content-wrapper.tsx +19 -77
  222. package/src/route-definition/dsl-helpers.ts +304 -309
  223. package/src/route-definition/helper-factories.ts +28 -140
  224. package/src/route-definition/helpers-types.ts +87 -59
  225. package/src/route-definition/index.ts +1 -2
  226. package/src/route-definition/redirect.ts +44 -11
  227. package/src/route-definition/resolve-handler-use.ts +12 -1
  228. package/src/route-definition/use-item-types.ts +29 -0
  229. package/src/route-map-builder.ts +41 -20
  230. package/src/route-types.ts +19 -46
  231. package/src/router/basename.ts +14 -0
  232. package/src/router/content-negotiation.ts +73 -25
  233. package/src/router/error-handling.ts +45 -18
  234. package/src/router/find-match.ts +129 -30
  235. package/src/router/handler-context.ts +27 -42
  236. package/src/router/instrument.ts +355 -0
  237. package/src/router/intercept-resolution.ts +39 -20
  238. package/src/router/lazy-includes.ts +82 -59
  239. package/src/router/loader-resolution.ts +167 -72
  240. package/src/router/logging.ts +0 -6
  241. package/src/router/manifest.ts +74 -40
  242. package/src/router/match-api.ts +80 -55
  243. package/src/router/match-context.ts +0 -22
  244. package/src/router/match-handlers.ts +211 -165
  245. package/src/router/match-middleware/background-revalidation.ts +40 -24
  246. package/src/router/match-middleware/cache-lookup.ts +159 -285
  247. package/src/router/match-middleware/cache-store.ts +64 -52
  248. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  249. package/src/router/match-middleware/segment-resolution.ts +0 -22
  250. package/src/router/match-pipelines.ts +1 -42
  251. package/src/router/match-result.ts +69 -79
  252. package/src/router/metrics.ts +0 -34
  253. package/src/router/middleware-types.ts +7 -134
  254. package/src/router/middleware.ts +298 -172
  255. package/src/router/navigation-snapshot.ts +7 -56
  256. package/src/router/params-util.ts +23 -0
  257. package/src/router/parse-pattern.ts +115 -0
  258. package/src/router/pattern-matching.ts +181 -150
  259. package/src/router/prefetch-cache-ttl.ts +51 -0
  260. package/src/router/prefetch-limits.ts +37 -0
  261. package/src/router/prerender-match.ts +112 -67
  262. package/src/router/preview-match.ts +6 -2
  263. package/src/router/request-classification.ts +50 -69
  264. package/src/router/revalidation.ts +123 -73
  265. package/src/router/route-snapshot.ts +14 -3
  266. package/src/router/router-context.ts +6 -29
  267. package/src/router/router-interfaces.ts +115 -36
  268. package/src/router/router-options.ts +166 -5
  269. package/src/router/router-registry.ts +2 -5
  270. package/src/router/segment-resolution/fresh.ts +131 -86
  271. package/src/router/segment-resolution/helpers.ts +86 -6
  272. package/src/router/segment-resolution/loader-cache.ts +139 -39
  273. package/src/router/segment-resolution/loader-mask.ts +67 -0
  274. package/src/router/segment-resolution/loader-snapshot.ts +251 -0
  275. package/src/router/segment-resolution/revalidation.ts +272 -320
  276. package/src/router/segment-resolution/static-store.ts +19 -5
  277. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  278. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  279. package/src/router/segment-resolution.ts +5 -1
  280. package/src/router/segment-wrappers.ts +6 -5
  281. package/src/router/state-cookie-name.ts +33 -0
  282. package/src/router/substitute-pattern-params.ts +75 -0
  283. package/src/router/telemetry-otel.ts +160 -200
  284. package/src/router/telemetry.ts +105 -20
  285. package/src/router/timeout.ts +0 -20
  286. package/src/router/tracing.ts +215 -0
  287. package/src/router/trie-matching.ts +171 -59
  288. package/src/router/types.ts +9 -63
  289. package/src/router/url-params.ts +57 -0
  290. package/src/router.ts +157 -71
  291. package/src/rsc/full-payload.ts +70 -0
  292. package/src/rsc/handler-context.ts +3 -2
  293. package/src/rsc/handler.ts +291 -217
  294. package/src/rsc/helpers.ts +168 -46
  295. package/src/rsc/index.ts +2 -5
  296. package/src/rsc/json-route-result.ts +38 -0
  297. package/src/rsc/loader-fetch.ts +114 -38
  298. package/src/rsc/manifest-init.ts +29 -42
  299. package/src/rsc/nonce.ts +10 -1
  300. package/src/rsc/origin-guard.ts +39 -25
  301. package/src/rsc/progressive-enhancement.ts +124 -13
  302. package/src/rsc/redirect-guard.ts +100 -0
  303. package/src/rsc/response-cache-serve.ts +238 -0
  304. package/src/rsc/response-error.ts +79 -12
  305. package/src/rsc/response-route-handler.ts +99 -189
  306. package/src/rsc/rsc-rendering.ts +421 -76
  307. package/src/rsc/runtime-warnings.ts +23 -10
  308. package/src/rsc/server-action.ts +282 -116
  309. package/src/rsc/shell-capture.ts +1158 -0
  310. package/src/rsc/shell-serve.ts +150 -0
  311. package/src/rsc/ssr-setup.ts +16 -0
  312. package/src/rsc/transition-gate.ts +89 -0
  313. package/src/rsc/types.ts +53 -5
  314. package/src/runtime-env.ts +18 -0
  315. package/src/search-params.ts +35 -30
  316. package/src/segment-loader-promise.ts +49 -4
  317. package/src/segment-system.tsx +350 -149
  318. package/src/serialize.ts +243 -0
  319. package/src/server/context.ts +208 -51
  320. package/src/server/cookie-parse.ts +32 -0
  321. package/src/server/cookie-store.ts +152 -5
  322. package/src/server/handle-store.ts +21 -38
  323. package/src/server/loader-registry.ts +33 -42
  324. package/src/server/request-context.ts +395 -176
  325. package/src/ssr/index.tsx +458 -178
  326. package/src/ssr/ssr-root.tsx +228 -0
  327. package/src/static-handler.ts +10 -13
  328. package/src/testing/cache-status.ts +162 -0
  329. package/src/testing/collect-handle.ts +46 -0
  330. package/src/testing/dispatch.ts +813 -0
  331. package/src/testing/dom.entry.ts +22 -0
  332. package/src/testing/e2e/fixture.ts +188 -0
  333. package/src/testing/e2e/index.ts +128 -0
  334. package/src/testing/e2e/matchers.ts +35 -0
  335. package/src/testing/e2e/page-helpers.ts +272 -0
  336. package/src/testing/e2e/parity.ts +387 -0
  337. package/src/testing/e2e/server.ts +195 -0
  338. package/src/testing/flight-matchers.ts +97 -0
  339. package/src/testing/flight-normalize.ts +11 -0
  340. package/src/testing/flight-runtime.d.ts +57 -0
  341. package/src/testing/flight-tree.ts +682 -0
  342. package/src/testing/flight.entry.ts +52 -0
  343. package/src/testing/flight.ts +257 -0
  344. package/src/testing/generated-routes.ts +199 -0
  345. package/src/testing/index.ts +105 -0
  346. package/src/testing/internal/context.ts +371 -0
  347. package/src/testing/internal/flight-client-globals.ts +30 -0
  348. package/src/testing/internal/seed-vars.ts +54 -0
  349. package/src/testing/render-handler.ts +357 -0
  350. package/src/testing/render-route.tsx +584 -0
  351. package/src/testing/run-loader.ts +385 -0
  352. package/src/testing/run-middleware.ts +205 -0
  353. package/src/testing/run-transition-when.ts +164 -0
  354. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  355. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  356. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  357. package/src/testing/vitest-stubs/version.ts +5 -0
  358. package/src/testing/vitest.ts +305 -0
  359. package/src/theme/ThemeProvider.tsx +56 -84
  360. package/src/theme/ThemeScript.tsx +7 -9
  361. package/src/theme/constants.ts +52 -13
  362. package/src/theme/index.ts +0 -7
  363. package/src/theme/theme-context.ts +1 -5
  364. package/src/theme/theme-script.ts +22 -21
  365. package/src/theme/use-theme.ts +0 -3
  366. package/src/types/boundaries.ts +0 -35
  367. package/src/types/cache-types.ts +13 -4
  368. package/src/types/error-types.ts +30 -90
  369. package/src/types/global-namespace.ts +54 -41
  370. package/src/types/handler-context.ts +110 -62
  371. package/src/types/index.ts +3 -10
  372. package/src/types/loader-types.ts +11 -9
  373. package/src/types/request-scope.ts +112 -0
  374. package/src/types/route-config.ts +20 -52
  375. package/src/types/route-entry.ts +0 -6
  376. package/src/types/segments.ts +135 -14
  377. package/src/urls/include-helper.ts +19 -64
  378. package/src/urls/include-provider.ts +71 -0
  379. package/src/urls/index.ts +2 -11
  380. package/src/urls/path-helper-types.ts +63 -17
  381. package/src/urls/path-helper.ts +22 -106
  382. package/src/urls/pattern-types.ts +72 -19
  383. package/src/urls/response-types.ts +22 -29
  384. package/src/urls/type-extraction.ts +98 -154
  385. package/src/urls/urls-function.ts +1 -19
  386. package/src/use-loader.tsx +292 -107
  387. package/src/vercel/index.ts +11 -0
  388. package/src/vercel/tracing.ts +88 -0
  389. package/src/vite/debug.ts +185 -0
  390. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  391. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  392. package/src/vite/discovery/discover-routers.ts +127 -86
  393. package/src/vite/discovery/discovery-errors.ts +255 -0
  394. package/src/vite/discovery/gate-state.ts +171 -0
  395. package/src/vite/discovery/prerender-collection.ts +96 -68
  396. package/src/vite/discovery/route-types-writer.ts +40 -84
  397. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  398. package/src/vite/discovery/state.ts +45 -1
  399. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  400. package/src/vite/index.ts +4 -0
  401. package/src/vite/inject-client-debug.ts +88 -0
  402. package/src/vite/plugin-types.ts +210 -10
  403. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  404. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  405. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  406. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  407. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  408. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  409. package/src/vite/plugins/expose-action-id.ts +48 -95
  410. package/src/vite/plugins/expose-id-utils.ts +88 -55
  411. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  412. package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
  413. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  414. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  415. package/src/vite/plugins/expose-internal-ids.ts +505 -486
  416. package/src/vite/plugins/performance-tracks.ts +26 -25
  417. package/src/vite/plugins/refresh-cmd.ts +1 -1
  418. package/src/vite/plugins/use-cache-transform.ts +73 -83
  419. package/src/vite/plugins/vercel-output.ts +384 -0
  420. package/src/vite/plugins/version-injector.ts +40 -29
  421. package/src/vite/plugins/version-plugin.ts +37 -40
  422. package/src/vite/plugins/virtual-entries.ts +138 -27
  423. package/src/vite/rango.ts +236 -138
  424. package/src/vite/router-discovery.ts +927 -136
  425. package/src/vite/utils/ast-handler-extract.ts +26 -35
  426. package/src/vite/utils/banner.ts +1 -1
  427. package/src/vite/utils/bundle-analysis.ts +10 -15
  428. package/src/vite/utils/client-chunks.ts +184 -0
  429. package/src/vite/utils/directive-prologue.ts +40 -0
  430. package/src/vite/utils/forward-user-plugins.ts +171 -0
  431. package/src/vite/utils/manifest-utils.ts +4 -59
  432. package/src/vite/utils/package-resolution.ts +20 -52
  433. package/src/vite/utils/prerender-utils.ts +71 -43
  434. package/src/vite/utils/shared-utils.ts +142 -43
  435. package/src/browser/action-response-classifier.ts +0 -99
  436. package/src/browser/react/use-client-cache.ts +0 -58
  437. package/src/browser/shallow.ts +0 -40
  438. package/src/handles/index.ts +0 -7
  439. package/src/network-error-thrower.tsx +0 -23
  440. package/src/router/middleware-cookies.ts +0 -55
@@ -20,7 +20,11 @@ import {
20
20
  encodeReply,
21
21
  createClientTemporaryReferenceSet,
22
22
  } from "@vitejs/plugin-rsc/rsc";
23
- import { getRequestContext } from "../server/request-context.js";
23
+ import {
24
+ getRequestContext,
25
+ runWithRequestContext,
26
+ } from "../server/request-context.js";
27
+ import { isUnderTestRunner } from "../runtime-env.js";
24
28
  import {
25
29
  isTainted,
26
30
  CACHED_FN_SYMBOL,
@@ -32,22 +36,177 @@ import {
32
36
  export { isCachedFunction };
33
37
  import { serializeResult, deserializeResult } from "./segment-codec.js";
34
38
  import { createHandleStore } from "../server/handle-store.js";
35
- import { restoreHandles } from "./handle-snapshot.js";
39
+ import {
40
+ restoreHandles,
41
+ encodeHandles,
42
+ decodeHandles,
43
+ } from "./handle-snapshot.js";
36
44
  import { startHandleCapture, type HandleCapture } from "./handle-capture.js";
37
45
  import { sortedSearchString } from "./cache-key-utils.js";
46
+ import { encodeKV } from "../encode-kv.js";
38
47
  import { runBackground } from "./background-task.js";
48
+ import {
49
+ normalizeTags,
50
+ recordRequestTags,
51
+ runWithCacheTagScope,
52
+ } from "./cache-tag.js";
53
+ import { reportCacheError } from "./cache-error.js";
54
+ import type { CacheItemResult } from "./types.js";
55
+
56
+ /**
57
+ * DJB2 hash returning an 8-char hex string. Deterministic across runtimes
58
+ * (no crypto import — cache-runtime runs on the edge). Mirrors prerender's
59
+ * param-hash djb2Hex so binary key parts hash consistently.
60
+ */
61
+ function djb2HexBytes(bytes: Uint8Array): string {
62
+ let hash = 5381;
63
+ for (let i = 0; i < bytes.length; i++) {
64
+ hash = ((hash << 5) + hash + bytes[i]!) >>> 0;
65
+ }
66
+ return hash.toString(16).padStart(8, "0");
67
+ }
39
68
 
40
69
  /**
41
70
  * Convert encodeReply result to a stable string key.
42
- * encodeReply may return string or FormData — normalize to string.
71
+ *
72
+ * encodeReply may return a string or FormData. A plain string is already
73
+ * deterministic for a given arg set, so return it verbatim. FormData (emitted
74
+ * whenever a key arg is a typed array / Blob / File / a large object React
75
+ * lazily chunks) carries a per-call RANDOM multipart boundary
76
+ * (`formdata-undici-<random>`); stringifying the whole body via
77
+ * `new Response(formData).text()` would therefore produce a DIFFERENT key on
78
+ * every call, so the cached function would always miss and the store would
79
+ * accumulate one duplicate entry per call (unbounded growth).
80
+ *
81
+ * Instead derive the key from the entries themselves, independent of the
82
+ * boundary: iterate in sorted-key order and, for each value, emit a
83
+ * boundary-free token — `s:<value>` for strings, `b:<size>:<type>:<name>:<hash>`
84
+ * for Blob/File (bytes folded via djb2 so distinct payloads of equal
85
+ * size/type/name still differ). Strings carry an `s:` type tag so a string whose
86
+ * value happens to equal a blob token (e.g. the literal `b:4::a:b:<hash>`) cannot
87
+ * collide with an actual Blob/File entry under the same FormData key. The
88
+ * user-controlled `type`/`name` are percent-encoded before joining so an embedded
89
+ * `:` cannot shift the field boundaries and collide two distinct files (e.g.
90
+ * {name:"a:b",type:""} vs {name:"b",type:":a"}). The result is stable across
91
+ * identical arg sets.
43
92
  */
44
- async function replyToCacheKey(encoded: string | FormData): Promise<string> {
93
+ export async function replyToCacheKey(
94
+ encoded: string | FormData,
95
+ ): Promise<string> {
45
96
  if (typeof encoded === "string") return encoded;
46
- // FormData: convert to Response body, then to string for deterministic key
47
- const text = await new Response(encoded).text();
48
- return text;
97
+
98
+ // Snapshot entries synchronously (forEach avoids relying on FormData's
99
+ // iterator typings), then fold any Blob/File bytes asynchronously.
100
+ const raw: [string, FormDataEntryValue][] = [];
101
+ encoded.forEach((value, key) => {
102
+ raw.push([key, value]);
103
+ });
104
+ const pairs: [string, string][] = [];
105
+ for (const [key, value] of raw) {
106
+ if (typeof value === "string") {
107
+ // Type-tag strings with `s:` so a string equal to a blob token (e.g.
108
+ // `b:4::a:b:<hash>`) cannot collide with a Blob/File entry under the same
109
+ // key (which carries the `b:` tag below).
110
+ pairs.push([key, "s:" + value]);
111
+ } else {
112
+ // Blob/File: fold the bytes into a deterministic, boundary-free token.
113
+ // Percent-encode the user-controlled type/name so an embedded `:` cannot
114
+ // shift the `:`-delimited field boundaries and collide distinct files.
115
+ const buf = await value.arrayBuffer();
116
+ const hash = djb2HexBytes(new Uint8Array(buf));
117
+ const name = "name" in value ? value.name : "";
118
+ const encType = encodeURIComponent(value.type);
119
+ const encName = encodeURIComponent(name);
120
+ pairs.push([key, `b:${value.size}:${encType}:${encName}:${hash}`]);
121
+ }
122
+ }
123
+ return encodeKV(pairs, { sort: true });
124
+ }
125
+
126
+ // Cached-fn ids already warned about running uncached under a test runner, so
127
+ // the test-ergonomics warning fires once per fn rather than once per call.
128
+ const warnedUncachedUnderTest = new Set<string>();
129
+
130
+ /**
131
+ * Fast-path cache-key builder for JSON-safe key args. Returns a deterministic
132
+ * string (object keys recursively sorted so insertion order can't change the
133
+ * key) when EVERY part is a primitive or a plain object/array of the same, and
134
+ * `undefined` otherwise so the caller falls back to the Flight reply encoder.
135
+ *
136
+ * Strings are always quoted via JSON.stringify, so they can never collide with
137
+ * the bareword encodings of null/true/false/undefined/numbers. Anything the
138
+ * reply encoder must handle instead — functions, symbols, bigint, non-finite
139
+ * numbers, Dates, Maps, class instances, promises, and React elements (whose
140
+ * `$$typeof` symbol value trips the symbol reject) — yields `undefined`.
141
+ */
142
+ function jsonSafeKeyPart(value: unknown): string | undefined {
143
+ if (value === null) return "null";
144
+ switch (typeof value) {
145
+ case "string":
146
+ return JSON.stringify(value);
147
+ case "boolean":
148
+ return value ? "true" : "false";
149
+ case "number":
150
+ return Number.isFinite(value) ? String(value) : undefined;
151
+ case "undefined":
152
+ return "undefined";
153
+ case "object": {
154
+ if (Array.isArray(value)) {
155
+ const parts: string[] = [];
156
+ for (const item of value) {
157
+ const encoded = jsonSafeKeyPart(item);
158
+ if (encoded === undefined) return undefined;
159
+ parts.push(encoded);
160
+ }
161
+ return `[${parts.join(",")}]`;
162
+ }
163
+ // Only plain objects (Object.prototype or null proto) are fast-path safe.
164
+ // Dates, Maps, class instances, promises, etc. carry a different prototype
165
+ // and fall back to the encoder.
166
+ const proto = Object.getPrototypeOf(value);
167
+ if (proto !== Object.prototype && proto !== null) return undefined;
168
+ const obj = value as Record<string, unknown>;
169
+ const parts: string[] = [];
170
+ for (const key of Object.keys(obj).sort()) {
171
+ const encoded = jsonSafeKeyPart(obj[key]);
172
+ if (encoded === undefined) return undefined;
173
+ parts.push(`${JSON.stringify(key)}:${encoded}`);
174
+ }
175
+ return `{${parts.join(",")}}`;
176
+ }
177
+ default:
178
+ // bigint, symbol, function
179
+ return undefined;
180
+ }
49
181
  }
50
182
 
183
+ /**
184
+ * The serialized product of one "use cache" execution: exactly what the store
185
+ * write persists. Followers that dedup onto an in-flight execution (see
186
+ * inFlightExecutions) await this and serve it as a synthetic cache hit — each
187
+ * deserializing its OWN copy of `serialized` and replaying `handles`/`tags`
188
+ * against its OWN request. A deserialized result object is never shared.
189
+ */
190
+ interface CacheEnvelope {
191
+ /** RSC-serialized return value (never null — a null serialize rejects). */
192
+ serialized: string;
193
+ /** Merged profile/DSL + runtime cacheTag() tags. */
194
+ tags: string[];
195
+ /** RSC-encoded handle blob captured during execution, if any. */
196
+ handles?: string;
197
+ }
198
+
199
+ /**
200
+ * In-flight "use cache" executions keyed by cache key. When N concurrent calls
201
+ * miss on the same key, only the first (the leader) runs the function; the rest
202
+ * await its {@link CacheEnvelope} and serve it as a synthetic hit rather than
203
+ * re-running the function and re-writing the store. Isolate-scoped (module
204
+ * singleton), and cleared for a key as soon as the leader settles: a rejected
205
+ * leader (function threw, or the result was not serializable) propagates to
206
+ * current waiters, which then retry fresh.
207
+ */
208
+ const inFlightExecutions = new Map<string, Promise<CacheEnvelope>>();
209
+
51
210
  // ============================================================================
52
211
  // Core: registerCachedFunction
53
212
  // ============================================================================
@@ -70,9 +229,38 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
70
229
  const store = requestCtx?._cacheStore;
71
230
  const resolvedProfileName = profileName || "default";
72
231
 
73
- // Bypass: no store or no getItem support
232
+ // Bypass: no store or no getItem support. Still run inside a tag scope so a
233
+ // cacheTag() call inside the function degrades to a no-op rather than
234
+ // throwing "must be called inside a use cache function" - adopting cacheTag()
235
+ // must not hard-fail in apps/tests without an item-capable cache configured.
236
+ // Note: the INSIDE_CACHE_EXEC guard (cookies()/headers()/ctx.set() rejection)
237
+ // is intentionally NOT stamped here. It is a cached-path-only check; in the
238
+ // bypass the body actually executes, so the guarded side effects take effect
239
+ // and nothing is lost on a (non-existent) hit. Same applies to the
240
+ // non-serializable-args bypass below.
74
241
  if (!store?.getItem) {
75
- return fn.apply(this, args);
242
+ // Test-ergonomics guard: under a test runner, a "use cache" function that
243
+ // executes with no item-capable store seeded is exercising the UNCACHED
244
+ // path — a green test that proves nothing about caching. Warn once per fn
245
+ // id so the author knows to seed a cacheStore. Advisory (never throws), so
246
+ // a test that DELIBERATELY runs uncached is unaffected. Gated on the test
247
+ // runner (process.env.VITEST, not folded) so production never evaluates it.
248
+ if (isUnderTestRunner() && !warnedUncachedUnderTest.has(id)) {
249
+ warnedUncachedUnderTest.add(id);
250
+ console.warn(
251
+ `[rango] "use cache" function "${id}" executed but no cacheStore was ` +
252
+ `seeded; the cached path is NOT under test (it ran uncached). Pass ` +
253
+ `{ cacheStore, cacheProfiles } to runLoader/runMiddleware/renderHandler/` +
254
+ `runInRequestContext (or configure createRouter({ cache }) for dispatch) ` +
255
+ `to exercise it.`,
256
+ );
257
+ }
258
+ const scoped = runWithCacheTagScope(() => fn.apply(this, args));
259
+ const result = await scoped.result;
260
+ // Still record the runtime tags into the request set so a cacheTag() in an
261
+ // uncached function tags the document, even with no item-capable store.
262
+ recordRequestTags(scoped.tags, requestCtx);
263
+ return result;
76
264
  }
77
265
 
78
266
  // Resolve profile strictly from request-scoped config (set by the
@@ -145,57 +333,95 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
145
333
  let cacheKey: string;
146
334
  try {
147
335
  if (keyArgs.length > 0) {
148
- const tempRefs = createClientTemporaryReferenceSet();
149
- const encoded = await encodeReply(keyArgs as unknown[], {
150
- temporaryReferences: tempRefs,
151
- });
152
- const argsKey = await replyToCacheKey(encoded);
153
- cacheKey = `use-cache:${id}:${argsKey}`;
336
+ // Fast path: when every key arg is JSON-safe, build the key with a
337
+ // deterministic stable-stringify and skip encodeReply (the Flight reply
338
+ // encoder runs on EVERY call, including hits). The `:j:` namespace keeps
339
+ // these keys disjoint from the encoder path so the two can never collide
340
+ // for one fn id. Entries cached under the old encoder key for JSON-safe
341
+ // args cold-start once after this upgrade.
342
+ const fastKey = jsonSafeKeyPart(keyArgs);
343
+ if (fastKey !== undefined) {
344
+ cacheKey = `use-cache:${id}:j:${fastKey}`;
345
+ } else {
346
+ const tempRefs = createClientTemporaryReferenceSet();
347
+ const encoded = await encodeReply(keyArgs as unknown[], {
348
+ temporaryReferences: tempRefs,
349
+ });
350
+ const argsKey = await replyToCacheKey(encoded);
351
+ cacheKey = `use-cache:${id}:${argsKey}`;
352
+ }
154
353
  } else {
155
354
  cacheKey = `use-cache:${id}`;
156
355
  }
157
356
  } catch {
158
- // Non-serializable args: run uncached
159
- return fn.apply(this, args);
357
+ // Non-serializable args: run uncached (within a tag scope so cacheTag()
358
+ // still does not throw). Record runtime tags so the document union still
359
+ // sees them even though this call is not itself cached.
360
+ const scoped = runWithCacheTagScope(() => fn.apply(this, args));
361
+ const result = await scoped.result;
362
+ recordRequestTags(scoped.tags, requestCtx);
363
+ return result;
160
364
  }
161
365
 
162
366
  // Cache lookup
163
367
  const cached = await store.getItem(cacheKey);
164
368
 
369
+ // Serve a cached entry on the hit path: deserialize the stored value,
370
+ // replay handle data (gated on tainted args), and surface the entry's tags
371
+ // to the request set (the function did not re-run, so its runtime cacheTag()
372
+ // tags are only available from the stored entry). Shared by the fresh-hit
373
+ // and stale-hit branches; the only divergence is the stale branch scheduling
374
+ // background revalidation, which it does after this returns.
375
+ const serveCached = async (entry: CacheItemResult): Promise<any> => {
376
+ const result = await deserializeResult(entry.value);
377
+ if (entry.handles && hasTaintedArgs) {
378
+ const handleStore = requestCtx?._handleStore;
379
+ if (handleStore) {
380
+ const r = await decodeHandles(entry.handles);
381
+ if (r) restoreHandles(r, handleStore);
382
+ }
383
+ }
384
+ recordRequestTags(entry.tags, requestCtx);
385
+ return result;
386
+ };
387
+
165
388
  if (cached && !cached.shouldRevalidate) {
166
389
  // Fresh hit: deserialize and return
167
390
  try {
168
- const result = await deserializeResult(cached.value);
169
- // Restore handle data if present
170
- if (cached.handles && hasTaintedArgs) {
171
- const handleStore = requestCtx?._handleStore;
172
- if (handleStore) {
173
- restoreHandles(cached.handles, handleStore);
174
- }
175
- }
176
- return result;
177
- } catch {
178
- // Deserialization failed, fall through to fresh execution
391
+ return await serveCached(cached);
392
+ } catch (error) {
393
+ // The stored value is corrupt/partial (failed RSC deserialize). Report
394
+ // it, then fall through to fresh execution - the miss path below re-runs
395
+ // and setItem() overwrites the faulty entry under the same key (self-heal).
396
+ reportCacheError(
397
+ error,
398
+ "cache-corrupt",
399
+ `[use cache] "${id}" fresh-hit`,
400
+ );
179
401
  }
180
402
  }
181
403
 
182
- if (cached?.shouldRevalidate) {
404
+ // foregroundOnAction (opt-in; see CacheProfile.foregroundOnAction): during an
405
+ // action's revalidation render, a stale entry falls through to the foreground
406
+ // miss path below instead of SWR. The flag is set by revalidateAfterAction.
407
+ const foregroundOnActionRevalidation =
408
+ requestCtx?._inActionRevalidation === true &&
409
+ profile.foregroundOnAction === true;
410
+ if (cached?.shouldRevalidate && !foregroundOnActionRevalidation) {
183
411
  // Stale hit: return stale value, revalidate in background
184
412
  try {
185
- const result = await deserializeResult(cached.value);
186
- if (cached.handles && hasTaintedArgs) {
187
- const handleStore = requestCtx?._handleStore;
188
- if (handleStore) {
189
- restoreHandles(cached.handles, handleStore);
190
- }
191
- }
413
+ const result = await serveCached(cached);
192
414
  // Background revalidation — must capture handles if tainted args present.
193
415
  // Use an isolated handle store so background pushes don't pollute the
194
416
  // live response or throw LateHandlePushError on the completed store.
195
417
  // Same isolation pattern as route-level background-revalidation.ts.
196
418
  runBackground(requestCtx, async () => {
197
- // Reuse closure-captured requestCtx instead of calling
198
- // getRequestContext() ALS context may be gone inside waitUntil.
419
+ // The closure-captured requestCtx is reused for the framework's own
420
+ // reads (handle store swap, error reporting) AND, below, to
421
+ // re-establish the request-context ALS around the user fn. ALS context
422
+ // may be gone inside waitUntil: on workerd a waitUntil task runs
423
+ // detached from the request's I/O context, so getRequestContext()
424
+ // inside the cached body would otherwise throw.
199
425
  let originalHandleStore:
200
426
  | ReturnType<typeof createHandleStore>
201
427
  | undefined;
@@ -238,20 +464,49 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
238
464
  }
239
465
 
240
466
  try {
241
- const freshResult = await fn.apply(this, args);
467
+ // Re-establish the request-context ALS so a "use cache" body that
468
+ // reads the ambient getRequestContext() (e.g.
469
+ // getRequestContext().env.ApiKey) resolves during the background
470
+ // revalidation instead of throwing "called outside of a request
471
+ // context". runWithRequestContext sets the store for fn's
472
+ // synchronous kickoff; its async continuations inherit it.
473
+ const scoped = runWithRequestContext(requestCtx, () =>
474
+ runWithCacheTagScope(() => fn.apply(this, args)),
475
+ );
476
+ const freshResult = await scoped.result;
242
477
  bgStopCapture?.();
478
+ // Merge profile/DSL tags with runtime cacheTag() tags, read after
479
+ // awaiting so post-await cacheTag() calls are included. Normalize
480
+ // (drops empty profile tags, matching the invalidate path) + dedupe.
481
+ const freshTags = [
482
+ ...new Set(
483
+ normalizeTags([...(profile.tags ?? []), ...scoped.tags]),
484
+ ),
485
+ ];
486
+ recordRequestTags(freshTags, requestCtx);
243
487
  const serialized = await serializeResult(freshResult);
244
488
  if (serialized !== null) {
489
+ const encodedHandles = bgCapture?.data
490
+ ? await encodeHandles(bgCapture.data)
491
+ : undefined;
245
492
  await store.setItem!(cacheKey, serialized, {
246
- handles: bgCapture?.data,
493
+ handles: encodedHandles,
247
494
  ttl: profile.ttl,
248
495
  swr: profile.swr,
249
- tags: profile.tags,
496
+ tags: freshTags.length > 0 ? freshTags : undefined,
250
497
  });
251
498
  }
252
499
  } catch (bgError) {
253
500
  bgStopCapture?.();
254
- requestCtx?._reportBackgroundError?.(bgError, "stale-revalidation");
501
+ // Pass requestCtx explicitly: this runs in a detached background
502
+ // task where the ALS context is gone, so onError can only fire if
503
+ // we hand it the context captured up front.
504
+ reportCacheError(
505
+ bgError,
506
+ "stale-revalidation",
507
+ "[use cache] background revalidation failed",
508
+ requestCtx,
509
+ );
255
510
  } finally {
256
511
  for (const arg of bgTaintedArgs) {
257
512
  unstampCacheExec(arg as object);
@@ -263,12 +518,76 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
263
518
  }
264
519
  });
265
520
  return result;
521
+ } catch (error) {
522
+ // Stale value is corrupt/partial; report and fall through to a fresh
523
+ // execution, which overwrites the faulty entry under the same key.
524
+ reportCacheError(
525
+ error,
526
+ "cache-corrupt",
527
+ `[use cache] "${id}" stale-hit`,
528
+ );
529
+ }
530
+ }
531
+
532
+ // Cache miss.
533
+ //
534
+ // In-flight dedup: if a concurrent call for this key is already executing,
535
+ // await its envelope and serve it as a synthetic hit rather than re-running
536
+ // the function. Each follower deserializes its OWN copy, replays handles
537
+ // against its OWN handle store (gated on ITS hasTaintedArgs), and records
538
+ // tags into its OWN request — no deserialized result is shared across
539
+ // requests. The store write stays exactly once (the leader's).
540
+ const existing = inFlightExecutions.get(cacheKey);
541
+ if (existing) {
542
+ let envelope: CacheEnvelope | undefined;
543
+ try {
544
+ envelope = await existing;
266
545
  } catch {
267
- // Deserialization of stale value failed, fall through
546
+ // Leader rejected (function threw or its result was not serializable);
547
+ // its map entry is already cleared, so fall through to a fresh run.
548
+ envelope = undefined;
549
+ }
550
+ if (envelope) {
551
+ try {
552
+ return await serveCached({
553
+ value: envelope.serialized,
554
+ handles: envelope.handles,
555
+ tags: envelope.tags,
556
+ shouldRevalidate: false,
557
+ });
558
+ } catch (error) {
559
+ reportCacheError(
560
+ error,
561
+ "cache-corrupt",
562
+ `[use cache] "${id}" inflight-hit`,
563
+ );
564
+ // Fall through to a fresh execution below.
565
+ }
268
566
  }
269
567
  }
270
568
 
271
- // Cache miss: execute, serialize, store
569
+ // This call becomes the leader. Register a deferred envelope so concurrent
570
+ // callers dedup onto it; it is resolved/rejected exactly once below (or on a
571
+ // function throw). clearSelf only deletes the map slot if it still holds
572
+ // THIS promise, so a fall-through retry (rare rejected-leader path) can't
573
+ // evict a newer leader's entry.
574
+ let resolveEnvelope!: (env: CacheEnvelope) => void;
575
+ let rejectEnvelope!: (err: unknown) => void;
576
+ const envelopePromise = new Promise<CacheEnvelope>((res, rej) => {
577
+ resolveEnvelope = res;
578
+ rejectEnvelope = rej;
579
+ });
580
+ // Followers attach their own catch; guard the map's own reference so a
581
+ // rejected envelope with no waiter is not an unhandled rejection.
582
+ envelopePromise.catch(() => {});
583
+ inFlightExecutions.set(cacheKey, envelopePromise);
584
+ const clearSelf = (): void => {
585
+ if (inFlightExecutions.get(cacheKey) === envelopePromise) {
586
+ inFlightExecutions.delete(cacheKey);
587
+ }
588
+ };
589
+
590
+ // execute, serialize, store
272
591
  const handleStore = hasTaintedArgs ? requestCtx?._handleStore : undefined;
273
592
  let capture: HandleCapture | undefined;
274
593
  let stopCapture: (() => void) | undefined;
@@ -297,8 +616,16 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
297
616
  }
298
617
 
299
618
  let result: any;
619
+ let scoped: ReturnType<typeof runWithCacheTagScope>;
300
620
  try {
301
- result = await fn.apply(this, args);
621
+ scoped = runWithCacheTagScope(() => fn.apply(this, args));
622
+ result = await scoped.result;
623
+ } catch (execError) {
624
+ // The function threw: drop the in-flight entry and reject any waiters so
625
+ // they retry fresh, then propagate to this caller.
626
+ clearSelf();
627
+ rejectEnvelope(execError);
628
+ throw execError;
302
629
  } finally {
303
630
  // Decrement ref count; symbol is deleted when it reaches zero
304
631
  for (const arg of taintedArgs) {
@@ -311,25 +638,61 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
311
638
  stopCapture?.();
312
639
  }
313
640
 
314
- // Serialize and store fully non-blocking when waitUntil is available.
315
- // The response does not need to wait for serialization or the store write.
316
- const cacheWrite = async () => {
641
+ // Merge profile/DSL tags with runtime cacheTag() tags. Read scoped.tags
642
+ // after awaiting result so post-await cacheTag() calls are included.
643
+ // Normalize (drops empty profile tags, matching the invalidate path) + dedupe.
644
+ const allTags = [
645
+ ...new Set(normalizeTags([...(profile.tags ?? []), ...scoped!.tags])),
646
+ ];
647
+ recordRequestTags(allTags, requestCtx);
648
+
649
+ // Serialize + encode handles ONCE, resolve the in-flight envelope so any
650
+ // concurrent followers can serve a synthetic hit, then persist to the store.
651
+ // Fully non-blocking when waitUntil is available — the leader's response
652
+ // waits on neither serialization nor the store write.
653
+ const finalizeAndWrite = async (): Promise<void> => {
654
+ let serialized: string | null;
655
+ let encodedHandles: string | undefined;
317
656
  try {
318
- const serialized = await serializeResult(result);
319
- if (serialized !== null) {
320
- await store.setItem!(cacheKey, serialized, {
321
- handles: capture?.data,
322
- ttl: profile.ttl,
323
- swr: profile.swr,
324
- tags: profile.tags,
325
- });
326
- }
657
+ serialized = await serializeResult(result);
658
+ encodedHandles = capture?.data
659
+ ? await encodeHandles(capture.data)
660
+ : undefined;
661
+ } catch (buildError) {
662
+ // Serialize/handle-encode failed: no envelope for followers (they run
663
+ // fresh) and nothing to write.
664
+ clearSelf();
665
+ rejectEnvelope(buildError);
666
+ requestCtx?._reportBackgroundError?.(buildError, "cache-write");
667
+ return;
668
+ }
669
+ clearSelf();
670
+ if (serialized === null) {
671
+ // Non-serializable result: no store write (matches the prior silent
672
+ // skip); reject so any waiter falls through to a fresh execution.
673
+ rejectEnvelope(
674
+ new Error(
675
+ `[use cache] "${id}" result is not serializable; not cached`,
676
+ ),
677
+ );
678
+ return;
679
+ }
680
+ // Hand followers the envelope before the store write so a slow/failed
681
+ // write never stalls them.
682
+ resolveEnvelope({ serialized, tags: allTags, handles: encodedHandles });
683
+ try {
684
+ await store.setItem!(cacheKey, serialized, {
685
+ handles: encodedHandles,
686
+ ttl: profile.ttl,
687
+ swr: profile.swr,
688
+ tags: allTags.length > 0 ? allTags : undefined,
689
+ });
327
690
  } catch (writeError) {
328
691
  requestCtx?._reportBackgroundError?.(writeError, "cache-write");
329
692
  }
330
693
  };
331
694
 
332
- await runBackground(requestCtx, cacheWrite, true);
695
+ await runBackground(requestCtx, finalizeAndWrite, true);
333
696
 
334
697
  return result;
335
698
  };