@rangojs/router 0.0.0-experimental.dfdb0387 → 0.0.0-experimental.e16b7c00

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 (237) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/vite/index.js +2106 -842
  4. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  5. package/package.json +13 -8
  6. package/skills/breadcrumbs/SKILL.md +3 -1
  7. package/skills/bundle-analysis/SKILL.md +159 -0
  8. package/skills/cache-guide/SKILL.md +222 -30
  9. package/skills/caching/SKILL.md +188 -8
  10. package/skills/composability/SKILL.md +27 -2
  11. package/skills/document-cache/SKILL.md +78 -55
  12. package/skills/handler-use/SKILL.md +364 -0
  13. package/skills/hooks/SKILL.md +229 -20
  14. package/skills/host-router/SKILL.md +45 -20
  15. package/skills/i18n/SKILL.md +276 -0
  16. package/skills/intercept/SKILL.md +46 -4
  17. package/skills/layout/SKILL.md +28 -7
  18. package/skills/links/SKILL.md +247 -17
  19. package/skills/loader/SKILL.md +219 -9
  20. package/skills/middleware/SKILL.md +47 -12
  21. package/skills/migrate-nextjs/SKILL.md +582 -0
  22. package/skills/migrate-react-router/SKILL.md +769 -0
  23. package/skills/mime-routes/SKILL.md +27 -0
  24. package/skills/observability/SKILL.md +137 -0
  25. package/skills/parallel/SKILL.md +71 -6
  26. package/skills/prerender/SKILL.md +14 -33
  27. package/skills/rango/SKILL.md +236 -22
  28. package/skills/react-compiler/SKILL.md +168 -0
  29. package/skills/response-routes/SKILL.md +66 -9
  30. package/skills/route/SKILL.md +57 -4
  31. package/skills/router-setup/SKILL.md +3 -3
  32. package/skills/server-actions/SKILL.md +751 -0
  33. package/skills/streams-and-websockets/SKILL.md +283 -0
  34. package/skills/typesafety/SKILL.md +319 -27
  35. package/skills/use-cache/SKILL.md +36 -5
  36. package/skills/view-transitions/SKILL.md +294 -0
  37. package/src/__augment-tests__/augment.ts +81 -0
  38. package/src/__augment-tests__/augmented.check.ts +117 -0
  39. package/src/browser/action-coordinator.ts +53 -36
  40. package/src/browser/app-shell.ts +52 -0
  41. package/src/browser/event-controller.ts +86 -70
  42. package/src/browser/history-state.ts +21 -0
  43. package/src/browser/index.ts +3 -3
  44. package/src/browser/navigation-bridge.ts +86 -11
  45. package/src/browser/navigation-client.ts +45 -25
  46. package/src/browser/navigation-store.ts +32 -9
  47. package/src/browser/navigation-transaction.ts +10 -28
  48. package/src/browser/partial-update.ts +61 -28
  49. package/src/browser/prefetch/cache.ts +124 -26
  50. package/src/browser/prefetch/fetch.ts +129 -37
  51. package/src/browser/prefetch/queue.ts +36 -5
  52. package/src/browser/rango-state.ts +53 -13
  53. package/src/browser/react/Link.tsx +18 -13
  54. package/src/browser/react/NavigationProvider.tsx +72 -31
  55. package/src/browser/react/filter-segment-order.ts +51 -7
  56. package/src/browser/react/index.ts +3 -0
  57. package/src/browser/react/location-state-shared.ts +175 -4
  58. package/src/browser/react/location-state.ts +39 -13
  59. package/src/browser/react/use-handle.ts +17 -9
  60. package/src/browser/react/use-navigation.ts +22 -2
  61. package/src/browser/react/use-params.ts +20 -8
  62. package/src/browser/react/use-reverse.ts +106 -0
  63. package/src/browser/react/use-router.ts +22 -2
  64. package/src/browser/react/use-segments.ts +11 -8
  65. package/src/browser/response-adapter.ts +25 -0
  66. package/src/browser/rsc-router.tsx +64 -22
  67. package/src/browser/scroll-restoration.ts +22 -14
  68. package/src/browser/segment-reconciler.ts +10 -14
  69. package/src/browser/segment-structure-assert.ts +2 -2
  70. package/src/browser/server-action-bridge.ts +23 -30
  71. package/src/browser/types.ts +21 -0
  72. package/src/build/collect-fallback-refs.ts +107 -0
  73. package/src/build/generate-manifest.ts +60 -35
  74. package/src/build/generate-route-types.ts +2 -0
  75. package/src/build/index.ts +2 -0
  76. package/src/build/route-trie.ts +52 -25
  77. package/src/build/route-types/codegen.ts +4 -4
  78. package/src/build/route-types/include-resolution.ts +1 -1
  79. package/src/build/route-types/per-module-writer.ts +7 -4
  80. package/src/build/route-types/router-processing.ts +55 -14
  81. package/src/build/route-types/scan-filter.ts +1 -1
  82. package/src/build/route-types/source-scan.ts +118 -0
  83. package/src/build/runtime-discovery.ts +9 -20
  84. package/src/cache/cache-error.ts +104 -0
  85. package/src/cache/cache-policy.ts +95 -1
  86. package/src/cache/cache-runtime.ts +79 -13
  87. package/src/cache/cache-scope.ts +77 -46
  88. package/src/cache/cache-tag.ts +135 -0
  89. package/src/cache/cf/cf-cache-store.ts +1067 -176
  90. package/src/cache/cf/index.ts +4 -1
  91. package/src/cache/document-cache.ts +59 -7
  92. package/src/cache/index.ts +6 -0
  93. package/src/cache/memory-segment-store.ts +158 -14
  94. package/src/cache/tag-invalidation.ts +206 -0
  95. package/src/cache/types.ts +27 -0
  96. package/src/client.rsc.tsx +3 -0
  97. package/src/client.tsx +92 -182
  98. package/src/context-var.ts +5 -5
  99. package/src/decode-loader-results.ts +36 -0
  100. package/src/errors.ts +30 -1
  101. package/src/handle.ts +4 -6
  102. package/src/host/index.ts +2 -2
  103. package/src/host/router.ts +129 -57
  104. package/src/host/types.ts +31 -2
  105. package/src/host/utils.ts +1 -1
  106. package/src/href-client.ts +140 -20
  107. package/src/index.rsc.ts +16 -4
  108. package/src/index.ts +65 -15
  109. package/src/loader-store.ts +500 -0
  110. package/src/loader.rsc.ts +2 -5
  111. package/src/loader.ts +3 -10
  112. package/src/missing-id-error.ts +68 -0
  113. package/src/outlet-context.ts +1 -1
  114. package/src/prerender.ts +4 -4
  115. package/src/response-utils.ts +37 -0
  116. package/src/reverse.ts +65 -36
  117. package/src/route-content-wrapper.tsx +6 -28
  118. package/src/route-definition/dsl-helpers.ts +384 -257
  119. package/src/route-definition/helper-factories.ts +29 -139
  120. package/src/route-definition/helpers-types.ts +100 -28
  121. package/src/route-definition/resolve-handler-use.ts +6 -0
  122. package/src/route-definition/use-item-types.ts +32 -0
  123. package/src/route-types.ts +26 -41
  124. package/src/router/content-negotiation.ts +15 -2
  125. package/src/router/error-handling.ts +1 -1
  126. package/src/router/handler-context.ts +21 -38
  127. package/src/router/intercept-resolution.ts +4 -18
  128. package/src/router/lazy-includes.ts +8 -8
  129. package/src/router/loader-resolution.ts +19 -2
  130. package/src/router/manifest.ts +22 -13
  131. package/src/router/match-api.ts +4 -3
  132. package/src/router/match-handlers.ts +1 -0
  133. package/src/router/match-middleware/cache-lookup.ts +46 -92
  134. package/src/router/match-middleware/cache-store.ts +3 -2
  135. package/src/router/match-result.ts +53 -32
  136. package/src/router/metrics.ts +1 -1
  137. package/src/router/middleware-types.ts +15 -26
  138. package/src/router/middleware.ts +99 -84
  139. package/src/router/pattern-matching.ts +101 -17
  140. package/src/router/prerender-match.ts +3 -1
  141. package/src/router/preview-match.ts +3 -1
  142. package/src/router/request-classification.ts +4 -28
  143. package/src/router/revalidation.ts +58 -2
  144. package/src/router/router-interfaces.ts +45 -28
  145. package/src/router/router-options.ts +25 -1
  146. package/src/router/router-registry.ts +2 -5
  147. package/src/router/segment-resolution/fresh.ts +27 -6
  148. package/src/router/segment-resolution/loader-cache.ts +8 -17
  149. package/src/router/segment-resolution/revalidation.ts +147 -106
  150. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  151. package/src/router/substitute-pattern-params.ts +56 -0
  152. package/src/router/trie-matching.ts +18 -13
  153. package/src/router/types.ts +8 -0
  154. package/src/router/url-params.ts +49 -0
  155. package/src/router.ts +23 -18
  156. package/src/rsc/handler-context.ts +2 -2
  157. package/src/rsc/handler.ts +38 -70
  158. package/src/rsc/helpers.ts +72 -43
  159. package/src/rsc/index.ts +1 -1
  160. package/src/rsc/origin-guard.ts +28 -10
  161. package/src/rsc/progressive-enhancement.ts +4 -0
  162. package/src/rsc/response-route-handler.ts +54 -54
  163. package/src/rsc/rsc-rendering.ts +35 -51
  164. package/src/rsc/runtime-warnings.ts +9 -10
  165. package/src/rsc/server-action.ts +17 -37
  166. package/src/rsc/ssr-setup.ts +16 -0
  167. package/src/rsc/types.ts +8 -2
  168. package/src/search-params.ts +4 -4
  169. package/src/segment-content-promise.ts +67 -0
  170. package/src/segment-loader-promise.ts +122 -0
  171. package/src/segment-system.tsx +132 -116
  172. package/src/serialize.ts +243 -0
  173. package/src/server/context.ts +143 -53
  174. package/src/server/cookie-store.ts +28 -4
  175. package/src/server/request-context.ts +46 -44
  176. package/src/ssr/index.tsx +5 -1
  177. package/src/static-handler.ts +1 -1
  178. package/src/types/cache-types.ts +13 -4
  179. package/src/types/error-types.ts +5 -1
  180. package/src/types/global-namespace.ts +39 -26
  181. package/src/types/handler-context.ts +68 -50
  182. package/src/types/index.ts +1 -0
  183. package/src/types/loader-types.ts +5 -6
  184. package/src/types/request-scope.ts +126 -0
  185. package/src/types/route-entry.ts +11 -0
  186. package/src/types/segments.ts +35 -2
  187. package/src/urls/include-helper.ts +34 -67
  188. package/src/urls/index.ts +0 -3
  189. package/src/urls/path-helper-types.ts +41 -7
  190. package/src/urls/path-helper.ts +17 -52
  191. package/src/urls/pattern-types.ts +36 -19
  192. package/src/urls/response-types.ts +22 -29
  193. package/src/urls/type-extraction.ts +26 -116
  194. package/src/urls/urls-function.ts +1 -5
  195. package/src/use-loader.tsx +413 -42
  196. package/src/vite/debug.ts +185 -0
  197. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  198. package/src/vite/discovery/discover-routers.ts +101 -51
  199. package/src/vite/discovery/discovery-errors.ts +194 -0
  200. package/src/vite/discovery/gate-state.ts +171 -0
  201. package/src/vite/discovery/prerender-collection.ts +67 -26
  202. package/src/vite/discovery/route-types-writer.ts +40 -84
  203. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  204. package/src/vite/discovery/state.ts +33 -0
  205. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  206. package/src/vite/index.ts +2 -0
  207. package/src/vite/plugin-types.ts +67 -0
  208. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  209. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  210. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  211. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  212. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  213. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  214. package/src/vite/plugins/expose-action-id.ts +54 -30
  215. package/src/vite/plugins/expose-id-utils.ts +12 -8
  216. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  217. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  218. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  219. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  220. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  221. package/src/vite/plugins/performance-tracks.ts +29 -25
  222. package/src/vite/plugins/use-cache-transform.ts +65 -50
  223. package/src/vite/plugins/version-injector.ts +39 -23
  224. package/src/vite/plugins/version-plugin.ts +59 -2
  225. package/src/vite/plugins/virtual-entries.ts +2 -2
  226. package/src/vite/rango.ts +116 -29
  227. package/src/vite/router-discovery.ts +750 -100
  228. package/src/vite/utils/ast-handler-extract.ts +15 -15
  229. package/src/vite/utils/banner.ts +1 -1
  230. package/src/vite/utils/bundle-analysis.ts +4 -2
  231. package/src/vite/utils/client-chunks.ts +190 -0
  232. package/src/vite/utils/forward-user-plugins.ts +193 -0
  233. package/src/vite/utils/manifest-utils.ts +21 -5
  234. package/src/vite/utils/package-resolution.ts +41 -1
  235. package/src/vite/utils/prerender-utils.ts +21 -6
  236. package/src/vite/utils/shared-utils.ts +107 -26
  237. package/src/browser/action-response-classifier.ts +0 -99
@@ -36,6 +36,12 @@ import { restoreHandles } from "./handle-snapshot.js";
36
36
  import { startHandleCapture, type HandleCapture } from "./handle-capture.js";
37
37
  import { sortedSearchString } from "./cache-key-utils.js";
38
38
  import { runBackground } from "./background-task.js";
39
+ import {
40
+ normalizeTags,
41
+ recordRequestTags,
42
+ runWithCacheTagScope,
43
+ } from "./cache-tag.js";
44
+ import { reportCacheError } from "./cache-error.js";
39
45
 
40
46
  /**
41
47
  * Convert encodeReply result to a stable string key.
@@ -70,9 +76,17 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
70
76
  const store = requestCtx?._cacheStore;
71
77
  const resolvedProfileName = profileName || "default";
72
78
 
73
- // Bypass: no store or no getItem support
79
+ // Bypass: no store or no getItem support. Still run inside a tag scope so a
80
+ // cacheTag() call inside the function degrades to a no-op rather than
81
+ // throwing "must be called inside a use cache function" - adopting cacheTag()
82
+ // must not hard-fail in apps/tests without an item-capable cache configured.
74
83
  if (!store?.getItem) {
75
- return fn.apply(this, args);
84
+ const scoped = runWithCacheTagScope(() => fn.apply(this, args));
85
+ const result = await scoped.result;
86
+ // Still record the runtime tags into the request set so a cacheTag() in an
87
+ // uncached function tags the document, even with no item-capable store.
88
+ recordRequestTags(scoped.tags, requestCtx);
89
+ return result;
76
90
  }
77
91
 
78
92
  // Resolve profile strictly from request-scoped config (set by the
@@ -155,8 +169,13 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
155
169
  cacheKey = `use-cache:${id}`;
156
170
  }
157
171
  } catch {
158
- // Non-serializable args: run uncached
159
- return fn.apply(this, args);
172
+ // Non-serializable args: run uncached (within a tag scope so cacheTag()
173
+ // still does not throw). Record runtime tags so the document union still
174
+ // sees them even though this call is not itself cached.
175
+ const scoped = runWithCacheTagScope(() => fn.apply(this, args));
176
+ const result = await scoped.result;
177
+ recordRequestTags(scoped.tags, requestCtx);
178
+ return result;
160
179
  }
161
180
 
162
181
  // Cache lookup
@@ -173,9 +192,20 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
173
192
  restoreHandles(cached.handles, handleStore);
174
193
  }
175
194
  }
195
+ // Surface the hit's tags to the request set so a document built from a
196
+ // cached item is still tagged (the function did not re-run, so its
197
+ // runtime cacheTag() tags are only available from the stored entry).
198
+ recordRequestTags(cached.tags, requestCtx);
176
199
  return result;
177
- } catch {
178
- // Deserialization failed, fall through to fresh execution
200
+ } catch (error) {
201
+ // The stored value is corrupt/partial (failed RSC deserialize). Report
202
+ // it, then fall through to fresh execution - the miss path below re-runs
203
+ // and setItem() overwrites the faulty entry under the same key (self-heal).
204
+ reportCacheError(
205
+ error,
206
+ "cache-corrupt",
207
+ `[use cache] "${id}" fresh-hit`,
208
+ );
179
209
  }
180
210
  }
181
211
 
@@ -189,6 +219,8 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
189
219
  restoreHandles(cached.handles, handleStore);
190
220
  }
191
221
  }
222
+ // Tag the request with the stale entry's tags (see fresh-hit note).
223
+ recordRequestTags(cached.tags, requestCtx);
192
224
  // Background revalidation — must capture handles if tainted args present.
193
225
  // Use an isolated handle store so background pushes don't pollute the
194
226
  // live response or throw LateHandlePushError on the completed store.
@@ -238,20 +270,38 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
238
270
  }
239
271
 
240
272
  try {
241
- const freshResult = await fn.apply(this, args);
273
+ const scoped = runWithCacheTagScope(() => fn.apply(this, args));
274
+ const freshResult = await scoped.result;
242
275
  bgStopCapture?.();
276
+ // Merge profile/DSL tags with runtime cacheTag() tags, read after
277
+ // awaiting so post-await cacheTag() calls are included. Normalize
278
+ // (drops empty profile tags, matching the invalidate path) + dedupe.
279
+ const freshTags = [
280
+ ...new Set(
281
+ normalizeTags([...(profile.tags ?? []), ...scoped.tags]),
282
+ ),
283
+ ];
284
+ recordRequestTags(freshTags, requestCtx);
243
285
  const serialized = await serializeResult(freshResult);
244
286
  if (serialized !== null) {
245
287
  await store.setItem!(cacheKey, serialized, {
246
288
  handles: bgCapture?.data,
247
289
  ttl: profile.ttl,
248
290
  swr: profile.swr,
249
- tags: profile.tags,
291
+ tags: freshTags.length > 0 ? freshTags : undefined,
250
292
  });
251
293
  }
252
294
  } catch (bgError) {
253
295
  bgStopCapture?.();
254
- requestCtx?._reportBackgroundError?.(bgError, "stale-revalidation");
296
+ // Pass requestCtx explicitly: this runs in a detached background
297
+ // task where the ALS context is gone, so onError can only fire if
298
+ // we hand it the context captured up front.
299
+ reportCacheError(
300
+ bgError,
301
+ "stale-revalidation",
302
+ "[use cache] background revalidation failed",
303
+ requestCtx,
304
+ );
255
305
  } finally {
256
306
  for (const arg of bgTaintedArgs) {
257
307
  unstampCacheExec(arg as object);
@@ -263,8 +313,14 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
263
313
  }
264
314
  });
265
315
  return result;
266
- } catch {
267
- // Deserialization of stale value failed, fall through
316
+ } catch (error) {
317
+ // Stale value is corrupt/partial; report and fall through to a fresh
318
+ // execution, which overwrites the faulty entry under the same key.
319
+ reportCacheError(
320
+ error,
321
+ "cache-corrupt",
322
+ `[use cache] "${id}" stale-hit`,
323
+ );
268
324
  }
269
325
  }
270
326
 
@@ -297,8 +353,10 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
297
353
  }
298
354
 
299
355
  let result: any;
356
+ let scoped: ReturnType<typeof runWithCacheTagScope>;
300
357
  try {
301
- result = await fn.apply(this, args);
358
+ scoped = runWithCacheTagScope(() => fn.apply(this, args));
359
+ result = await scoped.result;
302
360
  } finally {
303
361
  // Decrement ref count; symbol is deleted when it reaches zero
304
362
  for (const arg of taintedArgs) {
@@ -311,6 +369,14 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
311
369
  stopCapture?.();
312
370
  }
313
371
 
372
+ // Merge profile/DSL tags with runtime cacheTag() tags. Read scoped.tags
373
+ // after awaiting result so post-await cacheTag() calls are included.
374
+ // Normalize (drops empty profile tags, matching the invalidate path) + dedupe.
375
+ const allTags = [
376
+ ...new Set(normalizeTags([...(profile.tags ?? []), ...scoped!.tags])),
377
+ ];
378
+ recordRequestTags(allTags, requestCtx);
379
+
314
380
  // Serialize and store — fully non-blocking when waitUntil is available.
315
381
  // The response does not need to wait for serialization or the store write.
316
382
  const cacheWrite = async () => {
@@ -321,7 +387,7 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
321
387
  handles: capture?.data,
322
388
  ttl: profile.ttl,
323
389
  swr: profile.swr,
324
- tags: profile.tags,
390
+ tags: allTags.length > 0 ? allTags : undefined,
325
391
  });
326
392
  }
327
393
  } catch (writeError) {
@@ -16,6 +16,8 @@ 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
22
  import { captureHandles, restoreHandles } from "./handle-snapshot.js";
21
23
  import { sortedSearchString, sortedRouteParams } from "./cache-key-utils.js";
@@ -23,7 +25,23 @@ import {
23
25
  DEFAULT_ROUTE_TTL,
24
26
  resolveCacheKey,
25
27
  resolveCacheStore,
28
+ resolveTagsOption,
26
29
  } from "./cache-policy.js";
30
+ import type { RequestContext } from "../server/request-context.js";
31
+
32
+ /**
33
+ * Resolve tags for a cache() boundary from its config (static array or
34
+ * function of ctx). Thin wrapper over the shared resolveTagsOption so the
35
+ * cache() DSL and loader caching resolve tags identically.
36
+ * @internal
37
+ */
38
+ export function resolveCacheTags(
39
+ config: PartialCacheOptions | false,
40
+ ctx: RequestContext | undefined,
41
+ ): string[] | undefined {
42
+ if (config === false) return undefined;
43
+ return resolveTagsOption(config.tags, ctx, "CacheScope");
44
+ }
27
45
 
28
46
  function debugCacheLog(message: string): void {
29
47
  if (INTERNAL_RANGO_DEBUG) {
@@ -187,6 +205,32 @@ export class CacheScope {
187
205
  return resolveCacheKey(keyFn, this.getStore(), defaultKey, "CacheScope");
188
206
  }
189
207
 
208
+ /**
209
+ * Evaluate the cache `condition` predicate. Returns false (skip the cache
210
+ * operation) when the predicate returns false or throws; returns true when
211
+ * there is no condition or no request context to evaluate it against.
212
+ */
213
+ private conditionAllows(op: "read" | "write"): boolean {
214
+ if (this.config === false || !this.config.condition) return true;
215
+ const requestCtx = getRequestContext();
216
+ if (!requestCtx) return true;
217
+ try {
218
+ if (!this.config.condition(requestCtx)) {
219
+ debugCacheLog(
220
+ `[CacheScope] condition returned false, skipping cache ${op}`,
221
+ );
222
+ return false;
223
+ }
224
+ return true;
225
+ } catch (error) {
226
+ console.error(
227
+ `[CacheScope] condition function threw, skipping cache ${op}:`,
228
+ error,
229
+ );
230
+ return false;
231
+ }
232
+ }
233
+
190
234
  /**
191
235
  * Lookup cached segments for a route (single cache entry per request).
192
236
  * Returns { segments, shouldRevalidate } or null if cache miss.
@@ -204,27 +248,7 @@ export class CacheScope {
204
248
  shouldRevalidate: boolean;
205
249
  } | null> {
206
250
  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
- }
251
+ if (!this.conditionAllows("read")) return null;
228
252
 
229
253
  const store = this.getStore();
230
254
  if (!store) return null;
@@ -242,8 +266,26 @@ export class CacheScope {
242
266
 
243
267
  const { data: cached, shouldRevalidate } = result;
244
268
 
245
- // Deserialize segments
246
- const segments = await deserializeSegments(cached.segments);
269
+ // Deserialize segments. A failure means the cached segments are corrupt/
270
+ // partial: evict the entry (self-heal - the re-render re-caches under the
271
+ // same key) and report it as corruption, distinct from a transient infra
272
+ // error (handled by the outer catch).
273
+ let segments: ResolvedSegment[];
274
+ try {
275
+ segments = await deserializeSegments(cached.segments);
276
+ } catch (error) {
277
+ reportCacheError(
278
+ error,
279
+ "cache-corrupt",
280
+ `[CacheScope] ${key}: corrupt cached segments, evicting`,
281
+ );
282
+ await store
283
+ .delete(key)
284
+ .catch((e) =>
285
+ reportCacheError(e, "cache-delete", `[CacheScope] ${key}: evict`),
286
+ );
287
+ return null;
288
+ }
247
289
 
248
290
  // Replay handle data
249
291
  const handleStore = _getRequestContext()?._handleStore;
@@ -262,7 +304,7 @@ export class CacheScope {
262
304
 
263
305
  return { segments, shouldRevalidate };
264
306
  } catch (error) {
265
- console.error(`[CacheScope] Failed to lookup ${key}:`, error);
307
+ reportCacheError(error, "cache-read", `[CacheScope] lookup ${key}`);
266
308
  return null;
267
309
  }
268
310
  }
@@ -284,27 +326,7 @@ export class CacheScope {
284
326
  isIntercept?: boolean,
285
327
  ): Promise<void> {
286
328
  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
- }
329
+ if (!this.conditionAllows("write")) return;
308
330
 
309
331
  const store = this.getStore();
310
332
  if (!store) return;
@@ -325,6 +347,10 @@ export class CacheScope {
325
347
  // Resolve cache key early (while request context is available)
326
348
  const key = await this.resolveKey(pathname, params, isIntercept);
327
349
 
350
+ // Resolve tags early (while request context is available, before waitUntil)
351
+ const tags = resolveCacheTags(this.config, requestCtx);
352
+ recordRequestTags(tags, requestCtx);
353
+
328
354
  // Check if this is a partial request (navigation) vs document request
329
355
  const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
330
356
 
@@ -388,6 +414,7 @@ export class CacheScope {
388
414
  segments: serializedSegments,
389
415
  handles,
390
416
  expiresAt: Date.now() + ttl * 1000,
417
+ tags,
391
418
  };
392
419
 
393
420
  if (INTERNAL_RANGO_DEBUG) {
@@ -405,7 +432,11 @@ export class CacheScope {
405
432
  );
406
433
  }
407
434
  } catch (error) {
408
- console.error(`[CacheScope] Failed to cache ${key}:`, error);
435
+ reportCacheError(
436
+ error,
437
+ "cache-write",
438
+ `[CacheScope] Failed to cache ${key}`,
439
+ );
409
440
  }
410
441
  });
411
442
  }
@@ -0,0 +1,135 @@
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
+ /**
22
+ * Normalize a tag for storage.
23
+ *
24
+ * Returns the tag unchanged if usable, or null if it is empty/whitespace-only
25
+ * (dropped consistently in every environment - an empty tag matches nothing).
26
+ *
27
+ * Backend-specific constraints are intentionally NOT enforced here so the tag
28
+ * primitive stays backend-agnostic. In particular, the CFCacheStore
29
+ * encodeURIComponent's tags at serialization time so commas/spaces/non-Latin1
30
+ * characters cannot corrupt the comma-delimited Cloudflare Cache-Tag header or
31
+ * the HTTP marker header (it does not reject them). Keep tags short and
32
+ * low-cardinality: a tag's KV marker key must stay under Cloudflare's 512-byte
33
+ * limit, and a Cache-Tag value under 1024 bytes. The in-memory store has no
34
+ * such limitations.
35
+ *
36
+ * @internal
37
+ */
38
+ export function normalizeTag(tag: string): string | null {
39
+ if (!tag || !tag.trim()) return null;
40
+ return tag;
41
+ }
42
+
43
+ /**
44
+ * Normalize a tag collection: drop empty/whitespace-only tags so the WRITE path
45
+ * matches the invalidate path (updateTag/revalidateTag/cacheTag all normalize).
46
+ * Does not deduplicate - callers that need that wrap with a Set.
47
+ *
48
+ * @internal
49
+ */
50
+ export function normalizeTags(tags: Iterable<string>): string[] {
51
+ const out: string[] = [];
52
+ for (const tag of tags) {
53
+ const normalized = normalizeTag(tag);
54
+ if (normalized !== null) out.push(normalized);
55
+ }
56
+ return out;
57
+ }
58
+
59
+ /**
60
+ * Tag the current "use cache" entry for later invalidation via
61
+ * updateTag() / revalidateTag().
62
+ *
63
+ * Must be called inside a function marked with "use cache".
64
+ * Tags are additive - multiple calls accumulate.
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * async function getProduct(ctx) {
69
+ * "use cache";
70
+ * cacheTag(`product:${ctx.params.id}`, "products");
71
+ * return db.getProduct(ctx.params.id);
72
+ * }
73
+ * ```
74
+ */
75
+ export function cacheTag(...tags: string[]): void {
76
+ const store = cacheTagStorage.getStore();
77
+ if (!store) {
78
+ throw new Error('cacheTag() must be called inside a "use cache" function.');
79
+ }
80
+ for (const tag of tags) {
81
+ const normalized = normalizeTag(tag);
82
+ if (normalized === null) {
83
+ if (process.env.NODE_ENV !== "production") {
84
+ console.warn(`[cacheTag] Ignoring empty or whitespace-only tag.`);
85
+ }
86
+ continue;
87
+ }
88
+ store.add(normalized);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Record `tags` into the request-scoped tag set (ctx._requestTags), the union of
94
+ * every cache tag resolved while producing the response. The document cache reads
95
+ * this after the render settles so a full-page entry is tagged with everything its
96
+ * content used, making it invalidatable by updateTag()/revalidateTag().
97
+ *
98
+ * Called at the tag-resolution sites: "use cache" stores (cache-runtime, both the
99
+ * miss and read/hit paths), loader cache (cache-policy/loader-cache), and segment
100
+ * cache() (cache-scope). Writes the field directly (not via ctx.set()) so it does
101
+ * not trip the cache-scope side-effect guard, mirroring cacheTag() itself.
102
+ *
103
+ * @internal
104
+ */
105
+ export function recordRequestTags(
106
+ tags: Iterable<string> | undefined,
107
+ ctx: RequestContext | undefined = _getRequestContext(),
108
+ ): void {
109
+ if (!tags) return;
110
+ const set = ctx?._requestTags;
111
+ if (!set) return;
112
+ for (const tag of tags) {
113
+ const normalized = normalizeTag(tag);
114
+ if (normalized !== null) set.add(normalized);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Run a function within a cache tag scope. Any cacheTag() calls inside `fn`
120
+ * accumulate into the returned Set.
121
+ *
122
+ * The returned Set is the LIVE reference - the caller must await `result`
123
+ * before reading `tags`, because an async cached function may call cacheTag()
124
+ * after an await boundary.
125
+ *
126
+ * @internal Used by cache-runtime.ts to wrap "use cache" execution.
127
+ */
128
+ export function runWithCacheTagScope<T>(fn: () => T): {
129
+ result: T;
130
+ tags: Set<string>;
131
+ } {
132
+ const tagSet = new Set<string>();
133
+ const result = cacheTagStorage.run(tagSet, fn);
134
+ return { result, tags: tagSet };
135
+ }