@rangojs/router 0.22.1 → 0.24.0

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 (259) hide show
  1. package/dist/bin/rango.js +10 -4
  2. package/dist/testing/vitest.js +31 -1
  3. package/dist/types/browser/defer.d.ts +48 -0
  4. package/dist/types/browser/document-revealed.d.ts +17 -0
  5. package/dist/types/browser/event-controller.d.ts +7 -1
  6. package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
  7. package/dist/types/browser/navigation-transaction.d.ts +21 -7
  8. package/dist/types/browser/partial-update.d.ts +39 -8
  9. package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
  10. package/dist/types/browser/react/context.d.ts +3 -2
  11. package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
  12. package/dist/types/browser/response-adapter.d.ts +7 -0
  13. package/dist/types/browser/types.d.ts +24 -10
  14. package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
  15. package/dist/types/build/generate-route-types.d.ts +1 -1
  16. package/dist/types/build/route-types/router-processing.d.ts +1 -1
  17. package/dist/types/build/route-types/scan-filter.d.ts +22 -2
  18. package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
  19. package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
  20. package/dist/types/cache/document-cache-policy.d.ts +24 -0
  21. package/dist/types/cache/document-cache.d.ts +2 -10
  22. package/dist/types/client-urls/client-urls.d.ts +1 -1
  23. package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
  24. package/dist/types/client-urls/navigation.d.ts +0 -37
  25. package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
  26. package/dist/types/client-urls/server-projection.d.ts +29 -1
  27. package/dist/types/client-urls/types.d.ts +34 -69
  28. package/dist/types/client.d.ts +0 -1
  29. package/dist/types/context-var.d.ts +12 -0
  30. package/dist/types/errors.d.ts +15 -0
  31. package/dist/types/handles/deferred-resolution.d.ts +2 -1
  32. package/dist/types/handles/pushed-by.d.ts +31 -0
  33. package/dist/types/index.d.ts +4 -0
  34. package/dist/types/index.rsc.d.ts +2 -0
  35. package/dist/types/loader-store.d.ts +8 -0
  36. package/dist/types/prefetch-markers.d.ts +11 -0
  37. package/dist/types/response-utils.d.ts +7 -0
  38. package/dist/types/reverse.d.ts +8 -8
  39. package/dist/types/route-definition/helpers-types.d.ts +27 -0
  40. package/dist/types/route-definition/rewrite.d.ts +56 -0
  41. package/dist/types/route-map-builder.d.ts +7 -2
  42. package/dist/types/router/basename.d.ts +7 -0
  43. package/dist/types/router/handler-context.d.ts +1 -5
  44. package/dist/types/router/internal-params.d.ts +8 -0
  45. package/dist/types/router/is-action.d.ts +2 -1
  46. package/dist/types/router/middleware-types.d.ts +15 -0
  47. package/dist/types/router/revalidation.d.ts +1 -2
  48. package/dist/types/router/route-trie-builder.d.ts +10 -0
  49. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
  50. package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
  51. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
  52. package/dist/types/rsc/full-payload.d.ts +11 -0
  53. package/dist/types/rsc/handler.d.ts +33 -0
  54. package/dist/types/rsc/helpers.d.ts +8 -0
  55. package/dist/types/rsc/loader-fetch.d.ts +16 -0
  56. package/dist/types/rsc/rewrite-passes.d.ts +74 -0
  57. package/dist/types/rsc/ssr-setup.d.ts +3 -1
  58. package/dist/types/server/context.d.ts +21 -1
  59. package/dist/types/server/handle-store.d.ts +9 -1
  60. package/dist/types/server/request-context.d.ts +52 -2
  61. package/dist/types/server/rewrite-trail.d.ts +49 -0
  62. package/dist/types/server/slot-accessors.d.ts +29 -0
  63. package/dist/types/server.d.ts +1 -0
  64. package/dist/types/testing/flight.entry.d.ts +1 -1
  65. package/dist/types/testing/index.d.ts +4 -4
  66. package/dist/types/testing/internal/action-id.d.ts +10 -1
  67. package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
  68. package/dist/types/testing/internal/test-action.d.ts +8 -0
  69. package/dist/types/testing/render-route.d.ts +8 -0
  70. package/dist/types/testing/run-loader.d.ts +2 -1
  71. package/dist/types/testing/run-middleware.d.ts +20 -0
  72. package/dist/types/testing/run-revalidate.d.ts +74 -0
  73. package/dist/types/testing/run-transition-when.d.ts +2 -3
  74. package/dist/types/testing/serve-shell-request.d.ts +40 -1
  75. package/dist/types/testing/vitest.d.ts +11 -0
  76. package/dist/types/thenable-status.d.ts +6 -0
  77. package/dist/types/types/boundaries.d.ts +9 -0
  78. package/dist/types/types/handler-context.d.ts +14 -0
  79. package/dist/types/types/loader-types.d.ts +30 -0
  80. package/dist/types/types/request-scope.d.ts +7 -0
  81. package/dist/types/types/segments.d.ts +21 -4
  82. package/dist/types/urls/path-helper-types.d.ts +25 -4
  83. package/dist/types/urls/type-extraction.d.ts +6 -2
  84. package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
  85. package/dist/types/vite/discovery/state.d.ts +14 -4
  86. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
  87. package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
  88. package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
  89. package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
  90. package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
  91. package/dist/types/vite/router-discovery.d.ts +10 -0
  92. package/dist/vite/index.js +974 -379
  93. package/package.json +1 -1
  94. package/skills/caching/SKILL.md +54 -5
  95. package/skills/catalog.json +3 -3
  96. package/skills/client-urls/SKILL.md +197 -24
  97. package/skills/cloudflare/SKILL.md +14 -9
  98. package/skills/hooks/SKILL.md +16 -3
  99. package/skills/hooks/data.md +3 -1
  100. package/skills/links/SKILL.md +24 -0
  101. package/skills/loader/SKILL.md +199 -2
  102. package/skills/middleware/SKILL.md +183 -2
  103. package/skills/migrate-nextjs/SKILL.md +2 -2
  104. package/skills/migrate-react-router/SKILL.md +10 -7
  105. package/skills/migrate-react-router/data-and-actions.md +1 -1
  106. package/skills/ppr/SKILL.md +22 -0
  107. package/skills/prerender/SKILL.md +22 -8
  108. package/skills/rango/SKILL.md +13 -7
  109. package/skills/route/SKILL.md +139 -0
  110. package/skills/server-actions/SKILL.md +27 -0
  111. package/skills/testing/SKILL.md +3 -2
  112. package/skills/testing/client-components.md +2 -1
  113. package/skills/testing/middleware.md +31 -10
  114. package/skills/testing/predicates.md +67 -32
  115. package/skills/testing/response-routes.md +1 -0
  116. package/skills/testing/serve-shell-request.md +107 -19
  117. package/skills/testing/setup.md +2 -2
  118. package/skills/typesafety/route-types.md +2 -2
  119. package/skills/use-cache/SKILL.md +4 -2
  120. package/skills/view-transitions/SKILL.md +26 -0
  121. package/src/__augment-tests__/augmented.check.ts +20 -0
  122. package/src/browser/defer.ts +82 -0
  123. package/src/browser/document-revealed.ts +52 -0
  124. package/src/browser/event-controller.ts +46 -32
  125. package/src/browser/merge-segment-loaders.ts +31 -18
  126. package/src/browser/navigation-bridge.ts +82 -16
  127. package/src/browser/navigation-client.ts +48 -36
  128. package/src/browser/navigation-store.ts +13 -0
  129. package/src/browser/navigation-transaction.ts +25 -8
  130. package/src/browser/partial-update.ts +554 -146
  131. package/src/browser/prefetch/fetch.ts +2 -1
  132. package/src/browser/react/Link.tsx +7 -9
  133. package/src/browser/react/NavigationProvider.tsx +88 -49
  134. package/src/browser/react/context.ts +3 -2
  135. package/src/browser/react/filter-segment-order.ts +13 -2
  136. package/src/browser/react/use-router.ts +2 -4
  137. package/src/browser/response-adapter.ts +20 -0
  138. package/src/browser/rsc-router.tsx +17 -45
  139. package/src/browser/server-action-bridge.ts +25 -63
  140. package/src/browser/types.ts +24 -10
  141. package/src/browser/validate-redirect-origin.ts +16 -0
  142. package/src/build/generate-route-types.ts +1 -0
  143. package/src/build/route-types/router-processing.ts +3 -6
  144. package/src/build/route-types/scan-filter.ts +45 -11
  145. package/src/cache/cf/cf-cache-store.ts +59 -21
  146. package/src/cache/cf/cf-cache-types.ts +24 -2
  147. package/src/cache/document-cache-policy.ts +131 -0
  148. package/src/cache/document-cache.ts +21 -107
  149. package/src/client-urls/client-urls.ts +22 -6
  150. package/src/client-urls/hoisted-revalidate.ts +25 -0
  151. package/src/client-urls/navigation.ts +0 -96
  152. package/src/client-urls/revalidate-registry.ts +30 -0
  153. package/src/client-urls/server-projection.ts +151 -65
  154. package/src/client-urls/types.ts +39 -71
  155. package/src/client.tsx +9 -5
  156. package/src/context-var.ts +32 -2
  157. package/src/errors.ts +34 -0
  158. package/src/handles/deferred-resolution.ts +28 -5
  159. package/src/handles/pushed-by.ts +148 -0
  160. package/src/index.rsc.ts +8 -0
  161. package/src/index.ts +7 -0
  162. package/src/loader-store.ts +18 -7
  163. package/src/prefetch-markers.ts +13 -0
  164. package/src/response-utils.ts +19 -0
  165. package/src/reverse.ts +17 -14
  166. package/src/route-content-wrapper.tsx +18 -4
  167. package/src/route-definition/dsl-helpers.ts +27 -4
  168. package/src/route-definition/helpers-types.ts +27 -1
  169. package/src/route-definition/redirect.ts +2 -15
  170. package/src/route-definition/rewrite.ts +146 -0
  171. package/src/route-map-builder.ts +14 -11
  172. package/src/router/basename.ts +20 -0
  173. package/src/router/handler-context.ts +3 -13
  174. package/src/router/intercept-resolution.ts +35 -16
  175. package/src/router/internal-params.ts +16 -0
  176. package/src/router/is-action.ts +2 -1
  177. package/src/router/loader-resolution.ts +85 -4
  178. package/src/router/match-api.ts +36 -1
  179. package/src/router/match-middleware/background-revalidation.ts +16 -4
  180. package/src/router/match-middleware/cache-lookup.ts +35 -7
  181. package/src/router/match-middleware/cache-store.ts +15 -1
  182. package/src/router/match-middleware/intercept-resolution.ts +4 -2
  183. package/src/router/match-result.ts +14 -2
  184. package/src/router/middleware-types.ts +16 -0
  185. package/src/router/middleware.ts +127 -49
  186. package/src/router/navigation-snapshot.ts +2 -1
  187. package/src/router/revalidation.ts +1 -1
  188. package/src/router/route-trie-builder.ts +28 -1
  189. package/src/router/segment-resolution/fresh.ts +22 -18
  190. package/src/router/segment-resolution/loader-cache.ts +2 -1
  191. package/src/router/segment-resolution/loader-mask.ts +5 -3
  192. package/src/router/segment-resolution/loader-snapshot.ts +18 -4
  193. package/src/router/segment-resolution/mask-nested.ts +8 -4
  194. package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
  195. package/src/router/segment-resolution/revalidation.ts +215 -82
  196. package/src/rsc/full-payload.ts +37 -5
  197. package/src/rsc/handler.ts +82 -55
  198. package/src/rsc/helpers.ts +24 -0
  199. package/src/rsc/loader-fetch.ts +79 -24
  200. package/src/rsc/response-cache-serve.ts +18 -2
  201. package/src/rsc/response-route-handler.ts +4 -7
  202. package/src/rsc/rewrite-passes.ts +203 -0
  203. package/src/rsc/rsc-rendering.ts +54 -9
  204. package/src/rsc/shell-capture.ts +2 -1
  205. package/src/rsc/ssr-setup.ts +11 -5
  206. package/src/segment-loader-promise.ts +23 -20
  207. package/src/segment-system.tsx +60 -26
  208. package/src/server/context.ts +39 -5
  209. package/src/server/cookie-parse.ts +36 -12
  210. package/src/server/cookie-store.ts +7 -3
  211. package/src/server/handle-store.ts +152 -25
  212. package/src/server/request-context.ts +175 -64
  213. package/src/server/request-headers.ts +4 -14
  214. package/src/server/rewrite-trail.ts +190 -0
  215. package/src/server/slot-accessors.ts +57 -0
  216. package/src/server.ts +4 -0
  217. package/src/ssr/index.tsx +118 -16
  218. package/src/testing/action-form.ts +2 -11
  219. package/src/testing/dispatch.ts +138 -67
  220. package/src/testing/e2e/page-helpers.ts +3 -1
  221. package/src/testing/flight.entry.ts +1 -0
  222. package/src/testing/index.ts +7 -3
  223. package/src/testing/internal/action-id.ts +16 -1
  224. package/src/testing/internal/client-urls-projection.ts +70 -0
  225. package/src/testing/internal/test-action.ts +25 -0
  226. package/src/testing/render-route.tsx +16 -3
  227. package/src/testing/run-loader.ts +4 -1
  228. package/src/testing/run-middleware.ts +56 -11
  229. package/src/testing/run-revalidate.ts +142 -0
  230. package/src/testing/run-transition-when.ts +7 -8
  231. package/src/testing/serve-shell-request.ts +131 -8
  232. package/src/testing/vitest.ts +67 -1
  233. package/src/thenable-status.ts +11 -0
  234. package/src/types/boundaries.ts +12 -0
  235. package/src/types/handler-context.ts +14 -0
  236. package/src/types/loader-types.ts +30 -0
  237. package/src/types/request-scope.ts +8 -0
  238. package/src/types/segments.ts +26 -4
  239. package/src/urls/path-helper-types.ts +27 -5
  240. package/src/urls/type-extraction.ts +30 -10
  241. package/src/use-loader.tsx +18 -13
  242. package/src/vite/discovery/client-urls-projection.ts +20 -21
  243. package/src/vite/discovery/discover-routers.ts +94 -26
  244. package/src/vite/discovery/state.ts +28 -4
  245. package/src/vite/discovery/virtual-module-codegen.ts +22 -17
  246. package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
  247. package/src/vite/plugins/expose-internal-ids.ts +2 -3
  248. package/src/vite/plugins/hoist/literal.ts +417 -0
  249. package/src/vite/plugins/hoist/scope.ts +370 -0
  250. package/src/vite/plugins/hoist/source.ts +84 -0
  251. package/src/vite/plugins/transition-when-hoist.ts +113 -779
  252. package/src/vite/rango.ts +2 -0
  253. package/src/vite/router-discovery.ts +116 -53
  254. package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
  255. package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
  256. package/dist/types/testing/run-client-revalidate.d.ts +0 -43
  257. package/src/client-urls/revalidate-chain.ts +0 -83
  258. package/src/client-urls/revalidation-protocol.ts +0 -56
  259. package/src/testing/run-client-revalidate.ts +0 -92
package/dist/bin/rango.js CHANGED
@@ -210,9 +210,15 @@ var init_codegen = __esm({
210
210
  });
211
211
 
212
212
  // src/build/route-types/scan-filter.ts
213
- import { join, relative } from "node:path";
213
+ import { isAbsolute, join, relative } from "node:path";
214
214
  import { readdirSync } from "node:fs";
215
215
  import picomatch from "picomatch";
216
+ function isUniversallySkippedScanDir(name) {
217
+ return name === "node_modules" || name === "dist" || name === "coverage" || name.startsWith(".");
218
+ }
219
+ function isSkippedSourceScanDir(name) {
220
+ return isUniversallySkippedScanDir(name) || name === "build";
221
+ }
216
222
  function findTsFiles(dir, filter) {
217
223
  const results = [];
218
224
  let entries;
@@ -227,8 +233,7 @@ function findTsFiles(dir, filter) {
227
233
  for (const entry of entries) {
228
234
  const fullPath = join(dir, entry.name);
229
235
  if (entry.isDirectory()) {
230
- if (entry.name === "node_modules" || entry.name.startsWith(".") || entry.name === "dist" || entry.name === "build" || entry.name === "coverage")
231
- continue;
236
+ if (isSkippedSourceScanDir(entry.name)) continue;
232
237
  results.push(...findTsFiles(fullPath, filter));
233
238
  } else if ((entry.name.endsWith(".ts") || entry.name.endsWith(".tsx") || entry.name.endsWith(".js") || entry.name.endsWith(".jsx")) && !entry.name.includes(".gen.")) {
234
239
  if (filter && !filter(fullPath)) continue;
@@ -1032,7 +1037,7 @@ function isRoutableSourceFile(name) {
1032
1037
  return (name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".js") || name.endsWith(".jsx")) && !name.includes(".gen.") && !name.includes(".test.") && !name.includes(".spec.");
1033
1038
  }
1034
1039
  function isExcludedScanDir(name) {
1035
- return name === "node_modules" || name === "dist" || name === "coverage" || name === "__tests__" || name === "__mocks__" || name.startsWith(".");
1040
+ return isUniversallySkippedScanDir(name) || name === "__tests__" || name === "__mocks__";
1036
1041
  }
1037
1042
  function findCallSiteFilesRecursive(dir, pattern, patternG, stopAtMatchDir, filter, results) {
1038
1043
  let entries;
@@ -1443,6 +1448,7 @@ var init_router_processing = __esm({
1443
1448
  "src/build/route-types/router-processing.ts"() {
1444
1449
  "use strict";
1445
1450
  init_codegen();
1451
+ init_scan_filter();
1446
1452
  init_source_scan();
1447
1453
  init_include_resolution();
1448
1454
  init_per_module_writer();
@@ -243,6 +243,22 @@ function resolveFromRouter(spec) {
243
243
  }
244
244
  }
245
245
  var rsdServerEdgePath = resolveFromRouter(RSD_SERVER_EDGE_SPEC);
246
+ var CLIENT_URLS_SOURCE_QUERY = "rango-client-urls-source";
247
+ var CLIENT_ENTRY_RSC_PATH = here("src/client.rsc.tsx").replaceAll(
248
+ "\\",
249
+ "/"
250
+ );
251
+ var CLIENT_URLS_INSTALLER_PATH = here(
252
+ "src/testing/internal/client-urls-projection.ts"
253
+ ).replaceAll("\\", "/");
254
+ function clientUrlsProjectionInstall(id) {
255
+ const source = `${id}${id.includes("?") ? "&" : "?"}${CLIENT_URLS_SOURCE_QUERY}`;
256
+ return `
257
+ import * as $$RangoClientUrlsSource from ${JSON.stringify(source)};
258
+ import { installClientUrlsProjection as $$rangoInstallClientUrlsProjection } from ${JSON.stringify(CLIENT_URLS_INSTALLER_PATH)};
259
+ $$rangoInstallClientUrlsProjection(${JSON.stringify(`${id}#default`)}, $$RangoClientUrlsSource);
260
+ `;
261
+ }
246
262
  function rangoTestAliases(opts = {}) {
247
263
  const aliases = [
248
264
  // Real impls (index.rsc.ts) for the bare specifier ONLY — exact regex so
@@ -290,8 +306,19 @@ function rangoTestConfig(opts = {}) {
290
306
  function rangoUseClientTransform() {
291
307
  return {
292
308
  name: "rango:testing-use-client",
309
+ resolveId: {
310
+ // Before Vite's resolver, which resolves the bare specifier first.
311
+ order: "pre",
312
+ handler(source, importer) {
313
+ if (source === "@rangojs/router/client" && importer?.includes(CLIENT_URLS_SOURCE_QUERY)) {
314
+ return CLIENT_ENTRY_RSC_PATH;
315
+ }
316
+ return void 0;
317
+ }
318
+ },
293
319
  async transform(code, id) {
294
320
  if (id.includes("/node_modules/")) return void 0;
321
+ if (id.includes(CLIENT_URLS_SOURCE_QUERY)) return void 0;
295
322
  if (!code.includes("use client")) return void 0;
296
323
  const { parseAstAsync } = await import("vite");
297
324
  const { hasDirective, transformDirectiveProxyExport } = await import("@vitejs/plugin-rsc/transforms");
@@ -308,12 +335,15 @@ function rangoUseClientTransform() {
308
335
  runtime: (name) => `$$RangoRSD.registerClientReference(() => { throw new Error("client reference " + ${JSON.stringify(name)} + " is not callable on the server"); }, ${JSON.stringify(id)}, ${JSON.stringify(name)})`
309
336
  });
310
337
  if (!result) return void 0;
311
- const { output } = result;
338
+ const { output, exportNames } = result;
312
339
  const rsdHref = rsdServerEdgePath ? pathToFileURL(rsdServerEdgePath).href : RSD_SERVER_EDGE_SPEC;
313
340
  output.prepend(
314
341
  `import * as $$RangoRSD from ${JSON.stringify(rsdHref)};
315
342
  `
316
343
  );
344
+ if (code.includes("clientUrls(") && exportNames.includes("default")) {
345
+ output.append(clientUrlsProjectionInstall(id));
346
+ }
317
347
  return {
318
348
  code: output.toString(),
319
349
  map: output.generateMap({ hires: true })
@@ -0,0 +1,48 @@
1
+ /**
2
+ * One hole of a `prefetch: false` adoption (browser/partial-update.ts
3
+ * armHoles): the promise every reader of the hole waits on, and what the
4
+ * router knows of it. The router sets `status` and `value` here; it never
5
+ * writes React's thenable fields on `promise`.
6
+ *
7
+ * Resolve-only: a hole is filled or released (releaseHoles), never rejected.
8
+ * A failure arrives as a fulfilled value (a loader's error envelope, an
9
+ * error segment's node).
10
+ */
11
+ export declare class Defer<T> {
12
+ status: "pending" | "fulfilled";
13
+ value: T | undefined;
14
+ /** A reader has handed React `promise` (readDefer). */
15
+ read?: boolean;
16
+ /**
17
+ * Resolved with a value the router holds (a node), the hole is
18
+ * "fulfilled" at once. Resolved with a stream (a Flight chunk), its promise
19
+ * follows the stream, and so does the hole: "fulfilled" with the stream's
20
+ * value once it arrives, so a reader that mounts after that reads the value
21
+ * as it would read the chunk Flight marked. A rejected stream leaves the
22
+ * hole pending; its promise carries the rejection.
23
+ */
24
+ resolve: (value: T) => void;
25
+ readonly promise: Promise<T>;
26
+ }
27
+ /**
28
+ * The read rule, for a component that reads a hole (Suspender, useLoader).
29
+ * A hole that has fulfilled and that no reader has handed React is its
30
+ * value: use() of a promise React has not seen suspends an urgent render
31
+ * once, and the boundary shows its fallback. Once a reader has handed React
32
+ * the promise, every read is use() of that promise: React has marked it and
33
+ * returns its value at once, and a replay of the reader must hand use() the
34
+ * thenable it read before ("A component was suspended by an uncached
35
+ * promise"). Either way use() is called once, as at every useLoader read.
36
+ */
37
+ export declare function readDefer<T>(hole: Defer<T>): T;
38
+ /**
39
+ * A hole as the loader-streams convention has it: its promise while pending,
40
+ * its value once fulfilled. Anything else is returned as it is. For an
41
+ * aggregate, the pending-stream registry, and a node React reads itself (a
42
+ * segment or slot with no `loading()`): a router component there would be a
43
+ * fiber the filled page's node does not have, and the next render from the
44
+ * filled entry would remount the segment. React suspends such a read on the
45
+ * component above it, which commits nothing until the hole fulfils, so the
46
+ * value can stand where React read the promise.
47
+ */
48
+ export declare function asStream(value: unknown): unknown;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Resolves once the document has finished parsing and no Suspense boundary of
3
+ * it is unrevealed.
4
+ *
5
+ * Parsing ends with the stream, but React reveals a streamed boundary in
6
+ * batches up to 300 ms later (Fizz's $RC marks it `$~` and reveals it from a
7
+ * timer), so the boundary is still pending at DOMContentLoaded. A transition
8
+ * then would make React client-render it over its server HTML.
9
+ *
10
+ * A reveal rewrites the same comment's data, or React removes the node; a
11
+ * boundary nested in revealed content joins the document only then, so it
12
+ * resolves only once a full walk finds none. Polls with setTimeout, not
13
+ * requestAnimationFrame: a hidden tab never runs rAF, which would hold late
14
+ * handle pushes and location state until the tab is shown again. A tab hidden
15
+ * from load still waits: Fizz reveals boundaries from a rAF-gated script.
16
+ */
17
+ export declare function awaitDocumentRevealed(): Promise<void>;
@@ -178,7 +178,13 @@ export interface EventController {
178
178
  startNavigation(url: string, options?: NavigateOptionsInternal & {
179
179
  skipLoadingState?: boolean;
180
180
  }): NavigationHandle;
181
- abortNavigation(): void;
181
+ /**
182
+ * Abort the pending navigation, and the committed one whose stream is
183
+ * still open. `pendingOnly` leaves the committed one alone: a page
184
+ * restored from the back/forward cache is not being left
185
+ * (navigation-bridge.ts handlePageShow).
186
+ */
187
+ abortNavigation(pendingOnly?: boolean): void;
182
188
  startAction(actionId: string, args: unknown[],
183
189
  /** Originating history entry key; scopes location-state arbitration. */
184
190
  cohort?: string): ActionHandle;
@@ -3,8 +3,11 @@ import type { ResolvedSegment } from "./types.js";
3
3
  * Merge partial loader data from server with cached loader data.
4
4
  *
5
5
  * During partial revalidation (stale or action), the server may return only
6
- * some loaders that pass the revalidation check. The component still needs
7
- * all loader data, so we merge fresh data with cached data.
6
+ * some loaders that pass the revalidation check, none when every one is held.
7
+ * The component still needs all loader data, so we merge fresh data with
8
+ * cached data. Two settled aggregates merge into the array itself: a fresh
9
+ * Promise.all over settled parts is a promise React has not read, and content
10
+ * on screen handed one suspends (docs/internal/suspense-contract.md, rule 2).
8
11
  *
9
12
  * @param fromServer - Segment returned from server with partial loaders
10
13
  * @param fromCache - Cached segment with full loader data
@@ -65,6 +65,24 @@ interface BoundCommitOverrides {
65
65
  /** No payload follows this commit (CommitOptions.treeless). */
66
66
  treeless?: boolean;
67
67
  }
68
+ /**
69
+ * What a commit decided about scroll, in the shape the update that follows
70
+ * carries it (NavigationUpdate.scroll). A navigation always decides, and "do
71
+ * not scroll" is a decision (`<Link scroll={false}>`, an intercept): it
72
+ * replaces a pending one. A commit that is not a navigation decides nothing:
73
+ * an action refetch into the entry on screen (`storeOnly`) and a cache-only
74
+ * commit return no `scroll`, and their update leaves a pending decision alone
75
+ * (react/NavigationProvider.tsx).
76
+ *
77
+ * Scar tissue: those two used to answer `scroll: false`. An action refetch
78
+ * that landed before React committed a navigation then replaced the
79
+ * navigation's pending scroll with "do not scroll".
80
+ */
81
+ export interface CommitResult {
82
+ scroll?: {
83
+ enabled: boolean | undefined;
84
+ };
85
+ }
68
86
  /**
69
87
  * Bound transaction with pre-configured commit options (without segmentIds/segments)
70
88
  */
@@ -80,19 +98,15 @@ export interface BoundTransaction {
80
98
  readonly transitionGatedOff?: boolean;
81
99
  /** Start streaming and get a token to end it when the stream completes */
82
100
  startStreaming(): StreamingToken;
83
- /** Commit the navigation. Returns the effective scroll option for the caller to handle. */
84
- commit(segmentIds: string[], segments: ResolvedSegment[], overrides?: BoundCommitOverrides): {
85
- scroll?: boolean;
86
- };
101
+ /** Commit the navigation. Returns its scroll decision, for the update. */
102
+ commit(segmentIds: string[], segments: ResolvedSegment[], overrides?: BoundCommitOverrides): CommitResult;
87
103
  }
88
104
  /**
89
105
  * Navigation transaction for managing state during navigation
90
106
  * Uses the event controller handle for lifecycle management
91
107
  */
92
108
  interface NavigationTransaction extends Disposable {
93
- commit(options: CommitOptions): {
94
- scroll?: boolean;
95
- };
109
+ commit(options: CommitOptions): CommitResult;
96
110
  with(options: Omit<CommitOptions, "segmentIds" | "segments">): BoundTransaction;
97
111
  /** The navigation handle from the event controller */
98
112
  handle: NavigationHandle;
@@ -2,7 +2,7 @@ import type { NavigationStore, NavigationClient, UpdateSubscriber, ResolvedSegme
2
2
  import type { ReactNode } from "react";
3
3
  import type { RenderSegmentsOptions } from "../segment-system.js";
4
4
  import type { BoundTransaction } from "./navigation-transaction.js";
5
- import type { NavigationUpdate } from "./types.js";
5
+ import type { HandleData, NavigationUpdate, RscMetadata } from "./types.js";
6
6
  import type { LoaderStore } from "../loader-store.js";
7
7
  import { type TransitionWhenActionInput } from "./transition-when.js";
8
8
  /**
@@ -30,6 +30,31 @@ export interface PartialUpdateConfig {
30
30
  renderSegments: (segments: ResolvedSegment[], options?: RenderSegmentsOptions) => Promise<ReactNode> | ReactNode;
31
31
  /** RSC version getter — returns the current version (may change after HMR) */
32
32
  getVersion?: () => string | undefined;
33
+ /**
34
+ * A fill (`prefetch: false`) lands after its navigation has committed: the
35
+ * transaction has recorded the entry and handed React the tree, and the
36
+ * fill hands React no update that would carry what its response holds to
37
+ * the stores. The bridge that owns navigation supplies the writes, the
38
+ * ones its own updates make. Without them a redirect is a document
39
+ * navigation and the rest is dropped.
40
+ */
41
+ fill?: {
42
+ redirect(url: string, state?: Record<string, unknown>): void;
43
+ /** The location state the server set, to the entry and its readers. */
44
+ locationState(state: Record<string, unknown>): void;
45
+ /** What the response's handlers pushed, to the handle store. */
46
+ handles(metadata: RscMetadata): void;
47
+ /**
48
+ * Deliver the navigation state to its readers now. A page built with
49
+ * holes commits in the task of its click, before the notification of the
50
+ * click has gone out (event-controller.ts makeDebouncedNotifier), and
51
+ * React holds that commit where a hole is read with no boundary: without
52
+ * this useNavigation() reads idle, on the page being left, for the whole
53
+ * wait. Delivered first, `loading` is on screen when the commit is
54
+ * handed over, and stays while React holds it, as for a plain click.
55
+ */
56
+ flush(): void;
57
+ };
33
58
  }
34
59
  /**
35
60
  * Options that can override the pre-configured commit settings
@@ -58,18 +83,24 @@ export type UpdateMode = {
58
83
  * when we send cached segment IDs to the server - if the server returns empty diff,
59
84
  * we use the same segments we told the server we have. */
60
85
  targetCacheSegments?: ResolvedSegment[];
61
- /** Cached handle data for the target URL. When server returns empty diff and we're
62
- * rendering from cache, this is passed to the UI to restore breadcrumbs etc. */
63
- targetCacheHandleData?: Record<string, Record<string, unknown[]>>;
86
+ /** The handle state of `targetCacheSegments`: the entry's pushes, and
87
+ * the live pushes of the page on screen for each segment taken from it.
88
+ * Read at commit; the page starts from it and the response's handles,
89
+ * when it has any, merge onto it (NavigationProvider). */
90
+ targetCacheHandleData?: () => HandleData;
64
91
  /** Source URL for intercept restore (popstate cache miss) */
65
92
  interceptSourceUrl?: string;
66
93
  /**
67
- * The bridge already presented an optimistic clientUrls() destination
68
- * for this navigation: transition-lane commits add
94
+ * The optimistic clientUrls() destination the bridge presented for
95
+ * this navigation. Transition-lane commits add
69
96
  * OPTIMISTIC_COMMIT_TRANSITION_TYPE so router <ViewTransition>
70
- * boundaries do not animate the identical repaint.
97
+ * boundaries do not animate the identical repaint, and `clear()` runs
98
+ * once the canonical tree is handed to React: `useOutlet().pending`
99
+ * ends with that commit, not with a `prefetch: false` fill after it.
71
100
  */
72
- optimisticPresented?: boolean;
101
+ presentation?: {
102
+ clear(): void;
103
+ };
73
104
  /**
74
105
  * A router.refresh() (or cross-tab refresh) of the current URL:
75
106
  * transition({ when }) sees kind "revalidate", not "replace".
@@ -1,8 +1,15 @@
1
1
  import { type ReactNode } from "react";
2
- import type { NavigationStore, NavigationUpdate, NavigationBridge } from "../types.js";
2
+ import type { NavigationStore, NavigationUpdate, NavigationBridge, RscMetadata } from "../types.js";
3
3
  import type { EventController } from "../event-controller.js";
4
4
  import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
5
5
  import { type AppShellRef } from "../app-shell.js";
6
+ /**
7
+ * Read the handle stream of `metadata` into the event controller and the
8
+ * entry on screen. The provider's own call for every payload it is handed,
9
+ * and the one write for a response that hands React nothing (a fill,
10
+ * browser/partial-update.ts fillHoles).
11
+ */
12
+ export declare function consumeHandles(metadata: RscMetadata, eventController: EventController, store: NavigationStore): void;
6
13
  /**
7
14
  * Props for NavigationProvider
8
15
  */
@@ -67,10 +74,17 @@ export interface NavigationProviderProps {
67
74
  * The document this provider hydrates (initBrowserApp; renderRoute with
68
75
  * `hydrate: true`), whose store EventController.lockHydration locked.
69
76
  * `settled` resolves once the stream has ended and React has revealed every
70
- * boundary. Until then location state is `undefined`, as SSR rendered it;
71
- * then one transition gives the entry's state and changes
72
- * HydrationBarrierContext, and the store is released after that commit. A
73
- * navigation committed earlier releases the store before its update.
77
+ * boundary. Then a transition changes HydrationBarrierContext alone, and
78
+ * React commits it once every boundary has hydrated, one still suspended on
79
+ * the client (a lazy chunk, a deferred-hydration gate) included. Until that
80
+ * commit location state is `undefined`, as SSR rendered it, through any
81
+ * commit no payload follows (a shallow navigation), and the store is
82
+ * locked; after it the entry's state lands in a transition of its own
83
+ * and the store is released (handle data lands in that release
84
+ * notification). A navigation committed earlier releases the store before
85
+ * its update, for the whole hold when the navigation is held: an old-page
86
+ * boundary hydrating meanwhile reads the destination's values and can
87
+ * mismatch.
74
88
  * Absent for a provider mounted on the client.
75
89
  */
76
90
  hydration?: {
@@ -76,7 +76,8 @@ export declare const LocationStateContext: Context<LocationStateSnapshot>;
76
76
  * Read by no component. NavigationProvider changes its value in a transition so
77
77
  * that React hydrates every boundary still dehydrated beneath it before that
78
78
  * commit: React hydrates a dehydrated boundary first when a provider above it
79
- * changes, and nothing re-renders for a value nobody reads. See
80
- * NavigationProviderProps.hydration.
79
+ * changes, and nothing re-renders for a value nobody reads. It must stay
80
+ * unread and alone in its lane (docs/internal/execution-model.md, "The reveal
81
+ * barrier is alone in its lane"). See NavigationProviderProps.hydration.
81
82
  */
82
83
  export declare const HydrationBarrierContext: Context<number>;
@@ -1,3 +1,10 @@
1
+ /**
2
+ * A loader sub-id carries a `D${index}.` marker ("M0L0D1.user"). Non-global,
3
+ * so `.test` and `.replace` are stateless.
4
+ */
5
+ export declare const LOADER_SEGMENT_ID: RegExp;
6
+ /** True for a loader sub-id ("M0L0D1.user"), false for a layout/route/slot id. */
7
+ export declare function isLoaderSegmentId(id: string): boolean;
1
8
  /**
2
9
  * Build the handle-collection segment order from a raw `matched` list.
3
10
  *
@@ -6,6 +6,13 @@ type HeaderResult = {
6
6
  * Returns { url } if valid, "blocked" if present but invalid origin, null if absent.
7
7
  */
8
8
  export declare function extractRscHeaderUrl(response: Response, header: string): HeaderResult;
9
+ /**
10
+ * Run `request` with a signal that `signal` aborts until the request settles,
11
+ * and only until then. A response that has arrived streams to its end:
12
+ * aborting a Flight body mid-read rejects every chunk a mounted tree may be
13
+ * reading. Aborted, the request rejects with `signal.reason`.
14
+ */
15
+ export declare function abortUntilResponse<T>(signal: AbortSignal | undefined, request: (signal: AbortSignal | undefined) => Promise<T>): Promise<T>;
9
16
  /**
10
17
  * Empty 200 response that won't choke Flight parsing.
11
18
  * Used when a header URL is blocked by origin validation.
@@ -80,7 +80,11 @@ export interface RscMetadata {
80
80
  * positions and must complete at the handler barrier.
81
81
  */
82
82
  handlesLate?: AsyncGenerator<HandleData, void, unknown>;
83
- /** Cached handle data (for back/forward navigation from cache) */
83
+ /**
84
+ * Handle data of the history-cache entry the update renders from (a
85
+ * back/forward restore, or a link to a cached page). It replaces the handle
86
+ * state, and `handles`, when present, merge onto it.
87
+ */
84
88
  cachedHandleData?: HandleData;
85
89
  /**
86
90
  * RSC version string from the server.
@@ -277,7 +281,12 @@ export interface HistoryEntryMemory {
277
281
  export interface NavigationUpdate {
278
282
  root: ReactNode | Promise<ReactNode>;
279
283
  metadata: RscMetadata;
280
- /** Scroll behavior to apply after React commits this update */
284
+ /**
285
+ * The scroll decision of the navigation transaction this update commits,
286
+ * applied after React commits it. Absent on every update that is not a
287
+ * navigation's (server action, error, `prefetch: false` fill): such an
288
+ * update says nothing about scroll, and leaves a pending decision alone.
289
+ */
281
290
  scroll?: {
282
291
  /** For back/forward: restore saved position */
283
292
  restore?: boolean;
@@ -537,6 +546,12 @@ export interface NavigationStore {
537
546
  * probe and write into one historyCache scan for the per-yield streaming path.
538
547
  */
539
548
  updateCacheHandleDataIfOwned(historyKey: string, handleData: HandleData, ownerInstance: number, stale?: boolean, handlesPending?: boolean): void;
549
+ /**
550
+ * The history keys whose entries show the page `historyKey`'s entry shows:
551
+ * that entry and the copies a shallow navigation made of it, which share its
552
+ * segments array. `[historyKey]` when it has no entry.
553
+ */
554
+ getPageHistoryKeys(historyKey: string): string[];
540
555
  markHistoryCacheStale(): void;
541
556
  markCacheAsStaleAndBroadcast(): void;
542
557
  clearHistoryCache(): void;
@@ -558,13 +573,6 @@ export interface FetchPartialOptions {
558
573
  signal?: AbortSignal;
559
574
  /** If true, this is a stale cache revalidation request - server should force revalidators */
560
575
  staleRevalidation?: boolean;
561
- /**
562
- * Encoded client-run per-loader revalidation decisions
563
- * (clientUrls revalidate() predicates executed in the browser); sent as
564
- * X-Rango-Client-Reval and honored only by materialized client-urls loader
565
- * stubs. Null/absent = locked server defaults.
566
- */
567
- clientRevalidation?: string | null;
568
576
  interceptSourceUrl?: string;
569
577
  /** RSC version for cache invalidation detection */
570
578
  version?: string;
@@ -572,6 +580,12 @@ export interface FetchPartialOptions {
572
580
  routerId?: string;
573
581
  /** If true, this is an HMR refetch - server should invalidate manifest cache */
574
582
  hmr?: boolean;
583
+ /**
584
+ * The fill request of an adopted prefetch that carried deferred segments
585
+ * (`prefetch: false`): marked `_rsc_fill`, never answered from or written
586
+ * to a prefetch cache or the HTTP cache, and it cancels no prefetch.
587
+ */
588
+ fill?: boolean;
575
589
  }
576
590
  /**
577
591
  * Result of a partial fetch including stream completion tracking
@@ -644,7 +658,7 @@ export interface NavigationBridgeConfig {
644
658
  export type { ResolvedSegment };
645
659
  /**
646
660
  * Token for tracking an active stream.
647
- * Call end() when the stream completes.
661
+ * Call end() when the stream completes. Only the first call counts.
648
662
  */
649
663
  export interface StreamingToken {
650
664
  end(): void;
@@ -26,3 +26,14 @@ export declare function validateRedirectOrigin(url: string, currentOrigin: strin
26
26
  * @returns The normalized URL string on success, or null if blocked.
27
27
  */
28
28
  export declare function validateExternalRedirect(url: string, currentOrigin: string): string | null;
29
+ /**
30
+ * The target of a redirect a Flight payload carries (`metadata.redirect`), or
31
+ * null when the client refuses it. An explicit off-host one
32
+ * (`redirect(url, { external: true })`) is scheme-validated only:
33
+ * `external` waives the same-origin check the app opted out of, not scheme
34
+ * safety. Every other one must stay on this origin.
35
+ */
36
+ export declare function validatePayloadRedirect(redirect: {
37
+ url: string;
38
+ external?: boolean;
39
+ }): string | null;
@@ -1,7 +1,7 @@
1
1
  export { extractParamsFromPattern, formatRouteEntry, } from "./route-types/param-extraction.js";
2
2
  export { extractRoutesFromSource } from "./route-types/ast-route-extraction.js";
3
3
  export { generatePerModuleTypesSource, generateRouteTypesSource, } from "./route-types/codegen.js";
4
- export { DEFAULT_EXCLUDE_PATTERNS, type ScanFilter, createScanFilter, findTsFiles, } from "./route-types/scan-filter.js";
4
+ export { DEFAULT_EXCLUDE_PATTERNS, type ScanFilter, createScanFilter, findTsFiles, isInSkippedScanDir, } from "./route-types/scan-filter.js";
5
5
  export { writePerModuleRouteTypes, writePerModuleRouteTypesForFile, } from "./route-types/per-module-writer.js";
6
6
  export { type UnresolvableReason, type UnresolvableInclude, extractIncludesWithDiagnostics, } from "./route-types/include-resolution.js";
7
7
  export { extractUrlsFromRouter, extractBasenameFromRouter, type UrlsExtractionResult, buildCombinedRouteMapForRouterFile, detectUnresolvableIncludes, detectUnresolvableIncludesForUrlsFile, findNestedRouterConflict, formatNestedRouterConflictError, findRouterFiles, findHostRouterFiles, writeCombinedRouteTypes, genFileTsPath, resolveSearchSchemas, } from "./route-types/router-processing.js";
@@ -1,4 +1,4 @@
1
- import type { ScanFilter } from "./scan-filter.js";
1
+ import { type ScanFilter } from "./scan-filter.js";
2
2
  import { type UnresolvableInclude } from "./include-resolution.js";
3
3
  export declare function findNestedRouterConflict(routerFiles: string[]): {
4
4
  ancestor: string;
@@ -11,7 +11,27 @@ export declare function createScanFilter(root: string, opts: {
11
11
  exclude?: string[];
12
12
  }): ScanFilter | undefined;
13
13
  /**
14
- * Recursively find .ts/.tsx files under a directory, skipping node_modules
15
- * and .gen. files.
14
+ * Directory names no discovery scan enters: findTsFiles() and the router scans
15
+ * (findRouterFiles/findHostRouterFiles, router-processing.ts) all build their
16
+ * skip rule on this, so a file below one is never discovered. Dot-directories
17
+ * cover a nested git worktree, a tool cache, or a test's working copy of
18
+ * another app.
19
+ */
20
+ export declare function isUniversallySkippedScanDir(name: string): boolean;
21
+ /**
22
+ * Whether a file under `root` sits in a directory every discovery scan skips
23
+ * (isUniversallySkippedScanDir). The dev route-file watcher uses it to ignore
24
+ * files discovery would never find. A path outside `root` is false.
25
+ */
26
+ export declare function isInSkippedScanDir(root: string, absolutePath: string): boolean;
27
+ /**
28
+ * Directory names the source-file scans skip: findTsFiles() and the build
29
+ * loader pre-scan (expose-internal-ids.ts). The isUniversallySkippedScanDir()
30
+ * set plus `build`, which the router scans still enter.
31
+ */
32
+ export declare function isSkippedSourceScanDir(name: string): boolean;
33
+ /**
34
+ * Recursively find .ts/.tsx files under a directory, skipping the directories
35
+ * isSkippedSourceScanDir() names and .gen. files.
16
36
  */
17
37
  export declare function findTsFiles(dir: string, filter?: ScanFilter): string[];
@@ -53,6 +53,7 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
53
53
  private readonly tagPurge?;
54
54
  private readonly tagInvalidationTtl?;
55
55
  private readonly tagCacheTtl;
56
+ private readonly kvFallThrough;
56
57
  constructor(options: CFCacheStoreOptions<TEnv>);
57
58
  /**
58
59
  * Warn about a namespace-scoped misconfiguration once per namespace per
@@ -350,7 +351,10 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
350
351
  * dropped and read from the store (SWR recapture scheduling and each mode's
351
352
  * invalidation semantics stay with the store read). That read keeps the
352
353
  * caller's `tagHints`: cfTagHints may have evicted the key while the memo
353
- * still holds it.
354
+ * still holds it. A shell a tag marker rejects is dropped and read from
355
+ * the store too, which may hold a capture from after the marker (see
356
+ * get(), "Tag invalidation"), unless the request's own mask rejected it
357
+ * (declinesKvFallThrough).
354
358
  */
355
359
  private readMemoizedShell;
356
360
  /**
@@ -577,6 +581,17 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
577
581
  * @internal
578
582
  */
579
583
  private isL1Invalidated;
584
+ /**
585
+ * Whether a rejected L1 copy skips the KV read: always unless the
586
+ * `kvFallThrough` option is on, and with it on when this request's own
587
+ * updateTag()/revalidateTag() (its mask, request-tag-mask.ts) rejects an
588
+ * entry tagged at `taggedAt`. KV holds a newer entry only if something
589
+ * rendered after this request's invalidation, while the request re-renders
590
+ * every entry it masked, so the read would add one KV get per masked entry
591
+ * to that re-render.
592
+ * @internal
593
+ */
594
+ private declinesKvFallThrough;
580
595
  /**
581
596
  * Synthetic Cache API request for a tag's L1-cached invalidation marker.
582
597
  * Unversioned for the same reason as tagMarkerKey; the cache namespace
@@ -51,8 +51,11 @@ export interface CFCacheReadDebugEvent {
51
51
  * - kv-timeout: the L2/KV read budget fired (read abandoned, NOT a genuine
52
52
  * absence -- distinct from kv-miss so a degradation signal is separable)
53
53
  * - tag-invalidated: a live L1/KV entry whose cache tags were invalidated
54
- * after it was written -- treated as a miss so the next render re-populates
55
- * it (the tag-invalidation read path, distinct from a plain miss)
54
+ * after it was written -- never served (the tag-invalidation read path,
55
+ * distinct from a plain miss). With `kvFallThrough` on, an L1 copy then
56
+ * reads L2/KV, so a kv-* event follows unless the request invalidated the
57
+ * tag itself; from L2/KV it is a miss and the next render re-populates
58
+ * the entry
56
59
  * - error: the read threw
57
60
  */
58
61
  outcome: "l1-fresh" | "l1-stale-revalidate" | "l1-revalidating-guarded" | "match-timeout" | "match-error" | "body-timeout" | "body-error" | "non-200" | "tag-invalidated" | "l1-miss" | "kv-fresh" | "kv-stale" | "kv-stale-suppressed" | "kv-miss" | "kv-timeout" | "error";
@@ -335,6 +338,24 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
335
338
  * <= 0) to disable and always await KV.
336
339
  */
337
340
  kvReadTimeoutMs?: number;
341
+ /**
342
+ * After `updateTag()`/`revalidateTag()`, a location whose own L1 copy a tag
343
+ * marker rejects reads KV next, and serves the entry another location (or a
344
+ * `router.prerender()` warm) wrote after the invalidation, instead of
345
+ * rendering again. Covers `cache()` records, `"use cache"` results, cached
346
+ * responses and `ppr` shells. The request that invalidated the tag still
347
+ * re-renders its own entries without the read.
348
+ *
349
+ * Cost: one KV get per rejected copy per location after each invalidation
350
+ * (150 ms added wait, measured in #1069 locally at 50 ms per KV read on a
351
+ * document with a shell, a record and one `"use cache"` result, every copy
352
+ * rejected; not measured on a deployed worker), paid even when KV holds only
353
+ * the old twin. Worth it where re-rendering costs more than a KV read
354
+ * (several locations, expensive renders). Needs `kv`; ignored without it.
355
+ *
356
+ * Defaults to false: a rejected L1 copy is a miss and the location renders.
357
+ */
358
+ kvFallThrough?: boolean;
338
359
  /**
339
360
  * Per-isolate memos ({@link StoreMemoOptions}), one set per isolate and
340
361
  * shared by every CFCacheStore in it. `shellMs` (default 2000) and
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Document cache storage policy: which responses the document cache stores.
3
+ * A leaf (types and the per-client signal only), so request-scoped code can
4
+ * ask whether a render will be stored (documentCacheStoresRender) without
5
+ * importing the cache middleware.
6
+ */
7
+ import type { RequestContext } from "../server/request-context.js";
8
+ export interface CacheDirectives {
9
+ sMaxAge?: number;
10
+ staleWhileRevalidate?: number;
11
+ }
12
+ /**
13
+ * Check if response should be cached based on Cache-Control headers
14
+ */
15
+ export declare function shouldCacheResponse(response: Response, requestCtx?: Pick<RequestContext, "_payloadVisitorTheme" | "_payloadDeferred" | "_payloadVisitorLocation">): CacheDirectives | null;
16
+ /**
17
+ * Whether this cache will store the response the current render produces, as
18
+ * far as the render can tell: the middleware runs it (a MISS or a stale
19
+ * refresh) and the response stub already opts in. payloadInitialTheme
20
+ * (rsc/full-payload.ts) then renders the no-cookie default theme (#978). A
21
+ * Cache-Control written after next() is not on the stub yet; for that render
22
+ * shouldCacheResponse refuses a payload marked `_payloadVisitorTheme`.
23
+ */
24
+ export declare function documentCacheStoresRender(reqCtx: Pick<RequestContext, "_documentCacheRender" | "res">): boolean;