@wular/pnext 0.0.1

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 (371) hide show
  1. package/README.md +153 -0
  2. package/bin/pnext +67 -0
  3. package/config/lint/base.js +48 -0
  4. package/config/ts/base.json +26 -0
  5. package/config/ts/react.json +13 -0
  6. package/package.json +70 -0
  7. package/reference/compat.md +63 -0
  8. package/reference/config.md +120 -0
  9. package/reference/css.md +58 -0
  10. package/reference/dev.md +69 -0
  11. package/reference/env.md +40 -0
  12. package/reference/metadata.md +86 -0
  13. package/reference/navigation.md +149 -0
  14. package/reference/overview.md +35 -0
  15. package/reference/performance.md +97 -0
  16. package/reference/rendering.md +127 -0
  17. package/reference/routing.md +167 -0
  18. package/reference/typegen.md +64 -0
  19. package/src/api/cache.ts +80 -0
  20. package/src/api/client-cache.ts +9 -0
  21. package/src/api/client-navigation.ts +279 -0
  22. package/src/api/dynamic.tsx +102 -0
  23. package/src/api/link.tsx +119 -0
  24. package/src/api/navigation.ts +198 -0
  25. package/src/api/router/events.ts +53 -0
  26. package/src/api/router/history.ts +70 -0
  27. package/src/api/router/hub.ts +194 -0
  28. package/src/api/router/policies.ts +107 -0
  29. package/src/api/router/runtime.ts +5238 -0
  30. package/src/api/router/types.ts +299 -0
  31. package/src/api/router.ts +167 -0
  32. package/src/api/server.ts +323 -0
  33. package/src/api/suspense.ts +16 -0
  34. package/src/cache/context.ts +61 -0
  35. package/src/cli/adapters/vercel-warm.ts +375 -0
  36. package/src/cli/adapters/vercel.ts +1310 -0
  37. package/src/cli/analyze-print.ts +181 -0
  38. package/src/cli/analyze.ts +328 -0
  39. package/src/cli/boot-trace.ts +29 -0
  40. package/src/cli/build.ts +3114 -0
  41. package/src/cli/dev.ts +276 -0
  42. package/src/cli/index.ts +196 -0
  43. package/src/cli/named-bin.ts +119 -0
  44. package/src/cli/serve-ui.ts +160 -0
  45. package/src/cli/start.ts +1425 -0
  46. package/src/client/build.ts +2136 -0
  47. package/src/client/chunk-fold.ts +526 -0
  48. package/src/client/entry.ts +1525 -0
  49. package/src/client/paths.ts +22 -0
  50. package/src/client/prebuilt.ts +621 -0
  51. package/src/client/profile.ts +75 -0
  52. package/src/client/react-compiler.ts +94 -0
  53. package/src/client/reference-stub.ts +145 -0
  54. package/src/client/reference.ts +47 -0
  55. package/src/compat/actions/action-client.ts +531 -0
  56. package/src/compat/actions/action-dispatch.ts +676 -0
  57. package/src/compat/actions/action-router.ts +40 -0
  58. package/src/compat/actions/action-shared.ts +95 -0
  59. package/src/compat/actions/client-plugin.ts +135 -0
  60. package/src/compat/actions/client-stub.ts +65 -0
  61. package/src/compat/actions/config.ts +164 -0
  62. package/src/compat/actions/detect.ts +208 -0
  63. package/src/compat/actions/discovery.ts +244 -0
  64. package/src/compat/actions/early-submit.ts +40 -0
  65. package/src/compat/actions/endpoint.ts +602 -0
  66. package/src/compat/actions/flight.ts +52 -0
  67. package/src/compat/actions/form-state.ts +73 -0
  68. package/src/compat/actions/hoist.ts +485 -0
  69. package/src/compat/actions/ids.ts +39 -0
  70. package/src/compat/actions/index.ts +41 -0
  71. package/src/compat/actions/instances.ts +144 -0
  72. package/src/compat/actions/origin.ts +109 -0
  73. package/src/compat/actions/protocol.ts +125 -0
  74. package/src/compat/actions/registry.ts +74 -0
  75. package/src/compat/actions/rewrite.ts +282 -0
  76. package/src/compat/actions/serve.ts +414 -0
  77. package/src/compat/actions/server-tag.ts +21 -0
  78. package/src/compat/actions/unrecognized-error.ts +30 -0
  79. package/src/compat/adapter/build-complete.ts +257 -0
  80. package/src/compat/bundler/bun-externals.ts +53 -0
  81. package/src/compat/bundler/cjs-exports.ts +542 -0
  82. package/src/compat/bundler/config.ts +363 -0
  83. package/src/compat/bundler/externals.ts +34 -0
  84. package/src/compat/bundler/import-meta-url.ts +60 -0
  85. package/src/compat/bundler/modularize-imports.ts +119 -0
  86. package/src/compat/bundler/new-url-asset.ts +87 -0
  87. package/src/compat/bundler/optimize-package-imports.ts +273 -0
  88. package/src/compat/bundler/polyfill.ts +88 -0
  89. package/src/compat/bundler/react-compiler.ts +61 -0
  90. package/src/compat/bundler/react-profiler.tsx +25 -0
  91. package/src/compat/bundler/relay-transform.ts +116 -0
  92. package/src/compat/bundler/require-context.ts +281 -0
  93. package/src/compat/bundler/resolve-extensions.ts +75 -0
  94. package/src/compat/bundler/source-cache.ts +61 -0
  95. package/src/compat/bundler/static-imports.ts +25 -0
  96. package/src/compat/bundler/symlink-imports.ts +119 -0
  97. package/src/compat/bundler/tsconfig-paths.ts +50 -0
  98. package/src/compat/bundler/wasm.ts +153 -0
  99. package/src/compat/bundler/webpack-loaders.ts +685 -0
  100. package/src/compat/bundler/worker.ts +278 -0
  101. package/src/compat/cache/build-flags.ts +81 -0
  102. package/src/compat/cache/build-prerender-errors.ts +163 -0
  103. package/src/compat/cache/custom-handler.ts +159 -0
  104. package/src/compat/cache/fetch-patch.ts +745 -0
  105. package/src/compat/cache/handler.ts +100 -0
  106. package/src/compat/cache/modern-handler.ts +275 -0
  107. package/src/compat/cache/resume-data-cache.ts +143 -0
  108. package/src/compat/cache/revalidate.ts +759 -0
  109. package/src/compat/cache/runtime-error.ts +124 -0
  110. package/src/compat/cache/use-cache-transform.ts +961 -0
  111. package/src/compat/cache/use-cache.ts +1695 -0
  112. package/src/compat/cache-control.ts +269 -0
  113. package/src/compat/client/base-path.ts +64 -0
  114. package/src/compat/client/css-order.ts +36 -0
  115. package/src/compat/client/errors/control-flow.ts +92 -0
  116. package/src/compat/client/errors/error-boundary.ts +222 -0
  117. package/src/compat/client/errors/global-error.ts +238 -0
  118. package/src/compat/client/errors/install.ts +217 -0
  119. package/src/compat/client/errors/lazy.ts +53 -0
  120. package/src/compat/client/errors/primitive-throw.ts +126 -0
  121. package/src/compat/client/errors/soft-refresh.ts +14 -0
  122. package/src/compat/client/link-status.ts +86 -0
  123. package/src/compat/client/nav-compat-runtime.ts +57 -0
  124. package/src/compat/client/nav-compat.ts +42 -0
  125. package/src/compat/client/navigation-scroll.ts +154 -0
  126. package/src/compat/client/optimistic-routing.ts +206 -0
  127. package/src/compat/client/prefetch-cache.ts +111 -0
  128. package/src/compat/client/route-announcer.ts +72 -0
  129. package/src/compat/client/segment-cache-policy.ts +159 -0
  130. package/src/compat/client/segment-cache.ts +1077 -0
  131. package/src/compat/client/segment-prefetch.ts +375 -0
  132. package/src/compat/client/trailing-slash.ts +24 -0
  133. package/src/compat/css/chunking.ts +254 -0
  134. package/src/compat/css/inline-css.ts +73 -0
  135. package/src/compat/css/lightningcss.ts +90 -0
  136. package/src/compat/css/modules.ts +373 -0
  137. package/src/compat/css/nonce.ts +30 -0
  138. package/src/compat/css/sass-plugin.ts +65 -0
  139. package/src/compat/css/sass.ts +392 -0
  140. package/src/compat/css/styled-jsx-runtime.ts +80 -0
  141. package/src/compat/css/styled-jsx.ts +49 -0
  142. package/src/compat/edge-runtime.ts +71 -0
  143. package/src/compat/export/client.ts +112 -0
  144. package/src/compat/export/index.ts +272 -0
  145. package/src/compat/export/standalone.ts +207 -0
  146. package/src/compat/image-optimizer/cache.ts +119 -0
  147. package/src/compat/image-optimizer/detect.ts +143 -0
  148. package/src/compat/image-optimizer/index.ts +601 -0
  149. package/src/compat/image-optimizer/source.ts +243 -0
  150. package/src/compat/index.ts +458 -0
  151. package/src/compat/lifecycle/after-scope.ts +86 -0
  152. package/src/compat/lifecycle/after.ts +173 -0
  153. package/src/compat/lifecycle/error-funnel.ts +306 -0
  154. package/src/compat/lifecycle/error-serialize.ts +87 -0
  155. package/src/compat/lifecycle/error-ui.ts +167 -0
  156. package/src/compat/lifecycle/instrumentation-client.ts +138 -0
  157. package/src/compat/lifecycle/instrumentation.ts +277 -0
  158. package/src/compat/lifecycle/node-console.ts +19 -0
  159. package/src/compat/lifecycle/testmode.ts +263 -0
  160. package/src/compat/mdx/compile.ts +219 -0
  161. package/src/compat/mdx/next-mdx-stub.ts +46 -0
  162. package/src/compat/mdx/plugin.ts +37 -0
  163. package/src/compat/metadata-route-artifacts.ts +458 -0
  164. package/src/compat/metadata.ts +295 -0
  165. package/src/compat/middleware/manifest.ts +210 -0
  166. package/src/compat/misc/action-return.ts +173 -0
  167. package/src/compat/next/cache.ts +211 -0
  168. package/src/compat/next/canonical-url.ts +35 -0
  169. package/src/compat/next/client-cache.ts +57 -0
  170. package/src/compat/next/client-navigation.ts +313 -0
  171. package/src/compat/next/client-only.ts +3 -0
  172. package/src/compat/next/client-script.tsx +215 -0
  173. package/src/compat/next/client-server.ts +39 -0
  174. package/src/compat/next/config-loader.ts +569 -0
  175. package/src/compat/next/config.ts +29 -0
  176. package/src/compat/next/constants.cjs +6 -0
  177. package/src/compat/next/constants.ts +6 -0
  178. package/src/compat/next/custom-server.ts +236 -0
  179. package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
  180. package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
  181. package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
  182. package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
  183. package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
  184. package/src/compat/next/dynamic.tsx +46 -0
  185. package/src/compat/next/error.tsx +148 -0
  186. package/src/compat/next/font/cache.ts +171 -0
  187. package/src/compat/next/font/google.ts +2 -0
  188. package/src/compat/next/font/index.ts +8 -0
  189. package/src/compat/next/font/local.ts +5 -0
  190. package/src/compat/next/font/runtime-client.ts +71 -0
  191. package/src/compat/next/font/runtime.ts +974 -0
  192. package/src/compat/next/font/shared.ts +281 -0
  193. package/src/compat/next/form.tsx +156 -0
  194. package/src/compat/next/head.tsx +10 -0
  195. package/src/compat/next/headers.ts +247 -0
  196. package/src/compat/next/image/config.ts +196 -0
  197. package/src/compat/next/image/optimizer.ts +96 -0
  198. package/src/compat/next/image/patterns.ts +103 -0
  199. package/src/compat/next/image/shared.ts +141 -0
  200. package/src/compat/next/image/static-metadata.ts +283 -0
  201. package/src/compat/next/image/validate.ts +269 -0
  202. package/src/compat/next/image-client.tsx +215 -0
  203. package/src/compat/next/image-props.ts +575 -0
  204. package/src/compat/next/image-usage.ts +102 -0
  205. package/src/compat/next/image.tsx +56 -0
  206. package/src/compat/next/index.ts +1 -0
  207. package/src/compat/next/legacy-image.tsx +97 -0
  208. package/src/compat/next/link-usage.ts +29 -0
  209. package/src/compat/next/link-validation-transform.ts +200 -0
  210. package/src/compat/next/link.tsx +466 -0
  211. package/src/compat/next/navigation.cjs +21 -0
  212. package/src/compat/next/navigation.ts +188 -0
  213. package/src/compat/next/offline.ts +51 -0
  214. package/src/compat/next/og.ts +324 -0
  215. package/src/compat/next/optimistic-route-state.ts +188 -0
  216. package/src/compat/next/preferred-region.ts +39 -0
  217. package/src/compat/next/redirects.ts +131 -0
  218. package/src/compat/next/resource-hints.ts +136 -0
  219. package/src/compat/next/rewrites.ts +350 -0
  220. package/src/compat/next/root-params.ts +142 -0
  221. package/src/compat/next/router.cjs +49 -0
  222. package/src/compat/next/router.ts +143 -0
  223. package/src/compat/next/script.tsx +355 -0
  224. package/src/compat/next/server-only.ts +3 -0
  225. package/src/compat/next/server.ts +28 -0
  226. package/src/compat/next/svgr.ts +58 -0
  227. package/src/compat/next/telemetry.ts +77 -0
  228. package/src/compat/next/user-agent.ts +100 -0
  229. package/src/compat/next/web-vitals.ts +56 -0
  230. package/src/compat/otel/api.ts +95 -0
  231. package/src/compat/otel/client-trace-metadata.ts +71 -0
  232. package/src/compat/otel/fetch-span.ts +77 -0
  233. package/src/compat/otel/tracer.ts +944 -0
  234. package/src/compat/pages/client-plugin.ts +108 -0
  235. package/src/compat/pages/index.ts +527 -0
  236. package/src/compat/pages/router-state.ts +94 -0
  237. package/src/compat/ppr/io.ts +38 -0
  238. package/src/compat/ppr/missing-root-params.ts +105 -0
  239. package/src/compat/ppr/root-params-scan.ts +164 -0
  240. package/src/compat/ppr/root-params-transform.ts +75 -0
  241. package/src/compat/ppr/root-params.ts +129 -0
  242. package/src/compat/ppr/segment-config-incompat.ts +34 -0
  243. package/src/compat/protocol.ts +202 -0
  244. package/src/compat/react/client.ts +59 -0
  245. package/src/compat/react/compiler-runtime.ts +60 -0
  246. package/src/compat/react/dom-client.ts +115 -0
  247. package/src/compat/react/dom-react-server.ts +20 -0
  248. package/src/compat/react/dom-server.ts +40 -0
  249. package/src/compat/react/dom.ts +154 -0
  250. package/src/compat/react/preact.ts +522 -0
  251. package/src/compat/react/react-server.ts +84 -0
  252. package/src/compat/react/router-shim.ts +26 -0
  253. package/src/compat/react/server-component-use.ts +48 -0
  254. package/src/compat/react/server-inserted-html.ts +87 -0
  255. package/src/compat/react/server.ts +156 -0
  256. package/src/compat/react/view-transition.ts +60 -0
  257. package/src/compat/register/actions.ts +875 -0
  258. package/src/compat/register/boot.ts +141 -0
  259. package/src/compat/register/build-tier.ts +11 -0
  260. package/src/compat/register/build.ts +182 -0
  261. package/src/compat/register/bundler.ts +587 -0
  262. package/src/compat/register/cache.ts +85 -0
  263. package/src/compat/register/client-errors.ts +18 -0
  264. package/src/compat/register/config.ts +17 -0
  265. package/src/compat/register/css-extras.ts +120 -0
  266. package/src/compat/register/edge-runtime.ts +6 -0
  267. package/src/compat/register/errors.ts +46 -0
  268. package/src/compat/register/export.ts +23 -0
  269. package/src/compat/register/font.ts +36 -0
  270. package/src/compat/register/hooks.ts +34 -0
  271. package/src/compat/register/image.ts +133 -0
  272. package/src/compat/register/index.ts +111 -0
  273. package/src/compat/register/instrumentation-client.ts +35 -0
  274. package/src/compat/register/lifecycle.ts +86 -0
  275. package/src/compat/register/mdx.ts +48 -0
  276. package/src/compat/register/middleware.ts +36 -0
  277. package/src/compat/register/misc.ts +44 -0
  278. package/src/compat/register/otel.ts +288 -0
  279. package/src/compat/register/pages-api.ts +473 -0
  280. package/src/compat/register/ppr.ts +56 -0
  281. package/src/compat/register/protocol.ts +57 -0
  282. package/src/compat/register/proxy.ts +127 -0
  283. package/src/compat/register/render.ts +268 -0
  284. package/src/compat/register/routing.ts +410 -0
  285. package/src/compat/register/segment.ts +1903 -0
  286. package/src/compat/register/static-image.ts +21 -0
  287. package/src/compat/register/typed-routes.ts +35 -0
  288. package/src/compat/register/usecache.ts +131 -0
  289. package/src/compat/register/validation.ts +56 -0
  290. package/src/compat/segment/loading-boundary.ts +113 -0
  291. package/src/compat/segment/page-slot.ts +200 -0
  292. package/src/compat/segment/tree.ts +481 -0
  293. package/src/compat/segment/vary-key.ts +102 -0
  294. package/src/compat/segment/vary-params.ts +551 -0
  295. package/src/compat/static-params.ts +33 -0
  296. package/src/compat/tsconfig-defaults.ts +301 -0
  297. package/src/compat/typecheck/index.ts +1481 -0
  298. package/src/compat/typecheck/worker.ts +26 -0
  299. package/src/compat/typed-routes/index.ts +92 -0
  300. package/src/compat/typed-routes/manifest.ts +356 -0
  301. package/src/compat/typed-routes/typegen.ts +566 -0
  302. package/src/compat/validation/errors.ts +159 -0
  303. package/src/compat/validation/index.ts +1770 -0
  304. package/src/compat/validation/prerender-diagnostics.ts +1508 -0
  305. package/src/compat-bootstrap.ts +67 -0
  306. package/src/config.ts +218 -0
  307. package/src/css/build.ts +697 -0
  308. package/src/css/index.ts +2 -0
  309. package/src/css/postcss.ts +236 -0
  310. package/src/css/worker.ts +34 -0
  311. package/src/dev/client-actions.ts +35 -0
  312. package/src/dev/client-chunk-store.ts +92 -0
  313. package/src/dev/client-key-cache.ts +178 -0
  314. package/src/dev/global-css-cache.ts +212 -0
  315. package/src/dev/imports.ts +2430 -0
  316. package/src/dev/module-cache.ts +721 -0
  317. package/src/dev/module-generations.ts +38 -0
  318. package/src/dev/module-transform.ts +188 -0
  319. package/src/dev/node-module-bundle-cache.ts +63 -0
  320. package/src/dev/restart-cache.ts +10 -0
  321. package/src/dev/route-bundle-key-cache.ts +154 -0
  322. package/src/dev/route-facts-cache.ts +223 -0
  323. package/src/dev/server.ts +1710 -0
  324. package/src/dynamic/source.ts +307 -0
  325. package/src/dynamic/tree-shake.ts +262 -0
  326. package/src/env.ts +92 -0
  327. package/src/extensions.ts +1898 -0
  328. package/src/index.ts +34 -0
  329. package/src/internal.ts +43 -0
  330. package/src/islands/boundary-error.ts +8 -0
  331. package/src/islands/static-children.ts +37 -0
  332. package/src/islands/static-slots.ts +106 -0
  333. package/src/ppr-postpone.ts +24 -0
  334. package/src/ppr.ts +784 -0
  335. package/src/proxy.ts +752 -0
  336. package/src/render/hooks.ts +384 -0
  337. package/src/render/index.ts +1 -0
  338. package/src/render/island-context.ts +47 -0
  339. package/src/render/metadata.ts +857 -0
  340. package/src/render/renderer.ts +7391 -0
  341. package/src/render/resource-hints.ts +44 -0
  342. package/src/render/slots.tsx +679 -0
  343. package/src/request/context.ts +396 -0
  344. package/src/resolve/engine.ts +219 -0
  345. package/src/resolve/imports.ts +1104 -0
  346. package/src/resolve/scan-facts.ts +474 -0
  347. package/src/resolve/source-text.ts +86 -0
  348. package/src/routing/forwarded.ts +41 -0
  349. package/src/routing/handler.ts +271 -0
  350. package/src/routing/href.ts +203 -0
  351. package/src/routing/metadata.ts +1018 -0
  352. package/src/routing/request-runtime.ts +43 -0
  353. package/src/routing/routes.ts +2560 -0
  354. package/src/routing/slots.ts +432 -0
  355. package/src/runtime/server.ts +3453 -0
  356. package/src/runtime/vendor.ts +1160 -0
  357. package/src/style-modules.d.ts +9 -0
  358. package/src/typegen.ts +151 -0
  359. package/src/types.ts +725 -0
  360. package/src/utils/ansi.ts +9 -0
  361. package/src/utils/content-type.ts +31 -0
  362. package/src/utils/decode.ts +7 -0
  363. package/src/utils/dev-profile.ts +31 -0
  364. package/src/utils/error-log.ts +29 -0
  365. package/src/utils/fs-cache.ts +31 -0
  366. package/src/utils/fs.ts +119 -0
  367. package/src/utils/html.ts +46 -0
  368. package/src/utils/serialize.ts +378 -0
  369. package/src/utils/source.ts +35 -0
  370. package/src/utils/verbose.ts +39 -0
  371. package/tsconfig.json +10 -0
@@ -0,0 +1,745 @@
1
+ /**
2
+ * Next-compat fetch patch: honors `{ cache, next: { revalidate, tags } }`
3
+ * fetch options against the process-wide data cache, records tags/revalidate
4
+ * into the surrounding render's cache meta (so prerendered pages inherit
5
+ * them), and dedupes identical GET fetches within one render pass.
6
+ *
7
+ * Installed once per process by `pnext build` / `pnext start` when Next
8
+ * compat is enabled; plain fetches outside a render scope pass through.
9
+ */
10
+ import { currentCacheScope } from '../../cache/context';
11
+ import {
12
+ cacheComponents,
13
+ hangingPromise,
14
+ isPrerendering,
15
+ realNow,
16
+ trackPrerenderCacheFill,
17
+ } from '../../ppr';
18
+ import { cacheHandlerGet, cacheHandlerSet, hasCacheHandler } from './handler';
19
+ import {
20
+ currentRenderCacheMeta,
21
+ currentRenderIsStaticGeneration,
22
+ draftModeBypassActive,
23
+ recordCacheNoStore,
24
+ recordCacheRevalidate,
25
+ recordCacheTags,
26
+ registerDataCacheInvalidator,
27
+ insideDataCacheProducer,
28
+ currentTagRevalidationSeq,
29
+ rdcProcessId,
30
+ rebaseRdcTagSeq,
31
+ pathScopeRevalidated,
32
+ tagsRevalidatedAfterSeq,
33
+ tagsStaleSince,
34
+ } from './revalidate';
35
+ import { insideUseCacheProducer } from './use-cache';
36
+
37
+ const MAX_TAGS = 128;
38
+
39
+ interface NextFetchOptions {
40
+ revalidate?: number | false;
41
+ tags?: string[];
42
+ }
43
+
44
+ interface FetchSnapshot {
45
+ status: number;
46
+ statusText: string;
47
+ headers: [string, string][];
48
+ body: Uint8Array;
49
+ url: string;
50
+ }
51
+
52
+ interface FetchCacheEntry {
53
+ value: Promise<FetchSnapshot>;
54
+ storedAt: number;
55
+ tags: readonly string[];
56
+ revalidateSeconds?: number;
57
+ /** A background stale-while-revalidate refresh is already in flight. */
58
+ refreshing?: boolean;
59
+ /** Pathname of the render that produced this entry (path-revalidation scope). */
60
+ route?: string;
61
+ /**
62
+ * Other routes that have SERVED this entry. One fetch cache entry is shared by every route issuing the
63
+ * same request, and Next widens its implicit `_N_T_/<route>` soft tag on each read - so
64
+ * revalidatePath('/b') expires an entry that /a produced and /b reused.
65
+ */
66
+ readRoutes?: string[];
67
+ /** Hard-tag-revalidation sequence sampled when this entry was produced. */
68
+ tagSeq: number;
69
+ /**
70
+ * The render cache-meta this entry was produced under: an on-demand
71
+ * regeneration (refreshFetches) refetches each entry once per render, then
72
+ * repeated reads dedupe against it.
73
+ */
74
+ refreshedIn?: object;
75
+ }
76
+
77
+ export interface FetchRdcRecord {
78
+ key: string;
79
+ snapshot: {
80
+ status: number;
81
+ statusText: string;
82
+ headers: [string, string][];
83
+ body: string;
84
+ url: string;
85
+ };
86
+ tags: string[];
87
+ storedAt: number;
88
+ tagSeq: number;
89
+ /** Process that produced this record — `tagSeq` only means anything there. */
90
+ pid?: string;
91
+ revalidateSeconds?: number;
92
+ route?: string;
93
+ }
94
+
95
+ const FETCH_CACHE = Symbol.for('pnext.compat.fetchCache');
96
+ const FETCH_CACHE_INSTALLED = Symbol.for('pnext.compat.fetchCacheInstalled');
97
+
98
+ function fetchCacheStore(): Map<string, FetchCacheEntry> {
99
+ const state = globalThis as Record<PropertyKey, unknown>;
100
+ return (state[FETCH_CACHE] ??= new Map<string, FetchCacheEntry>()) as Map<string, FetchCacheEntry>;
101
+ }
102
+
103
+ function fetchCacheInstalled(): boolean {
104
+ return (globalThis as Record<PropertyKey, unknown>)[FETCH_CACHE_INSTALLED] === true;
105
+ }
106
+
107
+ function markFetchCacheInstalled(): void {
108
+ (globalThis as Record<PropertyKey, unknown>)[FETCH_CACHE_INSTALLED] = true;
109
+ }
110
+
111
+ const fetchCache = fetchCacheStore();
112
+ const fetchCacheLimit = 2048;
113
+ const FETCH_RDC_CAPTURE = Symbol.for('pnext.compat.fetchRdcCapture');
114
+ const FETCH_RDC_RECORDS = Symbol.for('pnext.compat.fetchRdcRecords');
115
+
116
+ function fetchRdcRecords(): Map<string, FetchCacheEntry> {
117
+ const state = globalThis as Record<PropertyKey, unknown>;
118
+ return (state[FETCH_RDC_RECORDS] ??= new Map<string, FetchCacheEntry>()) as Map<
119
+ string,
120
+ FetchCacheEntry
121
+ >;
122
+ }
123
+
124
+ function fetchRdcCapture(): Set<string> | undefined {
125
+ return (globalThis as Record<PropertyKey, unknown>)[FETCH_RDC_CAPTURE] as Set<string> | undefined;
126
+ }
127
+
128
+ function captureFetchRdcEntry(key: string) {
129
+ fetchRdcCapture()?.add(key);
130
+ }
131
+
132
+ export function beginFetchRdcCapture() {
133
+ (globalThis as Record<PropertyKey, unknown>)[FETCH_RDC_CAPTURE] = new Set<string>();
134
+ }
135
+
136
+ export async function collectFetchRdcRecords(): Promise<FetchRdcRecord[]> {
137
+ const capture = fetchRdcCapture();
138
+ if (!capture) return [];
139
+ const records: FetchRdcRecord[] = [];
140
+ for (const key of capture) {
141
+ const entry = fetchCache.get(key);
142
+ if (!entry) continue;
143
+ const snapshot = await entry.value;
144
+ records.push({
145
+ key,
146
+ snapshot: {
147
+ status: snapshot.status,
148
+ statusText: snapshot.statusText,
149
+ headers: [...snapshot.headers],
150
+ body: Buffer.from(snapshot.body).toString('base64'),
151
+ url: snapshot.url,
152
+ },
153
+ tags: [...entry.tags],
154
+ storedAt: entry.storedAt,
155
+ tagSeq: entry.tagSeq,
156
+ pid: rdcProcessId(),
157
+ ...(entry.revalidateSeconds !== undefined ? { revalidateSeconds: entry.revalidateSeconds } : {}),
158
+ ...(entry.route !== undefined ? { route: entry.route } : {}),
159
+ });
160
+ }
161
+ return records;
162
+ }
163
+
164
+ export function seedFetchRdcRecords(
165
+ records: readonly FetchRdcRecord[],
166
+ route?: string,
167
+ routeTags: readonly string[] = [],
168
+ ) {
169
+ for (const record of records) {
170
+ if (!isFetchRdcRecord(record)) continue;
171
+ // An RDC artifact is a snapshot: seeding it must never regress a live
172
+ // entry filled more recently (each regen seeds the stale .rdc first).
173
+ const live = fetchCache.get(record.key);
174
+ if (live !== undefined && live.storedAt > record.storedAt) continue;
175
+ const entry: FetchCacheEntry = {
176
+ value: Promise.resolve({
177
+ status: record.snapshot.status,
178
+ statusText: record.snapshot.statusText,
179
+ headers: [...record.snapshot.headers],
180
+ body: new Uint8Array(Buffer.from(record.snapshot.body, 'base64')),
181
+ url: record.snapshot.url,
182
+ }),
183
+ storedAt: record.storedAt,
184
+ // A fetch made inside a `use cache` scope carries no tags of its own - the scope's cacheTag()s are
185
+ // the invalidation surface. Stamp the route-level tag union on the seeded snapshot so updateTag()
186
+ // of any of those tags invalidates it (slightly over-broad within the route, never
187
+ // under-invalidating).
188
+ tags: [...new Set([...record.tags, ...routeTags])],
189
+ // Foreign-process sequences are meaningless here — see rebaseRdcTagSeq.
190
+ tagSeq: rebaseRdcTagSeq(record),
191
+ ...(record.revalidateSeconds !== undefined
192
+ ? { revalidateSeconds: record.revalidateSeconds }
193
+ : {}),
194
+ ...(route !== undefined ? { route } : record.route !== undefined ? { route: record.route } : {}),
195
+ };
196
+ fetchRdcRecords().set(record.key, entry);
197
+ fetchCache.set(record.key, entry);
198
+ }
199
+ }
200
+
201
+ /** Widen an entry's path-revalidation scope with the route now reading it. */
202
+ function noteFetchEntryRoute(entry: FetchCacheEntry, route: string | undefined): void {
203
+ if (route === undefined || route === entry.route) return;
204
+ const routes = (entry.readRoutes ??= []);
205
+ if (routes.includes(route)) return;
206
+ routes.push(route);
207
+ // A single entry read from an unbounded set of routes (dynamic pathnames)
208
+ // must not grow without limit; the oldest reader's scope is dropped first.
209
+ if (routes.length > 32) routes.shift();
210
+ }
211
+
212
+ /** True when this entry's producing OR any reading route was revalidated. */
213
+ function fetchEntryPathRevalidated(entry: FetchCacheEntry): boolean {
214
+ if (pathScopeRevalidated(entry.route, entry.storedAt)) return true;
215
+ return (entry.readRoutes ?? []).some(route => pathScopeRevalidated(route, entry.storedAt));
216
+ }
217
+
218
+ function isFetchRdcRecord(value: unknown): value is FetchRdcRecord {
219
+ if (!value || typeof value !== 'object') return false;
220
+ const record = value as Partial<FetchRdcRecord>;
221
+ return (
222
+ typeof record.key === 'string' &&
223
+ !!record.snapshot &&
224
+ typeof record.snapshot.status === 'number' &&
225
+ typeof record.snapshot.statusText === 'string' &&
226
+ Array.isArray(record.snapshot.headers) &&
227
+ record.snapshot.headers.every(
228
+ header => Array.isArray(header) && header.length === 2 && header.every(part => typeof part === 'string'),
229
+ ) &&
230
+ typeof record.snapshot.body === 'string' &&
231
+ typeof record.snapshot.url === 'string' &&
232
+ Array.isArray(record.tags) &&
233
+ record.tags.every(tag => typeof tag === 'string') &&
234
+ typeof record.storedAt === 'number' &&
235
+ Number.isFinite(record.storedAt) &&
236
+ typeof record.tagSeq === 'number' &&
237
+ Number.isInteger(record.tagSeq)
238
+ );
239
+ }
240
+
241
+ export function installCompatFetchCache() {
242
+ if (fetchCacheInstalled()) return;
243
+ markFetchCacheInstalled();
244
+ registerDataCacheInvalidator(clearFetchCache);
245
+ const original = globalThis.fetch.bind(globalThis);
246
+
247
+ const patched = async function fetch(
248
+ input: Parameters<typeof globalThis.fetch>[0],
249
+ init?: RequestInit & { next?: NextFetchOptions },
250
+ ): Promise<Response> {
251
+ const normalized = await normalizeFetchArgs(input, init);
252
+ if (!normalized) return original(input, init);
253
+ const { request } = normalized;
254
+ // A consumed stream body was buffered for the cache key; forward the bytes.
255
+ if (normalized.replacementInit) init = { ...init, ...normalized.replacementInit };
256
+
257
+ const nextOptions = init?.next ?? requestNextOptions(input);
258
+ let tags = nextOptions?.tags ?? [];
259
+ if (tags.length > MAX_TAGS) {
260
+ console.warn(
261
+ `Warning: exceeded max tag count for ${request.url}, dropped tags: ${tags
262
+ .slice(MAX_TAGS)
263
+ .join(', ')}`,
264
+ );
265
+ tags = tags.slice(0, MAX_TAGS);
266
+ }
267
+ const revalidate = nextOptions?.revalidate;
268
+ if (
269
+ revalidate !== undefined &&
270
+ revalidate !== false &&
271
+ (typeof revalidate !== 'number' || revalidate < 0 || !Number.isFinite(revalidate))
272
+ ) {
273
+ throw new Error(
274
+ `Invalid revalidate value "${String(revalidate)}" on "${
275
+ currentRenderCacheMeta()?.route ?? request.url
276
+ }", must be a non-negative number or false`,
277
+ );
278
+ }
279
+ recordCacheTags(tags);
280
+ if (typeof revalidate === 'number' && revalidate > 0) recordCacheRevalidate(revalidate);
281
+
282
+ const cacheOption = init?.cache ?? requestCacheOption(input);
283
+ if (
284
+ insideDataCacheProducer() &&
285
+ cacheOption === undefined &&
286
+ revalidate === undefined &&
287
+ tags.length === 0 &&
288
+ // During static generation a plain fetch inside an unstable_cache producer still lands in the
289
+ // incremental cache handler (Next's SSG fetch-sharing optimization; build logs are asserted). At
290
+ // runtime the outer cache boundary memoizes, so bypass. A `use cache` producer's plain fetches
291
+ // ALWAYS bypass: they belong to the outer entry, and the URL-keyed cache would wrongly share them
292
+ // across entries whose keys differ.
293
+ (!currentRenderIsStaticGeneration() || insideUseCacheProducer())
294
+ ) {
295
+ return original(input, init);
296
+ }
297
+
298
+ const segmentFetchCache = currentRenderCacheMeta()?.fetchCache;
299
+ const segmentForcesCache =
300
+ segmentFetchCache === 'force-cache' || segmentFetchCache === 'only-cache';
301
+ // Explicit no-store signals opt the surrounding route out of prebuilt
302
+ // static output (Next renders it dynamically per request) — unless the
303
+ // segment's fetchCache forces caching, which supersedes them.
304
+ if (
305
+ !segmentForcesCache &&
306
+ (cacheOption === 'no-store' || cacheOption === 'no-cache' || revalidate === 0)
307
+ ) {
308
+ recordCacheNoStore();
309
+ }
310
+ // Route handlers never auto-cache an implicit/default fetch (Next treats
311
+ // both forms as uncached), and it keeps the handler out of prebuilt static
312
+ // output. Pages are unaffected: their ISR output freezes the data at
313
+ // prerender time.
314
+ const handlerDefaultFetch =
315
+ (cacheOption === undefined || cacheOption === 'default') &&
316
+ currentRenderCacheMeta()?.handler === true;
317
+ if (handlerDefaultFetch && !segmentForcesCache) recordCacheNoStore();
318
+
319
+ const mode = cacheModeFor(
320
+ cacheOption,
321
+ revalidate,
322
+ segmentFetchCache,
323
+ request.method,
324
+ currentRenderIsStaticGeneration() && !handlerDefaultFetch,
325
+ );
326
+ if (
327
+ mode !== 'cache' &&
328
+ cacheComponents() &&
329
+ isPrerendering() &&
330
+ !insideDataCacheProducer()
331
+ ) {
332
+ return hangingPromise<Response>('fetch()');
333
+ }
334
+ if (mode === 'bypass') return original(input, init);
335
+
336
+ const key = fetchCacheKey(request);
337
+
338
+ if (mode === 'cache' && !draftModeBypassActive()) {
339
+ const meta = currentRenderCacheMeta();
340
+ const resumeEntry = fetchRdcRecords().get(key);
341
+ if (
342
+ resumeEntry &&
343
+ !tagsRevalidatedAfterSeq(
344
+ [...resumeEntry.tags, ...tags],
345
+ resumeEntry.tagSeq,
346
+ ) &&
347
+ !fetchEntryPathRevalidated(resumeEntry) &&
348
+ (fetchCache.get(key)?.storedAt ?? -1) <= resumeEntry.storedAt
349
+ ) {
350
+ fetchCache.set(key, resumeEntry);
351
+ }
352
+ const preRefreshEntry = fetchCache.get(key);
353
+ // On-demand revalidation refetches each entry ONCE per render (Next skips the cache read entirely
354
+ // under isOnDemandRevalidate), then dedupes via refreshedIn - but only entries the revalidation
355
+ // actually reached. See the matching gate in use-cache.ts: a still-marked route re-renders many
356
+ // times before its prebuilt HTML is written back.
357
+ const refresh =
358
+ meta?.refreshFetches === true &&
359
+ preRefreshEntry?.refreshedIn !== meta &&
360
+ (preRefreshEntry === undefined ||
361
+ tagsRevalidatedAfterSeq(
362
+ [...preRefreshEntry.tags, ...tags],
363
+ preRefreshEntry.tagSeq,
364
+ ) ||
365
+ fetchEntryPathRevalidated(preRefreshEntry) ||
366
+ tagsStaleSince(preRefreshEntry.tags, preRefreshEntry.storedAt) ||
367
+ fetchEntryExpired(preRefreshEntry));
368
+ const revalidateSeconds =
369
+ typeof revalidate === 'number' && revalidate > 0 ? revalidate : undefined;
370
+ const blockingStale = currentRenderCacheMeta()?.blockingStaleFetches === true;
371
+ // Seed the in-memory entry from the configured cache handler on a miss,
372
+ // so the handler observes every read (its get() logs are asserted) and
373
+ // persists entries across the SWR window / restarts.
374
+ let entry = fetchCache.get(key);
375
+ if (!entry && !refresh && hasCacheHandler()) {
376
+ const persisted = await cacheHandlerGet(key, { tags });
377
+ const snapshot = persistedSnapshot(persisted?.value);
378
+ if (snapshot) {
379
+ entry = {
380
+ value: Promise.resolve(snapshot),
381
+ storedAt: persisted?.lastModified ?? realNow(),
382
+ tags: persisted?.tags ?? tags,
383
+ revalidateSeconds,
384
+ tagSeq: currentTagRevalidationSeq(),
385
+ ...(currentRenderCacheMeta()?.route !== undefined
386
+ ? { route: currentRenderCacheMeta()?.route }
387
+ : {}),
388
+ };
389
+ fetchCache.set(key, entry);
390
+ }
391
+ }
392
+ // This render's route joins the entry's path-revalidation scope before
393
+ // the freshness check, so revalidatePath() of a route that only REUSES a
394
+ // shared entry expires it too.
395
+ if (entry) noteFetchEntryRoute(entry, meta?.route);
396
+ if (
397
+ entry &&
398
+ !refresh &&
399
+ !tagsRevalidatedAfterSeq([...entry.tags, ...tags], entry.tagSeq) &&
400
+ !fetchEntryPathRevalidated(entry)
401
+ ) {
402
+ const tagStale = tagsStaleSince(entry.tags, entry.storedAt);
403
+ // Staleness honours THIS call's revalidate, not only the value baked
404
+ // into the stored entry. The data cache is shared by URL across routes,
405
+ // so a shorter-lived reader (e.g. a `revalidate: 9` fetch) can find an
406
+ // entry first populated by a longer-lived / unversioned reader (a
407
+ // default fetch stores `revalidateSeconds: undefined`, which never
408
+ // expires). Without folding the current revalidate into the check, that
409
+ // reader's window would be silently ignored and its page would never
410
+ // refresh (app-static variable-config-revalidate under a full suite).
411
+ if (!tagStale && !fetchEntryExpired(entry) && !currentRequestExpired(entry, revalidateSeconds)) {
412
+ captureFetchRdcEntry(key);
413
+ return snapshotResponse(await entry.value);
414
+ }
415
+ // TTL expiry: serve stale, refresh in the background (Next's SWR
416
+ // data-cache semantics) — except inside an ISR regeneration render,
417
+ // which refetches inline. On-demand invalidation above blocks too.
418
+ if (!blockingStale) {
419
+ if (!entry.refreshing) {
420
+ entry.refreshing = true;
421
+ void storeFetchEntry(key, original, input, init, tags, revalidateSeconds, entry);
422
+ }
423
+ captureFetchRdcEntry(key);
424
+ return snapshotResponse(await entry.value);
425
+ }
426
+ }
427
+ const value = storeFetchEntry(key, original, input, init, tags, revalidateSeconds);
428
+ return snapshotResponse(await value);
429
+ }
430
+
431
+ // Uncached, but dedupe identical GETs within the render pass.
432
+ const scope = currentCacheScope();
433
+ if (!scope) return original(input, init);
434
+ scope.fetches ??= new Map();
435
+ let pending = scope.fetches.get(key) as Promise<FetchSnapshot> | undefined;
436
+ if (!pending) {
437
+ pending = snapshotFetch(original, input, init);
438
+ scope.fetches.set(key, pending);
439
+ }
440
+ return snapshotResponse(await pending);
441
+ };
442
+
443
+ globalThis.fetch = patched as typeof globalThis.fetch;
444
+ }
445
+
446
+ interface NormalizedFetchRequest {
447
+ url: string;
448
+ method: string;
449
+ headers: [string, string][];
450
+ body?: string;
451
+ }
452
+
453
+ interface NormalizedFetchArgs {
454
+ request: NormalizedFetchRequest;
455
+ /** Set when a one-shot body (stream) was buffered and must be re-sent as bytes. */
456
+ replacementInit?: { body: BodyInit };
457
+ }
458
+
459
+ async function normalizeFetchArgs(
460
+ input: Parameters<typeof globalThis.fetch>[0],
461
+ init?: RequestInit,
462
+ ): Promise<NormalizedFetchArgs | null> {
463
+ try {
464
+ let url: string;
465
+ let method = 'GET';
466
+ let headers: Headers;
467
+
468
+ if (typeof input === 'string' || input instanceof URL) {
469
+ url = String(input);
470
+ method = (init?.method ?? 'GET').toUpperCase();
471
+ headers = new Headers(init?.headers);
472
+ } else if (input instanceof Request) {
473
+ if (input.body != null) return null;
474
+ url = input.url;
475
+ method = (init?.method ?? input.method).toUpperCase();
476
+ headers = new Headers(init?.headers ?? input.headers);
477
+ } else {
478
+ return null;
479
+ }
480
+ if (!/^https?:/.test(url)) return null;
481
+
482
+ const normalizedBody = await normalizeFetchBody(init?.body);
483
+ if (normalizedBody === null) return null;
484
+ return {
485
+ request: { url, method, headers: [...headers.entries()], body: normalizedBody?.key },
486
+ ...(normalizedBody?.replacement !== undefined
487
+ ? { replacementInit: { body: normalizedBody.replacement } }
488
+ : {}),
489
+ };
490
+ } catch {
491
+ return null;
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Body to stable key string. Streams are buffered (and must be re-sent as the buffered bytes); FormData
497
+ * is keyed by its entries, since the multipart boundary is random and serializing the whole body would
498
+ * defeat caching; Blob and unknown shapes return null (bypass).
499
+ */
500
+ async function normalizeFetchBody(
501
+ body: BodyInit | null | undefined,
502
+ ): Promise<{ key: string; replacement?: BodyInit } | undefined | null> {
503
+ if (body == null) return undefined;
504
+ if (typeof body === 'string') return { key: body };
505
+ if (body instanceof URLSearchParams) return { key: body.toString() };
506
+ if (body instanceof ArrayBuffer) return { key: bytesKey(new Uint8Array(body)) };
507
+ if (ArrayBuffer.isView(body)) {
508
+ return { key: bytesKey(new Uint8Array(body.buffer, body.byteOffset, body.byteLength)) };
509
+ }
510
+ if (body instanceof ReadableStream) {
511
+ const bytes = new Uint8Array(await new Response(body).arrayBuffer());
512
+ return { key: bytesKey(bytes), replacement: bytes as BodyInit };
513
+ }
514
+ if (body instanceof FormData) {
515
+ // Reading entries does not consume the body; fetch re-serializes it.
516
+ const parts: string[] = [];
517
+ for (const [name, value] of body.entries()) {
518
+ parts.push(name, typeof value === 'string' ? value : await (value as Blob).text());
519
+ }
520
+ return { key: parts.join(' ') };
521
+ }
522
+ return null;
523
+ }
524
+
525
+ function bytesKey(bytes: Uint8Array) {
526
+ return Buffer.from(bytes).toString('base64');
527
+ }
528
+
529
+ function requestNextOptions(input: unknown): NextFetchOptions | undefined {
530
+ return (input as { next?: NextFetchOptions } | null | undefined)?.next;
531
+ }
532
+
533
+ function requestCacheOption(input: unknown): RequestCache | undefined {
534
+ if (input instanceof Request) return input.cache;
535
+ return undefined;
536
+ }
537
+
538
+ type CacheMode = 'cache' | 'dedupe' | 'bypass';
539
+
540
+ function cacheModeFor(
541
+ cacheOption: RequestCache | undefined,
542
+ revalidate: number | false | undefined,
543
+ segmentFetchCache: string | undefined,
544
+ method: string,
545
+ staticGeneration: boolean,
546
+ ): CacheMode {
547
+ if (segmentFetchCache === 'force-no-store' || segmentFetchCache === 'only-no-store') {
548
+ return 'dedupe';
549
+ }
550
+ // Segment-level force-cache supersedes even explicit no-store fetch options.
551
+ if (segmentFetchCache === 'force-cache' || segmentFetchCache === 'only-cache') {
552
+ return revalidate === 0 ? 'dedupe' : 'cache';
553
+ }
554
+ if (cacheOption === 'no-store' || cacheOption === 'no-cache' || revalidate === 0) {
555
+ return 'dedupe';
556
+ }
557
+ if (
558
+ cacheOption === 'force-cache' ||
559
+ revalidate === false ||
560
+ (typeof revalidate === 'number' && revalidate > 0)
561
+ ) {
562
+ return 'cache';
563
+ }
564
+ // Auto (no explicit option): uncached per-request at runtime, but during
565
+ // static generation a plain GET defaults to force-cache (Next semantics).
566
+ // POST/PUT/etc. with a request init are never cached.
567
+ //
568
+ // cacheComponents drops that implicit caching: an uncached fetch is DYNAMIC
569
+ // data, so a prerender postpones the boundary around it instead of baking the
570
+ // response in (cache-components-errors dynamic-boundary expects the prerender
571
+ // to keep its `<Suspense>` fallbacks).
572
+ if (staticGeneration && method === 'GET' && !cacheComponents()) return 'cache';
573
+ return 'dedupe';
574
+ }
575
+
576
+ function fetchCacheKey(request: NormalizedFetchRequest): string {
577
+ const headers = request.headers
578
+ .filter(([name]) => name !== 'traceparent' && name !== 'tracestate')
579
+ .sort((a, b) => a[0].localeCompare(b[0]));
580
+ return JSON.stringify([request.method, request.url, headers, request.body ?? null]);
581
+ }
582
+
583
+ function fetchEntryExpired(entry: FetchCacheEntry) {
584
+ return (
585
+ entry.revalidateSeconds !== undefined &&
586
+ realNow() - entry.storedAt >= entry.revalidateSeconds * 1000
587
+ );
588
+ }
589
+
590
+ // Staleness as seen by the CURRENT fetch call: its own `revalidate` window,
591
+ // applied against the (URL-shared) entry's age. Complements fetchEntryExpired,
592
+ // which only knows the window baked in when the entry was stored — a later
593
+ // reader with a shorter revalidate must still see its own expiry.
594
+ function currentRequestExpired(entry: FetchCacheEntry, revalidateSeconds: number | undefined) {
595
+ return (
596
+ revalidateSeconds !== undefined && realNow() - entry.storedAt >= revalidateSeconds * 1000
597
+ );
598
+ }
599
+
600
+ function storeFetchEntry(
601
+ key: string,
602
+ original: typeof globalThis.fetch,
603
+ input: Parameters<typeof globalThis.fetch>[0],
604
+ init: RequestInit | undefined,
605
+ tags: readonly string[],
606
+ revalidateSeconds: number | undefined,
607
+ serveStale?: FetchCacheEntry,
608
+ ): Promise<FetchSnapshot> {
609
+ const value = snapshotFetch(original, input, init);
610
+ // Same contract as the 'use cache' / data-cache fills: a build prerender's
611
+ // task boundary waits for an in-flight CACHED fetch rather than cutting it
612
+ // short, so its result still lands in the static shell (core ppr).
613
+ void trackPrerenderCacheFill(value);
614
+ const producingMeta = currentRenderCacheMeta();
615
+ const route = serveStale?.route ?? producingMeta?.route;
616
+ const stored: FetchCacheEntry = {
617
+ value,
618
+ // realNow: a build prerender freezes Date.now() to 0 (prerender
619
+ // determinism); a 0-stamped entry persisted into the resume-data sidecar
620
+ // reads as older than every revalidation mark forever.
621
+ storedAt: realNow(),
622
+ tags,
623
+ revalidateSeconds,
624
+ tagSeq: currentTagRevalidationSeq(),
625
+ ...(producingMeta !== undefined ? { refreshedIn: producingMeta } : {}),
626
+ ...(route !== undefined ? { route } : {}),
627
+ // A background refresh replaces the entry: the routes that had joined its
628
+ // revalidation scope keep it (they still read this key).
629
+ ...(serveStale?.readRoutes?.length ? { readRoutes: [...serveStale.readRoutes] } : {}),
630
+ };
631
+ if (serveStale) {
632
+ value.then(
633
+ snapshot => {
634
+ if (snapshot.status === 200) {
635
+ fetchCache.set(key, stored);
636
+ void persistFetchEntry(key, snapshot, tags, revalidateSeconds);
637
+ } else serveStale.refreshing = false;
638
+ },
639
+ () => {
640
+ serveStale.refreshing = false;
641
+ },
642
+ );
643
+ return value;
644
+ }
645
+ if (fetchCache.size >= fetchCacheLimit) {
646
+ const oldest = fetchCache.keys().next().value;
647
+ if (oldest !== undefined) fetchCache.delete(oldest);
648
+ }
649
+ fetchCache.set(key, stored);
650
+ captureFetchRdcEntry(key);
651
+ value.then(
652
+ snapshot => {
653
+ // Only 200s are cached (Next does not cache error statuses).
654
+ if (snapshot.status !== 200 && fetchCache.get(key) === stored) fetchCache.delete(key);
655
+ else void persistFetchEntry(key, snapshot, tags, revalidateSeconds);
656
+ },
657
+ () => {
658
+ if (fetchCache.get(key) === stored) fetchCache.delete(key);
659
+ },
660
+ );
661
+ return value;
662
+ }
663
+
664
+ /** Snapshot <-> serializable value stored through the custom cache handler. */
665
+ interface PersistedFetch {
666
+ status: number;
667
+ statusText: string;
668
+ headers: [string, string][];
669
+ body: string;
670
+ url: string;
671
+ }
672
+
673
+ function persistedSnapshot(value: unknown): FetchSnapshot | undefined {
674
+ if (!value || typeof value !== 'object') return undefined;
675
+ const v = value as Partial<PersistedFetch>;
676
+ if (typeof v.status !== 'number' || typeof v.body !== 'string') return undefined;
677
+ return {
678
+ status: v.status,
679
+ statusText: v.statusText ?? '',
680
+ headers: v.headers ?? [],
681
+ body: new Uint8Array(Buffer.from(v.body, 'base64')),
682
+ url: v.url ?? '',
683
+ };
684
+ }
685
+
686
+ async function persistFetchEntry(
687
+ key: string,
688
+ snapshot: FetchSnapshot,
689
+ tags: readonly string[],
690
+ revalidateSeconds: number | undefined,
691
+ ): Promise<void> {
692
+ if (!hasCacheHandler()) return;
693
+ const value: PersistedFetch = {
694
+ status: snapshot.status,
695
+ statusText: snapshot.statusText,
696
+ headers: snapshot.headers,
697
+ body: Buffer.from(snapshot.body).toString('base64'),
698
+ url: snapshot.url,
699
+ };
700
+ // fetchCache/fetchUrl mirror Next's incremental-cache ctx for fetch writes;
701
+ // custom handlers key their logging/persistence off them.
702
+ await cacheHandlerSet(key, value, {
703
+ tags,
704
+ revalidate: revalidateSeconds ?? false,
705
+ fetchCache: true,
706
+ fetchUrl: snapshot.url || undefined,
707
+ });
708
+ }
709
+
710
+ async function snapshotFetch(
711
+ original: typeof globalThis.fetch,
712
+ input: Parameters<typeof globalThis.fetch>[0],
713
+ init?: RequestInit,
714
+ ): Promise<FetchSnapshot> {
715
+ const response = await original(input, init);
716
+ const body = new Uint8Array(await response.arrayBuffer());
717
+ const headers = [...response.headers.entries()].filter(
718
+ ([name]) => !['content-encoding', 'content-length', 'transfer-encoding'].includes(name),
719
+ );
720
+ return {
721
+ status: response.status,
722
+ statusText: response.statusText,
723
+ headers,
724
+ body,
725
+ url: response.url,
726
+ };
727
+ }
728
+
729
+ function snapshotResponse(snapshot: FetchSnapshot): Response {
730
+ const response = new Response(snapshot.body.slice() as BodyInit, {
731
+ status: snapshot.status,
732
+ statusText: snapshot.statusText,
733
+ headers: snapshot.headers,
734
+ });
735
+ // Response.url is read-only and lost through the constructor; restore it.
736
+ if (snapshot.url) {
737
+ Object.defineProperty(response, 'url', { value: snapshot.url, configurable: true });
738
+ }
739
+ return response;
740
+ }
741
+
742
+ /** Test hook: drop all cached fetch entries. */
743
+ export function clearFetchCache() {
744
+ fetchCache.clear();
745
+ }