@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.
- package/README.md +153 -0
- package/bin/pnext +67 -0
- package/config/lint/base.js +48 -0
- package/config/ts/base.json +26 -0
- package/config/ts/react.json +13 -0
- package/package.json +70 -0
- package/reference/compat.md +63 -0
- package/reference/config.md +120 -0
- package/reference/css.md +58 -0
- package/reference/dev.md +69 -0
- package/reference/env.md +40 -0
- package/reference/metadata.md +86 -0
- package/reference/navigation.md +149 -0
- package/reference/overview.md +35 -0
- package/reference/performance.md +97 -0
- package/reference/rendering.md +127 -0
- package/reference/routing.md +167 -0
- package/reference/typegen.md +64 -0
- package/src/api/cache.ts +80 -0
- package/src/api/client-cache.ts +9 -0
- package/src/api/client-navigation.ts +279 -0
- package/src/api/dynamic.tsx +102 -0
- package/src/api/link.tsx +119 -0
- package/src/api/navigation.ts +198 -0
- package/src/api/router/events.ts +53 -0
- package/src/api/router/history.ts +70 -0
- package/src/api/router/hub.ts +194 -0
- package/src/api/router/policies.ts +107 -0
- package/src/api/router/runtime.ts +5238 -0
- package/src/api/router/types.ts +299 -0
- package/src/api/router.ts +167 -0
- package/src/api/server.ts +323 -0
- package/src/api/suspense.ts +16 -0
- package/src/cache/context.ts +61 -0
- package/src/cli/adapters/vercel-warm.ts +375 -0
- package/src/cli/adapters/vercel.ts +1310 -0
- package/src/cli/analyze-print.ts +181 -0
- package/src/cli/analyze.ts +328 -0
- package/src/cli/boot-trace.ts +29 -0
- package/src/cli/build.ts +3114 -0
- package/src/cli/dev.ts +276 -0
- package/src/cli/index.ts +196 -0
- package/src/cli/named-bin.ts +119 -0
- package/src/cli/serve-ui.ts +160 -0
- package/src/cli/start.ts +1425 -0
- package/src/client/build.ts +2136 -0
- package/src/client/chunk-fold.ts +526 -0
- package/src/client/entry.ts +1525 -0
- package/src/client/paths.ts +22 -0
- package/src/client/prebuilt.ts +621 -0
- package/src/client/profile.ts +75 -0
- package/src/client/react-compiler.ts +94 -0
- package/src/client/reference-stub.ts +145 -0
- package/src/client/reference.ts +47 -0
- package/src/compat/actions/action-client.ts +531 -0
- package/src/compat/actions/action-dispatch.ts +676 -0
- package/src/compat/actions/action-router.ts +40 -0
- package/src/compat/actions/action-shared.ts +95 -0
- package/src/compat/actions/client-plugin.ts +135 -0
- package/src/compat/actions/client-stub.ts +65 -0
- package/src/compat/actions/config.ts +164 -0
- package/src/compat/actions/detect.ts +208 -0
- package/src/compat/actions/discovery.ts +244 -0
- package/src/compat/actions/early-submit.ts +40 -0
- package/src/compat/actions/endpoint.ts +602 -0
- package/src/compat/actions/flight.ts +52 -0
- package/src/compat/actions/form-state.ts +73 -0
- package/src/compat/actions/hoist.ts +485 -0
- package/src/compat/actions/ids.ts +39 -0
- package/src/compat/actions/index.ts +41 -0
- package/src/compat/actions/instances.ts +144 -0
- package/src/compat/actions/origin.ts +109 -0
- package/src/compat/actions/protocol.ts +125 -0
- package/src/compat/actions/registry.ts +74 -0
- package/src/compat/actions/rewrite.ts +282 -0
- package/src/compat/actions/serve.ts +414 -0
- package/src/compat/actions/server-tag.ts +21 -0
- package/src/compat/actions/unrecognized-error.ts +30 -0
- package/src/compat/adapter/build-complete.ts +257 -0
- package/src/compat/bundler/bun-externals.ts +53 -0
- package/src/compat/bundler/cjs-exports.ts +542 -0
- package/src/compat/bundler/config.ts +363 -0
- package/src/compat/bundler/externals.ts +34 -0
- package/src/compat/bundler/import-meta-url.ts +60 -0
- package/src/compat/bundler/modularize-imports.ts +119 -0
- package/src/compat/bundler/new-url-asset.ts +87 -0
- package/src/compat/bundler/optimize-package-imports.ts +273 -0
- package/src/compat/bundler/polyfill.ts +88 -0
- package/src/compat/bundler/react-compiler.ts +61 -0
- package/src/compat/bundler/react-profiler.tsx +25 -0
- package/src/compat/bundler/relay-transform.ts +116 -0
- package/src/compat/bundler/require-context.ts +281 -0
- package/src/compat/bundler/resolve-extensions.ts +75 -0
- package/src/compat/bundler/source-cache.ts +61 -0
- package/src/compat/bundler/static-imports.ts +25 -0
- package/src/compat/bundler/symlink-imports.ts +119 -0
- package/src/compat/bundler/tsconfig-paths.ts +50 -0
- package/src/compat/bundler/wasm.ts +153 -0
- package/src/compat/bundler/webpack-loaders.ts +685 -0
- package/src/compat/bundler/worker.ts +278 -0
- package/src/compat/cache/build-flags.ts +81 -0
- package/src/compat/cache/build-prerender-errors.ts +163 -0
- package/src/compat/cache/custom-handler.ts +159 -0
- package/src/compat/cache/fetch-patch.ts +745 -0
- package/src/compat/cache/handler.ts +100 -0
- package/src/compat/cache/modern-handler.ts +275 -0
- package/src/compat/cache/resume-data-cache.ts +143 -0
- package/src/compat/cache/revalidate.ts +759 -0
- package/src/compat/cache/runtime-error.ts +124 -0
- package/src/compat/cache/use-cache-transform.ts +961 -0
- package/src/compat/cache/use-cache.ts +1695 -0
- package/src/compat/cache-control.ts +269 -0
- package/src/compat/client/base-path.ts +64 -0
- package/src/compat/client/css-order.ts +36 -0
- package/src/compat/client/errors/control-flow.ts +92 -0
- package/src/compat/client/errors/error-boundary.ts +222 -0
- package/src/compat/client/errors/global-error.ts +238 -0
- package/src/compat/client/errors/install.ts +217 -0
- package/src/compat/client/errors/lazy.ts +53 -0
- package/src/compat/client/errors/primitive-throw.ts +126 -0
- package/src/compat/client/errors/soft-refresh.ts +14 -0
- package/src/compat/client/link-status.ts +86 -0
- package/src/compat/client/nav-compat-runtime.ts +57 -0
- package/src/compat/client/nav-compat.ts +42 -0
- package/src/compat/client/navigation-scroll.ts +154 -0
- package/src/compat/client/optimistic-routing.ts +206 -0
- package/src/compat/client/prefetch-cache.ts +111 -0
- package/src/compat/client/route-announcer.ts +72 -0
- package/src/compat/client/segment-cache-policy.ts +159 -0
- package/src/compat/client/segment-cache.ts +1077 -0
- package/src/compat/client/segment-prefetch.ts +375 -0
- package/src/compat/client/trailing-slash.ts +24 -0
- package/src/compat/css/chunking.ts +254 -0
- package/src/compat/css/inline-css.ts +73 -0
- package/src/compat/css/lightningcss.ts +90 -0
- package/src/compat/css/modules.ts +373 -0
- package/src/compat/css/nonce.ts +30 -0
- package/src/compat/css/sass-plugin.ts +65 -0
- package/src/compat/css/sass.ts +392 -0
- package/src/compat/css/styled-jsx-runtime.ts +80 -0
- package/src/compat/css/styled-jsx.ts +49 -0
- package/src/compat/edge-runtime.ts +71 -0
- package/src/compat/export/client.ts +112 -0
- package/src/compat/export/index.ts +272 -0
- package/src/compat/export/standalone.ts +207 -0
- package/src/compat/image-optimizer/cache.ts +119 -0
- package/src/compat/image-optimizer/detect.ts +143 -0
- package/src/compat/image-optimizer/index.ts +601 -0
- package/src/compat/image-optimizer/source.ts +243 -0
- package/src/compat/index.ts +458 -0
- package/src/compat/lifecycle/after-scope.ts +86 -0
- package/src/compat/lifecycle/after.ts +173 -0
- package/src/compat/lifecycle/error-funnel.ts +306 -0
- package/src/compat/lifecycle/error-serialize.ts +87 -0
- package/src/compat/lifecycle/error-ui.ts +167 -0
- package/src/compat/lifecycle/instrumentation-client.ts +138 -0
- package/src/compat/lifecycle/instrumentation.ts +277 -0
- package/src/compat/lifecycle/node-console.ts +19 -0
- package/src/compat/lifecycle/testmode.ts +263 -0
- package/src/compat/mdx/compile.ts +219 -0
- package/src/compat/mdx/next-mdx-stub.ts +46 -0
- package/src/compat/mdx/plugin.ts +37 -0
- package/src/compat/metadata-route-artifacts.ts +458 -0
- package/src/compat/metadata.ts +295 -0
- package/src/compat/middleware/manifest.ts +210 -0
- package/src/compat/misc/action-return.ts +173 -0
- package/src/compat/next/cache.ts +211 -0
- package/src/compat/next/canonical-url.ts +35 -0
- package/src/compat/next/client-cache.ts +57 -0
- package/src/compat/next/client-navigation.ts +313 -0
- package/src/compat/next/client-only.ts +3 -0
- package/src/compat/next/client-script.tsx +215 -0
- package/src/compat/next/client-server.ts +39 -0
- package/src/compat/next/config-loader.ts +569 -0
- package/src/compat/next/config.ts +29 -0
- package/src/compat/next/constants.cjs +6 -0
- package/src/compat/next/constants.ts +6 -0
- package/src/compat/next/custom-server.ts +236 -0
- package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
- package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
- package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
- package/src/compat/next/dynamic.tsx +46 -0
- package/src/compat/next/error.tsx +148 -0
- package/src/compat/next/font/cache.ts +171 -0
- package/src/compat/next/font/google.ts +2 -0
- package/src/compat/next/font/index.ts +8 -0
- package/src/compat/next/font/local.ts +5 -0
- package/src/compat/next/font/runtime-client.ts +71 -0
- package/src/compat/next/font/runtime.ts +974 -0
- package/src/compat/next/font/shared.ts +281 -0
- package/src/compat/next/form.tsx +156 -0
- package/src/compat/next/head.tsx +10 -0
- package/src/compat/next/headers.ts +247 -0
- package/src/compat/next/image/config.ts +196 -0
- package/src/compat/next/image/optimizer.ts +96 -0
- package/src/compat/next/image/patterns.ts +103 -0
- package/src/compat/next/image/shared.ts +141 -0
- package/src/compat/next/image/static-metadata.ts +283 -0
- package/src/compat/next/image/validate.ts +269 -0
- package/src/compat/next/image-client.tsx +215 -0
- package/src/compat/next/image-props.ts +575 -0
- package/src/compat/next/image-usage.ts +102 -0
- package/src/compat/next/image.tsx +56 -0
- package/src/compat/next/index.ts +1 -0
- package/src/compat/next/legacy-image.tsx +97 -0
- package/src/compat/next/link-usage.ts +29 -0
- package/src/compat/next/link-validation-transform.ts +200 -0
- package/src/compat/next/link.tsx +466 -0
- package/src/compat/next/navigation.cjs +21 -0
- package/src/compat/next/navigation.ts +188 -0
- package/src/compat/next/offline.ts +51 -0
- package/src/compat/next/og.ts +324 -0
- package/src/compat/next/optimistic-route-state.ts +188 -0
- package/src/compat/next/preferred-region.ts +39 -0
- package/src/compat/next/redirects.ts +131 -0
- package/src/compat/next/resource-hints.ts +136 -0
- package/src/compat/next/rewrites.ts +350 -0
- package/src/compat/next/root-params.ts +142 -0
- package/src/compat/next/router.cjs +49 -0
- package/src/compat/next/router.ts +143 -0
- package/src/compat/next/script.tsx +355 -0
- package/src/compat/next/server-only.ts +3 -0
- package/src/compat/next/server.ts +28 -0
- package/src/compat/next/svgr.ts +58 -0
- package/src/compat/next/telemetry.ts +77 -0
- package/src/compat/next/user-agent.ts +100 -0
- package/src/compat/next/web-vitals.ts +56 -0
- package/src/compat/otel/api.ts +95 -0
- package/src/compat/otel/client-trace-metadata.ts +71 -0
- package/src/compat/otel/fetch-span.ts +77 -0
- package/src/compat/otel/tracer.ts +944 -0
- package/src/compat/pages/client-plugin.ts +108 -0
- package/src/compat/pages/index.ts +527 -0
- package/src/compat/pages/router-state.ts +94 -0
- package/src/compat/ppr/io.ts +38 -0
- package/src/compat/ppr/missing-root-params.ts +105 -0
- package/src/compat/ppr/root-params-scan.ts +164 -0
- package/src/compat/ppr/root-params-transform.ts +75 -0
- package/src/compat/ppr/root-params.ts +129 -0
- package/src/compat/ppr/segment-config-incompat.ts +34 -0
- package/src/compat/protocol.ts +202 -0
- package/src/compat/react/client.ts +59 -0
- package/src/compat/react/compiler-runtime.ts +60 -0
- package/src/compat/react/dom-client.ts +115 -0
- package/src/compat/react/dom-react-server.ts +20 -0
- package/src/compat/react/dom-server.ts +40 -0
- package/src/compat/react/dom.ts +154 -0
- package/src/compat/react/preact.ts +522 -0
- package/src/compat/react/react-server.ts +84 -0
- package/src/compat/react/router-shim.ts +26 -0
- package/src/compat/react/server-component-use.ts +48 -0
- package/src/compat/react/server-inserted-html.ts +87 -0
- package/src/compat/react/server.ts +156 -0
- package/src/compat/react/view-transition.ts +60 -0
- package/src/compat/register/actions.ts +875 -0
- package/src/compat/register/boot.ts +141 -0
- package/src/compat/register/build-tier.ts +11 -0
- package/src/compat/register/build.ts +182 -0
- package/src/compat/register/bundler.ts +587 -0
- package/src/compat/register/cache.ts +85 -0
- package/src/compat/register/client-errors.ts +18 -0
- package/src/compat/register/config.ts +17 -0
- package/src/compat/register/css-extras.ts +120 -0
- package/src/compat/register/edge-runtime.ts +6 -0
- package/src/compat/register/errors.ts +46 -0
- package/src/compat/register/export.ts +23 -0
- package/src/compat/register/font.ts +36 -0
- package/src/compat/register/hooks.ts +34 -0
- package/src/compat/register/image.ts +133 -0
- package/src/compat/register/index.ts +111 -0
- package/src/compat/register/instrumentation-client.ts +35 -0
- package/src/compat/register/lifecycle.ts +86 -0
- package/src/compat/register/mdx.ts +48 -0
- package/src/compat/register/middleware.ts +36 -0
- package/src/compat/register/misc.ts +44 -0
- package/src/compat/register/otel.ts +288 -0
- package/src/compat/register/pages-api.ts +473 -0
- package/src/compat/register/ppr.ts +56 -0
- package/src/compat/register/protocol.ts +57 -0
- package/src/compat/register/proxy.ts +127 -0
- package/src/compat/register/render.ts +268 -0
- package/src/compat/register/routing.ts +410 -0
- package/src/compat/register/segment.ts +1903 -0
- package/src/compat/register/static-image.ts +21 -0
- package/src/compat/register/typed-routes.ts +35 -0
- package/src/compat/register/usecache.ts +131 -0
- package/src/compat/register/validation.ts +56 -0
- package/src/compat/segment/loading-boundary.ts +113 -0
- package/src/compat/segment/page-slot.ts +200 -0
- package/src/compat/segment/tree.ts +481 -0
- package/src/compat/segment/vary-key.ts +102 -0
- package/src/compat/segment/vary-params.ts +551 -0
- package/src/compat/static-params.ts +33 -0
- package/src/compat/tsconfig-defaults.ts +301 -0
- package/src/compat/typecheck/index.ts +1481 -0
- package/src/compat/typecheck/worker.ts +26 -0
- package/src/compat/typed-routes/index.ts +92 -0
- package/src/compat/typed-routes/manifest.ts +356 -0
- package/src/compat/typed-routes/typegen.ts +566 -0
- package/src/compat/validation/errors.ts +159 -0
- package/src/compat/validation/index.ts +1770 -0
- package/src/compat/validation/prerender-diagnostics.ts +1508 -0
- package/src/compat-bootstrap.ts +67 -0
- package/src/config.ts +218 -0
- package/src/css/build.ts +697 -0
- package/src/css/index.ts +2 -0
- package/src/css/postcss.ts +236 -0
- package/src/css/worker.ts +34 -0
- package/src/dev/client-actions.ts +35 -0
- package/src/dev/client-chunk-store.ts +92 -0
- package/src/dev/client-key-cache.ts +178 -0
- package/src/dev/global-css-cache.ts +212 -0
- package/src/dev/imports.ts +2430 -0
- package/src/dev/module-cache.ts +721 -0
- package/src/dev/module-generations.ts +38 -0
- package/src/dev/module-transform.ts +188 -0
- package/src/dev/node-module-bundle-cache.ts +63 -0
- package/src/dev/restart-cache.ts +10 -0
- package/src/dev/route-bundle-key-cache.ts +154 -0
- package/src/dev/route-facts-cache.ts +223 -0
- package/src/dev/server.ts +1710 -0
- package/src/dynamic/source.ts +307 -0
- package/src/dynamic/tree-shake.ts +262 -0
- package/src/env.ts +92 -0
- package/src/extensions.ts +1898 -0
- package/src/index.ts +34 -0
- package/src/internal.ts +43 -0
- package/src/islands/boundary-error.ts +8 -0
- package/src/islands/static-children.ts +37 -0
- package/src/islands/static-slots.ts +106 -0
- package/src/ppr-postpone.ts +24 -0
- package/src/ppr.ts +784 -0
- package/src/proxy.ts +752 -0
- package/src/render/hooks.ts +384 -0
- package/src/render/index.ts +1 -0
- package/src/render/island-context.ts +47 -0
- package/src/render/metadata.ts +857 -0
- package/src/render/renderer.ts +7391 -0
- package/src/render/resource-hints.ts +44 -0
- package/src/render/slots.tsx +679 -0
- package/src/request/context.ts +396 -0
- package/src/resolve/engine.ts +219 -0
- package/src/resolve/imports.ts +1104 -0
- package/src/resolve/scan-facts.ts +474 -0
- package/src/resolve/source-text.ts +86 -0
- package/src/routing/forwarded.ts +41 -0
- package/src/routing/handler.ts +271 -0
- package/src/routing/href.ts +203 -0
- package/src/routing/metadata.ts +1018 -0
- package/src/routing/request-runtime.ts +43 -0
- package/src/routing/routes.ts +2560 -0
- package/src/routing/slots.ts +432 -0
- package/src/runtime/server.ts +3453 -0
- package/src/runtime/vendor.ts +1160 -0
- package/src/style-modules.d.ts +9 -0
- package/src/typegen.ts +151 -0
- package/src/types.ts +725 -0
- package/src/utils/ansi.ts +9 -0
- package/src/utils/content-type.ts +31 -0
- package/src/utils/decode.ts +7 -0
- package/src/utils/dev-profile.ts +31 -0
- package/src/utils/error-log.ts +29 -0
- package/src/utils/fs-cache.ts +31 -0
- package/src/utils/fs.ts +119 -0
- package/src/utils/html.ts +46 -0
- package/src/utils/serialize.ts +378 -0
- package/src/utils/source.ts +35 -0
- package/src/utils/verbose.ts +39 -0
- 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
|
+
}
|