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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (356) hide show
  1. package/README.md +24 -9
  2. package/dist/bin/rango.js +157 -63
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +1584 -639
  5. package/package.json +71 -21
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +60 -0
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +222 -30
  10. package/skills/caching/SKILL.md +263 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +235 -28
  16. package/skills/host-router/SKILL.md +122 -22
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +29 -5
  19. package/skills/layout/SKILL.md +13 -9
  20. package/skills/links/SKILL.md +173 -17
  21. package/skills/loader/SKILL.md +170 -23
  22. package/skills/middleware/SKILL.md +16 -10
  23. package/skills/migrate-nextjs/SKILL.md +38 -16
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +11 -7
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +250 -25
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +114 -47
  31. package/skills/route/SKILL.md +42 -5
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +78 -42
  34. package/skills/tailwind/SKILL.md +27 -3
  35. package/skills/testing/SKILL.md +129 -0
  36. package/skills/testing/bindings.md +89 -0
  37. package/skills/testing/cache-prerender.md +124 -0
  38. package/skills/testing/client-components.md +122 -0
  39. package/skills/testing/e2e-parity.md +125 -0
  40. package/skills/testing/flight.md +92 -0
  41. package/skills/testing/handles.md +129 -0
  42. package/skills/testing/loader.md +128 -0
  43. package/skills/testing/middleware.md +99 -0
  44. package/skills/testing/render-handler.md +121 -0
  45. package/skills/testing/response-routes.md +95 -0
  46. package/skills/testing/reverse-and-types.md +84 -0
  47. package/skills/testing/server-actions.md +107 -0
  48. package/skills/testing/server-tree.md +128 -0
  49. package/skills/testing/setup.md +120 -0
  50. package/skills/typesafety/SKILL.md +316 -26
  51. package/skills/use-cache/SKILL.md +36 -5
  52. package/skills/vercel/SKILL.md +107 -0
  53. package/skills/view-transitions/SKILL.md +294 -0
  54. package/src/__augment-tests__/augment.ts +81 -0
  55. package/src/__augment-tests__/augmented.check.ts +116 -0
  56. package/src/__internal.ts +0 -65
  57. package/src/browser/action-coordinator.ts +53 -36
  58. package/src/browser/action-fence.ts +47 -0
  59. package/src/browser/app-shell.ts +14 -27
  60. package/src/browser/cookie-name.ts +140 -0
  61. package/src/browser/event-controller.ts +37 -143
  62. package/src/browser/history-state.ts +21 -0
  63. package/src/browser/index.ts +3 -3
  64. package/src/browser/invalidate-client-cache.ts +52 -0
  65. package/src/browser/navigation-bridge.ts +30 -59
  66. package/src/browser/navigation-client.ts +96 -84
  67. package/src/browser/navigation-store-handle.ts +38 -0
  68. package/src/browser/navigation-store.ts +32 -82
  69. package/src/browser/navigation-transaction.ts +9 -59
  70. package/src/browser/partial-update.ts +60 -127
  71. package/src/browser/prefetch/cache.ts +82 -72
  72. package/src/browser/prefetch/fetch.ts +108 -33
  73. package/src/browser/prefetch/queue.ts +6 -3
  74. package/src/browser/rango-state.ts +157 -115
  75. package/src/browser/react/Link.tsx +0 -2
  76. package/src/browser/react/NavigationProvider.tsx +41 -48
  77. package/src/browser/react/ScrollRestoration.tsx +10 -6
  78. package/src/browser/react/filter-segment-order.ts +0 -2
  79. package/src/browser/react/index.ts +0 -48
  80. package/src/browser/react/location-state-shared.ts +166 -8
  81. package/src/browser/react/location-state.ts +39 -14
  82. package/src/browser/react/use-action.ts +6 -15
  83. package/src/browser/react/use-handle.ts +17 -14
  84. package/src/browser/react/use-link-status.ts +0 -4
  85. package/src/browser/react/use-navigation.ts +0 -3
  86. package/src/browser/react/use-params.ts +11 -11
  87. package/src/browser/react/use-reverse.ts +106 -0
  88. package/src/browser/react/use-router.ts +20 -5
  89. package/src/browser/react/use-search-params.ts +0 -5
  90. package/src/browser/react/use-segments.ts +0 -13
  91. package/src/browser/response-adapter.ts +52 -1
  92. package/src/browser/rsc-router.tsx +70 -34
  93. package/src/browser/scroll-restoration.ts +22 -14
  94. package/src/browser/segment-structure-assert.ts +2 -2
  95. package/src/browser/server-action-bridge.ts +168 -44
  96. package/src/browser/types.ts +36 -21
  97. package/src/browser/validate-redirect-origin.ts +43 -16
  98. package/src/build/collect-fallback-refs.ts +107 -0
  99. package/src/build/generate-manifest.ts +60 -35
  100. package/src/build/generate-route-types.ts +3 -0
  101. package/src/build/index.ts +8 -2
  102. package/src/build/prefix-tree-utils.ts +123 -0
  103. package/src/build/route-trie.ts +89 -10
  104. package/src/build/route-types/codegen.ts +4 -4
  105. package/src/build/route-types/include-resolution.ts +1 -1
  106. package/src/build/route-types/param-extraction.ts +6 -3
  107. package/src/build/route-types/per-module-writer.ts +7 -4
  108. package/src/build/route-types/router-processing.ts +122 -22
  109. package/src/build/route-types/scan-filter.ts +1 -1
  110. package/src/build/route-types/source-scan.ts +118 -0
  111. package/src/build/runtime-discovery.ts +9 -20
  112. package/src/cache/cache-error.ts +104 -0
  113. package/src/cache/cache-policy.ts +68 -28
  114. package/src/cache/cache-runtime.ts +134 -32
  115. package/src/cache/cache-scope.ts +100 -74
  116. package/src/cache/cache-tag.ts +98 -0
  117. package/src/cache/cf/cf-cache-store.ts +2255 -238
  118. package/src/cache/cf/index.ts +6 -16
  119. package/src/cache/document-cache.ts +61 -20
  120. package/src/cache/handle-snapshot.ts +63 -0
  121. package/src/cache/index.ts +22 -20
  122. package/src/cache/memory-segment-store.ts +136 -37
  123. package/src/cache/profile-registry.ts +6 -30
  124. package/src/cache/read-through-swr.ts +41 -11
  125. package/src/cache/segment-codec.ts +0 -16
  126. package/src/cache/tag-invalidation.ts +230 -0
  127. package/src/cache/types.ts +33 -100
  128. package/src/cache/vercel/index.ts +11 -0
  129. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  130. package/src/client.rsc.tsx +6 -21
  131. package/src/client.tsx +25 -61
  132. package/src/component-utils.ts +19 -0
  133. package/src/context-var.ts +17 -5
  134. package/src/decode-loader-results.ts +36 -0
  135. package/src/defer.ts +196 -0
  136. package/src/deps/ssr.ts +0 -1
  137. package/src/errors.ts +30 -4
  138. package/src/handle.ts +31 -23
  139. package/src/handles/MetaTags.tsx +0 -14
  140. package/src/handles/breadcrumbs.ts +16 -5
  141. package/src/handles/meta.ts +0 -39
  142. package/src/host/cookie-handler.ts +0 -36
  143. package/src/host/errors.ts +0 -24
  144. package/src/host/index.ts +8 -2
  145. package/src/host/pattern-matcher.ts +7 -50
  146. package/src/host/router.ts +107 -99
  147. package/src/host/testing.ts +40 -27
  148. package/src/host/types.ts +37 -4
  149. package/src/host/utils.ts +1 -1
  150. package/src/href-client.ts +137 -22
  151. package/src/index.rsc.ts +63 -9
  152. package/src/index.ts +64 -9
  153. package/src/internal-debug.ts +2 -4
  154. package/src/loader-store.ts +500 -0
  155. package/src/loader.rsc.ts +20 -13
  156. package/src/loader.ts +12 -11
  157. package/src/missing-id-error.ts +68 -0
  158. package/src/network-error-thrower.tsx +1 -6
  159. package/src/outlet-provider.tsx +1 -5
  160. package/src/prerender/param-hash.ts +10 -11
  161. package/src/prerender/store.ts +32 -37
  162. package/src/prerender.ts +61 -6
  163. package/src/redirect-origin.ts +100 -0
  164. package/src/response-utils.ts +9 -0
  165. package/src/reverse.ts +65 -40
  166. package/src/root-error-boundary.tsx +1 -19
  167. package/src/route-content-wrapper.tsx +7 -72
  168. package/src/route-definition/dsl-helpers.ts +244 -281
  169. package/src/route-definition/helper-factories.ts +29 -139
  170. package/src/route-definition/helpers-types.ts +40 -17
  171. package/src/route-definition/redirect.ts +43 -9
  172. package/src/route-definition/resolve-handler-use.ts +6 -0
  173. package/src/route-definition/use-item-types.ts +32 -0
  174. package/src/route-map-builder.ts +0 -16
  175. package/src/route-types.ts +19 -41
  176. package/src/router/basename.ts +14 -0
  177. package/src/router/content-negotiation.ts +15 -15
  178. package/src/router/error-handling.ts +13 -17
  179. package/src/router/find-match.ts +44 -23
  180. package/src/router/handler-context.ts +4 -41
  181. package/src/router/intercept-resolution.ts +14 -19
  182. package/src/router/lazy-includes.ts +9 -46
  183. package/src/router/loader-resolution.ts +91 -46
  184. package/src/router/logging.ts +0 -6
  185. package/src/router/manifest.ts +18 -29
  186. package/src/router/match-api.ts +0 -20
  187. package/src/router/match-context.ts +0 -22
  188. package/src/router/match-handlers.ts +57 -58
  189. package/src/router/match-middleware/background-revalidation.ts +0 -7
  190. package/src/router/match-middleware/cache-lookup.ts +150 -271
  191. package/src/router/match-middleware/cache-store.ts +3 -33
  192. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  193. package/src/router/match-middleware/segment-resolution.ts +0 -22
  194. package/src/router/match-pipelines.ts +1 -42
  195. package/src/router/match-result.ts +31 -80
  196. package/src/router/metrics.ts +0 -34
  197. package/src/router/middleware-types.ts +5 -112
  198. package/src/router/middleware.ts +118 -133
  199. package/src/router/navigation-snapshot.ts +0 -51
  200. package/src/router/params-util.ts +23 -0
  201. package/src/router/pattern-matching.ts +62 -67
  202. package/src/router/prerender-match.ts +99 -63
  203. package/src/router/preview-match.ts +3 -1
  204. package/src/router/request-classification.ts +28 -62
  205. package/src/router/revalidation.ts +50 -56
  206. package/src/router/route-snapshot.ts +0 -1
  207. package/src/router/router-context.ts +0 -27
  208. package/src/router/router-interfaces.ts +68 -35
  209. package/src/router/router-options.ts +55 -1
  210. package/src/router/router-registry.ts +2 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -63
  212. package/src/router/segment-resolution/helpers.ts +34 -0
  213. package/src/router/segment-resolution/loader-cache.ts +40 -37
  214. package/src/router/segment-resolution/revalidation.ts +203 -285
  215. package/src/router/segment-resolution/static-store.ts +19 -5
  216. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  217. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  218. package/src/router/segment-resolution.ts +4 -1
  219. package/src/router/segment-wrappers.ts +0 -3
  220. package/src/router/state-cookie-name.ts +33 -0
  221. package/src/router/substitute-pattern-params.ts +56 -0
  222. package/src/router/telemetry-otel.ts +0 -20
  223. package/src/router/telemetry.ts +96 -19
  224. package/src/router/timeout.ts +0 -20
  225. package/src/router/trie-matching.ts +87 -48
  226. package/src/router/types.ts +9 -63
  227. package/src/router/url-params.ts +0 -5
  228. package/src/router.ts +80 -41
  229. package/src/rsc/handler-context.ts +3 -2
  230. package/src/rsc/handler.ts +83 -78
  231. package/src/rsc/helpers.ts +93 -5
  232. package/src/rsc/index.ts +1 -1
  233. package/src/rsc/json-route-result.ts +38 -0
  234. package/src/rsc/manifest-init.ts +28 -41
  235. package/src/rsc/origin-guard.ts +39 -25
  236. package/src/rsc/progressive-enhancement.ts +12 -1
  237. package/src/rsc/redirect-guard.ts +99 -0
  238. package/src/rsc/response-error.ts +79 -12
  239. package/src/rsc/response-route-handler.ts +76 -62
  240. package/src/rsc/rsc-rendering.ts +41 -60
  241. package/src/rsc/runtime-warnings.ts +23 -10
  242. package/src/rsc/server-action.ts +62 -67
  243. package/src/rsc/ssr-setup.ts +16 -0
  244. package/src/rsc/types.ts +10 -5
  245. package/src/runtime-env.ts +18 -0
  246. package/src/search-params.ts +4 -20
  247. package/src/segment-loader-promise.ts +14 -2
  248. package/src/segment-system.tsx +199 -142
  249. package/src/serialize.ts +243 -0
  250. package/src/server/context.ts +150 -51
  251. package/src/server/cookie-store.ts +80 -5
  252. package/src/server/handle-store.ts +7 -24
  253. package/src/server/loader-registry.ts +5 -24
  254. package/src/server/request-context.ts +165 -87
  255. package/src/ssr/index.tsx +14 -14
  256. package/src/static-handler.ts +10 -13
  257. package/src/testing/cache-status.ts +162 -0
  258. package/src/testing/collect-handle.ts +40 -0
  259. package/src/testing/dispatch.ts +618 -0
  260. package/src/testing/dom.entry.ts +22 -0
  261. package/src/testing/e2e/fixture.ts +188 -0
  262. package/src/testing/e2e/index.ts +128 -0
  263. package/src/testing/e2e/matchers.ts +35 -0
  264. package/src/testing/e2e/page-helpers.ts +272 -0
  265. package/src/testing/e2e/parity.ts +387 -0
  266. package/src/testing/e2e/server.ts +195 -0
  267. package/src/testing/flight-matchers.ts +97 -0
  268. package/src/testing/flight-normalize.ts +11 -0
  269. package/src/testing/flight-runtime.d.ts +57 -0
  270. package/src/testing/flight-tree.ts +682 -0
  271. package/src/testing/flight.entry.ts +52 -0
  272. package/src/testing/flight.ts +232 -0
  273. package/src/testing/generated-routes.ts +183 -0
  274. package/src/testing/index.ts +99 -0
  275. package/src/testing/internal/context.ts +348 -0
  276. package/src/testing/internal/flight-client-globals.ts +30 -0
  277. package/src/testing/internal/seed-vars.ts +54 -0
  278. package/src/testing/render-handler.ts +330 -0
  279. package/src/testing/render-route.tsx +566 -0
  280. package/src/testing/run-loader.ts +378 -0
  281. package/src/testing/run-middleware.ts +205 -0
  282. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  283. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  284. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  285. package/src/testing/vitest-stubs/version.ts +5 -0
  286. package/src/testing/vitest.ts +305 -0
  287. package/src/theme/ThemeProvider.tsx +0 -52
  288. package/src/theme/ThemeScript.tsx +0 -6
  289. package/src/theme/constants.ts +0 -12
  290. package/src/theme/index.ts +0 -7
  291. package/src/theme/theme-context.ts +1 -5
  292. package/src/theme/theme-script.ts +0 -14
  293. package/src/theme/use-theme.ts +0 -3
  294. package/src/types/boundaries.ts +0 -35
  295. package/src/types/cache-types.ts +13 -4
  296. package/src/types/error-types.ts +30 -90
  297. package/src/types/global-namespace.ts +54 -41
  298. package/src/types/handler-context.ts +97 -22
  299. package/src/types/index.ts +1 -10
  300. package/src/types/loader-types.ts +6 -3
  301. package/src/types/request-scope.ts +0 -19
  302. package/src/types/route-config.ts +6 -50
  303. package/src/types/route-entry.ts +0 -6
  304. package/src/types/segments.ts +18 -14
  305. package/src/urls/include-helper.ts +9 -56
  306. package/src/urls/index.ts +1 -11
  307. package/src/urls/path-helper-types.ts +19 -5
  308. package/src/urls/path-helper.ts +17 -106
  309. package/src/urls/pattern-types.ts +36 -19
  310. package/src/urls/response-types.ts +20 -19
  311. package/src/urls/type-extraction.ts +58 -139
  312. package/src/urls/urls-function.ts +1 -18
  313. package/src/use-loader.tsx +292 -107
  314. package/src/vite/debug.ts +1 -0
  315. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  316. package/src/vite/discovery/discover-routers.ts +95 -82
  317. package/src/vite/discovery/discovery-errors.ts +194 -0
  318. package/src/vite/discovery/prerender-collection.ts +26 -34
  319. package/src/vite/discovery/route-types-writer.ts +40 -84
  320. package/src/vite/discovery/state.ts +39 -1
  321. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  322. package/src/vite/index.ts +4 -0
  323. package/src/vite/plugin-types.ts +185 -10
  324. package/src/vite/plugins/cjs-to-esm.ts +3 -18
  325. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  326. package/src/vite/plugins/client-ref-hashing.ts +12 -11
  327. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  328. package/src/vite/plugins/expose-action-id.ts +4 -75
  329. package/src/vite/plugins/expose-id-utils.ts +3 -54
  330. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  331. package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
  332. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  333. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  334. package/src/vite/plugins/expose-internal-ids.ts +57 -67
  335. package/src/vite/plugins/performance-tracks.ts +9 -16
  336. package/src/vite/plugins/refresh-cmd.ts +1 -1
  337. package/src/vite/plugins/use-cache-transform.ts +26 -49
  338. package/src/vite/plugins/vercel-output.ts +258 -0
  339. package/src/vite/plugins/version-injector.ts +2 -32
  340. package/src/vite/plugins/version-plugin.ts +32 -23
  341. package/src/vite/plugins/virtual-entries.ts +35 -17
  342. package/src/vite/rango.ts +148 -115
  343. package/src/vite/router-discovery.ts +220 -68
  344. package/src/vite/utils/ast-handler-extract.ts +15 -31
  345. package/src/vite/utils/bundle-analysis.ts +10 -15
  346. package/src/vite/utils/client-chunks.ts +184 -0
  347. package/src/vite/utils/forward-user-plugins.ts +171 -0
  348. package/src/vite/utils/manifest-utils.ts +4 -59
  349. package/src/vite/utils/package-resolution.ts +1 -73
  350. package/src/vite/utils/prerender-utils.ts +0 -34
  351. package/src/vite/utils/shared-utils.ts +95 -43
  352. package/src/browser/action-response-classifier.ts +0 -99
  353. package/src/browser/react/use-client-cache.ts +0 -58
  354. package/src/browser/shallow.ts +0 -40
  355. package/src/handles/index.ts +0 -7
  356. package/src/router/middleware-cookies.ts +0 -55
@@ -21,6 +21,7 @@ import {
21
21
  createClientTemporaryReferenceSet,
22
22
  } from "@vitejs/plugin-rsc/rsc";
23
23
  import { getRequestContext } from "../server/request-context.js";
24
+ import { isUnderTestRunner } from "../runtime-env.js";
24
25
  import {
25
26
  isTainted,
26
27
  CACHED_FN_SYMBOL,
@@ -32,10 +33,21 @@ import {
32
33
  export { isCachedFunction };
33
34
  import { serializeResult, deserializeResult } from "./segment-codec.js";
34
35
  import { createHandleStore } from "../server/handle-store.js";
35
- import { restoreHandles } from "./handle-snapshot.js";
36
+ import {
37
+ restoreHandles,
38
+ encodeHandles,
39
+ decodeHandles,
40
+ } from "./handle-snapshot.js";
36
41
  import { startHandleCapture, type HandleCapture } from "./handle-capture.js";
37
42
  import { sortedSearchString } from "./cache-key-utils.js";
38
43
  import { runBackground } from "./background-task.js";
44
+ import {
45
+ normalizeTags,
46
+ recordRequestTags,
47
+ runWithCacheTagScope,
48
+ } from "./cache-tag.js";
49
+ import { reportCacheError } from "./cache-error.js";
50
+ import type { CacheItemResult } from "./types.js";
39
51
 
40
52
  /**
41
53
  * Convert encodeReply result to a stable string key.
@@ -48,6 +60,10 @@ async function replyToCacheKey(encoded: string | FormData): Promise<string> {
48
60
  return text;
49
61
  }
50
62
 
63
+ // Cached-fn ids already warned about running uncached under a test runner, so
64
+ // the test-ergonomics warning fires once per fn rather than once per call.
65
+ const warnedUncachedUnderTest = new Set<string>();
66
+
51
67
  // ============================================================================
52
68
  // Core: registerCachedFunction
53
69
  // ============================================================================
@@ -70,9 +86,38 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
70
86
  const store = requestCtx?._cacheStore;
71
87
  const resolvedProfileName = profileName || "default";
72
88
 
73
- // Bypass: no store or no getItem support
89
+ // Bypass: no store or no getItem support. Still run inside a tag scope so a
90
+ // cacheTag() call inside the function degrades to a no-op rather than
91
+ // throwing "must be called inside a use cache function" - adopting cacheTag()
92
+ // must not hard-fail in apps/tests without an item-capable cache configured.
93
+ // Note: the INSIDE_CACHE_EXEC guard (cookies()/headers()/ctx.set() rejection)
94
+ // is intentionally NOT stamped here. It is a cached-path-only check; in the
95
+ // bypass the body actually executes, so the guarded side effects take effect
96
+ // and nothing is lost on a (non-existent) hit. Same applies to the
97
+ // non-serializable-args bypass below.
74
98
  if (!store?.getItem) {
75
- return fn.apply(this, args);
99
+ // Test-ergonomics guard: under a test runner, a "use cache" function that
100
+ // executes with no item-capable store seeded is exercising the UNCACHED
101
+ // path — a green test that proves nothing about caching. Warn once per fn
102
+ // id so the author knows to seed a cacheStore. Advisory (never throws), so
103
+ // a test that DELIBERATELY runs uncached is unaffected. Gated on the test
104
+ // runner (process.env.VITEST, not folded) so production never evaluates it.
105
+ if (isUnderTestRunner() && !warnedUncachedUnderTest.has(id)) {
106
+ warnedUncachedUnderTest.add(id);
107
+ console.warn(
108
+ `[rango] "use cache" function "${id}" executed but no cacheStore was ` +
109
+ `seeded; the cached path is NOT under test (it ran uncached). Pass ` +
110
+ `{ cacheStore, cacheProfiles } to runLoader/runMiddleware/renderHandler/` +
111
+ `runInRequestContext (or configure createRouter({ cache }) for dispatch) ` +
112
+ `to exercise it.`,
113
+ );
114
+ }
115
+ const scoped = runWithCacheTagScope(() => fn.apply(this, args));
116
+ const result = await scoped.result;
117
+ // Still record the runtime tags into the request set so a cacheTag() in an
118
+ // uncached function tags the document, even with no item-capable store.
119
+ recordRequestTags(scoped.tags, requestCtx);
120
+ return result;
76
121
  }
77
122
 
78
123
  // Resolve profile strictly from request-scoped config (set by the
@@ -155,40 +200,57 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
155
200
  cacheKey = `use-cache:${id}`;
156
201
  }
157
202
  } catch {
158
- // Non-serializable args: run uncached
159
- return fn.apply(this, args);
203
+ // Non-serializable args: run uncached (within a tag scope so cacheTag()
204
+ // still does not throw). Record runtime tags so the document union still
205
+ // sees them even though this call is not itself cached.
206
+ const scoped = runWithCacheTagScope(() => fn.apply(this, args));
207
+ const result = await scoped.result;
208
+ recordRequestTags(scoped.tags, requestCtx);
209
+ return result;
160
210
  }
161
211
 
162
212
  // Cache lookup
163
213
  const cached = await store.getItem(cacheKey);
164
214
 
215
+ // Serve a cached entry on the hit path: deserialize the stored value,
216
+ // replay handle data (gated on tainted args), and surface the entry's tags
217
+ // to the request set (the function did not re-run, so its runtime cacheTag()
218
+ // tags are only available from the stored entry). Shared by the fresh-hit
219
+ // and stale-hit branches; the only divergence is the stale branch scheduling
220
+ // background revalidation, which it does after this returns.
221
+ const serveCached = async (entry: CacheItemResult): Promise<any> => {
222
+ const result = await deserializeResult(entry.value);
223
+ if (entry.handles && hasTaintedArgs) {
224
+ const handleStore = requestCtx?._handleStore;
225
+ if (handleStore) {
226
+ const r = await decodeHandles(entry.handles);
227
+ if (r) restoreHandles(r, handleStore);
228
+ }
229
+ }
230
+ recordRequestTags(entry.tags, requestCtx);
231
+ return result;
232
+ };
233
+
165
234
  if (cached && !cached.shouldRevalidate) {
166
235
  // Fresh hit: deserialize and return
167
236
  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
237
+ return await serveCached(cached);
238
+ } catch (error) {
239
+ // The stored value is corrupt/partial (failed RSC deserialize). Report
240
+ // it, then fall through to fresh execution - the miss path below re-runs
241
+ // and setItem() overwrites the faulty entry under the same key (self-heal).
242
+ reportCacheError(
243
+ error,
244
+ "cache-corrupt",
245
+ `[use cache] "${id}" fresh-hit`,
246
+ );
179
247
  }
180
248
  }
181
249
 
182
250
  if (cached?.shouldRevalidate) {
183
251
  // Stale hit: return stale value, revalidate in background
184
252
  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
- }
253
+ const result = await serveCached(cached);
192
254
  // Background revalidation — must capture handles if tainted args present.
193
255
  // Use an isolated handle store so background pushes don't pollute the
194
256
  // live response or throw LateHandlePushError on the completed store.
@@ -238,20 +300,41 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
238
300
  }
239
301
 
240
302
  try {
241
- const freshResult = await fn.apply(this, args);
303
+ const scoped = runWithCacheTagScope(() => fn.apply(this, args));
304
+ const freshResult = await scoped.result;
242
305
  bgStopCapture?.();
306
+ // Merge profile/DSL tags with runtime cacheTag() tags, read after
307
+ // awaiting so post-await cacheTag() calls are included. Normalize
308
+ // (drops empty profile tags, matching the invalidate path) + dedupe.
309
+ const freshTags = [
310
+ ...new Set(
311
+ normalizeTags([...(profile.tags ?? []), ...scoped.tags]),
312
+ ),
313
+ ];
314
+ recordRequestTags(freshTags, requestCtx);
243
315
  const serialized = await serializeResult(freshResult);
244
316
  if (serialized !== null) {
317
+ const encodedHandles = bgCapture?.data
318
+ ? await encodeHandles(bgCapture.data)
319
+ : undefined;
245
320
  await store.setItem!(cacheKey, serialized, {
246
- handles: bgCapture?.data,
321
+ handles: encodedHandles,
247
322
  ttl: profile.ttl,
248
323
  swr: profile.swr,
249
- tags: profile.tags,
324
+ tags: freshTags.length > 0 ? freshTags : undefined,
250
325
  });
251
326
  }
252
327
  } catch (bgError) {
253
328
  bgStopCapture?.();
254
- requestCtx?._reportBackgroundError?.(bgError, "stale-revalidation");
329
+ // Pass requestCtx explicitly: this runs in a detached background
330
+ // task where the ALS context is gone, so onError can only fire if
331
+ // we hand it the context captured up front.
332
+ reportCacheError(
333
+ bgError,
334
+ "stale-revalidation",
335
+ "[use cache] background revalidation failed",
336
+ requestCtx,
337
+ );
255
338
  } finally {
256
339
  for (const arg of bgTaintedArgs) {
257
340
  unstampCacheExec(arg as object);
@@ -263,8 +346,14 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
263
346
  }
264
347
  });
265
348
  return result;
266
- } catch {
267
- // Deserialization of stale value failed, fall through
349
+ } catch (error) {
350
+ // Stale value is corrupt/partial; report and fall through to a fresh
351
+ // execution, which overwrites the faulty entry under the same key.
352
+ reportCacheError(
353
+ error,
354
+ "cache-corrupt",
355
+ `[use cache] "${id}" stale-hit`,
356
+ );
268
357
  }
269
358
  }
270
359
 
@@ -297,8 +386,10 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
297
386
  }
298
387
 
299
388
  let result: any;
389
+ let scoped: ReturnType<typeof runWithCacheTagScope>;
300
390
  try {
301
- result = await fn.apply(this, args);
391
+ scoped = runWithCacheTagScope(() => fn.apply(this, args));
392
+ result = await scoped.result;
302
393
  } finally {
303
394
  // Decrement ref count; symbol is deleted when it reaches zero
304
395
  for (const arg of taintedArgs) {
@@ -311,17 +402,28 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
311
402
  stopCapture?.();
312
403
  }
313
404
 
405
+ // Merge profile/DSL tags with runtime cacheTag() tags. Read scoped.tags
406
+ // after awaiting result so post-await cacheTag() calls are included.
407
+ // Normalize (drops empty profile tags, matching the invalidate path) + dedupe.
408
+ const allTags = [
409
+ ...new Set(normalizeTags([...(profile.tags ?? []), ...scoped!.tags])),
410
+ ];
411
+ recordRequestTags(allTags, requestCtx);
412
+
314
413
  // Serialize and store — fully non-blocking when waitUntil is available.
315
414
  // The response does not need to wait for serialization or the store write.
316
415
  const cacheWrite = async () => {
317
416
  try {
318
417
  const serialized = await serializeResult(result);
319
418
  if (serialized !== null) {
419
+ const encodedHandles = capture?.data
420
+ ? await encodeHandles(capture.data)
421
+ : undefined;
320
422
  await store.setItem!(cacheKey, serialized, {
321
- handles: capture?.data,
423
+ handles: encodedHandles,
322
424
  ttl: profile.ttl,
323
425
  swr: profile.swr,
324
- tags: profile.tags,
426
+ tags: allTags.length > 0 ? allTags : undefined,
325
427
  });
326
428
  }
327
429
  } catch (writeError) {
@@ -16,14 +16,31 @@ import {
16
16
  getRequestContext,
17
17
  _getRequestContext,
18
18
  } from "../server/request-context.js";
19
+ import { recordRequestTags } from "./cache-tag.js";
20
+ import { reportCacheError } from "./cache-error.js";
19
21
  import { serializeSegments, deserializeSegments } from "./segment-codec.js";
20
- import { captureHandles, restoreHandles } from "./handle-snapshot.js";
22
+ import {
23
+ captureHandles,
24
+ restoreHandles,
25
+ encodeHandles,
26
+ decodeHandles,
27
+ } from "./handle-snapshot.js";
21
28
  import { sortedSearchString, sortedRouteParams } from "./cache-key-utils.js";
22
29
  import {
23
30
  DEFAULT_ROUTE_TTL,
24
31
  resolveCacheKey,
25
32
  resolveCacheStore,
33
+ resolveTagsOption,
26
34
  } from "./cache-policy.js";
35
+ import type { RequestContext } from "../server/request-context.js";
36
+
37
+ export function resolveCacheTags(
38
+ config: PartialCacheOptions | false,
39
+ ctx: RequestContext | undefined,
40
+ ): string[] | undefined {
41
+ if (config === false) return undefined;
42
+ return resolveTagsOption(config.tags, ctx, "CacheScope");
43
+ }
27
44
 
28
45
  function debugCacheLog(message: string): void {
29
46
  if (INTERNAL_RANGO_DEBUG) {
@@ -31,17 +48,6 @@ function debugCacheLog(message: string): void {
31
48
  }
32
49
  }
33
50
 
34
- // ============================================================================
35
- // Key Generation (internal)
36
- // ============================================================================
37
-
38
- /**
39
- * Generate cache key base from host, pathname, route params, and search params.
40
- * Host is included to prevent cross-host cache collisions on shared stores.
41
- * Route params and search params are sorted alphabetically for deterministic keys.
42
- * Internal _rsc* and __* query params are excluded.
43
- * @internal
44
- */
45
51
  function getCacheKeyBase(
46
52
  host: string,
47
53
  pathname: string,
@@ -57,16 +63,6 @@ function getCacheKeyBase(
57
63
  return key;
58
64
  }
59
65
 
60
- /**
61
- * Generate default cache key for a route request.
62
- * Includes pathname, route params, and user-facing search params for
63
- * correct scoping. Internal _rsc* params are excluded.
64
- * Includes request type prefix since they produce different segment sets:
65
- * - doc: document requests (full page load)
66
- * - partial: navigation requests (client-side navigation)
67
- * - intercept: intercept navigation (modal/overlay routes)
68
- * @internal
69
- */
70
66
  function getDefaultRouteCacheKey(
71
67
  pathname: string,
72
68
  params?: Record<string, string>,
@@ -187,6 +183,32 @@ export class CacheScope {
187
183
  return resolveCacheKey(keyFn, this.getStore(), defaultKey, "CacheScope");
188
184
  }
189
185
 
186
+ /**
187
+ * Evaluate the cache `condition` predicate. Returns false (skip the cache
188
+ * operation) when the predicate returns false or throws; returns true when
189
+ * there is no condition or no request context to evaluate it against.
190
+ */
191
+ private conditionAllows(op: "read" | "write"): boolean {
192
+ if (this.config === false || !this.config.condition) return true;
193
+ const requestCtx = getRequestContext();
194
+ if (!requestCtx) return true;
195
+ try {
196
+ if (!this.config.condition(requestCtx)) {
197
+ debugCacheLog(
198
+ `[CacheScope] condition returned false, skipping cache ${op}`,
199
+ );
200
+ return false;
201
+ }
202
+ return true;
203
+ } catch (error) {
204
+ console.error(
205
+ `[CacheScope] condition function threw, skipping cache ${op}:`,
206
+ error,
207
+ );
208
+ return false;
209
+ }
210
+ }
211
+
190
212
  /**
191
213
  * Lookup cached segments for a route (single cache entry per request).
192
214
  * Returns { segments, shouldRevalidate } or null if cache miss.
@@ -204,27 +226,7 @@ export class CacheScope {
204
226
  shouldRevalidate: boolean;
205
227
  } | null> {
206
228
  if (!this.enabled) return null;
207
-
208
- // Evaluate condition — skip cache read when condition returns false
209
- if (this.config !== false && this.config.condition) {
210
- const requestCtx = getRequestContext();
211
- if (requestCtx) {
212
- try {
213
- if (!this.config.condition(requestCtx)) {
214
- debugCacheLog(
215
- `[CacheScope] condition returned false, skipping cache read`,
216
- );
217
- return null;
218
- }
219
- } catch (error) {
220
- console.error(
221
- `[CacheScope] condition function threw, skipping cache read:`,
222
- error,
223
- );
224
- return null;
225
- }
226
- }
227
- }
229
+ if (!this.conditionAllows("read")) return null;
228
230
 
229
231
  const store = this.getStore();
230
232
  if (!store) return null;
@@ -242,13 +244,43 @@ export class CacheScope {
242
244
 
243
245
  const { data: cached, shouldRevalidate } = result;
244
246
 
245
- // Deserialize segments
246
- const segments = await deserializeSegments(cached.segments);
247
+ // Deserialize segments. A failure means the cached segments are corrupt/
248
+ // partial: evict the entry (self-heal - the re-render re-caches under the
249
+ // same key) and report it as corruption, distinct from a transient infra
250
+ // error (handled by the outer catch).
251
+ let segments: ResolvedSegment[];
252
+ try {
253
+ segments = await deserializeSegments(cached.segments);
254
+ } catch (error) {
255
+ reportCacheError(
256
+ error,
257
+ "cache-corrupt",
258
+ `[CacheScope] ${key}: corrupt cached segments, evicting`,
259
+ );
260
+ await store
261
+ .delete(key)
262
+ .catch((e) =>
263
+ reportCacheError(e, "cache-delete", `[CacheScope] ${key}: evict`),
264
+ );
265
+ return null;
266
+ }
247
267
 
248
- // Replay handle data
268
+ // A hit serves content that was tagged at write time, so the document
269
+ // tag union must include this entry's tags for updateTag()/revalidateTag()
270
+ // to invalidate any full-page entry built on top of it. The write path
271
+ // records via cacheRoute (resolveCacheTags); the hit path records here.
272
+ recordRequestTags(cached.tags);
273
+
274
+ // Replay handle data. An empty string means the route pushed no handles —
275
+ // skip the decode entirely (the common case). Otherwise decode the
276
+ // Flight-encoded blob; a decode failure skips handle restore but keeps the
277
+ // valid cached segments.
249
278
  const handleStore = _getRequestContext()?._handleStore;
250
- if (handleStore) {
251
- restoreHandles(cached.handles, handleStore);
279
+ if (handleStore && cached.handles) {
280
+ const handlesRecord = await decodeHandles(cached.handles);
281
+ if (handlesRecord) {
282
+ restoreHandles(handlesRecord, handleStore);
283
+ }
252
284
  }
253
285
 
254
286
  if (INTERNAL_RANGO_DEBUG) {
@@ -262,7 +294,7 @@ export class CacheScope {
262
294
 
263
295
  return { segments, shouldRevalidate };
264
296
  } catch (error) {
265
- console.error(`[CacheScope] Failed to lookup ${key}:`, error);
297
+ reportCacheError(error, "cache-read", `[CacheScope] lookup ${key}`);
266
298
  return null;
267
299
  }
268
300
  }
@@ -284,27 +316,7 @@ export class CacheScope {
284
316
  isIntercept?: boolean,
285
317
  ): Promise<void> {
286
318
  if (!this.enabled || segments.length === 0) return;
287
-
288
- // Evaluate condition — skip cache write when condition returns false
289
- if (this.config !== false && this.config.condition) {
290
- const conditionCtx = getRequestContext();
291
- if (conditionCtx) {
292
- try {
293
- if (!this.config.condition(conditionCtx)) {
294
- debugCacheLog(
295
- `[CacheScope] condition returned false, skipping cache write`,
296
- );
297
- return;
298
- }
299
- } catch (error) {
300
- console.error(
301
- `[CacheScope] condition function threw, skipping cache write:`,
302
- error,
303
- );
304
- return;
305
- }
306
- }
307
- }
319
+ if (!this.conditionAllows("write")) return;
308
320
 
309
321
  const store = this.getStore();
310
322
  if (!store) return;
@@ -325,6 +337,10 @@ export class CacheScope {
325
337
  // Resolve cache key early (while request context is available)
326
338
  const key = await this.resolveKey(pathname, params, isIntercept);
327
339
 
340
+ // Resolve tags early (while request context is available, before waitUntil)
341
+ const tags = resolveCacheTags(this.config, requestCtx);
342
+ recordRequestTags(tags, requestCtx);
343
+
328
344
  // Check if this is a partial request (navigation) vs document request
329
345
  const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
330
346
 
@@ -381,13 +397,19 @@ export class CacheScope {
381
397
  );
382
398
  }
383
399
 
384
- // Serialize non-loader segments only
385
- const serializedSegments = await serializeSegments(nonLoaderSegments);
400
+ // Serialize segments and Flight-encode handles in parallel. Handles go
401
+ // through the codec (not raw into the entry) so Promise/ReactNode handle
402
+ // values survive a JSON-serializing store — see encodeHandles.
403
+ const [serializedSegments, encodedHandles] = await Promise.all([
404
+ serializeSegments(nonLoaderSegments),
405
+ encodeHandles(handles),
406
+ ]);
386
407
 
387
408
  const data: CachedEntryData = {
388
409
  segments: serializedSegments,
389
- handles,
410
+ handles: encodedHandles,
390
411
  expiresAt: Date.now() + ttl * 1000,
412
+ tags,
391
413
  };
392
414
 
393
415
  if (INTERNAL_RANGO_DEBUG) {
@@ -405,7 +427,11 @@ export class CacheScope {
405
427
  );
406
428
  }
407
429
  } catch (error) {
408
- console.error(`[CacheScope] Failed to cache ${key}:`, error);
430
+ reportCacheError(
431
+ error,
432
+ "cache-write",
433
+ `[CacheScope] Failed to cache ${key}`,
434
+ );
409
435
  }
410
436
  });
411
437
  }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Cache Tag API
3
+ *
4
+ * Provides cacheTag() for tagging cached entries at runtime inside "use cache"
5
+ * functions. Tags are scoped via AsyncLocalStorage; calling cacheTag() outside
6
+ * a "use cache" execution throws.
7
+ *
8
+ * The runtime (cache-runtime.ts) wraps "use cache" execution in
9
+ * runWithCacheTagScope(), collects the runtime tags, and merges them with the
10
+ * profile/DSL tags before storing.
11
+ */
12
+
13
+ import { AsyncLocalStorage } from "node:async_hooks";
14
+ import {
15
+ _getRequestContext,
16
+ type RequestContext,
17
+ } from "../server/request-context.js";
18
+
19
+ const cacheTagStorage = new AsyncLocalStorage<Set<string>>();
20
+
21
+ export function normalizeTag(tag: string): string | null {
22
+ if (!tag || !tag.trim()) return null;
23
+ return tag;
24
+ }
25
+
26
+ export function normalizeTags(tags: Iterable<string>): string[] {
27
+ const out: string[] = [];
28
+ for (const tag of tags) {
29
+ const normalized = normalizeTag(tag);
30
+ if (normalized !== null) out.push(normalized);
31
+ }
32
+ return out;
33
+ }
34
+
35
+ /**
36
+ * Tag the current "use cache" entry for later invalidation via
37
+ * updateTag() / revalidateTag().
38
+ *
39
+ * Must be called inside a function marked with "use cache".
40
+ * Tags are additive - multiple calls accumulate.
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * async function getProduct(ctx) {
45
+ * "use cache";
46
+ * cacheTag(`product:${ctx.params.id}`, "products");
47
+ * return db.getProduct(ctx.params.id);
48
+ * }
49
+ * ```
50
+ */
51
+ export function cacheTag(...tags: string[]): void {
52
+ const store = cacheTagStorage.getStore();
53
+ if (!store) {
54
+ throw new Error('cacheTag() must be called inside a "use cache" function.');
55
+ }
56
+ for (const tag of tags) {
57
+ const normalized = normalizeTag(tag);
58
+ if (normalized === null) {
59
+ if (process.env.NODE_ENV !== "production") {
60
+ console.warn(`[cacheTag] Ignoring empty or whitespace-only tag.`);
61
+ }
62
+ continue;
63
+ }
64
+ store.add(normalized);
65
+ }
66
+ }
67
+
68
+ export function recordRequestTags(
69
+ tags: Iterable<string> | undefined,
70
+ ctx: RequestContext | undefined = _getRequestContext(),
71
+ ): void {
72
+ if (!tags) return;
73
+ const set = ctx?._requestTags;
74
+ if (!set) return;
75
+ for (const tag of tags) {
76
+ const normalized = normalizeTag(tag);
77
+ if (normalized !== null) set.add(normalized);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Run a function within a cache tag scope. Any cacheTag() calls inside `fn`
83
+ * accumulate into the returned Set.
84
+ *
85
+ * The returned Set is the LIVE reference - the caller must await `result`
86
+ * before reading `tags`, because an async cached function may call cacheTag()
87
+ * after an await boundary.
88
+ *
89
+ * @internal Used by cache-runtime.ts to wrap "use cache" execution.
90
+ */
91
+ export function runWithCacheTagScope<T>(fn: () => T): {
92
+ result: T;
93
+ tags: Set<string>;
94
+ } {
95
+ const tagSet = new Set<string>();
96
+ const result = cacheTagStorage.run(tagSet, fn);
97
+ return { result, tags: tagSet };
98
+ }