@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2

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 (222) hide show
  1. package/AGENTS.md +6 -10
  2. package/README.md +289 -938
  3. package/dist/bin/rango.js +271 -46
  4. package/dist/vite/index.js +673 -193
  5. package/package.json +10 -8
  6. package/skills/api-client/SKILL.md +1 -1
  7. package/skills/breadcrumbs/SKILL.md +31 -14
  8. package/skills/cache-guide/SKILL.md +5 -2
  9. package/skills/caching/SKILL.md +59 -4
  10. package/skills/catalog.json +271 -0
  11. package/skills/comparison/SKILL.md +50 -0
  12. package/skills/comparison/agents/openai.yaml +4 -0
  13. package/skills/comparison/references/framework-comparison.md +837 -0
  14. package/skills/composability/SKILL.md +83 -2
  15. package/skills/debug-manifest/SKILL.md +1 -1
  16. package/skills/defer-hydration/SKILL.md +235 -0
  17. package/skills/document-cache/SKILL.md +9 -1
  18. package/skills/fonts/SKILL.md +1 -1
  19. package/skills/handler-use/SKILL.md +8 -8
  20. package/skills/hooks/SKILL.md +54 -892
  21. package/skills/hooks/data.md +273 -0
  22. package/skills/hooks/handle-and-actions.md +103 -0
  23. package/skills/hooks/navigation.md +110 -0
  24. package/skills/hooks/outlets.md +41 -0
  25. package/skills/hooks/state.md +228 -0
  26. package/skills/hooks/urls.md +135 -0
  27. package/skills/host-router/SKILL.md +4 -4
  28. package/skills/i18n/SKILL.md +1 -1
  29. package/skills/intercept/SKILL.md +46 -14
  30. package/skills/layout/SKILL.md +27 -10
  31. package/skills/links/SKILL.md +1 -1
  32. package/skills/loader/SKILL.md +23 -1
  33. package/skills/middleware/SKILL.md +7 -3
  34. package/skills/migrate-nextjs/SKILL.md +167 -6
  35. package/skills/migrate-react-router/SKILL.md +59 -677
  36. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  37. package/skills/migrate-react-router/component-migration.md +196 -0
  38. package/skills/migrate-react-router/data-and-actions.md +225 -0
  39. package/skills/migrate-react-router/route-mapping.md +271 -0
  40. package/skills/mime-routes/SKILL.md +1 -1
  41. package/skills/observability/SKILL.md +9 -1
  42. package/skills/parallel/SKILL.md +23 -4
  43. package/skills/ppr/SKILL.md +622 -0
  44. package/skills/prerender/SKILL.md +28 -18
  45. package/skills/rango/SKILL.md +84 -25
  46. package/skills/response-routes/SKILL.md +15 -1
  47. package/skills/route/SKILL.md +71 -4
  48. package/skills/router-setup/SKILL.md +14 -3
  49. package/skills/scripts/SKILL.md +1 -1
  50. package/skills/server-actions/SKILL.md +3 -2
  51. package/skills/shell-manifest/SKILL.md +185 -0
  52. package/skills/streams-and-websockets/SKILL.md +1 -1
  53. package/skills/tailwind/SKILL.md +1 -1
  54. package/skills/testing/SKILL.md +2 -1
  55. package/skills/testing/handles.md +4 -2
  56. package/skills/testing/render-handler.md +15 -14
  57. package/skills/testing/reverse-and-types.md +8 -7
  58. package/skills/theme/SKILL.md +1 -1
  59. package/skills/typesafety/SKILL.md +45 -919
  60. package/skills/typesafety/env-and-bindings.md +254 -0
  61. package/skills/typesafety/generated-files-and-cli.md +335 -0
  62. package/skills/typesafety/params-and-search.md +153 -0
  63. package/skills/typesafety/route-types.md +209 -0
  64. package/skills/use-cache/SKILL.md +30 -3
  65. package/skills/vercel/SKILL.md +1 -1
  66. package/skills/view-transitions/SKILL.md +44 -1
  67. package/src/browser/event-controller.ts +62 -10
  68. package/src/browser/logging.ts +28 -0
  69. package/src/browser/merge-segment-loaders.ts +6 -4
  70. package/src/browser/navigation-bridge.ts +65 -16
  71. package/src/browser/navigation-client.ts +32 -2
  72. package/src/browser/navigation-store.ts +128 -14
  73. package/src/browser/network-error-handler.ts +34 -7
  74. package/src/browser/partial-update.ts +76 -17
  75. package/src/browser/prefetch/cache.ts +51 -11
  76. package/src/browser/prefetch/fetch.ts +59 -21
  77. package/src/browser/prefetch/queue.ts +19 -4
  78. package/src/browser/react/Link.tsx +13 -3
  79. package/src/browser/react/NavigationProvider.tsx +108 -4
  80. package/src/browser/response-adapter.ts +38 -9
  81. package/src/browser/rsc-router.tsx +54 -4
  82. package/src/browser/scroll-restoration.ts +7 -5
  83. package/src/browser/segment-reconciler.ts +31 -21
  84. package/src/browser/server-action-bridge.ts +22 -10
  85. package/src/browser/types.ts +54 -1
  86. package/src/build/generate-manifest.ts +155 -131
  87. package/src/build/index.ts +3 -1
  88. package/src/build/route-trie.ts +35 -7
  89. package/src/build/route-types/include-resolution.ts +347 -47
  90. package/src/build/runtime-discovery.ts +4 -1
  91. package/src/cache/cache-key-utils.ts +29 -0
  92. package/src/cache/cache-runtime.ts +262 -71
  93. package/src/cache/cache-scope.ts +2 -17
  94. package/src/cache/cache-tag.ts +60 -14
  95. package/src/cache/cf/cf-cache-store.ts +243 -20
  96. package/src/cache/document-cache.ts +54 -21
  97. package/src/cache/index.ts +1 -0
  98. package/src/cache/memory-segment-store.ts +110 -3
  99. package/src/cache/profile-registry.ts +15 -0
  100. package/src/cache/read-through-swr.ts +15 -1
  101. package/src/cache/segment-codec.ts +4 -4
  102. package/src/cache/shell-snapshot.ts +417 -0
  103. package/src/cache/types.ts +158 -0
  104. package/src/cache/vercel/vercel-cache-store.ts +401 -124
  105. package/src/client.rsc.tsx +0 -3
  106. package/src/client.tsx +0 -3
  107. package/src/cloudflare/tracing.ts +7 -8
  108. package/src/defer.ts +11 -22
  109. package/src/handle.ts +37 -15
  110. package/src/handles/MetaTags.tsx +16 -82
  111. package/src/handles/breadcrumbs.ts +12 -14
  112. package/src/handles/deferred-resolution.ts +127 -0
  113. package/src/handles/is-thenable.ts +7 -8
  114. package/src/handles/meta.ts +7 -44
  115. package/src/host/errors.ts +15 -0
  116. package/src/host/index.ts +1 -0
  117. package/src/index.rsc.ts +8 -2
  118. package/src/index.ts +19 -13
  119. package/src/internal-debug.ts +11 -8
  120. package/src/prerender.ts +17 -4
  121. package/src/redirect-origin.ts +14 -0
  122. package/src/render-error-thrower.tsx +20 -0
  123. package/src/route-content-wrapper.tsx +12 -5
  124. package/src/route-definition/dsl-helpers.ts +21 -32
  125. package/src/route-definition/helper-factories.ts +0 -2
  126. package/src/route-definition/helpers-types.ts +43 -43
  127. package/src/route-definition/index.ts +1 -2
  128. package/src/route-definition/resolve-handler-use.ts +0 -1
  129. package/src/route-definition/use-item-types.ts +3 -6
  130. package/src/route-map-builder.ts +41 -4
  131. package/src/route-types.ts +0 -5
  132. package/src/router/find-match.ts +86 -8
  133. package/src/router/instrument.ts +9 -4
  134. package/src/router/lazy-includes.ts +72 -12
  135. package/src/router/loader-resolution.ts +14 -2
  136. package/src/router/manifest.ts +56 -11
  137. package/src/router/match-api.ts +76 -32
  138. package/src/router/match-handlers.ts +181 -135
  139. package/src/router/match-middleware/background-revalidation.ts +40 -23
  140. package/src/router/match-middleware/cache-store.ts +39 -24
  141. package/src/router/match-result.ts +35 -15
  142. package/src/router/middleware.ts +64 -38
  143. package/src/router/navigation-snapshot.ts +7 -5
  144. package/src/router/parse-pattern.ts +115 -0
  145. package/src/router/pattern-matching.ts +53 -64
  146. package/src/router/prefetch-limits.ts +37 -0
  147. package/src/router/prerender-match.ts +11 -5
  148. package/src/router/preview-match.ts +3 -1
  149. package/src/router/request-classification.ts +23 -8
  150. package/src/router/route-snapshot.ts +14 -2
  151. package/src/router/router-context.ts +3 -1
  152. package/src/router/router-interfaces.ts +32 -1
  153. package/src/router/router-options.ts +30 -0
  154. package/src/router/segment-resolution/fresh.ts +39 -3
  155. package/src/router/segment-resolution/loader-cache.ts +93 -2
  156. package/src/router/segment-resolution/loader-mask.ts +60 -0
  157. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  158. package/src/router/segment-resolution/mask-nested.ts +83 -0
  159. package/src/router/segment-resolution/revalidation.ts +3 -0
  160. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  161. package/src/router/substitute-pattern-params.ts +54 -35
  162. package/src/router/telemetry-otel.ts +6 -8
  163. package/src/router/telemetry.ts +9 -1
  164. package/src/router/tracing.ts +14 -5
  165. package/src/router/trie-matching.ts +19 -11
  166. package/src/router/url-params.ts +13 -0
  167. package/src/router.ts +47 -16
  168. package/src/rsc/full-payload.ts +70 -0
  169. package/src/rsc/handler.ts +60 -33
  170. package/src/rsc/manifest-init.ts +1 -1
  171. package/src/rsc/nonce.ts +10 -1
  172. package/src/rsc/progressive-enhancement.ts +61 -4
  173. package/src/rsc/redirect-guard.ts +2 -1
  174. package/src/rsc/rsc-rendering.ts +429 -37
  175. package/src/rsc/server-action.ts +25 -2
  176. package/src/rsc/shell-capture.ts +1190 -0
  177. package/src/rsc/shell-serve.ts +181 -0
  178. package/src/rsc/transition-gate.ts +89 -0
  179. package/src/rsc/types.ts +30 -0
  180. package/src/segment-loader-promise.ts +18 -0
  181. package/src/segment-system.tsx +149 -14
  182. package/src/server/context.ts +67 -9
  183. package/src/server/cookie-store.ts +73 -1
  184. package/src/server/loader-registry.ts +13 -1
  185. package/src/server/request-context.ts +169 -10
  186. package/src/ssr/index.tsx +462 -178
  187. package/src/ssr/inject-rsc-eager.ts +167 -0
  188. package/src/ssr/ssr-root.tsx +228 -0
  189. package/src/testing/collect-handle.ts +14 -8
  190. package/src/testing/dispatch.ts +152 -40
  191. package/src/testing/generated-routes.ts +27 -11
  192. package/src/testing/index.ts +6 -0
  193. package/src/testing/render-handler.ts +14 -0
  194. package/src/testing/render-route.tsx +13 -10
  195. package/src/testing/run-transition-when.ts +164 -0
  196. package/src/theme/ThemeProvider.tsx +36 -26
  197. package/src/types/handler-context.ts +1 -1
  198. package/src/types/index.ts +2 -0
  199. package/src/types/route-config.ts +19 -7
  200. package/src/types/segments.ts +100 -0
  201. package/src/urls/include-helper.ts +10 -8
  202. package/src/urls/include-provider.ts +71 -0
  203. package/src/urls/index.ts +1 -0
  204. package/src/urls/path-helper-types.ts +44 -12
  205. package/src/urls/path-helper.ts +5 -0
  206. package/src/urls/pattern-types.ts +36 -0
  207. package/src/urls/type-extraction.ts +43 -18
  208. package/src/urls/urls-function.ts +0 -1
  209. package/src/vercel/tracing.ts +7 -7
  210. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  211. package/src/vite/discovery/discover-routers.ts +1 -1
  212. package/src/vite/discovery/discovery-errors.ts +61 -0
  213. package/src/vite/index.ts +7 -0
  214. package/src/vite/inject-client-debug.ts +88 -0
  215. package/src/vite/plugins/vercel-output.ts +114 -25
  216. package/src/vite/plugins/version-injector.ts +22 -7
  217. package/src/vite/plugins/virtual-entries.ts +80 -22
  218. package/src/vite/rango.ts +29 -19
  219. package/src/vite/router-discovery.ts +171 -43
  220. package/src/vite/utils/prerender-utils.ts +17 -4
  221. package/src/vite/utils/shared-utils.ts +47 -0
  222. package/src/network-error-thrower.tsx +0 -18
@@ -12,6 +12,7 @@ import type {
12
12
  CacheGetResult,
13
13
  CacheItemResult,
14
14
  CacheItemOptions,
15
+ ShellCacheEntry,
15
16
  } from "./types.js";
16
17
  import type { RequestContext } from "../server/request-context.js";
17
18
  import { isPerClientSignalHeader } from "../browser/cookie-name.js";
@@ -26,6 +27,7 @@ import { reportCacheError } from "./cache-error.js";
26
27
  const CACHE_REGISTRY_KEY = "__rsc_router_segment_cache_registry__";
27
28
  const RESPONSE_CACHE_REGISTRY_KEY = "__rsc_router_response_cache_registry__";
28
29
  const ITEM_CACHE_REGISTRY_KEY = "__rsc_router_item_cache_registry__";
30
+ const SHELL_CACHE_REGISTRY_KEY = "__rsc_router_shell_cache_registry__";
29
31
  const TAG_INDEX_REGISTRY_KEY = "__rsc_router_tag_index_registry__";
30
32
  const KEY_TAGS_REGISTRY_KEY = "__rsc_router_key_tags_registry__";
31
33
 
@@ -65,6 +67,13 @@ interface CachedItemEntry {
65
67
  tags?: string[];
66
68
  }
67
69
 
70
+ interface CachedShellEntry {
71
+ entry: ShellCacheEntry;
72
+ expiresAt: number;
73
+ staleAt: number;
74
+ tags?: string[];
75
+ }
76
+
68
77
  /**
69
78
  * Options for MemorySegmentCacheStore
70
79
  */
@@ -121,8 +130,25 @@ export interface MemorySegmentCacheStoreOptions<TEnv = unknown> {
121
130
  ctx: RequestContext<TEnv>,
122
131
  defaultKey: string,
123
132
  ) => string | Promise<string>;
133
+
134
+ /**
135
+ * Maximum number of entries kept per internal family (segment, response,
136
+ * item, shell). On insert, once a family reaches the cap its oldest entry is
137
+ * evicted FIFO (insertion order) and its tag-index entries are cleaned up, so
138
+ * a long-lived instance can't grow without bound. Lazy TTL expiry still runs
139
+ * independently. Defaults to DEFAULT_MAX_ENTRIES (1000).
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * const store = new MemorySegmentCacheStore({ maxEntries: 500 });
144
+ * ```
145
+ */
146
+ maxEntries?: number;
124
147
  }
125
148
 
149
+ /** Default per-family entry cap for MemorySegmentCacheStore (FIFO eviction). */
150
+ const DEFAULT_MAX_ENTRIES = 1000;
151
+
126
152
  /**
127
153
  * In-memory segment cache store.
128
154
  *
@@ -130,10 +156,16 @@ export interface MemorySegmentCacheStoreOptions<TEnv = unknown> {
130
156
  * For production with multiple instances, use a distributed store
131
157
  * like Cloudflare KV or Redis.
132
158
  *
159
+ * Each family is size-capped at `maxEntries` (default 1000) with FIFO eviction
160
+ * on insert, so a long-lived instance can't grow unbounded; TTL expiry stays
161
+ * lazy on top of that.
162
+ *
133
163
  * Tag-index cleanup is lazy, mirroring the data maps: a tagged entry that
134
164
  * expires but is never re-read or invalidated leaves its forward+reverse index
135
- * entries resident until the key is reused or invalidated. This is bounded by
136
- * the distinct-tag count and acceptable for a dev/single-instance store.
165
+ * entries resident until the key is reused, invalidated, or FIFO-evicted. This
166
+ * is bounded by the distinct-tag count and acceptable for a dev/single-instance
167
+ * store. FIFO eviction unregisters the evicted key's tags, so the tag maps do
168
+ * not outlive the capped data maps.
137
169
  *
138
170
  * @example
139
171
  * ```typescript
@@ -157,7 +189,8 @@ export class MemorySegmentCacheStore<
157
189
  private cache: Map<string, CachedEntryData>;
158
190
  private responseCache: Map<string, CachedResponseEntry>;
159
191
  private itemCache: Map<string, CachedItemEntry>;
160
- /** tag -> set of prefixed cache keys (seg:key, res:key, item:key) */
192
+ private shellCache: Map<string, CachedShellEntry>;
193
+ /** tag -> set of prefixed cache keys (seg:key, res:key, item:key, shell:key) */
161
194
  private tagIndex: Map<string, Set<string>>;
162
195
  /** prefixed cache key -> set of tags (reverse index for O(tags) unregister) */
163
196
  private keyTags: Map<string, Set<string>>;
@@ -166,6 +199,8 @@ export class MemorySegmentCacheStore<
166
199
  ctx: RequestContext<TEnv>,
167
200
  defaultKey: string,
168
201
  ) => string | Promise<string>;
202
+ /** Per-family FIFO entry cap. */
203
+ readonly maxEntries: number;
169
204
 
170
205
  constructor(options?: MemorySegmentCacheStoreOptions<TEnv>) {
171
206
  if (options?.name != null) {
@@ -181,6 +216,10 @@ export class MemorySegmentCacheStore<
181
216
  ITEM_CACHE_REGISTRY_KEY,
182
217
  options.name,
183
218
  );
219
+ this.shellCache = getNamedMap<CachedShellEntry>(
220
+ SHELL_CACHE_REGISTRY_KEY,
221
+ options.name,
222
+ );
184
223
  this.tagIndex = getNamedMap<Set<string>>(
185
224
  TAG_INDEX_REGISTRY_KEY,
186
225
  options.name,
@@ -193,11 +232,34 @@ export class MemorySegmentCacheStore<
193
232
  this.cache = new Map<string, CachedEntryData>();
194
233
  this.responseCache = new Map<string, CachedResponseEntry>();
195
234
  this.itemCache = new Map<string, CachedItemEntry>();
235
+ this.shellCache = new Map<string, CachedShellEntry>();
196
236
  this.tagIndex = new Map<string, Set<string>>();
197
237
  this.keyTags = new Map<string, Set<string>>();
198
238
  }
199
239
  this.defaults = options?.defaults;
200
240
  this.keyGenerator = options?.keyGenerator;
241
+ this.maxEntries = options?.maxEntries ?? DEFAULT_MAX_ENTRIES;
242
+ }
243
+
244
+ /**
245
+ * FIFO cap enforcement, called before inserting `incomingKey` into `map`.
246
+ * When the key is new and the family is at capacity, the oldest entries are
247
+ * evicted in insertion order and their tag-index entries unregistered (so the
248
+ * tag maps don't outlive the capped data map). Overwriting an existing key
249
+ * grows nothing, so it is a no-op.
250
+ */
251
+ private evictIfNeeded<V>(
252
+ map: Map<string, V>,
253
+ incomingKey: string,
254
+ prefix: string,
255
+ ): void {
256
+ if (map.has(incomingKey)) return;
257
+ while (map.size >= this.maxEntries) {
258
+ const oldest = map.keys().next().value;
259
+ if (oldest === undefined) break;
260
+ map.delete(oldest);
261
+ this.unregisterTags(`${prefix}:${oldest}`);
262
+ }
201
263
  }
202
264
 
203
265
  async get(key: string): Promise<CacheGetResult | null> {
@@ -230,6 +292,7 @@ export class MemorySegmentCacheStore<
230
292
  };
231
293
  const prefixedKey = `seg:${key}`;
232
294
  this.unregisterTags(prefixedKey);
295
+ this.evictIfNeeded(this.cache, key, "seg");
233
296
  this.cache.set(key, entry);
234
297
  if (data.tags && data.tags.length > 0) {
235
298
  this.registerTags(data.tags, prefixedKey);
@@ -245,6 +308,7 @@ export class MemorySegmentCacheStore<
245
308
  this.cache.clear();
246
309
  this.responseCache.clear();
247
310
  this.itemCache.clear();
311
+ this.shellCache.clear();
248
312
  this.tagIndex.clear();
249
313
  this.keyTags.clear();
250
314
  }
@@ -292,6 +356,7 @@ export class MemorySegmentCacheStore<
292
356
 
293
357
  const prefixedKey = `res:${key}`;
294
358
  this.unregisterTags(prefixedKey);
359
+ this.evictIfNeeded(this.responseCache, key, "res");
295
360
  this.responseCache.set(key, {
296
361
  body,
297
362
  status: response.status,
@@ -341,6 +406,7 @@ export class MemorySegmentCacheStore<
341
406
  const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
342
407
  const prefixedKey = `item:${key}`;
343
408
  this.unregisterTags(prefixedKey);
409
+ this.evictIfNeeded(this.itemCache, key, "item");
344
410
  this.itemCache.set(key, {
345
411
  value,
346
412
  handles: options?.handles,
@@ -353,6 +419,44 @@ export class MemorySegmentCacheStore<
353
419
  }
354
420
  }
355
421
 
422
+ async getShell(
423
+ key: string,
424
+ ): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
425
+ const cached = this.shellCache.get(key);
426
+ if (!cached) return null;
427
+
428
+ const now = Date.now();
429
+ if (now > cached.expiresAt) {
430
+ this.unregisterTags(`shell:${key}`);
431
+ this.shellCache.delete(key);
432
+ return null;
433
+ }
434
+
435
+ // SWR mirrors the item family: stale within the swr window still serves, and
436
+ // signals shouldRevalidate so the middleware schedules a background recapture.
437
+ const shouldRevalidate = cached.staleAt > 0 && now > cached.staleAt;
438
+ return { entry: cached.entry, shouldRevalidate };
439
+ }
440
+
441
+ async putShell(
442
+ key: string,
443
+ entry: ShellCacheEntry,
444
+ ttlSeconds?: number,
445
+ swrSeconds?: number,
446
+ tags?: string[],
447
+ ): Promise<void> {
448
+ const ttl = resolveTtl(ttlSeconds, this.defaults, DEFAULT_FUNCTION_TTL);
449
+ const swrWindow = resolveSwrWindow(swrSeconds, this.defaults);
450
+ const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
451
+ const prefixedKey = `shell:${key}`;
452
+ this.unregisterTags(prefixedKey);
453
+ this.evictIfNeeded(this.shellCache, key, "shell");
454
+ this.shellCache.set(key, { entry, expiresAt, staleAt, tags });
455
+ if (tags && tags.length > 0) {
456
+ this.registerTags(tags, prefixedKey);
457
+ }
458
+ }
459
+
356
460
  async invalidateTags(tags: string[]): Promise<void> {
357
461
  for (const tag of tags) {
358
462
  const keys = this.tagIndex.get(tag);
@@ -371,6 +475,8 @@ export class MemorySegmentCacheStore<
371
475
  this.responseCache.delete(rawKey);
372
476
  } else if (prefix === "item") {
373
477
  this.itemCache.delete(rawKey);
478
+ } else if (prefix === "shell") {
479
+ this.shellCache.delete(rawKey);
374
480
  }
375
481
 
376
482
  this.unregisterTags(prefixedKey);
@@ -421,6 +527,7 @@ export class MemorySegmentCacheStore<
421
527
  delete (globalThis as any)[CACHE_REGISTRY_KEY];
422
528
  delete (globalThis as any)[RESPONSE_CACHE_REGISTRY_KEY];
423
529
  delete (globalThis as any)[ITEM_CACHE_REGISTRY_KEY];
530
+ delete (globalThis as any)[SHELL_CACHE_REGISTRY_KEY];
424
531
  delete (globalThis as any)[TAG_INDEX_REGISTRY_KEY];
425
532
  delete (globalThis as any)[KEY_TAGS_REGISTRY_KEY];
426
533
  }
@@ -15,6 +15,21 @@ export interface CacheProfile {
15
15
  swr?: number;
16
16
  /** Default cache tags for invalidation */
17
17
  tags?: string[];
18
+ /**
19
+ * When true, a stale entry encountered during a server action's revalidation
20
+ * render is re-executed in the FOREGROUND (the action response reflects the
21
+ * refreshed value) instead of being served stale + revalidated in the
22
+ * background. Only the store write is deferred. Use this for mutation-related
23
+ * cached data that an action should refresh immediately; ordinary navigations
24
+ * always keep SWR. Default false: incidental TTL staleness must not turn an
25
+ * unrelated action into a synchronous cache-refresh barrier — for strong
26
+ * read-your-own-writes after a mutation, prefer updateTag() (a hard purge, so
27
+ * the action's own re-render is a fresh foreground miss).
28
+ *
29
+ * Honored only by `"use cache"` functions; route-level `cache()` and cached
30
+ * loaders ignore it (use updateTag() there).
31
+ */
32
+ foregroundOnAction?: boolean;
18
33
  }
19
34
 
20
35
  const DEFAULT_PROFILE: CacheProfile = { ttl: 900, swr: 1800 };
@@ -36,6 +36,14 @@ export interface ReadThroughItemConfig<T> {
36
36
  key: string;
37
37
  /** Execute the underlying function/loader on miss or revalidation */
38
38
  execute: () => Promise<T>;
39
+ /**
40
+ * Optional wrapper applied to execute() ONLY on the background
41
+ * stale-revalidation path (not the foreground miss, where the caller's context
42
+ * is already established). Used to re-establish the request-context ALS, which
43
+ * a detached waitUntil task loses on workerd. Defaults to calling execute()
44
+ * directly.
45
+ */
46
+ wrapBackground?: (run: () => Promise<T>) => Promise<T>;
39
47
  /** Serialize result for storage. Return null to skip caching. */
40
48
  serialize: (data: T) => Promise<string | null>;
41
49
  /** Deserialize cached value back to the original type */
@@ -77,6 +85,7 @@ export async function readThroughItem<T>(
77
85
  onMiss,
78
86
  onCached,
79
87
  host,
88
+ wrapBackground,
80
89
  } = config;
81
90
 
82
91
  // Cache lookup. An infra read failure (getItem) is reported by the store
@@ -106,7 +115,12 @@ export async function readThroughItem<T>(
106
115
  host,
107
116
  async () => {
108
117
  try {
109
- const fresh = await execute();
118
+ // Re-establish the caller's context (request-context ALS) for the
119
+ // detached background execution; the foreground miss below calls
120
+ // execute() directly since its context is already established.
121
+ const fresh = await (wrapBackground
122
+ ? wrapBackground(execute)
123
+ : execute());
110
124
  const serialized = await serialize(fresh);
111
125
  if (serialized !== null) {
112
126
  await setItem(key, serialized, storeOptions);
@@ -25,16 +25,16 @@ export async function streamToString(
25
25
  ): Promise<string> {
26
26
  const reader = stream.getReader();
27
27
  const decoder = new TextDecoder();
28
- let result = "";
28
+ const chunks: string[] = [];
29
29
 
30
30
  while (true) {
31
31
  const { done, value } = await reader.read();
32
32
  if (done) break;
33
- result += decoder.decode(value, { stream: true });
33
+ chunks.push(decoder.decode(value, { stream: true }));
34
34
  }
35
35
 
36
- result += decoder.decode(); // flush
37
- return result;
36
+ chunks.push(decoder.decode()); // flush
37
+ return chunks.join("");
38
38
  }
39
39
 
40
40
  /**