@rangojs/router 0.0.0-experimental.14 → 0.0.0-experimental.140

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 (460) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +432 -7
  3. package/dist/bin/rango.js +2073 -213
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +7258 -2714
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +140 -67
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +329 -0
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +487 -0
  12. package/skills/caching/SKILL.md +357 -25
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +246 -0
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +16 -10
  19. package/skills/document-cache/SKILL.md +87 -62
  20. package/skills/fonts/SKILL.md +6 -4
  21. package/skills/handler-use/SKILL.md +364 -0
  22. package/skills/hooks/SKILL.md +557 -79
  23. package/skills/host-router/SKILL.md +320 -0
  24. package/skills/i18n/SKILL.md +276 -0
  25. package/skills/intercept/SKILL.md +207 -15
  26. package/skills/layout/SKILL.md +146 -6
  27. package/skills/links/SKILL.md +304 -25
  28. package/skills/loader/SKILL.md +616 -54
  29. package/skills/middleware/SKILL.md +217 -37
  30. package/skills/migrate-nextjs/SKILL.md +611 -0
  31. package/skills/migrate-react-router/SKILL.md +927 -0
  32. package/skills/mime-routes/SKILL.md +42 -11
  33. package/skills/observability/SKILL.md +194 -0
  34. package/skills/parallel/SKILL.md +284 -3
  35. package/skills/ppr/SKILL.md +426 -0
  36. package/skills/prerender/SKILL.md +437 -52
  37. package/skills/rango/SKILL.md +369 -22
  38. package/skills/react-compiler/SKILL.md +168 -0
  39. package/skills/response-routes/SKILL.md +263 -121
  40. package/skills/route/SKILL.md +350 -21
  41. package/skills/router-setup/SKILL.md +246 -33
  42. package/skills/scripts/SKILL.md +179 -0
  43. package/skills/server-actions/SKILL.md +775 -0
  44. package/skills/shell-manifest/SKILL.md +185 -0
  45. package/skills/streams-and-websockets/SKILL.md +283 -0
  46. package/skills/tailwind/SKILL.md +27 -3
  47. package/skills/testing/SKILL.md +126 -222
  48. package/skills/testing/bindings.md +103 -0
  49. package/skills/testing/cache-prerender.md +127 -0
  50. package/skills/testing/client-components.md +124 -0
  51. package/skills/testing/e2e-parity.md +125 -0
  52. package/skills/testing/flight.md +91 -0
  53. package/skills/testing/handles.md +131 -0
  54. package/skills/testing/loader.md +128 -0
  55. package/skills/testing/middleware.md +99 -0
  56. package/skills/testing/render-handler.md +122 -0
  57. package/skills/testing/response-routes.md +95 -0
  58. package/skills/testing/reverse-and-types.md +85 -0
  59. package/skills/testing/server-actions.md +107 -0
  60. package/skills/testing/server-tree.md +128 -0
  61. package/skills/testing/setup.md +123 -0
  62. package/skills/theme/SKILL.md +9 -8
  63. package/skills/typesafety/SKILL.md +532 -103
  64. package/skills/use-cache/SKILL.md +367 -0
  65. package/skills/vercel/SKILL.md +128 -0
  66. package/skills/view-transitions/SKILL.md +337 -0
  67. package/src/__augment-tests__/augment.ts +81 -0
  68. package/src/__augment-tests__/augmented.check.ts +116 -0
  69. package/src/__internal.ts +77 -44
  70. package/src/bin/rango.ts +312 -15
  71. package/src/browser/action-coordinator.ts +114 -0
  72. package/src/browser/action-fence.ts +47 -0
  73. package/src/browser/app-shell.ts +39 -0
  74. package/src/browser/app-version.ts +14 -0
  75. package/src/browser/connection-warmup.ts +134 -0
  76. package/src/browser/cookie-name.ts +140 -0
  77. package/src/browser/event-controller.ts +293 -202
  78. package/src/browser/history-state.ts +101 -0
  79. package/src/browser/index.ts +3 -3
  80. package/src/browser/intercept-utils.ts +52 -0
  81. package/src/browser/invalidate-client-cache.ts +52 -0
  82. package/src/browser/link-interceptor.ts +24 -4
  83. package/src/browser/logging.ts +11 -0
  84. package/src/browser/merge-segment-loaders.ts +20 -12
  85. package/src/browser/navigation-bridge.ts +385 -576
  86. package/src/browser/navigation-client.ts +245 -75
  87. package/src/browser/navigation-store-handle.ts +38 -0
  88. package/src/browser/navigation-store.ts +184 -118
  89. package/src/browser/navigation-transaction.ts +247 -0
  90. package/src/browser/network-error-handler.ts +88 -0
  91. package/src/browser/partial-update.ts +412 -364
  92. package/src/browser/prefetch/cache.ts +359 -0
  93. package/src/browser/prefetch/fetch.ts +452 -0
  94. package/src/browser/prefetch/observer.ts +65 -0
  95. package/src/browser/prefetch/policy.ts +48 -0
  96. package/src/browser/prefetch/queue.ts +209 -0
  97. package/src/browser/prefetch/resource-ready.ts +77 -0
  98. package/src/browser/rango-state.ts +194 -0
  99. package/src/browser/react/Link.tsx +275 -68
  100. package/src/browser/react/NavigationProvider.tsx +265 -109
  101. package/src/browser/react/ScrollRestoration.tsx +10 -6
  102. package/src/browser/react/context.ts +11 -0
  103. package/src/browser/react/filter-segment-order.ts +70 -0
  104. package/src/browser/react/index.ts +0 -48
  105. package/src/browser/react/location-state-shared.ts +272 -60
  106. package/src/browser/react/location-state.ts +90 -20
  107. package/src/browser/react/mount-context.ts +6 -1
  108. package/src/browser/react/nonce-context.ts +23 -0
  109. package/src/browser/react/shallow-equal.ts +27 -0
  110. package/src/browser/react/use-action.ts +35 -66
  111. package/src/browser/react/use-handle.ts +39 -126
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +39 -13
  114. package/src/browser/react/use-navigation.ts +53 -69
  115. package/src/browser/react/use-params.ts +75 -0
  116. package/src/browser/react/use-pathname.ts +47 -0
  117. package/src/browser/react/use-reverse.ts +106 -0
  118. package/src/browser/react/use-router.ts +98 -0
  119. package/src/browser/react/use-search-params.ts +51 -0
  120. package/src/browser/react/use-segments.ts +72 -99
  121. package/src/browser/response-adapter.ts +164 -0
  122. package/src/browser/rsc-router.tsx +300 -72
  123. package/src/browser/scroll-restoration.ts +138 -50
  124. package/src/browser/segment-reconciler.ts +243 -0
  125. package/src/browser/segment-structure-assert.ts +17 -1
  126. package/src/browser/server-action-bridge.ts +668 -613
  127. package/src/browser/types.ts +223 -51
  128. package/src/browser/validate-redirect-origin.ts +56 -0
  129. package/src/build/collect-fallback-refs.ts +107 -0
  130. package/src/build/generate-manifest.ts +252 -161
  131. package/src/build/generate-route-types.ts +41 -1038
  132. package/src/build/index.ts +12 -7
  133. package/src/build/prefix-tree-utils.ts +123 -0
  134. package/src/build/route-trie.ts +225 -42
  135. package/src/build/route-types/ast-helpers.ts +25 -0
  136. package/src/build/route-types/ast-route-extraction.ts +105 -0
  137. package/src/build/route-types/codegen.ts +113 -0
  138. package/src/build/route-types/include-resolution.ts +812 -0
  139. package/src/build/route-types/param-extraction.ts +51 -0
  140. package/src/build/route-types/per-module-writer.ts +144 -0
  141. package/src/build/route-types/router-processing.ts +695 -0
  142. package/src/build/route-types/scan-filter.ts +85 -0
  143. package/src/build/route-types/source-scan.ts +216 -0
  144. package/src/build/runtime-discovery.ts +223 -0
  145. package/src/cache/background-task.ts +34 -0
  146. package/src/cache/cache-error.ts +104 -0
  147. package/src/cache/cache-key-utils.ts +60 -0
  148. package/src/cache/cache-policy.ts +199 -0
  149. package/src/cache/cache-runtime.ts +525 -0
  150. package/src/cache/cache-scope.ts +298 -332
  151. package/src/cache/cache-tag.ts +103 -0
  152. package/src/cache/cf/cf-base64.ts +33 -0
  153. package/src/cache/cf/cf-cache-constants.ts +127 -0
  154. package/src/cache/cf/cf-cache-store.ts +2500 -158
  155. package/src/cache/cf/cf-cache-types.ts +349 -0
  156. package/src/cache/cf/cf-kv-utils.ts +46 -0
  157. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  158. package/src/cache/cf/index.ts +17 -17
  159. package/src/cache/document-cache.ts +199 -92
  160. package/src/cache/handle-capture.ts +81 -0
  161. package/src/cache/handle-snapshot.ts +111 -0
  162. package/src/cache/index.ts +29 -35
  163. package/src/cache/memory-segment-store.ts +363 -30
  164. package/src/cache/profile-registry.ts +88 -0
  165. package/src/cache/read-through-swr.ts +178 -0
  166. package/src/cache/segment-codec.ts +248 -0
  167. package/src/cache/shell-cache.ts +386 -0
  168. package/src/cache/tag-invalidation.ts +230 -0
  169. package/src/cache/taint.ts +153 -0
  170. package/src/cache/types.ts +156 -211
  171. package/src/cache/vercel/index.ts +11 -0
  172. package/src/cache/vercel/vercel-cache-store.ts +1102 -0
  173. package/src/client.rsc.tsx +43 -21
  174. package/src/client.tsx +131 -347
  175. package/src/cloudflare/index.ts +11 -0
  176. package/src/cloudflare/tracing.ts +109 -0
  177. package/src/component-utils.ts +23 -4
  178. package/src/components/DefaultDocument.tsx +13 -3
  179. package/src/context-var.ts +168 -0
  180. package/src/debug.ts +19 -9
  181. package/src/decode-loader-results.ts +52 -0
  182. package/src/defer.ts +185 -0
  183. package/src/deps/ssr.ts +0 -1
  184. package/src/encode-kv.ts +49 -0
  185. package/src/errors.ts +106 -10
  186. package/src/escape-script.ts +52 -0
  187. package/src/handle.ts +110 -35
  188. package/src/handles/MetaTags.tsx +83 -59
  189. package/src/handles/Scripts.tsx +183 -0
  190. package/src/handles/breadcrumbs.ts +93 -0
  191. package/src/handles/deferred-resolution.ts +127 -0
  192. package/src/handles/is-thenable.ts +18 -0
  193. package/src/handles/meta.ts +44 -53
  194. package/src/handles/script.ts +244 -0
  195. package/src/host/cookie-handler.ts +20 -65
  196. package/src/host/errors.ts +21 -30
  197. package/src/host/index.ts +13 -9
  198. package/src/host/pattern-matcher.ts +50 -79
  199. package/src/host/router.ts +151 -121
  200. package/src/host/testing.ts +45 -32
  201. package/src/host/types.ts +52 -11
  202. package/src/host/utils.ts +2 -2
  203. package/src/href-client.ts +192 -57
  204. package/src/index.rsc.ts +177 -35
  205. package/src/index.ts +255 -71
  206. package/src/internal-debug.ts +9 -2
  207. package/src/loader-store.ts +500 -0
  208. package/src/loader.rsc.ts +31 -99
  209. package/src/loader.ts +30 -12
  210. package/src/missing-id-error.ts +68 -0
  211. package/src/outlet-context.ts +1 -1
  212. package/src/outlet-provider.tsx +41 -0
  213. package/src/prerender/param-hash.ts +16 -14
  214. package/src/prerender/store.ts +121 -21
  215. package/src/prerender.ts +460 -26
  216. package/src/redirect-origin.ts +100 -0
  217. package/src/regex-escape.ts +8 -0
  218. package/src/render-error-thrower.tsx +20 -0
  219. package/src/response-utils.ts +62 -0
  220. package/src/reverse.ts +198 -128
  221. package/src/root-error-boundary.tsx +42 -48
  222. package/src/route-content-wrapper.tsx +22 -77
  223. package/src/route-definition/dsl-helpers.ts +1116 -0
  224. package/src/route-definition/helper-factories.ts +88 -0
  225. package/src/route-definition/helpers-types.ts +505 -0
  226. package/src/route-definition/index.ts +54 -0
  227. package/src/route-definition/redirect.ts +134 -0
  228. package/src/route-definition/resolve-handler-use.ts +160 -0
  229. package/src/route-definition/use-item-types.ts +29 -0
  230. package/src/route-definition.ts +1 -1481
  231. package/src/route-map-builder.ts +82 -144
  232. package/src/route-name.ts +53 -0
  233. package/src/route-types.ts +71 -45
  234. package/src/router/basename.ts +14 -0
  235. package/src/router/content-negotiation.ts +263 -0
  236. package/src/router/debug-manifest.ts +72 -0
  237. package/src/router/error-handling.ts +54 -27
  238. package/src/router/find-match.ts +245 -0
  239. package/src/router/handler-context.ts +377 -125
  240. package/src/router/instrument.ts +350 -0
  241. package/src/router/intercept-resolution.ts +59 -28
  242. package/src/router/lazy-includes.ts +254 -0
  243. package/src/router/loader-resolution.ts +421 -157
  244. package/src/router/logging.ts +106 -6
  245. package/src/router/manifest.ts +131 -57
  246. package/src/router/match-api.ts +167 -246
  247. package/src/router/match-context.ts +4 -24
  248. package/src/router/match-handlers.ts +440 -0
  249. package/src/router/match-middleware/background-revalidation.ts +117 -93
  250. package/src/router/match-middleware/cache-lookup.ts +297 -150
  251. package/src/router/match-middleware/cache-store.ts +123 -51
  252. package/src/router/match-middleware/intercept-resolution.ts +44 -43
  253. package/src/router/match-middleware/segment-resolution.ts +64 -22
  254. package/src/router/match-pipelines.ts +11 -87
  255. package/src/router/match-result.ts +121 -50
  256. package/src/router/metrics.ts +219 -28
  257. package/src/router/middleware-types.ts +93 -0
  258. package/src/router/middleware.ts +505 -441
  259. package/src/router/navigation-snapshot.ts +133 -0
  260. package/src/router/params-util.ts +23 -0
  261. package/src/router/parse-pattern.ts +115 -0
  262. package/src/router/pattern-matching.ts +311 -142
  263. package/src/router/prefetch-cache-ttl.ts +51 -0
  264. package/src/router/prefetch-limits.ts +37 -0
  265. package/src/router/prerender-match.ts +547 -0
  266. package/src/router/preview-match.ts +102 -0
  267. package/src/router/request-classification.ts +278 -0
  268. package/src/router/revalidation.ts +203 -62
  269. package/src/router/route-snapshot.ts +246 -0
  270. package/src/router/router-context.ts +45 -48
  271. package/src/router/router-interfaces.ts +554 -0
  272. package/src/router/router-options.ts +779 -0
  273. package/src/router/router-registry.ts +21 -0
  274. package/src/router/segment-resolution/fresh.ts +772 -0
  275. package/src/router/segment-resolution/helpers.ts +348 -0
  276. package/src/router/segment-resolution/loader-cache.ts +250 -0
  277. package/src/router/segment-resolution/loader-mask.ts +44 -0
  278. package/src/router/segment-resolution/revalidation.ts +1331 -0
  279. package/src/router/segment-resolution/static-store.ts +81 -0
  280. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  281. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  282. package/src/router/segment-resolution.ts +25 -1354
  283. package/src/router/segment-wrappers.ts +292 -0
  284. package/src/router/state-cookie-name.ts +33 -0
  285. package/src/router/substitute-pattern-params.ts +75 -0
  286. package/src/router/telemetry-otel.ts +261 -0
  287. package/src/router/telemetry.ts +377 -0
  288. package/src/router/timeout.ts +128 -0
  289. package/src/router/tracing.ts +206 -0
  290. package/src/router/trie-matching.ts +240 -61
  291. package/src/router/types.ts +23 -70
  292. package/src/router/url-params.ts +57 -0
  293. package/src/router.ts +781 -2378
  294. package/src/rsc/full-payload.ts +70 -0
  295. package/src/rsc/handler-context.ts +46 -0
  296. package/src/rsc/handler.ts +905 -1142
  297. package/src/rsc/helpers.ts +275 -19
  298. package/src/rsc/index.ts +2 -25
  299. package/src/rsc/json-route-result.ts +38 -0
  300. package/src/rsc/loader-fetch.ts +305 -0
  301. package/src/rsc/manifest-init.ts +77 -0
  302. package/src/rsc/nonce.ts +14 -0
  303. package/src/rsc/origin-guard.ts +155 -0
  304. package/src/rsc/progressive-enhancement.ts +502 -0
  305. package/src/rsc/redirect-guard.ts +99 -0
  306. package/src/rsc/response-cache-serve.ts +238 -0
  307. package/src/rsc/response-error.ts +104 -0
  308. package/src/rsc/response-route-handler.ts +257 -0
  309. package/src/rsc/rsc-rendering.ts +337 -0
  310. package/src/rsc/runtime-warnings.ts +55 -0
  311. package/src/rsc/server-action.ts +522 -0
  312. package/src/rsc/shell-capture.ts +439 -0
  313. package/src/rsc/ssr-setup.ts +144 -0
  314. package/src/rsc/transition-gate.ts +89 -0
  315. package/src/rsc/types.ts +95 -12
  316. package/src/runtime-env.ts +18 -0
  317. package/src/search-params.ts +99 -82
  318. package/src/segment-content-promise.ts +67 -0
  319. package/src/segment-loader-promise.ts +149 -0
  320. package/src/segment-system.tsx +349 -134
  321. package/src/serialize.ts +243 -0
  322. package/src/server/context.ts +452 -85
  323. package/src/server/cookie-parse.ts +32 -0
  324. package/src/server/cookie-store.ts +310 -0
  325. package/src/server/fetchable-loader-store.ts +11 -6
  326. package/src/server/handle-store.ts +123 -42
  327. package/src/server/live.ts +130 -0
  328. package/src/server/loader-registry.ts +51 -100
  329. package/src/server/request-context.ts +842 -157
  330. package/src/server.ts +15 -8
  331. package/src/ssr/index.tsx +412 -136
  332. package/src/ssr/ssr-root.tsx +228 -0
  333. package/src/static-handler.ts +45 -18
  334. package/src/testing/cache-status.ts +162 -0
  335. package/src/testing/collect-handle.ts +46 -0
  336. package/src/testing/dispatch.ts +701 -0
  337. package/src/testing/dom.entry.ts +22 -0
  338. package/src/testing/e2e/fixture.ts +188 -0
  339. package/src/testing/e2e/index.ts +128 -0
  340. package/src/testing/e2e/matchers.ts +35 -0
  341. package/src/testing/e2e/page-helpers.ts +272 -0
  342. package/src/testing/e2e/parity.ts +387 -0
  343. package/src/testing/e2e/server.ts +195 -0
  344. package/src/testing/flight-matchers.ts +97 -0
  345. package/src/testing/flight-normalize.ts +11 -0
  346. package/src/testing/flight-runtime.d.ts +57 -0
  347. package/src/testing/flight-tree.ts +682 -0
  348. package/src/testing/flight.entry.ts +52 -0
  349. package/src/testing/flight.ts +257 -0
  350. package/src/testing/generated-routes.ts +199 -0
  351. package/src/testing/index.ts +105 -0
  352. package/src/testing/internal/context.ts +371 -0
  353. package/src/testing/internal/flight-client-globals.ts +30 -0
  354. package/src/testing/internal/seed-vars.ts +54 -0
  355. package/src/testing/render-handler.ts +357 -0
  356. package/src/testing/render-route.tsx +584 -0
  357. package/src/testing/run-loader.ts +385 -0
  358. package/src/testing/run-middleware.ts +205 -0
  359. package/src/testing/run-transition-when.ts +164 -0
  360. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  361. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  362. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  363. package/src/testing/vitest-stubs/version.ts +5 -0
  364. package/src/testing/vitest.ts +305 -0
  365. package/src/theme/ThemeProvider.tsx +40 -72
  366. package/src/theme/ThemeScript.tsx +12 -14
  367. package/src/theme/constants.ts +57 -15
  368. package/src/theme/index.ts +3 -20
  369. package/src/theme/theme-context.ts +5 -35
  370. package/src/theme/theme-script.ts +43 -39
  371. package/src/theme/use-theme.ts +0 -3
  372. package/src/types/boundaries.ts +123 -0
  373. package/src/types/cache-types.ts +207 -0
  374. package/src/types/error-types.ts +132 -0
  375. package/src/types/global-namespace.ts +113 -0
  376. package/src/types/handler-context.ts +839 -0
  377. package/src/types/index.ts +81 -0
  378. package/src/types/loader-types.ts +212 -0
  379. package/src/types/request-scope.ts +112 -0
  380. package/src/types/route-config.ts +138 -0
  381. package/src/types/route-entry.ts +114 -0
  382. package/src/types/segments.ts +271 -0
  383. package/src/types.ts +1 -1795
  384. package/src/urls/include-helper.ts +162 -0
  385. package/src/urls/include-provider.ts +71 -0
  386. package/src/urls/index.ts +43 -0
  387. package/src/urls/path-helper-types.ts +413 -0
  388. package/src/urls/path-helper.ts +275 -0
  389. package/src/urls/pattern-types.ts +124 -0
  390. package/src/urls/response-types.ts +109 -0
  391. package/src/urls/type-extraction.ts +316 -0
  392. package/src/urls/urls-function.ts +80 -0
  393. package/src/urls.ts +1 -1341
  394. package/src/use-loader.tsx +406 -141
  395. package/src/vercel/index.ts +11 -0
  396. package/src/vercel/tracing.ts +88 -0
  397. package/src/vite/debug.ts +185 -0
  398. package/src/vite/discovery/bundle-postprocess.ts +182 -0
  399. package/src/vite/discovery/discover-routers.ts +389 -0
  400. package/src/vite/discovery/discovery-errors.ts +255 -0
  401. package/src/vite/discovery/gate-state.ts +171 -0
  402. package/src/vite/discovery/prerender-collection.ts +467 -0
  403. package/src/vite/discovery/route-types-writer.ts +214 -0
  404. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  405. package/src/vite/discovery/state.ts +161 -0
  406. package/src/vite/discovery/virtual-module-codegen.ts +183 -0
  407. package/src/vite/index.ts +23 -2255
  408. package/src/vite/inject-client-debug.ts +36 -0
  409. package/src/vite/plugin-types.ts +303 -0
  410. package/src/vite/plugins/cjs-to-esm.ts +90 -0
  411. package/src/vite/plugins/client-ref-dedup.ts +120 -0
  412. package/src/vite/plugins/client-ref-hashing.ts +118 -0
  413. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  414. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  415. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  416. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +88 -110
  417. package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +89 -79
  418. package/src/vite/plugins/expose-ids/export-analysis.ts +363 -0
  419. package/src/vite/plugins/expose-ids/handler-transform.ts +130 -0
  420. package/src/vite/plugins/expose-ids/loader-transform.ts +64 -0
  421. package/src/vite/plugins/expose-ids/router-transform.ts +199 -0
  422. package/src/vite/plugins/expose-ids/types.ts +45 -0
  423. package/src/vite/plugins/expose-internal-ids.ts +805 -0
  424. package/src/vite/plugins/performance-tracks.ts +89 -0
  425. package/src/vite/plugins/refresh-cmd.ts +127 -0
  426. package/src/vite/plugins/use-cache-transform.ts +313 -0
  427. package/src/vite/plugins/vercel-output.ts +384 -0
  428. package/src/vite/plugins/version-injector.ts +94 -0
  429. package/src/vite/plugins/version-plugin.ts +263 -0
  430. package/src/vite/plugins/virtual-entries.ts +234 -0
  431. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  432. package/src/vite/rango.ts +560 -0
  433. package/src/vite/router-discovery.ts +1638 -0
  434. package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +200 -37
  435. package/src/vite/utils/banner.ts +36 -0
  436. package/src/vite/utils/bundle-analysis.ts +132 -0
  437. package/src/vite/utils/client-chunks.ts +184 -0
  438. package/src/vite/utils/directive-prologue.ts +40 -0
  439. package/src/vite/utils/forward-user-plugins.ts +171 -0
  440. package/src/vite/utils/manifest-utils.ts +15 -0
  441. package/src/vite/utils/package-resolution.ts +89 -0
  442. package/src/vite/utils/prerender-utils.ts +249 -0
  443. package/src/vite/utils/shared-utils.ts +269 -0
  444. package/CLAUDE.md +0 -43
  445. package/dist/vite/index.named-routes.gen.ts +0 -103
  446. package/src/browser/lru-cache.ts +0 -69
  447. package/src/browser/react/use-client-cache.ts +0 -56
  448. package/src/browser/request-controller.ts +0 -164
  449. package/src/browser/shallow.ts +0 -35
  450. package/src/cache/memory-store.ts +0 -253
  451. package/src/handles/index.ts +0 -6
  452. package/src/href-context.ts +0 -33
  453. package/src/network-error-thrower.tsx +0 -21
  454. package/src/router.gen.ts +0 -6
  455. package/src/static-handler.gen.ts +0 -5
  456. package/src/urls.gen.ts +0 -8
  457. package/src/vite/expose-internal-ids.ts +0 -1167
  458. package/src/vite/package-resolution.ts +0 -125
  459. package/src/vite/virtual-entries.ts +0 -114
  460. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,1102 @@
1
+ /**
2
+ * Vercel Runtime Cache Store
3
+ *
4
+ * A SegmentCacheStore backed by Vercel's Runtime Cache (`getCache()` from
5
+ * `@vercel/functions`). It is the production store analogue to CFCacheStore, but
6
+ * far smaller: Vercel's Runtime Cache already IS a distributed, tag-aware cache
7
+ * (regional storage, global tag-expire within ~300ms), so none of CFCacheStore's
8
+ * L1/L2 tiering, KV tag-marker comparison, marker memoization, or per-tier
9
+ * timeout budgets are needed here - the platform does that work. What this store
10
+ * adds on top of the raw primitive is the parts Vercel does NOT give us:
11
+ *
12
+ * 1. Stale-while-revalidate. `getCache` has no stale-but-serve: a TTL'd entry
13
+ * simply becomes a miss. We store our own {staleAt, expiresAt} envelope and
14
+ * set the Vercel ttl to (ttl + swr) so the entry survives its SWR window,
15
+ * computing staleness ourselves via the shared cache-policy helpers.
16
+ * 2. A single-keyspace family split. `getCache` is one flat keyspace, whereas
17
+ * the interface has three value families (segments, "use cache" items, full
18
+ * responses). The memory store keeps three separate Maps; here they must be
19
+ * namespaced by a prefix or a set() of a response would clobber a segment
20
+ * cached under the same router key.
21
+ * 3. The platform guardrails: the 2 MB per-item ceiling (oversized writes
22
+ * silently no-op on Vercel, so we skip + report), the per-item tag cap, and
23
+ * cross-deploy non-reconciliation (TTL/tag updates are not reconciled across
24
+ * deployments - bake a build id into `version` or the getCache namespace).
25
+ *
26
+ * Dependency stance: this module imports NOTHING from `@vercel/functions`. The
27
+ * runtime cache handle (and `waitUntil`) are injected through the constructor and
28
+ * typed against the local VercelRuntimeCache shape below, exactly as CFCacheStore
29
+ * takes its `kv`/`ctx` bindings by injection. The consumer passes the real
30
+ * `getCache(...)` result, which satisfies the shape structurally. That keeps the
31
+ * router free of a hard Vercel dependency.
32
+ */
33
+
34
+ import type {
35
+ SegmentCacheStore,
36
+ CachedEntryData,
37
+ CacheDefaults,
38
+ CacheGetResult,
39
+ CacheItemResult,
40
+ CacheItemOptions,
41
+ ShellCacheEntry,
42
+ } from "../types.js";
43
+ import type { RequestContext } from "../../server/request-context.js";
44
+ import { isPerClientSignalHeader } from "../../browser/cookie-name.js";
45
+ import {
46
+ resolveTtl,
47
+ resolveSwrWindow,
48
+ computeExpiration,
49
+ DEFAULT_FUNCTION_TTL,
50
+ } from "../cache-policy.js";
51
+ import { reportCacheError, reportingAsync } from "../cache-error.js";
52
+ import type { CacheErrorCategory } from "../cache-error.js";
53
+
54
+ /**
55
+ * Minimal structural shape of the Vercel Runtime Cache returned by `getCache()`
56
+ * from `@vercel/functions`. Declared locally so @rangojs/router carries no hard
57
+ * dependency on `@vercel/functions`; the real handle satisfies it structurally.
58
+ *
59
+ * @see https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package#getcache
60
+ */
61
+ export interface VercelRuntimeCache {
62
+ /** Returns the stored value, or a nullish value on miss. Never throws on miss. */
63
+ get(key: string): Promise<unknown>;
64
+ /** Stores a value with optional TTL (seconds), tags, and observability name. */
65
+ set(
66
+ key: string,
67
+ value: unknown,
68
+ options?: { ttl?: number; tags?: string[]; name?: string },
69
+ ): Promise<void>;
70
+ /** Removes a value by key. */
71
+ delete(key: string): Promise<void>;
72
+ /** Expires every entry tagged with any of `tag`. Global, ~300ms propagation. */
73
+ expireTag(tag: string | string[]): Promise<void>;
74
+ }
75
+
76
+ /**
77
+ * Vercel Runtime Cache hard per-item ceiling: 2 MB. Writes above this silently
78
+ * no-op on the platform, so the store measures the serialized envelope and skips
79
+ * (fail-open) entries at or above this size.
80
+ */
81
+ export const VERCEL_MAX_ITEM_BYTES: number = 2 * 1024 * 1024;
82
+
83
+ /**
84
+ * Per-item tag ceiling. Vercel's getCache API reference lists 128 tags per item
85
+ * (https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package#getcache).
86
+ * Tags beyond this are dropped with a warning at write time. Does NOT cap
87
+ * invalidateTags() - an invalidation must reach every requested tag.
88
+ */
89
+ export const VERCEL_MAX_TAGS_PER_ITEM: number = 128;
90
+
91
+ /** Max tag length in UTF-8 bytes accepted by Vercel; longer tags are skipped. */
92
+ export const VERCEL_MAX_TAG_BYTES: number = 256;
93
+
94
+ /**
95
+ * URL metacharacters a tag must not contain. The official `@vercel/functions`
96
+ * client interpolates the tag straight into `revalidate?tags=${tag}` with NO
97
+ * URL-encoding (and also sends it in a request header), so a tag carrying one of
98
+ * these is stored intact on write but silently mangled server-side on
99
+ * invalidate: `&` starts a new query param, `?` a query, `#` truncates as a
100
+ * fragment, `%` is read as a (bad) percent-escape, `,` splits the tag list. The
101
+ * entry then never clears while `updateTag()`/`expireTag()` report success -
102
+ * the exact false-success the store's one deliberate throw exists to prevent.
103
+ * Such tags are dropped symmetrically on both the write and invalidate paths.
104
+ */
105
+ const VERCEL_UNSAFE_TAG_CHARS = /[,&#%?]/;
106
+
107
+ /**
108
+ * Herd-dampening window (ms). On a stale read the store pushes the entry's
109
+ * staleAt forward by this much and re-writes it, so other readers in the same
110
+ * region briefly see it as fresh while one revalidates. Best-effort and
111
+ * non-atomic: `getCache` has no compare-and-set, and storage is regional, so a
112
+ * race can still let two readers both trigger revalidation. Matches the intent of
113
+ * CFCacheStore's MAX_REVALIDATION_INTERVAL without its Cache-API atomicity.
114
+ */
115
+ const REVALIDATION_LOCK_MS = 30_000;
116
+
117
+ /** Family prefixes that keep the value tiers from colliding in the single Vercel
118
+ * keyspace. The router's own semantic prefixes (doc:/partial:/use-cache:) become
119
+ * the suffix; `rg:` namespaces every Rango entry. `h` is the PPR shell tier. */
120
+ type CacheFamily = "s" | "i" | "r" | "h";
121
+
122
+ /** Stored envelope for a segment-tree entry (get/set). */
123
+ interface VercelSegmentEnvelope {
124
+ /** The cached entry data. */
125
+ d: CachedEntryData;
126
+ /** staleAt (ms since epoch). */
127
+ s: number;
128
+ /** expiresAt (ms since epoch). */
129
+ e: number;
130
+ }
131
+
132
+ /** Stored envelope for a "use cache" function result (getItem/setItem). */
133
+ interface VercelItemEnvelope {
134
+ /** RSC-serialized value. */
135
+ v: string;
136
+ /** RSC-encoded handle blob. */
137
+ h?: string;
138
+ /** staleAt (ms since epoch). */
139
+ s: number;
140
+ /** expiresAt (ms since epoch). */
141
+ e: number;
142
+ /** Tags, surfaced on read so a hit still contributes to the document tag set. */
143
+ t?: string[];
144
+ }
145
+
146
+ /** Stored envelope for a full Response (getResponse/putResponse). */
147
+ interface VercelResponseEnvelope {
148
+ /** base64-encoded body bytes. */
149
+ b: string;
150
+ /** HTTP status. */
151
+ st: number;
152
+ /** Header entries (per-client signal headers already stripped). */
153
+ hd: [string, string][];
154
+ /** staleAt (ms since epoch). */
155
+ s: number;
156
+ /** expiresAt (ms since epoch). */
157
+ e: number;
158
+ /** Tags, preserved so a stale re-stamp keeps them. */
159
+ t?: string[];
160
+ }
161
+
162
+ /** Stored envelope for a PPR shell entry (getShell/putShell). */
163
+ interface VercelShellEnvelope {
164
+ /** base64-encoded prelude bytes. */
165
+ p: string;
166
+ /** postponed state JSON, or null (DATA variant). */
167
+ po: string | null;
168
+ /** React.version at capture. */
169
+ rv: string;
170
+ /** createdAt (ms since epoch). */
171
+ c: number;
172
+ /** staleAt (ms since epoch). */
173
+ s: number;
174
+ /** expiresAt (ms since epoch). */
175
+ e: number;
176
+ /** Tags, preserved so a stale re-stamp keeps them. */
177
+ t?: string[];
178
+ }
179
+
180
+ /** Read-path outcome for the debug sink. */
181
+ export type VercelCacheReadOutcome =
182
+ | "miss"
183
+ | "fresh"
184
+ | "stale-revalidate"
185
+ | "expired"
186
+ | "corrupt"
187
+ | "error";
188
+
189
+ /** Diagnostic event emitted on every read when `debug` is set. */
190
+ export interface VercelCacheReadDebugEvent {
191
+ op: "get" | "getItem" | "getResponse" | "getShell";
192
+ key: string;
193
+ outcome: VercelCacheReadOutcome;
194
+ staleAt?: number;
195
+ expiresAt?: number;
196
+ shouldRevalidate?: boolean;
197
+ /** Wall-clock ms spent in the backing cache.get(). */
198
+ readMs?: number;
199
+ }
200
+
201
+ /** `true` logs each read outcome; a function receives the structured event. */
202
+ export type VercelCacheDebug =
203
+ | boolean
204
+ | ((event: VercelCacheReadDebugEvent) => void);
205
+
206
+ /**
207
+ * Options for VercelCacheStore.
208
+ */
209
+ export interface VercelCacheStoreOptions<TEnv = unknown> {
210
+ /**
211
+ * The Vercel Runtime Cache handle - `getCache()` from `@vercel/functions`.
212
+ * Required. Construct it with a build-hash namespace to bust stale-shaped
213
+ * entries across deployments, since Vercel does not reconcile TTL/tags between
214
+ * deploys:
215
+ *
216
+ * ```ts
217
+ * import { getCache } from "@vercel/functions";
218
+ * new VercelCacheStore({ cache: getCache({ namespace: BUILD_ID }) });
219
+ * ```
220
+ */
221
+ cache: VercelRuntimeCache;
222
+
223
+ /**
224
+ * `waitUntil` from `@vercel/functions`. Used only to run the stale-read
225
+ * re-stamp (herd dampening) off the response path - the router already
226
+ * backgrounds the actual writes. When omitted, the re-stamp runs detached
227
+ * (fire-and-forget) instead.
228
+ */
229
+ waitUntil?: (promise: Promise<unknown>) => void;
230
+
231
+ /**
232
+ * Default ttl/swr for cache() boundaries when not explicitly specified.
233
+ */
234
+ defaults?: CacheDefaults;
235
+
236
+ /**
237
+ * Custom key generator applied to all cache operations (region/locale/user
238
+ * segmentation). Receives the request context and the default key.
239
+ */
240
+ keyGenerator?: (
241
+ ctx: RequestContext<TEnv>,
242
+ defaultKey: string,
243
+ ) => string | Promise<string>;
244
+
245
+ /**
246
+ * Build/version id folded into every stored key as `v/{version}/...`. A second
247
+ * cross-deploy busting layer in addition to (or instead of) the getCache
248
+ * namespace. Changing it invalidates everything this store wrote previously.
249
+ */
250
+ version?: string;
251
+
252
+ /**
253
+ * Max serialized entry size in bytes before a write is skipped. Defaults to
254
+ * VERCEL_MAX_ITEM_BYTES (2 MB - the platform's hard cap).
255
+ */
256
+ maxItemBytes?: number;
257
+
258
+ /**
259
+ * Human-readable label passed as the Vercel `set({ name })` option for
260
+ * observability in the Vercel dashboard.
261
+ */
262
+ name?: string;
263
+
264
+ /**
265
+ * Read diagnostics. `true` logs each read outcome to the console; a function
266
+ * receives the structured VercelCacheReadDebugEvent.
267
+ */
268
+ debug?: VercelCacheDebug;
269
+ }
270
+
271
+ function isRecord(value: unknown): value is Record<string, unknown> {
272
+ return typeof value === "object" && value !== null;
273
+ }
274
+
275
+ /** Encode binary body bytes to base64 in chunks (avoids call-stack blowups). */
276
+ function bufferToBase64(buffer: ArrayBuffer): string {
277
+ const bytes = new Uint8Array(buffer);
278
+ let binary = "";
279
+ const CHUNK = 0x8000;
280
+ for (let i = 0; i < bytes.length; i += CHUNK) {
281
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
282
+ }
283
+ return btoa(binary);
284
+ }
285
+
286
+ /** Decode a base64 body back into bytes. */
287
+ function base64ToBuffer(b64: string): ArrayBuffer {
288
+ const binary = atob(b64);
289
+ const bytes = new Uint8Array(binary.length);
290
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
291
+ return bytes.buffer;
292
+ }
293
+
294
+ /**
295
+ * Vercel Runtime Cache-backed segment cache store.
296
+ *
297
+ * Suitable for production deployments on Vercel Functions (Node runtime). The
298
+ * store is best-effort: every read failure degrades to a miss and every write
299
+ * failure to a no-op (reported, never thrown) - the sole exception is
300
+ * invalidateTags(), which rejects when the injected cache's expireTag() rejects
301
+ * so an awaited updateTag() can surface the failure.
302
+ *
303
+ * Read-your-own-writes caveat: this honesty is only as strong as the cache
304
+ * handle. The official `@vercel/functions` cache (`BuildCache.expireTag`)
305
+ * CATCHES backend failures and resolves, so with the official client a failed
306
+ * expireTag is invisible here and an awaited updateTag() can report success that
307
+ * did not happen. Treat tag invalidation on the official Vercel client as
308
+ * best-effort, not a hard read-your-own-writes guarantee. (A custom cache handle
309
+ * that propagates expireTag rejections does get the strict guarantee, which is
310
+ * what the unit suite exercises.)
311
+ *
312
+ * Key hashing / family isolation. The store namespaces its three value tiers as
313
+ * `rg:{s|i|r}:{key}` so segment, `"use cache"`, and response entries occupy
314
+ * disjoint keyspaces. That separation is only as strong as the cache's
315
+ * `keyHashFunction`: `getCache`'s default is djb2, which folds every key to 32
316
+ * bits (8 hex chars), so at a large live-key count a collision can let one
317
+ * entry's body be served for another key (same-family) or be misread as corrupt
318
+ * (cross-family). For deployments with many cached keys, pass a wider hash so
319
+ * the family split is real isolation, e.g. `getCache({ keyHashFunction })` with
320
+ * a sha256-based function.
321
+ *
322
+ * @example
323
+ * ```ts
324
+ * import { getCache, waitUntil } from "@vercel/functions";
325
+ * import { VercelCacheStore } from "@rangojs/router/cache";
326
+ *
327
+ * export const router = createRouter({
328
+ * cache: () => ({
329
+ * store: new VercelCacheStore({
330
+ * // `process.env` (not `import.meta.env`, which needs a VITE_ prefix and
331
+ * // would be undefined here) so cross-deploy busting via `version` works.
332
+ * cache: getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID }),
333
+ * waitUntil,
334
+ * defaults: { ttl: 60, swr: 300 },
335
+ * }),
336
+ * }),
337
+ * });
338
+ * ```
339
+ */
340
+ export class VercelCacheStore<
341
+ TEnv = unknown,
342
+ > implements SegmentCacheStore<TEnv> {
343
+ readonly defaults?: CacheDefaults;
344
+ readonly keyGenerator?: (
345
+ ctx: RequestContext<TEnv>,
346
+ defaultKey: string,
347
+ ) => string | Promise<string>;
348
+
349
+ private readonly cache: VercelRuntimeCache;
350
+ private readonly waitUntil?: (promise: Promise<unknown>) => void;
351
+ private readonly version?: string;
352
+ private readonly maxItemBytes: number;
353
+ private readonly name?: string;
354
+ private readonly debug?: VercelCacheDebug;
355
+
356
+ constructor(options: VercelCacheStoreOptions<TEnv>) {
357
+ if (!options || !options.cache) {
358
+ throw new Error(
359
+ "[VercelCacheStore] requires `cache` (the getCache() handle from @vercel/functions)",
360
+ );
361
+ }
362
+ this.cache = options.cache;
363
+ this.waitUntil = options.waitUntil;
364
+ this.defaults = options.defaults;
365
+ this.keyGenerator = options.keyGenerator;
366
+ this.version = options.version;
367
+ this.maxItemBytes = options.maxItemBytes ?? VERCEL_MAX_ITEM_BYTES;
368
+ this.name = options.name;
369
+ this.debug = options.debug;
370
+ }
371
+
372
+ // --- Segment family (get/set/delete) ---
373
+
374
+ async get(key: string): Promise<CacheGetResult | null> {
375
+ const storeKey = this.toStoreKey(key, "s");
376
+ const started = Date.now();
377
+ let raw: unknown;
378
+ try {
379
+ raw = await this.cache.get(storeKey);
380
+ } catch (error) {
381
+ reportCacheError(error, "cache-read", "[VercelCacheStore] get");
382
+ this.emitDebug({ op: "get", key, outcome: "error" });
383
+ return null;
384
+ }
385
+ const readMs = Date.now() - started;
386
+
387
+ if (raw == null) {
388
+ this.emitDebug({ op: "get", key, outcome: "miss", readMs });
389
+ return null;
390
+ }
391
+ const env = this.asSegmentEnvelope(raw);
392
+ if (!env) {
393
+ reportCacheError(
394
+ new Error("malformed segment envelope"),
395
+ "cache-corrupt",
396
+ "[VercelCacheStore] get",
397
+ );
398
+ void this.safeDelete(storeKey);
399
+ this.emitDebug({ op: "get", key, outcome: "corrupt", readMs });
400
+ return null;
401
+ }
402
+
403
+ const now = Date.now();
404
+ if (now > env.e) {
405
+ void this.safeDelete(storeKey);
406
+ this.emitDebug({
407
+ op: "get",
408
+ key,
409
+ outcome: "expired",
410
+ staleAt: env.s,
411
+ expiresAt: env.e,
412
+ readMs,
413
+ });
414
+ return null;
415
+ }
416
+
417
+ const isStale = env.s > 0 && now > env.s;
418
+ if (isStale) {
419
+ this.markRevalidating(
420
+ storeKey,
421
+ env,
422
+ env.d.tags,
423
+ "[VercelCacheStore] get",
424
+ );
425
+ this.emitDebug({
426
+ op: "get",
427
+ key,
428
+ outcome: "stale-revalidate",
429
+ shouldRevalidate: true,
430
+ staleAt: env.s,
431
+ expiresAt: env.e,
432
+ readMs,
433
+ });
434
+ return { data: env.d, shouldRevalidate: true };
435
+ }
436
+
437
+ this.emitDebug({
438
+ op: "get",
439
+ key,
440
+ outcome: "fresh",
441
+ shouldRevalidate: false,
442
+ staleAt: env.s,
443
+ expiresAt: env.e,
444
+ readMs,
445
+ });
446
+ return { data: env.d, shouldRevalidate: false };
447
+ }
448
+
449
+ async set(
450
+ key: string,
451
+ data: CachedEntryData,
452
+ ttl: number,
453
+ swr?: number,
454
+ ): Promise<void> {
455
+ try {
456
+ const swrWindow = resolveSwrWindow(swr, this.defaults);
457
+ const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
458
+ // Embed the CLAMPED tags, like putResponse/setItem: the segment family
459
+ // carries its tags inside `d`, and a raw list would resurface dropped
460
+ // tags into recordRequestTags on every hit AND get re-clamped (with a
461
+ // spurious cache-write report) by markRevalidating on every stale read.
462
+ const safeTags = this.clampTagsForWrite(
463
+ data.tags,
464
+ "[VercelCacheStore] set",
465
+ );
466
+ const d: CachedEntryData = data.tags
467
+ ? { ...data, tags: safeTags.length > 0 ? safeTags : undefined }
468
+ : data;
469
+ const env: VercelSegmentEnvelope = { d, s: staleAt, e: expiresAt };
470
+ await this.write(
471
+ this.toStoreKey(key, "s"),
472
+ env,
473
+ ttl + swrWindow,
474
+ safeTags,
475
+ "[VercelCacheStore] set",
476
+ );
477
+ } catch (error) {
478
+ reportCacheError(error, "cache-write", "[VercelCacheStore] set");
479
+ }
480
+ }
481
+
482
+ async delete(key: string): Promise<boolean> {
483
+ try {
484
+ await this.cache.delete(this.toStoreKey(key, "s"));
485
+ // Vercel's delete returns void; we cannot know whether the key existed.
486
+ // The router uses the boolean only for self-heal eviction, so report success.
487
+ return true;
488
+ } catch (error) {
489
+ reportCacheError(error, "cache-delete", "[VercelCacheStore] delete");
490
+ return false;
491
+ }
492
+ }
493
+
494
+ // --- Response family (getResponse/putResponse) ---
495
+
496
+ async getResponse(
497
+ key: string,
498
+ ): Promise<{ response: Response; shouldRevalidate: boolean } | null> {
499
+ const storeKey = this.toStoreKey(key, "r");
500
+ const started = Date.now();
501
+ let raw: unknown;
502
+ try {
503
+ raw = await this.cache.get(storeKey);
504
+ } catch (error) {
505
+ reportCacheError(error, "cache-read", "[VercelCacheStore] getResponse");
506
+ this.emitDebug({ op: "getResponse", key, outcome: "error" });
507
+ return null;
508
+ }
509
+ const readMs = Date.now() - started;
510
+
511
+ if (raw == null) {
512
+ this.emitDebug({ op: "getResponse", key, outcome: "miss", readMs });
513
+ return null;
514
+ }
515
+ const env = this.asResponseEnvelope(raw);
516
+ if (!env) {
517
+ reportCacheError(
518
+ new Error("malformed response envelope"),
519
+ "cache-corrupt",
520
+ "[VercelCacheStore] getResponse",
521
+ );
522
+ void this.safeDelete(storeKey);
523
+ this.emitDebug({ op: "getResponse", key, outcome: "corrupt", readMs });
524
+ return null;
525
+ }
526
+
527
+ const now = Date.now();
528
+ if (now > env.e) {
529
+ void this.safeDelete(storeKey);
530
+ this.emitDebug({
531
+ op: "getResponse",
532
+ key,
533
+ outcome: "expired",
534
+ staleAt: env.s,
535
+ expiresAt: env.e,
536
+ readMs,
537
+ });
538
+ return null;
539
+ }
540
+
541
+ // Reconstruct the Response BEFORE marking revalidating. A corrupt body (e.g.
542
+ // invalid base64 from base64ToBuffer, or bad header entries) would otherwise
543
+ // throw out of getResponse — breaking the fail-open contract — and a stale
544
+ // re-stamp would re-persist the bad value. Treat a reconstruction failure as
545
+ // a corrupt entry: report, evict, and miss.
546
+ let response: Response;
547
+ try {
548
+ response = new Response(base64ToBuffer(env.b), {
549
+ status: env.st,
550
+ headers: new Headers(env.hd),
551
+ });
552
+ } catch (error) {
553
+ reportCacheError(
554
+ error,
555
+ "cache-corrupt",
556
+ "[VercelCacheStore] getResponse",
557
+ );
558
+ void this.safeDelete(storeKey);
559
+ this.emitDebug({ op: "getResponse", key, outcome: "corrupt", readMs });
560
+ return null;
561
+ }
562
+
563
+ const isStale = env.s > 0 && now > env.s;
564
+ if (isStale) {
565
+ this.markRevalidating(
566
+ storeKey,
567
+ env,
568
+ env.t,
569
+ "[VercelCacheStore] getResponse",
570
+ );
571
+ }
572
+ this.emitDebug({
573
+ op: "getResponse",
574
+ key,
575
+ outcome: isStale ? "stale-revalidate" : "fresh",
576
+ shouldRevalidate: isStale,
577
+ staleAt: env.s,
578
+ expiresAt: env.e,
579
+ readMs,
580
+ });
581
+ return { response, shouldRevalidate: isStale };
582
+ }
583
+
584
+ async putResponse(
585
+ key: string,
586
+ response: Response,
587
+ ttl: number,
588
+ swr?: number,
589
+ tags?: string[],
590
+ ): Promise<void> {
591
+ try {
592
+ const body = await response.clone().arrayBuffer();
593
+ const headers: [string, string][] = [];
594
+ response.headers.forEach((value, name) => {
595
+ if (isPerClientSignalHeader(name)) return;
596
+ headers.push([name, value]);
597
+ });
598
+ const swrWindow = resolveSwrWindow(swr, this.defaults);
599
+ const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
600
+ // Embed the CLAMPED tags in the envelope, not the raw list: tags dropped
601
+ // on write (unsafe/over-cap) would otherwise resurface on a hit and be
602
+ // merged into an upstream document's tag set, letting it claim a tag this
603
+ // entry can't actually be invalidated by. write() re-clamps (idempotent
604
+ // and silent on an already-clean list).
605
+ const safeTags = this.clampTagsForWrite(
606
+ tags,
607
+ "[VercelCacheStore] putResponse",
608
+ );
609
+ const env: VercelResponseEnvelope = {
610
+ b: bufferToBase64(body),
611
+ st: response.status,
612
+ hd: headers,
613
+ s: staleAt,
614
+ e: expiresAt,
615
+ t: safeTags.length > 0 ? safeTags : undefined,
616
+ };
617
+ await this.write(
618
+ this.toStoreKey(key, "r"),
619
+ env,
620
+ ttl + swrWindow,
621
+ safeTags,
622
+ "[VercelCacheStore] putResponse",
623
+ );
624
+ } catch (error) {
625
+ reportCacheError(error, "cache-write", "[VercelCacheStore] putResponse");
626
+ }
627
+ }
628
+
629
+ // --- Item family ("use cache" - getItem/setItem) ---
630
+
631
+ async getItem(key: string): Promise<CacheItemResult | null> {
632
+ const storeKey = this.toStoreKey(key, "i");
633
+ const started = Date.now();
634
+ let raw: unknown;
635
+ try {
636
+ raw = await this.cache.get(storeKey);
637
+ } catch (error) {
638
+ reportCacheError(error, "cache-read", "[VercelCacheStore] getItem");
639
+ this.emitDebug({ op: "getItem", key, outcome: "error" });
640
+ return null;
641
+ }
642
+ const readMs = Date.now() - started;
643
+
644
+ if (raw == null) {
645
+ this.emitDebug({ op: "getItem", key, outcome: "miss", readMs });
646
+ return null;
647
+ }
648
+ const env = this.asItemEnvelope(raw);
649
+ if (!env) {
650
+ reportCacheError(
651
+ new Error("malformed item envelope"),
652
+ "cache-corrupt",
653
+ "[VercelCacheStore] getItem",
654
+ );
655
+ void this.safeDelete(storeKey);
656
+ this.emitDebug({ op: "getItem", key, outcome: "corrupt", readMs });
657
+ return null;
658
+ }
659
+
660
+ const now = Date.now();
661
+ if (now > env.e) {
662
+ void this.safeDelete(storeKey);
663
+ this.emitDebug({
664
+ op: "getItem",
665
+ key,
666
+ outcome: "expired",
667
+ staleAt: env.s,
668
+ expiresAt: env.e,
669
+ readMs,
670
+ });
671
+ return null;
672
+ }
673
+
674
+ const isStale = env.s > 0 && now > env.s;
675
+ if (isStale) {
676
+ this.markRevalidating(storeKey, env, env.t, "[VercelCacheStore] getItem");
677
+ }
678
+ this.emitDebug({
679
+ op: "getItem",
680
+ key,
681
+ outcome: isStale ? "stale-revalidate" : "fresh",
682
+ shouldRevalidate: isStale,
683
+ staleAt: env.s,
684
+ expiresAt: env.e,
685
+ readMs,
686
+ });
687
+ return {
688
+ value: env.v,
689
+ handles: env.h,
690
+ shouldRevalidate: isStale,
691
+ tags: env.t,
692
+ };
693
+ }
694
+
695
+ async setItem(
696
+ key: string,
697
+ value: string,
698
+ options?: CacheItemOptions,
699
+ ): Promise<void> {
700
+ try {
701
+ const ttl = resolveTtl(options?.ttl, this.defaults, DEFAULT_FUNCTION_TTL);
702
+ const swrWindow = resolveSwrWindow(options?.swr, this.defaults);
703
+ const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
704
+ // Store the clamped tags (see putResponse) so dropped tags don't resurface
705
+ // on a hit; write() re-clamps idempotently.
706
+ const safeTags = this.clampTagsForWrite(
707
+ options?.tags,
708
+ "[VercelCacheStore] setItem",
709
+ );
710
+ const env: VercelItemEnvelope = {
711
+ v: value,
712
+ h: options?.handles,
713
+ s: staleAt,
714
+ e: expiresAt,
715
+ t: safeTags.length > 0 ? safeTags : undefined,
716
+ };
717
+ await this.write(
718
+ this.toStoreKey(key, "i"),
719
+ env,
720
+ ttl + swrWindow,
721
+ safeTags,
722
+ "[VercelCacheStore] setItem",
723
+ );
724
+ } catch (error) {
725
+ reportCacheError(error, "cache-write", "[VercelCacheStore] setItem");
726
+ }
727
+ }
728
+
729
+ // --- Shell family (PPR shell resume - getShell/putShell) ---
730
+
731
+ async getShell(
732
+ key: string,
733
+ ): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
734
+ const storeKey = this.toStoreKey(key, "h");
735
+ const started = Date.now();
736
+ let raw: unknown;
737
+ try {
738
+ raw = await this.cache.get(storeKey);
739
+ } catch (error) {
740
+ reportCacheError(error, "cache-read", "[VercelCacheStore] getShell");
741
+ this.emitDebug({ op: "getShell", key, outcome: "error" });
742
+ return null;
743
+ }
744
+ const readMs = Date.now() - started;
745
+
746
+ if (raw == null) {
747
+ this.emitDebug({ op: "getShell", key, outcome: "miss", readMs });
748
+ return null;
749
+ }
750
+ const env = this.asShellEnvelope(raw);
751
+ if (!env) {
752
+ reportCacheError(
753
+ new Error("malformed shell envelope"),
754
+ "cache-corrupt",
755
+ "[VercelCacheStore] getShell",
756
+ );
757
+ void this.safeDelete(storeKey);
758
+ this.emitDebug({ op: "getShell", key, outcome: "corrupt", readMs });
759
+ return null;
760
+ }
761
+
762
+ const now = Date.now();
763
+ if (now > env.e) {
764
+ void this.safeDelete(storeKey);
765
+ this.emitDebug({
766
+ op: "getShell",
767
+ key,
768
+ outcome: "expired",
769
+ staleAt: env.s,
770
+ expiresAt: env.e,
771
+ readMs,
772
+ });
773
+ return null;
774
+ }
775
+
776
+ const isStale = env.s > 0 && now > env.s;
777
+ if (isStale) {
778
+ this.markRevalidating(
779
+ storeKey,
780
+ env,
781
+ env.t,
782
+ "[VercelCacheStore] getShell",
783
+ );
784
+ }
785
+ this.emitDebug({
786
+ op: "getShell",
787
+ key,
788
+ outcome: isStale ? "stale-revalidate" : "fresh",
789
+ shouldRevalidate: isStale,
790
+ staleAt: env.s,
791
+ expiresAt: env.e,
792
+ readMs,
793
+ });
794
+ return {
795
+ entry: {
796
+ prelude: env.p,
797
+ postponed: env.po,
798
+ reactVersion: env.rv,
799
+ createdAt: env.c,
800
+ },
801
+ shouldRevalidate: isStale,
802
+ };
803
+ }
804
+
805
+ async putShell(
806
+ key: string,
807
+ entry: ShellCacheEntry,
808
+ ttlSeconds?: number,
809
+ swrSeconds?: number,
810
+ tags?: string[],
811
+ ): Promise<void> {
812
+ try {
813
+ const ttl = resolveTtl(ttlSeconds, this.defaults, DEFAULT_FUNCTION_TTL);
814
+ const swrWindow = resolveSwrWindow(swrSeconds, this.defaults);
815
+ const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
816
+ // Store the CLAMPED tags (see putResponse/setItem) so dropped tags don't
817
+ // resurface on a hit; write() re-clamps idempotently.
818
+ const safeTags = this.clampTagsForWrite(
819
+ tags,
820
+ "[VercelCacheStore] putShell",
821
+ );
822
+ const env: VercelShellEnvelope = {
823
+ p: entry.prelude,
824
+ po: entry.postponed,
825
+ rv: entry.reactVersion,
826
+ c: entry.createdAt,
827
+ s: staleAt,
828
+ e: expiresAt,
829
+ t: safeTags.length > 0 ? safeTags : undefined,
830
+ };
831
+ // write() enforces the 2 MB per-item ceiling (withinSizeLimit): an
832
+ // oversized shell prelude is reported and skipped (fail-open to a full
833
+ // render), never silently no-op'd on the platform.
834
+ await this.write(
835
+ this.toStoreKey(key, "h"),
836
+ env,
837
+ ttl + swrWindow,
838
+ safeTags,
839
+ "[VercelCacheStore] putShell",
840
+ );
841
+ } catch (error) {
842
+ reportCacheError(error, "cache-write", "[VercelCacheStore] putShell");
843
+ }
844
+ }
845
+
846
+ // --- Tags ---
847
+
848
+ async invalidateTags(tags: string[]): Promise<void> {
849
+ if (!tags || tags.length === 0) return;
850
+ // No per-item cap here: an invalidation must reach every requested tag.
851
+ const safe = this.validateTags(
852
+ tags,
853
+ "[VercelCacheStore] invalidateTags",
854
+ "cache-invalidate",
855
+ );
856
+ if (safe.length === 0) return;
857
+ try {
858
+ await this.cache.expireTag(safe);
859
+ } catch (error) {
860
+ // The one deliberate throw: a failed durable invalidation must reject so an
861
+ // awaited updateTag() surfaces it instead of reporting false success.
862
+ // NOTE: this only fires if the injected cache's expireTag() actually
863
+ // rejects. The official @vercel/functions cache catches backend failures
864
+ // and resolves, so on that client a failure is invisible and this throw
865
+ // never fires (tag invalidation is best-effort there). See the class doc.
866
+ reportCacheError(
867
+ error,
868
+ "cache-invalidate",
869
+ "[VercelCacheStore] invalidateTags",
870
+ );
871
+ throw error instanceof Error ? error : new Error(String(error));
872
+ }
873
+ }
874
+
875
+ // --- Internals ---
876
+
877
+ // The `rg:{family}:` prefix keeps the three value tiers in disjoint
878
+ // keyspaces, but the isolation is only as strong as the cache's
879
+ // keyHashFunction: djb2 (getCache's default) folds this to 32 bits, so at a
880
+ // large live-key count a same-family collision can serve the wrong body and a
881
+ // cross-family one reads as corrupt. See the class doc for passing a wider
882
+ // hash (sha256) when many keys are live.
883
+ private toStoreKey(key: string, family: CacheFamily): string {
884
+ const versionPrefix = this.version ? `v/${this.version}/` : "";
885
+ return `${versionPrefix}rg:${family}:${key}`;
886
+ }
887
+
888
+ private async write(
889
+ storeKey: string,
890
+ value: unknown,
891
+ totalTtlSeconds: number,
892
+ tags: string[] | undefined,
893
+ label: string,
894
+ ): Promise<void> {
895
+ if (!this.withinSizeLimit(value, label)) return;
896
+ const safeTags = this.clampTagsForWrite(tags, label);
897
+ const options: { ttl: number; tags?: string[]; name?: string } = {
898
+ ttl: Math.max(1, Math.ceil(totalTtlSeconds)),
899
+ };
900
+ if (safeTags.length > 0) options.tags = safeTags;
901
+ if (this.name) options.name = this.name;
902
+ await this.cache.set(storeKey, value, options);
903
+ }
904
+
905
+ /**
906
+ * Stale-read herd dampening: push staleAt forward by REVALIDATION_LOCK_MS
907
+ * (clamped to the hard expiry) and re-write the same envelope under the
908
+ * remaining lifetime, so concurrent same-region readers see it as fresh while
909
+ * one revalidates. Best-effort and non-blocking; never throws.
910
+ *
911
+ * Two races are accepted here because `getCache` offers no compare-and-set: a
912
+ * fast background revalidation that lands between this stale read and the
913
+ * re-stamp write can be overwritten by the (staler) locked envelope, and an
914
+ * `expireTag` that deletes the entry in the same window can be resurrected by
915
+ * the re-stamp. Both self-heal within REVALIDATION_LOCK_MS (the re-stamped
916
+ * copy is then re-read as stale and revalidated again, or re-deleted). The
917
+ * window is narrow and the cost is at most one extra revalidation / one brief
918
+ * stale serve; a read-compare would not close it atomically without CAS.
919
+ */
920
+ private markRevalidating<E extends { s: number; e: number }>(
921
+ storeKey: string,
922
+ env: E,
923
+ tags: string[] | undefined,
924
+ label: string,
925
+ ): void {
926
+ const now = Date.now();
927
+ const remainingSeconds = Math.ceil((env.e - now) / 1000);
928
+ if (remainingSeconds <= 0) return;
929
+ const locked: E = {
930
+ ...env,
931
+ s: Math.min(now + REVALIDATION_LOCK_MS, env.e),
932
+ };
933
+ const task = (): Promise<void> =>
934
+ this.write(storeKey, locked, remainingSeconds, tags, label);
935
+ if (this.waitUntil) {
936
+ this.waitUntil(reportingAsync(task, "cache-write", label));
937
+ } else {
938
+ void reportingAsync(task, "cache-write", label);
939
+ }
940
+ }
941
+
942
+ private withinSizeLimit(value: unknown, label: string): boolean {
943
+ try {
944
+ const bytes = new TextEncoder().encode(JSON.stringify(value)).length;
945
+ if (bytes >= this.maxItemBytes) {
946
+ reportCacheError(
947
+ new Error(
948
+ `entry is ${bytes}B, at/above the ${this.maxItemBytes}B cap; not cached`,
949
+ ),
950
+ "cache-write",
951
+ label,
952
+ );
953
+ return false;
954
+ }
955
+ } catch {
956
+ // If the value cannot be measured, allow the write (best-effort).
957
+ }
958
+ return true;
959
+ }
960
+
961
+ /**
962
+ * Drop tags Vercel cannot round-trip: over-length tags, and tags carrying a
963
+ * URL metacharacter the `@vercel/functions` client does not encode (see
964
+ * VERCEL_UNSAFE_TAG_CHARS). Runs on BOTH paths - the write path
965
+ * (clampTagsForWrite) and invalidateTags - so a tag that would silently fail
966
+ * invalidation is never stored in the first place. `category` reports the
967
+ * drop under the caller's real operation (write vs invalidate).
968
+ */
969
+ private validateTags(
970
+ tags: string[],
971
+ label: string,
972
+ category: CacheErrorCategory,
973
+ ): string[] {
974
+ const encoder = new TextEncoder();
975
+ const out: string[] = [];
976
+ for (const tag of tags) {
977
+ const unsafe = VERCEL_UNSAFE_TAG_CHARS.exec(tag);
978
+ if (unsafe) {
979
+ reportCacheError(
980
+ new Error(
981
+ `tag "${tag}" contains an unencodable URL character "${unsafe[0]}"; ` +
982
+ `skipped (it would not round-trip through Vercel tag invalidation)`,
983
+ ),
984
+ category,
985
+ label,
986
+ );
987
+ continue;
988
+ }
989
+ if (encoder.encode(tag).length > VERCEL_MAX_TAG_BYTES) {
990
+ reportCacheError(
991
+ new Error(`tag exceeds ${VERCEL_MAX_TAG_BYTES} bytes; skipped`),
992
+ category,
993
+ label,
994
+ );
995
+ continue;
996
+ }
997
+ out.push(tag);
998
+ }
999
+ return out;
1000
+ }
1001
+
1002
+ /** validateTags + the per-item tag cap. Used on the write path. */
1003
+ private clampTagsForWrite(
1004
+ tags: string[] | undefined,
1005
+ label: string,
1006
+ ): string[] {
1007
+ if (!tags || tags.length === 0) return [];
1008
+ const valid = this.validateTags(tags, label, "cache-write");
1009
+ if (valid.length > VERCEL_MAX_TAGS_PER_ITEM) {
1010
+ reportCacheError(
1011
+ new Error(
1012
+ `entry has ${valid.length} tags, over the ${VERCEL_MAX_TAGS_PER_ITEM}-tag cap; ` +
1013
+ `keeping the first ${VERCEL_MAX_TAGS_PER_ITEM}`,
1014
+ ),
1015
+ "cache-write",
1016
+ label,
1017
+ );
1018
+ return valid.slice(0, VERCEL_MAX_TAGS_PER_ITEM);
1019
+ }
1020
+ return valid;
1021
+ }
1022
+
1023
+ private async safeDelete(storeKey: string): Promise<void> {
1024
+ try {
1025
+ await this.cache.delete(storeKey);
1026
+ } catch (error) {
1027
+ reportCacheError(error, "cache-delete", "[VercelCacheStore] evict");
1028
+ }
1029
+ }
1030
+
1031
+ private emitDebug(event: VercelCacheReadDebugEvent): void {
1032
+ const sink = this.debug;
1033
+ if (!sink) return;
1034
+ try {
1035
+ if (typeof sink === "function") sink(event);
1036
+ else console.log("[VercelCacheStore]", event);
1037
+ } catch {
1038
+ // The debug sink must never break the cache path.
1039
+ }
1040
+ }
1041
+
1042
+ private asSegmentEnvelope(raw: unknown): VercelSegmentEnvelope | null {
1043
+ if (!isRecord(raw)) return null;
1044
+ const { d, s, e } = raw;
1045
+ if (
1046
+ !isRecord(d) ||
1047
+ !Array.isArray((d as Record<string, unknown>).segments)
1048
+ ) {
1049
+ return null;
1050
+ }
1051
+ if (typeof s !== "number" || typeof e !== "number") return null;
1052
+ return { d: d as unknown as CachedEntryData, s, e };
1053
+ }
1054
+
1055
+ private asItemEnvelope(raw: unknown): VercelItemEnvelope | null {
1056
+ if (!isRecord(raw)) return null;
1057
+ const { v, h, s, e, t } = raw;
1058
+ if (typeof v !== "string") return null;
1059
+ if (typeof s !== "number" || typeof e !== "number") return null;
1060
+ return {
1061
+ v,
1062
+ h: typeof h === "string" ? h : undefined,
1063
+ s,
1064
+ e,
1065
+ t: Array.isArray(t) ? (t as string[]) : undefined,
1066
+ };
1067
+ }
1068
+
1069
+ private asShellEnvelope(raw: unknown): VercelShellEnvelope | null {
1070
+ if (!isRecord(raw)) return null;
1071
+ const { p, po, rv, c, s, e, t } = raw;
1072
+ if (typeof p !== "string" || typeof rv !== "string") return null;
1073
+ if (po !== null && typeof po !== "string") return null;
1074
+ if (typeof c !== "number") return null;
1075
+ if (typeof s !== "number" || typeof e !== "number") return null;
1076
+ return {
1077
+ p,
1078
+ po: po as string | null,
1079
+ rv,
1080
+ c,
1081
+ s,
1082
+ e,
1083
+ t: Array.isArray(t) ? (t as string[]) : undefined,
1084
+ };
1085
+ }
1086
+
1087
+ private asResponseEnvelope(raw: unknown): VercelResponseEnvelope | null {
1088
+ if (!isRecord(raw)) return null;
1089
+ const { b, st, hd, s, e, t } = raw;
1090
+ if (typeof b !== "string" || typeof st !== "number") return null;
1091
+ if (!Array.isArray(hd)) return null;
1092
+ if (typeof s !== "number" || typeof e !== "number") return null;
1093
+ return {
1094
+ b,
1095
+ st,
1096
+ hd: hd as [string, string][],
1097
+ s,
1098
+ e,
1099
+ t: Array.isArray(t) ? (t as string[]) : undefined,
1100
+ };
1101
+ }
1102
+ }