@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,1695 @@
1
+ /**
2
+ * Runtime for the 'use cache' directive (minimum honest implementation).
3
+ *
4
+ * A directive-marked function is wrapped by the build-time source rewrite
5
+ * (see use-cache-transform.ts) into `pnextUseCache(id, fn)`: calls memoize
6
+ * per (id + JSON args) in a process-wide store, honoring cacheTag()/
7
+ * cacheLife() from inside the function and revalidateTag() invalidation.
8
+ * Unserializable arguments (functions, cyclic values) bypass the cache.
9
+ *
10
+ * Divergences from Next: values are shared by reference (no RSC
11
+ * serialization boundary), and named custom cache handler kinds fall back to
12
+ * the default handler.
13
+ */
14
+ import { AsyncLocalStorage } from 'node:async_hooks';
15
+ import { Fragment, h, type ComponentChildren } from 'preact';
16
+ import { Suspense as CoreSuspense } from '../../api/suspense';
17
+ import { currentParams, currentRequest, getWorkUnit } from '../../request/context';
18
+ import {
19
+ hangingPromise,
20
+ isPrefetchShellPrerender,
21
+ isPrerendering,
22
+ isRuntimePrefetchPrerender,
23
+ markRuntimeApiOnAwait,
24
+ PostponeError,
25
+ realNow,
26
+ trackPrerenderCacheFill,
27
+ } from '../../ppr';
28
+ import {
29
+ currentRenderCacheMeta,
30
+ currentTagRevalidationSeq,
31
+ rdcProcessId,
32
+ rebaseRdcTagSeq,
33
+ draftModeBypassActive,
34
+ pathScopeRevalidated,
35
+ recordCacheExpire,
36
+ recordCacheNoStore,
37
+ recordCacheRevalidate,
38
+ recordCacheStale,
39
+ recordCacheTags,
40
+ runInsideDataCacheProducer,
41
+ setUseCacheRevalidateSink,
42
+ tagsRevalidatedAfterSeq,
43
+ tagsStaleSince,
44
+ } from './revalidate';
45
+ import {
46
+ hasModernCacheHandler,
47
+ modernCacheEntry,
48
+ modernCacheGet,
49
+ modernCacheGetExpiration,
50
+ modernCacheKey,
51
+ modernCacheRefreshTags,
52
+ modernCacheSet,
53
+ } from './modern-handler';
54
+ import { funnelCacheRuntimeError } from './runtime-error';
55
+ import {
56
+ prerenderErrorCollectionActive,
57
+ recordPrerenderError,
58
+ routeFromCacheId,
59
+ useCacheSearchParamsMessage,
60
+ USE_CACHE_CLOSE_OVER_FUNCTION_BLOCK,
61
+ USE_CACHE_CLOSE_OVER_FUNCTION_MESSAGE,
62
+ USE_CACHE_HANGING_MESSAGE,
63
+ } from './build-prerender-errors';
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Build-prerender `use cache` fill timeout (E236 hanging-input detection).
67
+ //
68
+ // A `use cache` fill that never resolves during a build prerender would hang
69
+ // the build forever. Next races the fill against `experimental.useCacheTimeout`
70
+ // (seconds) and, on timeout, rejects with the E236 message. We mirror that only
71
+ // while a compat build prerender is armed (see build-prerender-errors); the
72
+ // serving runtime and pure-core builds are untouched. The value is seeded from
73
+ // next.config by register-usecache at build start.
74
+ // ---------------------------------------------------------------------------
75
+
76
+ // Anchored on globalThis: begin/seed runs in the build process's copy of this
77
+ // module, but `withFillTimeout` runs in the bundled compat-server's copy — a
78
+ // module-local `let` would leave the bundle reading the stale default (see the
79
+ // same duplication note in build-prerender-errors.ts).
80
+ const USE_CACHE_FILL_TIMEOUT = Symbol.for('pnext.compat.useCacheFillTimeout');
81
+
82
+ function fillTimeoutHolder(): { ms: number } {
83
+ const root = globalThis as Record<PropertyKey, unknown>;
84
+ return (root[USE_CACHE_FILL_TIMEOUT] ??= { ms: 50_000 }) as { ms: number };
85
+ }
86
+
87
+ /**
88
+ * Seed the build-time `use cache` fill timeout (milliseconds). Next derives it
89
+ * from `experimental.useCacheTimeout`, clamped during a build to just under
90
+ * `staticPageGenerationTimeout` (register-usecache passes the already-resolved
91
+ * value). Ignored (left at the 50s default) when undefined.
92
+ */
93
+ export function setUseCacheFillTimeout(ms: number | undefined): void {
94
+ if (typeof ms === 'number' && Number.isFinite(ms) && ms > 0) {
95
+ fillTimeoutHolder().ms = ms;
96
+ }
97
+ }
98
+
99
+ interface UseCacheEntry {
100
+ value: Promise<unknown>;
101
+ storedAt: number;
102
+ tags: string[];
103
+ revalidateSeconds?: number;
104
+ expireSeconds?: number;
105
+ staleSeconds?: number;
106
+ /** A background stale-while-revalidate refresh is already in flight. */
107
+ refreshing?: boolean;
108
+ modernKey?: string;
109
+ /** Pathname of the render that produced this entry (path-revalidation scope). */
110
+ route?: string;
111
+ /** Hard-tag-revalidation sequence sampled when production began. */
112
+ tagSeq: number;
113
+ /**
114
+ * The render cache-meta this entry was produced under. An on-demand
115
+ * regeneration render (refreshFetches) re-produces each entry ONCE and then
116
+ * reuses it, so repeated reads within the regen dedupe (Next refreshes per
117
+ * unique cache, not per call).
118
+ */
119
+ refreshedIn?: object;
120
+ /**
121
+ * Nested `use cache` wrapper vnodes embedded in this entry's produced tree
122
+ * that have not yet rendered. Their scopes fold tags + min cacheLife into
123
+ * this entry when they settle (see attributeNestedCacheWrapper); persisting
124
+ * the entry to a modern cache handler waits for the count to drain.
125
+ */
126
+ nestedPending?: number;
127
+ /** Invoked each time a pending nested wrapper settles. */
128
+ onNestedSettled?: () => void;
129
+ }
130
+
131
+ type RdcValue = null | boolean | number | string | RdcValue[] | { [key: string]: RdcValue };
132
+
133
+ export interface UseCacheRdcRecord {
134
+ key: string;
135
+ value: RdcValue;
136
+ tags: string[];
137
+ storedAt: number;
138
+ revalidateSeconds?: number;
139
+ expireSeconds?: number;
140
+ staleSeconds?: number;
141
+ tagSeq: number;
142
+ /** Process that produced this record — `tagSeq` only means anything there. */
143
+ pid?: string;
144
+ route?: string;
145
+ }
146
+
147
+ interface UseCacheScope {
148
+ tags: Set<string>;
149
+ revalidateSeconds?: number;
150
+ expireSeconds?: number;
151
+ staleSeconds?: number;
152
+ /**
153
+ * The cached-function id producing inside this scope. Root-param reads made
154
+ * during production attribute to this id so the id's read-set (see
155
+ * knownRootParamsByFunctionId) widens and later calls key on those params.
156
+ */
157
+ functionId?: string;
158
+ /** Root param NAMES read (via next/root-params) while producing this entry. */
159
+ rootParamsRead?: Set<string>;
160
+ }
161
+
162
+ const USE_CACHE_ENTRIES = Symbol.for('pnext.compat.useCacheEntries');
163
+ const USE_CACHE_SCOPE_STORAGE = Symbol.for('pnext.compat.useCacheScopeStorage');
164
+ const USE_CACHE_KNOWN_ROOT_PARAMS = Symbol.for('pnext.compat.knownRootParamsByFunctionId');
165
+ const USE_CACHE_REQUEST_TOKENS = Symbol.for('pnext.compat.useCacheRequestTokens');
166
+
167
+ function useCacheGlobal<T>(key: symbol, create: () => T): T {
168
+ const state = globalThis as Record<PropertyKey, unknown>;
169
+ const existing = state[key] as T | undefined;
170
+ if (existing !== undefined) return existing;
171
+ const value = create();
172
+ state[key] = value;
173
+ return value;
174
+ }
175
+
176
+ const entries = useCacheGlobal(USE_CACHE_ENTRIES, () => new Map<string, UseCacheEntry>());
177
+ const entriesLimit = 4096;
178
+ const scopeStorage = useCacheGlobal(USE_CACHE_SCOPE_STORAGE, () => new AsyncLocalStorage<UseCacheScope>());
179
+ const requestTokens = useCacheGlobal(USE_CACHE_REQUEST_TOKENS, () => new WeakMap<object, object>());
180
+ const USE_CACHE_RDC_CAPTURE = Symbol.for('pnext.compat.useCacheRdcCapture');
181
+ const USE_CACHE_RDC_RECORDS = Symbol.for('pnext.compat.useCacheRdcRecords');
182
+
183
+ function useCacheRdcRecords(): Map<string, UseCacheRdcRecord> {
184
+ return useCacheGlobal(USE_CACHE_RDC_RECORDS, () => new Map<string, UseCacheRdcRecord>());
185
+ }
186
+
187
+ function useCacheRdcCapture(): Set<string> | undefined {
188
+ return (globalThis as Record<PropertyKey, unknown>)[USE_CACHE_RDC_CAPTURE] as Set<string> | undefined;
189
+ }
190
+
191
+ function captureUseCacheRdcEntry(key: string) {
192
+ useCacheRdcCapture()?.add(key);
193
+ }
194
+
195
+ export function beginUseCacheRdcCapture() {
196
+ (globalThis as Record<PropertyKey, unknown>)[USE_CACHE_RDC_CAPTURE] = new Set<string>();
197
+ }
198
+
199
+ export async function collectUseCacheRdcRecords(): Promise<UseCacheRdcRecord[]> {
200
+ const capture = useCacheRdcCapture();
201
+ if (!capture) return [];
202
+ const records: UseCacheRdcRecord[] = [];
203
+ for (const key of capture) {
204
+ const entry = entries.get(key);
205
+ if (!entry) continue;
206
+ const value = toRdcValue(await entry.value);
207
+ if (value === undefined) continue;
208
+ records.push({
209
+ key,
210
+ value,
211
+ tags: [...entry.tags],
212
+ storedAt: entry.storedAt,
213
+ ...(entry.revalidateSeconds !== undefined ? { revalidateSeconds: entry.revalidateSeconds } : {}),
214
+ ...(entry.expireSeconds !== undefined ? { expireSeconds: entry.expireSeconds } : {}),
215
+ ...(entry.staleSeconds !== undefined ? { staleSeconds: entry.staleSeconds } : {}),
216
+ tagSeq: entry.tagSeq,
217
+ pid: rdcProcessId(),
218
+ ...(entry.route !== undefined ? { route: entry.route } : {}),
219
+ });
220
+ }
221
+ return records;
222
+ }
223
+
224
+ /**
225
+ * Union of the tags of every entry captured this prerender - INCLUDING entries whose value cannot
226
+ * serialize into an RDC record. A page-level `use cache` caches its rendered tree; the tree is
227
+ * unserializable but its cacheTag()s must still reach the shell's staleness check.
228
+ */
229
+ export async function collectUseCacheRdcTags(): Promise<string[]> {
230
+ const capture = useCacheRdcCapture();
231
+ if (!capture) return [];
232
+ const tags = new Set<string>();
233
+ for (const key of capture) {
234
+ const entry = entries.get(key);
235
+ if (!entry) continue;
236
+ // Tags are applied to the entry once its production settles.
237
+ await entry.value.catch(() => undefined);
238
+ for (const tag of entry.tags) tags.add(tag);
239
+ }
240
+ return [...tags];
241
+ }
242
+
243
+ export function seedUseCacheRdcRecords(records: readonly UseCacheRdcRecord[], route?: string) {
244
+ for (const record of records) {
245
+ if (!isUseCacheRdcRecord(record)) continue;
246
+ // Foreign-process sequences are meaningless here — see rebaseRdcTagSeq.
247
+ const tagSeq = rebaseRdcTagSeq(record);
248
+ // An RDC artifact is a snapshot: seeding it must never regress a live
249
+ // entry produced more recently (each regen seeds the stale .rdc first).
250
+ const live = entries.get(record.key);
251
+ if (live !== undefined && live.storedAt > record.storedAt) continue;
252
+ useCacheRdcRecords().set(record.key, { ...record, tagSeq });
253
+ entries.set(record.key, {
254
+ value: Promise.resolve(record.value),
255
+ storedAt: record.storedAt,
256
+ tags: [...record.tags],
257
+ ...(record.revalidateSeconds !== undefined
258
+ ? { revalidateSeconds: record.revalidateSeconds }
259
+ : {}),
260
+ ...(record.expireSeconds !== undefined ? { expireSeconds: record.expireSeconds } : {}),
261
+ ...(record.staleSeconds !== undefined ? { staleSeconds: record.staleSeconds } : {}),
262
+ tagSeq,
263
+ ...(route !== undefined ? { route } : record.route !== undefined ? { route: record.route } : {}),
264
+ });
265
+ }
266
+ }
267
+
268
+ function isUseCacheRdcRecord(value: unknown): value is UseCacheRdcRecord {
269
+ if (!value || typeof value !== 'object') return false;
270
+ const record = value as Partial<UseCacheRdcRecord>;
271
+ return (
272
+ typeof record.key === 'string' &&
273
+ Array.isArray(record.tags) &&
274
+ record.tags.every(tag => typeof tag === 'string') &&
275
+ typeof record.storedAt === 'number' &&
276
+ Number.isFinite(record.storedAt) &&
277
+ typeof record.tagSeq === 'number' &&
278
+ Number.isInteger(record.tagSeq) &&
279
+ isRdcValue(record.value)
280
+ );
281
+ }
282
+
283
+ function toRdcValue(value: unknown): RdcValue | undefined {
284
+ if (!isRdcValue(value)) return undefined;
285
+ return JSON.parse(JSON.stringify(value)) as RdcValue;
286
+ }
287
+
288
+ function isRdcValue(value: unknown, seen = new WeakSet<object>()): value is RdcValue {
289
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return true;
290
+ if (typeof value === 'number') return Number.isFinite(value);
291
+ if (typeof value !== 'object' || seen.has(value)) return false;
292
+ if (Array.isArray(value)) {
293
+ seen.add(value);
294
+ return value.every(item => isRdcValue(item, seen));
295
+ }
296
+ const prototype = Object.getPrototypeOf(value) as object | null;
297
+ if (prototype !== Object.prototype && prototype !== null) return false;
298
+ seen.add(value);
299
+ return Object.values(value).every(item => isRdcValue(item, seen));
300
+ }
301
+
302
+ // ---------------------------------------------------------------------------
303
+ // Root-param cache-key participation (Stage B-3).
304
+ //
305
+ // Root params (next/root-params) implicitly join a `use cache` entry's key: two
306
+ // invocations under different root params must not share a cached value. But the
307
+ // key is computed BEFORE the function runs, and which root params a function
308
+ // reads is only discovered DURING production ("conditional reads"). So we track,
309
+ // per cached-function id, the set of root-param NAMES it has been observed to
310
+ // read, and widen the key on subsequent invocations by joining those params'
311
+ // current values. The read-set only grows (a param read on a later, branch-
312
+ // dependent invocation upgrades the key for that id going forward), matching
313
+ // Next's `knownRootParamsByFunctionId`.
314
+ //
315
+ // The values are sourced from the request param ALS (currentParams) at key-build
316
+ // time; a root param is a plain string / string[] (catch-all) / undefined
317
+ // (absent optional catch-all).
318
+ // ---------------------------------------------------------------------------
319
+
320
+ const knownRootParamsByFunctionId = useCacheGlobal(
321
+ USE_CACHE_KNOWN_ROOT_PARAMS,
322
+ () => new Map<string, Set<string>>(),
323
+ );
324
+
325
+ // The app's ROOT param names (segments at/above a root layout), seeded from the
326
+ // build-time scan at registration. Anchored on globalThis so the build process
327
+ // and the built server bundle read one copy. Only these names may join a cache
328
+ // key: a non-root route param must never discriminate entries, and a fallback
329
+ // prerender's unknown params must never poison the key.
330
+ const USE_CACHE_ROOT_PARAM_NAMES = Symbol.for('pnext.compat.rootParamNames');
331
+
332
+ /** Seed the scanned root-param name set (register-usecache, build + serve). */
333
+ export function setUseCacheRootParamNames(names: Iterable<string>): void {
334
+ (globalThis as Record<PropertyKey, unknown>)[USE_CACHE_ROOT_PARAM_NAMES] = new Set(names);
335
+ }
336
+
337
+ function rootParamNames(): Set<string> | undefined {
338
+ return (globalThis as Record<PropertyKey, unknown>)[USE_CACHE_ROOT_PARAM_NAMES] as
339
+ | Set<string>
340
+ | undefined;
341
+ }
342
+
343
+ /**
344
+ * Called by next/root-params' `rootParam(name)` when read inside a `use cache`
345
+ * producer: record the name against the producing function id so future calls
346
+ * key on it. Returns true when a use-cache producer scope was active (the read
347
+ * participated in a cache key); false otherwise (plain request read).
348
+ */
349
+ export function recordUseCacheRootParamRead(name: string): boolean {
350
+ const scope = scopeStorage.getStore();
351
+ if (!scope?.functionId) return false;
352
+ (scope.rootParamsRead ??= new Set()).add(name);
353
+ let known = knownRootParamsByFunctionId.get(scope.functionId);
354
+ if (!known) {
355
+ known = new Set();
356
+ knownRootParamsByFunctionId.set(scope.functionId, known);
357
+ }
358
+ known.add(name);
359
+ return true;
360
+ }
361
+
362
+ /** True when a `use cache` producer is currently on the stack (E1140 guard). */
363
+ export function insideUseCacheProducer(): boolean {
364
+ return scopeStorage.getStore()?.functionId !== undefined;
365
+ }
366
+
367
+ /**
368
+ * Extend the raw cache-key args for `id` with the current values of every root
369
+ * param the id has been observed to read. Sourced lazily so a param discovered
370
+ * on a later invocation immediately participates. Returns the args unchanged
371
+ * when the id has no known root-param reads.
372
+ */
373
+ function withRootParamKeyArgs(id: string, args: unknown[]): unknown[] {
374
+ const known = knownRootParamsByFunctionId.get(id);
375
+ const params = currentParams();
376
+ // The first call has no observed read-set yet, but it must still not seed a cross-root entry. Once
377
+ // reads are known, narrow back to that set; until then every KNOWN (resolved) root param is a
378
+ // conservative cache discriminator - never non-root route params (they must not split entries) and
379
+ // never a fallback prerender's unfilled params (they must not poison the key). Over-discrimination
380
+ // costs a duplicate entry, under-discrimination shares a value across roots.
381
+ const roots = rootParamNames();
382
+ const names = known?.size
383
+ ? [...known]
384
+ : Object.keys(params).filter(
385
+ name => (!roots || roots.has(name)) && params[name] !== undefined,
386
+ );
387
+ if (names.length === 0) return args;
388
+ // Stable order (sorted names) so the key is deterministic regardless of the
389
+ // order reads were discovered in.
390
+ const rootParamEntries = names
391
+ .sort()
392
+ .map(name => [name, params[name]] as const);
393
+ return [{ $pnextRootParams: rootParamEntries }, ...args];
394
+ }
395
+
396
+ /** Test hook. */
397
+ export function clearKnownRootParams() {
398
+ knownRootParamsByFunctionId.clear();
399
+ }
400
+
401
+ // Next's `default` cacheLife profile, applied to a `use cache` entry that never
402
+ // calls cacheLife(). Kept here (not read from config) so the runtime has no
403
+ // dependency on the compat config loader; a config `expireTime`/custom default
404
+ // override only affects explicit cacheLife() calls (see compat/next/cache.ts).
405
+ const DEFAULT_REVALIDATE_SECONDS = 900;
406
+ const DEFAULT_EXPIRE_SECONDS = 31536000;
407
+ const DEFAULT_STALE_SECONDS = 300;
408
+
409
+ // A runtime prefetch (`unstable_instant`) includes a `use cache` scope only when its effective STALE
410
+ // window is at least this long; a shorter-lived cache stays a postponed hole. The threshold applies to
411
+ // `stale` ONLY, never `expire` - a public cache with a short expire but a long stale is still included.
412
+ const RUNTIME_PREFETCH_DYNAMIC_STALE = 30;
413
+
414
+ // A STATIC prerender whose output the client prefetches omits a `use cache` scope that expires inside
415
+ // this window: data that short-lived is not worth prerendering, so its boundary stays a hole the
416
+ // navigation fills live. Note the two thresholds read DIFFERENT fields: a runtime prefetch gates on
417
+ // `stale` (a short-expire/long-stale cache is still prefetchable), a static prefetch shell on `expire`.
418
+ const PREFETCH_SHELL_DYNAMIC_EXPIRE = 300;
419
+
420
+ /** How the active prerender gates a short-lived `use cache` scope, if at all. */
421
+ function prefetchCacheGate(): 'stale' | 'expire' | null {
422
+ if (isRuntimePrefetchPrerender()) return 'stale';
423
+ if (isPrefetchShellPrerender()) return 'expire';
424
+ return null;
425
+ }
426
+
427
+ /**
428
+ * Gate a `use cache` result for a prerender whose output is PREFETCHED. The effective cacheLife is only
429
+ * known once the scope has produced (cacheLife() runs inside it), so resolve the value first, then swap
430
+ * it for a hanging promise - the same postpone mechanism connection() uses - when the window the gate
431
+ * looks at falls short of its threshold.
432
+ */
433
+ function gatePrefetchCacheLife<T>(
434
+ result: Promise<T>,
435
+ gate: 'stale' | 'expire',
436
+ life: () => { staleSeconds?: number; expireSeconds?: number } | undefined,
437
+ ): Promise<T> {
438
+ // Awaiting a cache read inside a runtime prefetch arms the sync-IO abort, the
439
+ // same way awaiting cookies()/params does (identity for the shell gate).
440
+ return markRuntimeApiOnAwait(
441
+ Promise.resolve(result).then(value => {
442
+ const effective = life();
443
+ const short =
444
+ gate === 'stale'
445
+ ? (effective?.staleSeconds ?? DEFAULT_STALE_SECONDS) < RUNTIME_PREFETCH_DYNAMIC_STALE
446
+ : (effective?.expireSeconds ?? DEFAULT_EXPIRE_SECONDS) < PREFETCH_SHELL_DYNAMIC_EXPIRE;
447
+ if (short) return hangingPromise<T>('use cache');
448
+ return value;
449
+ }),
450
+ );
451
+ }
452
+
453
+ // ---------------------------------------------------------------------------
454
+ // cacheLife/response-header plumbing.
455
+ //
456
+ // A `use cache` entry's cacheLife (revalidate/expire/stale) is stashed on the
457
+ // request work unit as it resolves during a render; the register-usecache
458
+ // responseFinalizer reads the aggregate and emits the SWR `cache-control` +
459
+ // `x-nextjs-stale-time` headers (mirroring how the font runtime stashes
460
+ // preloads for a `Link` header). Only the LONGEST-lived cache scope on the
461
+ // page drives the page's cache-control (Next lowers by the min revalidate but
462
+ // the page header reflects the effective revalidate/expire/stale of the route).
463
+ // ---------------------------------------------------------------------------
464
+
465
+ /** Well-known key for the per-request cache-life aggregate stash. */
466
+ export const USE_CACHE_LIFE = Symbol.for('pnext.compat.useCacheLife');
467
+
468
+ export interface CacheLifeStash {
469
+ revalidateSeconds?: number;
470
+ expireSeconds?: number;
471
+ staleSeconds?: number;
472
+ }
473
+
474
+ function stashCacheLife(life: CacheLifeStash): void {
475
+ if (
476
+ life.revalidateSeconds === undefined &&
477
+ life.expireSeconds === undefined &&
478
+ life.staleSeconds === undefined
479
+ ) {
480
+ return;
481
+ }
482
+ const unit = getWorkUnit();
483
+ if (!unit) return;
484
+ const compat = (unit.compat ??= {});
485
+ const existing = (compat[USE_CACHE_LIFE] as CacheLifeStash | undefined) ?? {};
486
+ // Track the minimum revalidate (Next lowers the page revalidate to the
487
+ // shortest cache used) but pair it with that same entry's expire so the
488
+ // emitted SWR window is self-consistent.
489
+ const nextRevalidate = minDefined(existing.revalidateSeconds, life.revalidateSeconds);
490
+ const drivenByThis =
491
+ life.revalidateSeconds !== undefined && life.revalidateSeconds === nextRevalidate;
492
+ // `staleSeconds` is its OWN minimum, NOT the min-revalidate entry's: the
493
+ // client router cache is only as fresh as the shortest-lived content on the
494
+ // page, and coupling it let whichever scope rendered first win the window
495
+ // whenever the revalidates tied (the default case).
496
+ const nextStale = minDefined(existing.staleSeconds, life.staleSeconds);
497
+ compat[USE_CACHE_LIFE] = {
498
+ ...(nextRevalidate !== undefined ? { revalidateSeconds: nextRevalidate } : {}),
499
+ ...(drivenByThis
500
+ ? { ...(life.expireSeconds !== undefined ? { expireSeconds: life.expireSeconds } : {}) }
501
+ : {
502
+ ...(existing.expireSeconds !== undefined ? { expireSeconds: existing.expireSeconds } : {}),
503
+ }),
504
+ ...(nextStale !== undefined ? { staleSeconds: nextStale } : {}),
505
+ };
506
+ }
507
+
508
+ /** Read + clear the cache-life aggregate stashed during the current render. */
509
+ export function takeCacheLifeStash(): CacheLifeStash | undefined {
510
+ const unit = getWorkUnit();
511
+ const compat = unit?.compat;
512
+ if (!compat) return undefined;
513
+ const life = compat[USE_CACHE_LIFE] as CacheLifeStash | undefined;
514
+ compat[USE_CACHE_LIFE] = undefined;
515
+ return life;
516
+ }
517
+
518
+ function minDefined(a: number | undefined, b: number | undefined): number | undefined {
519
+ if (a === undefined) return b;
520
+ if (b === undefined) return a;
521
+ return Math.min(a, b);
522
+ }
523
+
524
+ // An explicit data-cache revalidate (e.g. `fetch(..., { next: { revalidate } })`
525
+ // or a nested unstable_cache read) recorded while a `use cache` entry is
526
+ // producing lowers the producing entry's own cacheLife, matching Next.
527
+ setUseCacheRevalidateSink(seconds => {
528
+ recordUseCacheLife(seconds);
529
+ });
530
+
531
+ /** cacheTag() inside a 'use cache' function: tag the active entry. */
532
+ export function recordUseCacheTags(tags: readonly string[]): boolean {
533
+ const scope = scopeStorage.getStore();
534
+ if (!scope) return false;
535
+ for (const tag of tags) scope.tags.add(tag);
536
+ return true;
537
+ }
538
+
539
+ /** cacheLife() inside a 'use cache' function: bound the active entry's TTL. */
540
+ export function recordUseCacheLife(life: number | CacheLifeStash): boolean {
541
+ const scope = scopeStorage.getStore();
542
+ if (!scope) return false;
543
+ const revalidate = typeof life === 'number' ? life : life.revalidateSeconds;
544
+ const expire = typeof life === 'number' ? undefined : life.expireSeconds;
545
+ const stale = typeof life === 'number' ? undefined : life.staleSeconds;
546
+ if (revalidate !== undefined) {
547
+ scope.revalidateSeconds =
548
+ scope.revalidateSeconds === undefined
549
+ ? revalidate
550
+ : Math.min(scope.revalidateSeconds, revalidate);
551
+ }
552
+ if (expire !== undefined) {
553
+ scope.expireSeconds =
554
+ scope.expireSeconds === undefined ? expire : Math.min(scope.expireSeconds, expire);
555
+ }
556
+ if (stale !== undefined) {
557
+ scope.staleSeconds =
558
+ scope.staleSeconds === undefined ? stale : Math.min(scope.staleSeconds, stale);
559
+ }
560
+ return true;
561
+ }
562
+
563
+ function entryExpired(entry: UseCacheEntry) {
564
+ return (
565
+ entry.revalidateSeconds !== undefined &&
566
+ realNow() - entry.storedAt >= entry.revalidateSeconds * 1000
567
+ );
568
+ }
569
+
570
+ /** Propagate an entry's cache meta to the surrounding render/'use cache' scopes. */
571
+ function propagate(entry: UseCacheEntry, parent?: UseCacheScope) {
572
+ const { tags, revalidateSeconds, expireSeconds, staleSeconds } = entry;
573
+ recordCacheTags(tags);
574
+ if (revalidateSeconds !== undefined) recordCacheRevalidate(revalidateSeconds);
575
+ // Also aggregate the expire/stale windows onto the render meta so a build
576
+ // shell prerender can persist the route's SWR headers (the work-unit stash
577
+ // below is not in scope during this async propagation — see build.ts).
578
+ if (expireSeconds !== undefined) recordCacheExpire(expireSeconds);
579
+ if (staleSeconds !== undefined) recordCacheStale(staleSeconds);
580
+ // Stash the effective cache-life on the request work unit so the finalizer
581
+ // can emit the SWR cache-control + x-nextjs-stale-time headers.
582
+ stashCacheLife({
583
+ ...(revalidateSeconds !== undefined ? { revalidateSeconds } : {}),
584
+ ...(expireSeconds !== undefined ? { expireSeconds } : {}),
585
+ ...(staleSeconds !== undefined ? { staleSeconds } : {}),
586
+ });
587
+ if (parent) propagateToParent(entry, parent);
588
+ }
589
+
590
+ function propagateToParent(entry: UseCacheEntry, parent: UseCacheScope): void {
591
+ for (const tag of entry.tags) parent.tags.add(tag);
592
+ parent.revalidateSeconds = minDefined(parent.revalidateSeconds, entry.revalidateSeconds);
593
+ parent.expireSeconds = minDefined(parent.expireSeconds, entry.expireSeconds);
594
+ parent.staleSeconds = minDefined(parent.staleSeconds, entry.staleSeconds);
595
+ }
596
+
597
+ export function pnextUseCache<Args extends unknown[], Result>(
598
+ id: string,
599
+ fn: (...args: Args) => Result,
600
+ ): (...args: Args) => Result {
601
+ const wrapper = (...args: Args): Result => {
602
+ const serializedArgs = installSearchParamsTripwire(serializeCacheBoundaryArgs(args), id) as Args;
603
+ // Draft mode skips cache reads/writes but KEEPS the cache-scope rules:
604
+ // dynamic APIs (cookies()/headers()) inside 'use cache' still throw.
605
+ if (draftModeBypassActive()) return draftBypassRun(id, fn, serializedArgs);
606
+ // Element-like arguments become indexed holes: they never affect the key
607
+ // (an element keys as an opaque token either way), the produced tree embeds
608
+ // the hole markers, and each invocation re-instantiates it with ITS fills.
609
+ const holes = extractElementHoles(serializedArgs);
610
+ const keyArgs = declaredArgs(fn, serializedArgs);
611
+ const key = cacheKeyFor(id, withRootParamKeyArgs(id, keyArgs));
612
+ if (isPromiseLike(key)) {
613
+ return key.then(
614
+ resolvedKey => {
615
+ if (resolvedKey === null) return fn(...serializedArgs);
616
+ return readOrProduceEntry(
617
+ resolvedKey,
618
+ id,
619
+ argsJsonFromKey(id, resolvedKey),
620
+ fn,
621
+ holes.args as Args,
622
+ holes.fills,
623
+ );
624
+ },
625
+ () => fn(...serializedArgs),
626
+ ) as Result;
627
+ }
628
+ if (key === null) return fn(...serializedArgs);
629
+
630
+ return readOrProduceEntry(key, id, argsJsonFromKey(id, key), fn, holes.args as Args, holes.fills);
631
+ };
632
+ (wrapper as unknown as Record<symbol, unknown>)[USE_CACHE_WRAPPER] = true;
633
+ return wrapper;
634
+ }
635
+
636
+ // ---------------------------------------------------------------------------
637
+ // searchParams-inside-`use cache` detection (E842).
638
+ //
639
+ // A cached page receives `{ params, searchParams }` props. Awaiting
640
+ // `searchParams` INSIDE the cache scope is unsupported (it is dynamic request
641
+ // data). Next passes a searchParams promise that rejects with E842 when read
642
+ // inside a cache scope; we mirror it during a build prerender by swapping the
643
+ // props' `searchParams` for a tripwire thenable. The tripwire only fires when
644
+ // awaited while a `use cache` producer is on the stack (insideUseCacheProducer),
645
+ // so cache-key computation (which resolves it OUTSIDE any producer scope) sees
646
+ // the real value, and a page that never awaits searchParams (search-params-
647
+ // unused) is never tripped. The violation is recorded even when the page catches
648
+ // the throw (search-params-caught) so the build still fails.
649
+ // ---------------------------------------------------------------------------
650
+
651
+ function installSearchParamsTripwire(args: unknown[], id: string): unknown[] {
652
+ if (!prerenderErrorCollectionActive()) return args;
653
+ const route = routeFromCacheId(id);
654
+ let changed = false;
655
+ const out = args.map(arg => {
656
+ if (
657
+ arg !== null &&
658
+ typeof arg === 'object' &&
659
+ !Array.isArray(arg) &&
660
+ !isPromiseLike(arg) &&
661
+ !isElementLike(arg) &&
662
+ isPromiseLike((arg as Record<string, unknown>).searchParams)
663
+ ) {
664
+ changed = true;
665
+ return {
666
+ ...arg,
667
+ searchParams: searchParamsTripwire(route, (arg as Record<string, unknown>).searchParams),
668
+ };
669
+ }
670
+ return arg;
671
+ });
672
+ return changed ? out : args;
673
+ }
674
+
675
+ function withFillTimeout(value: Promise<unknown>, route: string | undefined): Promise<unknown> {
676
+ return new Promise<unknown>((resolve, reject) => {
677
+ const timer = setTimeout(() => {
678
+ recordPrerenderError({
679
+ kind: 'hanging',
680
+ route: route ?? '',
681
+ consoleBlock: `Error: ${USE_CACHE_HANGING_MESSAGE}`,
682
+ });
683
+ reject(new Error(USE_CACHE_HANGING_MESSAGE));
684
+ }, fillTimeoutHolder().ms);
685
+ // The timer must not keep the build process alive past the fill settling.
686
+ if (typeof timer.unref === 'function') timer.unref();
687
+ const clear = () => clearTimeout(timer);
688
+ void value.then(resolve, reject).then(clear, clear);
689
+ });
690
+ }
691
+
692
+ function searchParamsTripwire(route: string, real: unknown): PromiseLike<unknown> {
693
+ return {
694
+ then<TResult1 = unknown, TResult2 = never>(
695
+ onFulfilled?: ((value: unknown) => TResult1 | PromiseLike<TResult1>) | null,
696
+ onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
697
+ ): PromiseLike<TResult1 | TResult2> {
698
+ if (insideUseCacheProducer()) {
699
+ const message = useCacheSearchParamsMessage(route);
700
+ recordPrerenderError({ kind: 'search-params', route, consoleBlock: `Error: ${message}` });
701
+ return Promise.reject(new Error(message)).then(onFulfilled, onRejected);
702
+ }
703
+ return Promise.resolve(real).then(onFulfilled, onRejected);
704
+ },
705
+ };
706
+ }
707
+
708
+ /**
709
+ * `use cache` wrapper for a function that closes over enclosing-scope bindings. Next's compiler encodes
710
+ * closed-over values as bound arguments so they participate in the cache key; the regex transform
711
+ * cannot rewrite the closure, so the captured values are threaded in via `capture()` - evaluated at
712
+ * call time in the closure's scope - and prepended to the key args. The captured values are keyed on
713
+ * but NOT passed to `fn` (the closure reads them directly), matching Next's bound-arg semantics.
714
+ */
715
+ export function pnextUseCacheBound<Args extends unknown[], Result>(
716
+ id: string,
717
+ fn: (...args: Args) => Result,
718
+ capture: () => unknown[],
719
+ ): (...args: Args) => Result {
720
+ const wrapper = (...args: Args): Result => {
721
+ const serializedArgs = serializeCacheBoundaryArgs(args) as Args;
722
+ if (draftModeBypassActive()) return draftBypassRun(id, fn, serializedArgs);
723
+ let captured: unknown[];
724
+ try {
725
+ captured = serializeCacheBoundaryArgs(capture());
726
+ } catch {
727
+ return fn(...serializedArgs);
728
+ }
729
+ // Close-over-function detection: a `use cache` closure that captures a non-serializable function
730
+ // cannot cross the cache/RSC boundary. During a build prerender, reject with Next's "Functions
731
+ // cannot be passed directly to Client Components..." error, recorded so the build fails. Gated on
732
+ // the armed build-prerender window, since the cache-components shell path sets no render cache-meta.
733
+ if (prerenderErrorCollectionActive() && captured.some(value => typeof value === 'function')) {
734
+ recordPrerenderError({
735
+ kind: 'close-over',
736
+ route: routeFromCacheId(id),
737
+ consoleBlock: USE_CACHE_CLOSE_OVER_FUNCTION_BLOCK,
738
+ });
739
+ throw new Error(USE_CACHE_CLOSE_OVER_FUNCTION_MESSAGE);
740
+ }
741
+ const holes = extractElementHoles(serializedArgs);
742
+ const keyArgs = [captured, ...declaredArgs(fn, serializedArgs)];
743
+ const key = cacheKeyFor(id, withRootParamKeyArgs(id, keyArgs));
744
+ if (isPromiseLike(key)) {
745
+ return key.then(
746
+ resolvedKey => {
747
+ if (resolvedKey === null) return fn(...serializedArgs);
748
+ return readOrProduceEntry(
749
+ resolvedKey,
750
+ id,
751
+ argsJsonFromKey(id, resolvedKey),
752
+ fn,
753
+ holes.args as Args,
754
+ holes.fills,
755
+ );
756
+ },
757
+ () => fn(...serializedArgs),
758
+ ) as Result;
759
+ }
760
+ if (key === null) return fn(...serializedArgs);
761
+ return readOrProduceEntry(key, id, argsJsonFromKey(id, key), fn, holes.args as Args, holes.fills);
762
+ };
763
+ (wrapper as unknown as Record<symbol, unknown>)[USE_CACHE_WRAPPER] = true;
764
+ return wrapper;
765
+ }
766
+
767
+ /**
768
+ * `use cache: private` wrapper. Private caches are NOT shared across requests
769
+ * (they may read request data), but Next still dedupes identical invocations
770
+ * WITHIN a single request: two calls with the same args resolve to one value.
771
+ * We memoize on a per-request store keyed on the work unit, so concurrent
772
+ * requests each get their own store (no cross-request sharing) while repeated
773
+ * calls inside one render join the same in-flight result. recordCacheNoStore()
774
+ * keeps the owning route dynamic.
775
+ */
776
+ export function pnextUseCachePrivate<Args extends unknown[], Result>(
777
+ id: string,
778
+ fn: (...args: Args) => Result,
779
+ ): (...args: Args) => Result {
780
+ const wrapper = (...args: Args): Result => {
781
+ recordCacheNoStore();
782
+ // Private caches are EXCLUDED from prerenders: they may read per-request data, so a build/shell
783
+ // prerender postpones the scope (a dynamic hole under the nearest Suspense boundary; a classic
784
+ // static prerender skips the route to dynamic) and the function is evaluated per request instead.
785
+ // EXCEPTION: a runtime prefetch samples the real request, so a private cache with a long enough
786
+ // stale window IS included; a shorter-lived one stays a postponed hole.
787
+ if (isPrerendering()) {
788
+ if (!isRuntimePrefetchPrerender()) throw new PostponeError('use cache: private');
789
+ return runtimePrefetchPrivate(id, fn, serializeCacheBoundaryArgs(args) as Args) as Result;
790
+ }
791
+ const serializedArgs = serializeCacheBoundaryArgs(args) as Args;
792
+ if (draftModeBypassActive()) return draftBypassRun(id, fn, serializedArgs);
793
+ // Run the REQUEST-TIME body inside a cache scope too (the prerender path
794
+ // already does, via runtimePrefetchPrivate): without one, a cacheLife()
795
+ // inside a `use cache: private` body is a no-op for the response aggregate,
796
+ // so its stale window never reaches x-nextjs-stale-time and the client
797
+ // reuses the navigation for the page's LONGEST window instead.
798
+ const runScoped = (call: () => Result): Result => {
799
+ const scope: UseCacheScope = { tags: new Set(), functionId: id };
800
+ const result = scopeStorage.run(scope, call);
801
+ const settle = () => {
802
+ if (scope.staleSeconds !== undefined) stashCacheLife({ staleSeconds: scope.staleSeconds });
803
+ };
804
+ if (isPromiseLike(result)) {
805
+ return (result as PromiseLike<unknown>).then(value => {
806
+ settle();
807
+ return value;
808
+ }) as Result;
809
+ }
810
+ settle();
811
+ return result;
812
+ };
813
+ const store = privateCacheStore();
814
+ if (!store) return runScoped(() => fn(...serializedArgs));
815
+ const keyArgs = declaredArgs(fn, serializedArgs);
816
+ const key = cacheKeyFor(id, keyArgs);
817
+ if (isPromiseLike(key)) {
818
+ return key.then(
819
+ resolvedKey =>
820
+ resolvedKey === null
821
+ ? runScoped(() => fn(...serializedArgs))
822
+ : runScoped(() => privateMemo(store, resolvedKey, fn, serializedArgs)),
823
+ () => runScoped(() => fn(...serializedArgs)),
824
+ ) as Result;
825
+ }
826
+ if (key === null) return runScoped(() => fn(...serializedArgs));
827
+ return runScoped(() => privateMemo(store, key, fn, serializedArgs));
828
+ };
829
+ (wrapper as unknown as Record<symbol, unknown>)[USE_CACHE_WRAPPER] = true;
830
+ return wrapper;
831
+ }
832
+
833
+ /**
834
+ * Produce a `use cache: private` scope inside a runtime-prefetch prerender and
835
+ * gate it on its effective stale window. Runs inside a cache SCOPE (so
836
+ * cacheLife() is captured) but deliberately NOT inside the data-cache producer:
837
+ * a private cache is allowed to read request data (cookies()/headers()), which
838
+ * a data-cache producer scope would reject. The value is discarded and the
839
+ * boundary postpones when the stale window is under
840
+ * RUNTIME_PREFETCH_DYNAMIC_STALE.
841
+ */
842
+ function runtimePrefetchPrivate<Args extends unknown[], Result>(
843
+ id: string,
844
+ fn: (...args: Args) => Result,
845
+ args: Args,
846
+ ): Promise<unknown> {
847
+ const scope: UseCacheScope = { tags: new Set(), functionId: id };
848
+ const produced = (async () => await scopeStorage.run(scope, () => fn(...args)))();
849
+ // Awaiting the private cache arms the sync-IO abort (see gatePrefetchCacheLife).
850
+ return markRuntimeApiOnAwait(
851
+ produced.then(value => {
852
+ const stale = scope.staleSeconds ?? DEFAULT_STALE_SECONDS;
853
+ if (stale < RUNTIME_PREFETCH_DYNAMIC_STALE) return hangingPromise('use cache: private');
854
+ // The sampled value IS in this response, so the response is only as fresh
855
+ // as that private window — the runtime-prefetch payload must expire with
856
+ // it, not with the (longer) public `use cache` window baked into the
857
+ // route's segment meta.
858
+ recordCacheStale(stale);
859
+ return value;
860
+ }),
861
+ );
862
+ }
863
+
864
+ /** Well-known key for the per-request private-cache dedup store. */
865
+ const PRIVATE_CACHE_STORE = Symbol.for('pnext.compat.privateCacheStore');
866
+
867
+ // Fallback per-request private-cache store, used when a private `use cache`
868
+ // resolves outside the work-unit ALS but still inside the request ALS. Keyed on
869
+ // the request object (WeakMap → auto-evicts when the request is GC'd), anchored
870
+ // on globalThis so the build orchestrator and the built server bundle share it.
871
+ const PRIVATE_STORE_BY_REQUEST = Symbol.for('pnext.compat.privateStoreByRequest');
872
+ const privateStoreByRequest = useCacheGlobal(
873
+ PRIVATE_STORE_BY_REQUEST,
874
+ () => new WeakMap<object, Map<string, unknown>>(),
875
+ );
876
+
877
+ function privateCacheStore(): Map<string, unknown> | undefined {
878
+ const unit = getWorkUnit();
879
+ if (unit) {
880
+ const compat = (unit.compat ??= {});
881
+ let store = compat[PRIVATE_CACHE_STORE] as Map<string, unknown> | undefined;
882
+ if (!store) {
883
+ store = new Map<string, unknown>();
884
+ compat[PRIVATE_CACHE_STORE] = store;
885
+ }
886
+ return store;
887
+ }
888
+ // Async server components may resolve outside the work-unit ALS (the renderer re-enters the request
889
+ // ALS, but not always the work unit), and Next still dedupes identical private-cache invocations
890
+ // WITHIN one request. Fall back to a per-request store keyed on the live request object, so two
891
+ // placements in one render join one value while concurrent requests stay isolated.
892
+ const request = currentRequest() as object | undefined;
893
+ if (!request) return undefined;
894
+ let store = privateStoreByRequest.get(request);
895
+ if (!store) {
896
+ store = new Map<string, unknown>();
897
+ privateStoreByRequest.set(request, store);
898
+ }
899
+ return store;
900
+ }
901
+
902
+ function privateMemo<Args extends unknown[], Result>(
903
+ store: Map<string, unknown>,
904
+ key: string,
905
+ fn: (...args: Args) => Result,
906
+ args: Args,
907
+ ): Result {
908
+ if (store.has(key)) return store.get(key) as Result;
909
+ const value = fn(...args);
910
+ store.set(key, value);
911
+ return value;
912
+ }
913
+
914
+ function declaredArgs<Args extends unknown[]>(fn: (...args: Args) => unknown, args: Args): unknown[] {
915
+ return args.slice(0, fn.length);
916
+ }
917
+
918
+ function serializeCacheBoundaryArgs(args: unknown[]): unknown[] {
919
+ return args.map(arg => serializeCacheBoundaryValue(arg, new WeakSet<object>()));
920
+ }
921
+
922
+ function serializeCacheBoundaryValue(value: unknown, stack: WeakSet<object>): unknown {
923
+ if (typeof value !== 'object' || value === null || isPromiseLike(value) || isElementLike(value)) {
924
+ return value;
925
+ }
926
+ if (stack.has(value)) return value;
927
+ stack.add(value);
928
+ try {
929
+ if (Array.isArray(value)) return value.map(item => serializeCacheBoundaryValue(item, stack));
930
+ if (value instanceof Date) return value.toJSON();
931
+ if (value instanceof Map) {
932
+ return [...value].map(([key, nested]) => [
933
+ serializeCacheBoundaryValue(key, stack),
934
+ serializeCacheBoundaryValue(nested, stack),
935
+ ]);
936
+ }
937
+ if (value instanceof Set) {
938
+ return [...value].map(item => serializeCacheBoundaryValue(item, stack));
939
+ }
940
+ const iterator = (value as { [Symbol.iterator]?: unknown })[Symbol.iterator];
941
+ if (typeof iterator === 'function') {
942
+ return Array.from(value as Iterable<unknown>, item => serializeCacheBoundaryValue(item, stack));
943
+ }
944
+ const entriesForObject = Object.entries(value);
945
+ if (entriesForObject.length === 0) return value;
946
+ return Object.fromEntries(
947
+ entriesForObject.map(([key, nested]) => [
948
+ key,
949
+ serializeCacheBoundaryValue(nested, stack),
950
+ ]),
951
+ );
952
+ } finally {
953
+ stack.delete(value);
954
+ }
955
+ }
956
+
957
+ /** Draft-mode bypass: run the producer uncached but still inside the cache
958
+ * scope, so the dynamic-API restrictions (cookies()/headers() inside
959
+ * 'use cache' throw) apply exactly as they do for cached production. */
960
+ function draftBypassRun<Args extends unknown[], Result>(
961
+ id: string,
962
+ fn: (...args: Args) => Result,
963
+ args: Args,
964
+ ): Result {
965
+ const scope: UseCacheScope = { tags: new Set(), functionId: id };
966
+ return runInsideDataCacheProducer(() => scopeStorage.run(scope, () => fn(...args)));
967
+ }
968
+
969
+ function readOrProduceEntry<Args extends unknown[], Result>(
970
+ key: string,
971
+ id: string,
972
+ argsJson: string,
973
+ fn: (...args: Args) => Result,
974
+ args: Args,
975
+ fills: unknown[],
976
+ ): Result {
977
+ const parent = scopeStorage.getStore();
978
+ const modernKey = hasModernCacheHandler() ? modernCacheKey(id, argsJson) : undefined;
979
+ const resumeRecord = useCacheRdcRecords().get(key);
980
+ if (
981
+ resumeRecord &&
982
+ !tagsRevalidatedAfterSeq(resumeRecord.tags, resumeRecord.tagSeq) &&
983
+ !pathScopeRevalidated(resumeRecord.route, resumeRecord.storedAt) &&
984
+ (entries.get(key)?.storedAt ?? -1) <= resumeRecord.storedAt
985
+ ) {
986
+ entries.set(key, {
987
+ value: Promise.resolve(resumeRecord.value),
988
+ storedAt: resumeRecord.storedAt,
989
+ tags: [...resumeRecord.tags],
990
+ ...(resumeRecord.revalidateSeconds !== undefined
991
+ ? { revalidateSeconds: resumeRecord.revalidateSeconds }
992
+ : {}),
993
+ ...(resumeRecord.expireSeconds !== undefined ? { expireSeconds: resumeRecord.expireSeconds } : {}),
994
+ ...(resumeRecord.staleSeconds !== undefined ? { staleSeconds: resumeRecord.staleSeconds } : {}),
995
+ tagSeq: resumeRecord.tagSeq,
996
+ ...(resumeRecord.route !== undefined ? { route: resumeRecord.route } : {}),
997
+ });
998
+ }
999
+ const existing = entries.get(key);
1000
+ const meta = currentRenderCacheMeta();
1001
+ const expired = existing !== undefined && entryExpired(existing);
1002
+ // Hard on-demand invalidation: a tag update (revalidateTag/updateTag) after
1003
+ // this entry was produced, or a revalidatePath covering the entry's owning
1004
+ // route. Both block and re-produce so the read observes the fresh value
1005
+ // (read-your-writes within an action; path-scoped revalidation).
1006
+ const tagStale =
1007
+ existing !== undefined && tagsRevalidatedAfterSeq(existing.tags, existing.tagSeq);
1008
+ const pathStale =
1009
+ existing !== undefined && pathScopeRevalidated(existing.route, existing.storedAt);
1010
+ const invalidated = tagStale || pathStale;
1011
+ const swrStale = existing !== undefined && tagsStaleSince(existing.tags, existing.storedAt);
1012
+ // On-demand revalidation re-produces each entry ONCE per render, deduped via refreshedIn - but only
1013
+ // entries the revalidation actually reached. A revalidation mark outlives a single render (a soft-nav
1014
+ // render never writes the prebuilt HTML back), so refreshing unconditionally churned every cache
1015
+ // scope on every later render of a still-marked route.
1016
+ const refresh =
1017
+ meta?.refreshFetches === true &&
1018
+ existing?.refreshedIn !== meta &&
1019
+ (invalidated || swrStale || expired);
1020
+ let modernGet: Promise<unknown> | undefined;
1021
+ if (modernKey) {
1022
+ void modernCacheRefreshTags();
1023
+ // Keep the handler read's promise: serving an existing entry awaits it, so the handler's get() -
1024
+ // whose logs are asserted - lands on the critical path like Next's, where a custom handler IS the
1025
+ // cache. A fire-and-forget read could otherwise log only after the response already flushed.
1026
+ modernGet = modernCacheGet(modernKey);
1027
+ if (existing && !invalidated) void modernCacheGetExpiration();
1028
+ }
1029
+ // A regeneration render (blockingStaleFetches) must re-produce not only
1030
+ // TTL-expired entries but also soft-stale ones (revalidateTag with a
1031
+ // profile): serving them stale would bake the old value into the shell it
1032
+ // is regenerating, and the rewritten file's mtime then masks the staleness.
1033
+ if (
1034
+ existing &&
1035
+ !refresh &&
1036
+ !invalidated &&
1037
+ (!(expired || swrStale) || meta?.blockingStaleFetches !== true)
1038
+ ) {
1039
+ if ((expired || swrStale) && !existing.refreshing) {
1040
+ // TTL expiry outside an ISR regen: serve stale while refreshing in
1041
+ // the background (Next's SWR data-cache semantics).
1042
+ existing.refreshing = true;
1043
+ void produceEntry(key, id, argsJson, fn, args, parent, existing, modernKey);
1044
+ }
1045
+ if (modernKey && parent && existing.revalidateSeconds === undefined) {
1046
+ void existing.value.then(
1047
+ () => propagateToParent(existing, parent),
1048
+ () => undefined,
1049
+ );
1050
+ }
1051
+ propagate(existing, parent);
1052
+ captureUseCacheRdcEntry(key);
1053
+ const served = withHoleFills(existing.value, fills);
1054
+ const out = modernGet ? modernGet.then(() => served) : served;
1055
+ const servedGate = prefetchCacheGate();
1056
+ return (servedGate ? gatePrefetchCacheLife(out, servedGate, () => existing) : out) as Result;
1057
+ }
1058
+
1059
+ const value = produceEntry(key, id, argsJson, fn, args, parent, undefined, modernKey);
1060
+ captureUseCacheRdcEntry(key);
1061
+ const out = withHoleFills(value, fills);
1062
+ // The producing entry's cacheLife is set once `value` settles (see
1063
+ // produceEntry); `entries.get(key)` reads it back after production.
1064
+ const producedGate = prefetchCacheGate();
1065
+ return (
1066
+ producedGate ? gatePrefetchCacheLife(out, producedGate, () => entries.get(key)) : out
1067
+ ) as Result;
1068
+ }
1069
+
1070
+ function produceEntry<Args extends unknown[], Result>(
1071
+ key: string,
1072
+ id: string,
1073
+ argsJson: string,
1074
+ fn: (...args: Args) => Result,
1075
+ args: Args,
1076
+ parent: UseCacheScope | undefined,
1077
+ serveStale?: UseCacheEntry,
1078
+ modernKey?: string,
1079
+ ): Promise<unknown> {
1080
+ const scope: UseCacheScope = { tags: new Set(), functionId: id };
1081
+ // Sample the invalidation-scope inputs at production start: the hard-tag
1082
+ // sequence (read-your-writes) and the producing route (path revalidation),
1083
+ // mirroring how the fetch/unstable_cache data caches tag entries with their
1084
+ // owning route.
1085
+ const tagSeq = currentTagRevalidationSeq();
1086
+ const producingMeta = currentRenderCacheMeta();
1087
+ const route = producingMeta?.route;
1088
+ const entry: UseCacheEntry = {
1089
+ value: undefined as unknown as Promise<unknown>,
1090
+ storedAt: realNow(),
1091
+ tags: [],
1092
+ modernKey,
1093
+ tagSeq,
1094
+ ...(producingMeta !== undefined ? { refreshedIn: producingMeta } : {}),
1095
+ ...(route !== undefined ? { route } : serveStale?.route !== undefined ? { route: serveStale.route } : {}),
1096
+ };
1097
+ const rawValue = (async () =>
1098
+ await runInsideDataCacheProducer(() => scopeStorage.run(scope, () => fn(...args))))().then(
1099
+ // Render-once semantics: nested server components inside the produced tree
1100
+ // execute once for the entry's lifetime (see wrapCachedValue).
1101
+ produced => wrapCachedValue(produced, scope, entry),
1102
+ // A throw inside the cache scope crosses Next's cache flight boundary
1103
+ // before any caller sees it: at runtime that reshapes the error (and, in a
1104
+ // plain production build, logs it here and redacts what escapes).
1105
+ error => {
1106
+ throw prerenderErrorCollectionActive() ? error : funnelCacheRuntimeError(error);
1107
+ },
1108
+ );
1109
+ // E236 hanging-input guard: during a build prerender a fill that never
1110
+ // resolves would hang the build; race it against the configured fill timeout
1111
+ // and reject with Next's timeout message (recorded so the build fails). Gated
1112
+ // on the armed build-prerender window (the cache-components shell path sets no
1113
+ // render cache-meta, so we can't rely on meta.prerender here).
1114
+ const value = prerenderErrorCollectionActive()
1115
+ ? withFillTimeout(rawValue, routeFromCacheId(id))
1116
+ : rawValue;
1117
+ // A build prerender's task boundary must not cut a cache fill short: the
1118
+ // whole point of a cached read is that its (possibly task-settled) IO belongs
1119
+ // in the static shell. Register it so the boundary waits (core ppr).
1120
+ void trackPrerenderCacheFill(value);
1121
+ entry.value = value;
1122
+ let resolveModernEntry: ((entry: ReturnType<typeof modernCacheEntry>) => void) | undefined;
1123
+ let rejectModernEntry: ((reason?: unknown) => void) | undefined;
1124
+ if (modernKey) {
1125
+ const pendingModernEntry = new Promise<ReturnType<typeof modernCacheEntry>>((resolve, reject) => {
1126
+ resolveModernEntry = resolve;
1127
+ rejectModernEntry = reject;
1128
+ });
1129
+ void modernCacheSet(modernKey, pendingModernEntry);
1130
+ }
1131
+ if (!serveStale) {
1132
+ if (entries.size >= entriesLimit) {
1133
+ const oldest = entries.keys().next().value;
1134
+ if (oldest !== undefined) entries.delete(oldest);
1135
+ }
1136
+ entries.set(key, entry);
1137
+ }
1138
+ value.then(
1139
+ produced => {
1140
+ entry.tags = [...scope.tags];
1141
+ // Conditional-read upgrade (Stage B-3): production may have read root
1142
+ // params that were not in this entry's key (first-ever call for the id, or
1143
+ // a newly-discovered branch-dependent read). Re-store the settled entry
1144
+ // under the param-aware key so a concurrent/later call that already keyed
1145
+ // on those root params finds this value instead of re-producing. The
1146
+ // original key stays populated too (harmless; both point at one value).
1147
+ if (scope.rootParamsRead && scope.rootParamsRead.size > 0) {
1148
+ const upgradedKey = cacheKeyFor(id, withRootParamKeyArgs(id, args));
1149
+ if (typeof upgradedKey === 'string' && upgradedKey !== key) {
1150
+ entries.set(upgradedKey, entry);
1151
+ }
1152
+ }
1153
+ // A `use cache` entry with no explicit cacheLife() takes Next's `default`
1154
+ // profile (revalidate 900, expire one year, stale 300) so its route
1155
+ // emits the SWR cache-control + x-nextjs-stale-time headers.
1156
+ entry.revalidateSeconds = scope.revalidateSeconds ?? DEFAULT_REVALIDATE_SECONDS;
1157
+ entry.expireSeconds = scope.expireSeconds ?? DEFAULT_EXPIRE_SECONDS;
1158
+ entry.staleSeconds = scope.staleSeconds ?? DEFAULT_STALE_SECONDS;
1159
+ // Persisting to a modern cache handler waits for embedded nested `use cache` vnodes to render
1160
+ // (their scopes fold tags and min cacheLife into this entry - Next resolves the entry only once
1161
+ // the whole cached tree has serialized). A fallback timer guards against a tree that is never
1162
+ // rendered, so the handler's pending set cannot deadlock.
1163
+ const resolveModern = resolveModernEntry;
1164
+ if (resolveModern) {
1165
+ const finalize = () =>
1166
+ resolveModern(
1167
+ modernCacheEntry({
1168
+ value: produced,
1169
+ tags: entry.tags,
1170
+ staleSeconds: entry.staleSeconds ?? DEFAULT_STALE_SECONDS,
1171
+ expireSeconds: entry.expireSeconds ?? DEFAULT_EXPIRE_SECONDS,
1172
+ revalidateSeconds: entry.revalidateSeconds ?? DEFAULT_REVALIDATE_SECONDS,
1173
+ storedAt: entry.storedAt,
1174
+ }),
1175
+ );
1176
+ if (entry.nestedPending !== undefined && entry.nestedPending > 0) {
1177
+ const timer = setTimeout(finalize, 10_000);
1178
+ if (typeof timer.unref === 'function') timer.unref();
1179
+ entry.onNestedSettled = () => {
1180
+ if (entry.nestedPending === 0) {
1181
+ clearTimeout(timer);
1182
+ finalize();
1183
+ }
1184
+ };
1185
+ } else {
1186
+ finalize();
1187
+ }
1188
+ }
1189
+ // Background refresh replaces the stale entry only once settled.
1190
+ if (serveStale) entries.set(key, entry);
1191
+ propagate(entry, parent);
1192
+ },
1193
+ () => {
1194
+ rejectModernEntry?.();
1195
+ if (serveStale) serveStale.refreshing = false;
1196
+ else if (entries.get(key) === entry) entries.delete(key);
1197
+ },
1198
+ );
1199
+ return value;
1200
+ }
1201
+
1202
+ /**
1203
+ * Serialize call arguments to a cache key. Non-data values (functions, symbols, React/preact element
1204
+ * trees) are replaced with a stable token, mirroring how Next passes such inputs through the cache
1205
+ * boundary without keying on them. Cyclic structures bail out (null key = run uncached).
1206
+ */
1207
+ function cacheKeyFor(id: string, args: unknown[]): string | Promise<string | null> | null {
1208
+ try {
1209
+ const keyValue = resolvePromisesForKey(args, new WeakSet<object>());
1210
+ if (isPromiseLike(keyValue)) {
1211
+ return Promise.resolve(keyValue).then(
1212
+ resolved => stringifyCacheKey(id, resolved),
1213
+ () => null,
1214
+ );
1215
+ }
1216
+ return stringifyCacheKey(id, keyValue);
1217
+ } catch {
1218
+ return null;
1219
+ }
1220
+ }
1221
+
1222
+ function argsJsonFromKey(id: string, key: string): string {
1223
+ return key.startsWith(`${id}:`) ? key.slice(id.length + 1) : '[]';
1224
+ }
1225
+
1226
+ function stringifyCacheKey(id: string, value: unknown): string | null {
1227
+ try {
1228
+ const json = JSON.stringify(value, (_key, nested: unknown) => {
1229
+ if (typeof nested === 'function' || typeof nested === 'symbol') return '$pnext:ref';
1230
+ if (typeof nested === 'bigint') return `$pnext:bigint:${nested}`;
1231
+ if (isElementLike(nested)) return '$pnext:element';
1232
+ return nested;
1233
+ });
1234
+ return `${id}:${json}`;
1235
+ } catch {
1236
+ return null;
1237
+ }
1238
+ }
1239
+
1240
+ function resolvePromisesForKey(value: unknown, stack: WeakSet<object>): unknown {
1241
+ if (isPromiseLike(value)) {
1242
+ return Promise.resolve(value).then(resolved =>
1243
+ resolvePromisesForKey(resolved, new WeakSet<object>()),
1244
+ );
1245
+ }
1246
+ if (typeof value !== 'object' || value === null || isElementLike(value)) return value;
1247
+
1248
+ if (stack.has(value)) throw new Error('cyclic cache key');
1249
+ stack.add(value);
1250
+ try {
1251
+ if (Array.isArray(value)) {
1252
+ const items = value.map(item => resolvePromisesForKey(item, stack));
1253
+ return items.some(isPromiseLike) ? Promise.all(items) : value;
1254
+ }
1255
+
1256
+ const entriesForObject = Object.entries(value);
1257
+ if (entriesForObject.length === 0) return value;
1258
+
1259
+ const resolvedEntries = entriesForObject.map(([key, nested]) => [
1260
+ key,
1261
+ resolvePromisesForKey(nested, stack),
1262
+ ] as const);
1263
+ if (!resolvedEntries.some(([, nested]) => isPromiseLike(nested))) return value;
1264
+
1265
+ return Promise.all(
1266
+ resolvedEntries.map(([key, nested]) => Promise.resolve(nested).then(resolved => [key, resolved] as const)),
1267
+ ).then(entriesList => Object.fromEntries(entriesList));
1268
+ } finally {
1269
+ stack.delete(value);
1270
+ }
1271
+ }
1272
+
1273
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
1274
+ return (
1275
+ typeof value === 'object' &&
1276
+ value !== null &&
1277
+ 'then' in value &&
1278
+ typeof (value as { then?: unknown }).then === 'function'
1279
+ );
1280
+ }
1281
+
1282
+ // Cached element trees: argument holes + render-once subtree semantics.
1283
+ //
1284
+ // In Next, everything a `use cache` function CREATES is part of the cached RSC payload (nested server
1285
+ // components execute once, during the fill), while element/node values PASSED IN are serialized as
1286
+ // references ("holes") that each invocation fills with its own content. pnext shares values by
1287
+ // reference instead of serializing, so both halves are mirrored structurally:
1288
+ //
1289
+ // 1. Holes: element-like arguments are swapped for hole marker vnodes before keying/producing (they
1290
+ // already key as an opaque token, so different children share one entry), and each call
1291
+ // re-instantiates the cached tree with ITS elements substituted back in - cloning only the vnodes
1292
+ // on hole paths, so unrelated subtrees keep their identity.
1293
+ // 2. Render-once: when a produced value settles, every plain server-component vnode in it gets its
1294
+ // `type` swapped for a per-vnode memo wrapper. The first render executes the component inside the
1295
+ // entry's cache scope, so cacheTag()/cacheLife()/no-store semantics attribute correctly, and the
1296
+ // result - recursively wrapped the same way - is reused by every other placement and request for
1297
+ // the entry's lifetime. Subtrees that received a hole re-execute per invocation.
1298
+ //
1299
+ // Wrappers deliberately skip: Suspense identities (the renderer must keep recognizing boundaries),
1300
+ // Fragment, class components, island/client references and other marked component kinds, and
1301
+ // `use cache` wrappers themselves (they self-memoize through the entry store, preserving their own
1302
+ // revalidation). Sync components that throw on detached invocation rethrow unmemoized, so the
1303
+ // renderer's existing hook-dispatcher fallback still applies.
1304
+
1305
+ /** Marks pnextUseCache* wrappers so tree wrapping leaves them to self-memoize. */
1306
+ const USE_CACHE_WRAPPER = Symbol.for('pnext.compat.useCacheWrapper');
1307
+ const MEMO_WRAPPED = Symbol.for('pnext.compat.useCacheMemoWrapped');
1308
+ // Renderer component-kind marks (global symbol registry; see render/renderer.ts,
1309
+ // client/reference.ts, api/dynamic.tsx, render/slots.tsx).
1310
+ const clientReferenceSymbol = Symbol.for('pnext.clientReference');
1311
+ const dynamicReferenceSymbol = Symbol.for('pnext.dynamic');
1312
+ const paramsScopeSymbol = Symbol.for('pnext.paramsScope');
1313
+ const clientPageComponentSymbol = Symbol.for('pnext.clientPageComponent');
1314
+ const useCacheJoinerPromiseSymbol = Symbol.for('pnext.compat.useCacheJoinerPromise');
1315
+
1316
+ const HOLE_INDEX_PROP = '__pnextCacheHoleIndex';
1317
+
1318
+ /** Inert placeholder; instantiation replaces it before it can ever render. */
1319
+ function CachedArgumentHole(): null {
1320
+ return null;
1321
+ }
1322
+
1323
+ function holeElement(index: number) {
1324
+ return h(CachedArgumentHole as never, { [HOLE_INDEX_PROP]: index } as never);
1325
+ }
1326
+
1327
+ interface ExtractedHoles {
1328
+ args: unknown[];
1329
+ fills: unknown[];
1330
+ }
1331
+
1332
+ /**
1333
+ * Replace element-like values inside the (already-serialized) argument list
1334
+ * with indexed hole vnodes, collecting the real elements in order. Containers
1335
+ * are cloned only along changed paths; an argument list with no elements is
1336
+ * returned unchanged with an empty fills list.
1337
+ */
1338
+ function extractElementHoles(args: unknown[]): ExtractedHoles {
1339
+ const fills: unknown[] = [];
1340
+ const stack = new WeakSet<object>();
1341
+ const walk = (value: unknown): unknown => {
1342
+ if (value === null || typeof value !== 'object' || isPromiseLike(value)) return value;
1343
+ if (isElementLike(value)) {
1344
+ fills.push(value);
1345
+ return holeElement(fills.length - 1);
1346
+ }
1347
+ if (stack.has(value)) return value;
1348
+ stack.add(value);
1349
+ try {
1350
+ if (Array.isArray(value)) {
1351
+ let changed = false;
1352
+ const next = value.map(item => {
1353
+ const out = walk(item);
1354
+ if (out !== item) changed = true;
1355
+ return out;
1356
+ });
1357
+ return changed ? next : value;
1358
+ }
1359
+ const proto: unknown = Object.getPrototypeOf(value);
1360
+ if (proto !== Object.prototype && proto !== null) return value;
1361
+ let changed = false;
1362
+ const entriesForObject = Object.entries(value).map(([key, nested]) => {
1363
+ const out = walk(nested);
1364
+ if (out !== nested) changed = true;
1365
+ return [key, out] as const;
1366
+ });
1367
+ return changed ? Object.fromEntries(entriesForObject) : value;
1368
+ } finally {
1369
+ stack.delete(value);
1370
+ }
1371
+ };
1372
+ const out = args.map(walk);
1373
+ return fills.length > 0 ? { args: out, fills } : { args, fills };
1374
+ }
1375
+
1376
+ /** Resolve a cached value for one invocation, substituting its hole fills. */
1377
+ function withHoleFills(value: Promise<unknown>, fills: unknown[]): Promise<unknown> {
1378
+ if (fills.length === 0) return value;
1379
+ return value.then(resolved => instantiateHoles(resolved, fills, new WeakSet<object>()));
1380
+ }
1381
+
1382
+ function instantiateHoles(value: unknown, fills: unknown[], stack: WeakSet<object>): unknown {
1383
+ if (value === null || typeof value !== 'object' || isPromiseLike(value)) return value;
1384
+ if (stack.has(value)) return value;
1385
+ stack.add(value);
1386
+ try {
1387
+ if (isElementLike(value)) {
1388
+ const vnode = value as {
1389
+ type?: unknown;
1390
+ props?: Record<string, unknown> | null;
1391
+ key?: unknown;
1392
+ ref?: unknown;
1393
+ };
1394
+ if (vnode.type === CachedArgumentHole) {
1395
+ const index = vnode.props?.[HOLE_INDEX_PROP];
1396
+ return typeof index === 'number' ? (fills[index] ?? null) : null;
1397
+ }
1398
+ if (!vnode.props || typeof vnode.props !== 'object') return value;
1399
+ let changed = false;
1400
+ const nextProps = Object.fromEntries(
1401
+ Object.entries(vnode.props).map(([key, nested]) => {
1402
+ const out = instantiateHoles(nested, fills, stack);
1403
+ if (out !== nested) changed = true;
1404
+ return [key, out] as const;
1405
+ }),
1406
+ );
1407
+ if (!changed) return value;
1408
+ return cloneElementLike(vnode, vnode.type, nextProps);
1409
+ }
1410
+ if (Array.isArray(value)) {
1411
+ let changed = false;
1412
+ const next = value.map(item => {
1413
+ const out = instantiateHoles(item, fills, stack);
1414
+ if (out !== item) changed = true;
1415
+ return out;
1416
+ });
1417
+ return changed ? next : value;
1418
+ }
1419
+ const proto: unknown = Object.getPrototypeOf(value);
1420
+ if (proto !== Object.prototype && proto !== null) return value;
1421
+ let changed = false;
1422
+ const entriesForObject = Object.entries(value).map(([key, nested]) => {
1423
+ const out = instantiateHoles(nested, fills, stack);
1424
+ if (out !== nested) changed = true;
1425
+ return [key, out] as const;
1426
+ });
1427
+ return changed ? Object.fromEntries(entriesForObject) : value;
1428
+ } finally {
1429
+ stack.delete(value);
1430
+ }
1431
+ }
1432
+
1433
+ /** Re-create a vnode via h() so preact's internal fields stay pristine. */
1434
+ function cloneElementLike(
1435
+ vnode: { key?: unknown; ref?: unknown },
1436
+ type: unknown,
1437
+ props: Record<string, unknown>,
1438
+ ): unknown {
1439
+ return h(type as never, {
1440
+ ...props,
1441
+ ...(vnode.key !== undefined && vnode.key !== null ? { key: vnode.key } : {}),
1442
+ ...(vnode.ref !== undefined && vnode.ref !== null ? { ref: vnode.ref } : {}),
1443
+ } as never);
1444
+ }
1445
+
1446
+ /** Wrap a settled cached value's server-component vnodes for render-once reuse. */
1447
+ function wrapCachedValue(value: unknown, scope: UseCacheScope, entry: UseCacheEntry): unknown {
1448
+ return wrapCachedNode(value, scope, entry, new WeakSet<object>()).value;
1449
+ }
1450
+
1451
+ interface WrapNodeResult {
1452
+ value: unknown;
1453
+ hasHole: boolean;
1454
+ }
1455
+
1456
+ function wrapCachedNode(
1457
+ value: unknown,
1458
+ scope: UseCacheScope,
1459
+ entry: UseCacheEntry,
1460
+ stack: WeakSet<object>,
1461
+ ): WrapNodeResult {
1462
+ if (value === null || typeof value !== 'object' || isPromiseLike(value)) {
1463
+ return { value, hasHole: false };
1464
+ }
1465
+ if (stack.has(value)) return { value, hasHole: false };
1466
+ stack.add(value);
1467
+ try {
1468
+ if (isElementLike(value)) {
1469
+ const vnode = value as {
1470
+ type?: unknown;
1471
+ props?: Record<string, unknown> | null;
1472
+ key?: unknown;
1473
+ ref?: unknown;
1474
+ };
1475
+ if (vnode.type === CachedArgumentHole) return { value, hasHole: true };
1476
+ let hasHole = false;
1477
+ let changed = false;
1478
+ let nextProps = vnode.props ?? undefined;
1479
+ if (vnode.props && typeof vnode.props === 'object') {
1480
+ const entriesForProps = Object.entries(vnode.props).map(([key, nested]) => {
1481
+ const result = wrapCachedNode(nested, scope, entry, stack);
1482
+ hasHole ||= result.hasHole;
1483
+ if (result.value !== nested) changed = true;
1484
+ return [key, result.value] as const;
1485
+ });
1486
+ if (changed) nextProps = Object.fromEntries(entriesForProps);
1487
+ }
1488
+ // A component whose props carry a hole re-executes per invocation (its
1489
+ // props genuinely differ per call); everything else renders once.
1490
+ const type = vnode.type;
1491
+ if (!hasHole && isUseCacheWrapperType(type)) {
1492
+ entry.nestedPending = (entry.nestedPending ?? 0) + 1;
1493
+ }
1494
+ const nextType =
1495
+ !hasHole && isUseCacheWrapperType(type)
1496
+ ? attributeNestedCacheWrapper(
1497
+ type as (props: Record<string, unknown>) => unknown,
1498
+ scope,
1499
+ entry,
1500
+ )
1501
+ : !hasHole && shouldMemoWrapType(type)
1502
+ ? memoServerComponent(type as (props: Record<string, unknown>) => unknown, scope, entry)
1503
+ : type;
1504
+ if (!changed && nextType === type) return { value, hasHole };
1505
+ return {
1506
+ value: cloneElementLike(vnode, nextType, nextProps ?? {}),
1507
+ hasHole,
1508
+ };
1509
+ }
1510
+ if (Array.isArray(value)) {
1511
+ let hasHole = false;
1512
+ let changed = false;
1513
+ const next = value.map(item => {
1514
+ const result = wrapCachedNode(item, scope, entry, stack);
1515
+ hasHole ||= result.hasHole;
1516
+ if (result.value !== item) changed = true;
1517
+ return result.value;
1518
+ });
1519
+ return { value: changed ? next : value, hasHole };
1520
+ }
1521
+ const proto: unknown = Object.getPrototypeOf(value);
1522
+ if (proto !== Object.prototype && proto !== null) return { value, hasHole: false };
1523
+ let hasHole = false;
1524
+ let changed = false;
1525
+ const entriesForObject = Object.entries(value).map(([key, nested]) => {
1526
+ const result = wrapCachedNode(nested, scope, entry, stack);
1527
+ hasHole ||= result.hasHole;
1528
+ if (result.value !== nested) changed = true;
1529
+ return [key, result.value] as const;
1530
+ });
1531
+ return { value: changed ? Object.fromEntries(entriesForObject) : value, hasHole };
1532
+ } finally {
1533
+ stack.delete(value);
1534
+ }
1535
+ }
1536
+
1537
+ function isUseCacheWrapperType(type: unknown): boolean {
1538
+ return (
1539
+ typeof type === 'function' &&
1540
+ Boolean((type as unknown as Record<symbol, unknown>)[USE_CACHE_WRAPPER]) &&
1541
+ !(type as unknown as Record<symbol, unknown>)[MEMO_WRAPPED]
1542
+ );
1543
+ }
1544
+
1545
+ /**
1546
+ * A nested `use cache` component embedded in an outer cached tree renders OUTSIDE the outer entry's
1547
+ * fill (the renderer invokes the vnode after the outer value settled), so its cache metadata would
1548
+ * never attribute to the enclosing entry. Wrap the vnode type so the inner wrapper executes inside the
1549
+ * outer scope, and fold the accumulated scope aggregates back onto the outer entry once the inner
1550
+ * result settles - Next folds a nested cache scope's tags and MIN lifetimes into the enclosing entry.
1551
+ * The inner wrapper still self-memoizes through the entry store, preserving its own revalidation.
1552
+ */
1553
+ function attributeNestedCacheWrapper(
1554
+ fn: (props: Record<string, unknown>) => unknown,
1555
+ scope: UseCacheScope,
1556
+ entry: UseCacheEntry,
1557
+ ): (props: Record<string, unknown>) => unknown {
1558
+ let settledOnce = false;
1559
+ const settle = (fold: boolean) => {
1560
+ if (fold) foldScopeIntoEntry(scope, entry);
1561
+ if (settledOnce) return;
1562
+ settledOnce = true;
1563
+ if (entry.nestedPending !== undefined && entry.nestedPending > 0) {
1564
+ entry.nestedPending -= 1;
1565
+ entry.onNestedSettled?.();
1566
+ }
1567
+ };
1568
+ const wrapper = function (this: unknown, props: Record<string, unknown>): unknown {
1569
+ if (this !== undefined) return fn.call(this, props);
1570
+ const result = scopeStorage.run(scope, () => fn(props));
1571
+ if (isPromiseLike(result)) {
1572
+ return Promise.resolve(result).then(
1573
+ value => {
1574
+ settle(true);
1575
+ return value;
1576
+ },
1577
+ (error: unknown) => {
1578
+ settle(false);
1579
+ throw error;
1580
+ },
1581
+ );
1582
+ }
1583
+ settle(true);
1584
+ return result;
1585
+ };
1586
+ const marked = wrapper as unknown as Record<symbol, unknown>;
1587
+ marked[MEMO_WRAPPED] = true;
1588
+ marked[USE_CACHE_WRAPPER] = true;
1589
+ return wrapper;
1590
+ }
1591
+
1592
+ /** Fold the scope's accumulated tags + min cacheLife onto a settled entry. */
1593
+ function foldScopeIntoEntry(scope: UseCacheScope, entry: UseCacheEntry): void {
1594
+ entry.tags = [...new Set([...entry.tags, ...scope.tags])];
1595
+ entry.revalidateSeconds = minDefined(entry.revalidateSeconds, scope.revalidateSeconds);
1596
+ entry.expireSeconds = minDefined(entry.expireSeconds, scope.expireSeconds);
1597
+ entry.staleSeconds = minDefined(entry.staleSeconds, scope.staleSeconds);
1598
+ }
1599
+
1600
+ function shouldMemoWrapType(type: unknown): boolean {
1601
+ if (typeof type !== 'function') return false;
1602
+ if (type === Fragment || type === (CoreSuspense as unknown)) return false;
1603
+ const component = type as unknown as { prototype?: { render?: unknown } } & Record<
1604
+ symbol,
1605
+ unknown
1606
+ >;
1607
+ // Class components (incl. preact/compat Suspense) need a preact instance.
1608
+ if (component.prototype && typeof component.prototype.render === 'function') return false;
1609
+ if (component[MEMO_WRAPPED] || component[USE_CACHE_WRAPPER]) return false;
1610
+ if (
1611
+ component[clientReferenceSymbol] ||
1612
+ component[dynamicReferenceSymbol] ||
1613
+ component[paramsScopeSymbol] ||
1614
+ component[clientPageComponentSymbol]
1615
+ ) {
1616
+ return false;
1617
+ }
1618
+ return true;
1619
+ }
1620
+
1621
+ /**
1622
+ * Per-vnode render-once wrapper. The first detached (server-resolver) invocation runs the real component
1623
+ * inside the entry's cache scope and caches the recursively-wrapped result for every later
1624
+ * placement/request. Throws - use() thenables, hook-dispatcher errors from non-server components - are
1625
+ * rethrown unmemoized so the renderer's replay/fallback paths behave exactly as without the wrapper, and
1626
+ * preact-instance invocations bypass the memo entirely.
1627
+ */
1628
+ function memoServerComponent(
1629
+ fn: (props: Record<string, unknown>) => unknown,
1630
+ scope: UseCacheScope,
1631
+ entry: UseCacheEntry,
1632
+ ): (props: Record<string, unknown>) => unknown {
1633
+ let memo: { value: unknown; requestToken?: object } | undefined;
1634
+ const wrapper = function (this: unknown, props: Record<string, unknown>): unknown {
1635
+ if (this !== undefined) return fn.call(this, props);
1636
+ if (memo) {
1637
+ const requestToken = currentUseCacheRequestToken();
1638
+ if (
1639
+ memo.requestToken &&
1640
+ requestToken &&
1641
+ memo.requestToken !== requestToken &&
1642
+ isPromiseLike(memo.value)
1643
+ ) {
1644
+ const joined = Promise.resolve(memo.value).then(value => value);
1645
+ (joined as unknown as Record<symbol, unknown>)[useCacheJoinerPromiseSymbol] = true;
1646
+ return joined;
1647
+ }
1648
+ return memo.value;
1649
+ }
1650
+ const requestToken = currentUseCacheRequestToken();
1651
+ const result = runInsideDataCacheProducer(() => scopeStorage.run(scope, () => fn(props)));
1652
+ if (isPromiseLike(result)) {
1653
+ const settled = Promise.resolve(result).then(resolved => {
1654
+ // Tags declared while the subtree rendered attribute to the entry.
1655
+ entry.tags = [...scope.tags];
1656
+ return wrapCachedValue(resolved, scope, entry) as ComponentChildren;
1657
+ });
1658
+ memo = { value: settled, ...(requestToken ? { requestToken } : {}) };
1659
+ settled.then(undefined, () => {
1660
+ // Failures don't stick: the next placement re-executes.
1661
+ if (memo?.value === settled) memo = undefined;
1662
+ });
1663
+ return settled;
1664
+ }
1665
+ entry.tags = [...scope.tags];
1666
+ const wrapped = wrapCachedValue(result, scope, entry);
1667
+ memo = { value: wrapped, ...(requestToken ? { requestToken } : {}) };
1668
+ return wrapped;
1669
+ };
1670
+ (wrapper as unknown as Record<symbol, unknown>)[MEMO_WRAPPED] = true;
1671
+ return wrapper;
1672
+ }
1673
+
1674
+ function currentUseCacheRequestToken(): object | undefined {
1675
+ const request = currentRequest() as object | undefined;
1676
+ if (!request) return undefined;
1677
+ let token = requestTokens.get(request);
1678
+ if (!token) {
1679
+ token = {};
1680
+ requestTokens.set(request, token);
1681
+ }
1682
+ return token;
1683
+ }
1684
+
1685
+ function isElementLike(value: unknown): boolean {
1686
+ if (typeof value !== 'object' || value === null) return false;
1687
+ const record = value as Record<string, unknown>;
1688
+ if ('$$typeof' in record) return true;
1689
+ return 'type' in record && 'props' in record;
1690
+ }
1691
+
1692
+ /** Test hook. */
1693
+ export function clearUseCacheEntries() {
1694
+ entries.clear();
1695
+ }