@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,759 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-wide revalidation registry backing the next/cache compat surface.
|
|
3
|
+
*
|
|
4
|
+
* pnext renders dynamic routes fresh on every request, so revalidation only
|
|
5
|
+
* has observable meaning for (a) prebuilt static HTML served from the build
|
|
6
|
+
* output and (b) unstable_cache data entries. Paths and tags record the time
|
|
7
|
+
* they were revalidated; consumers compare against their stored-at time.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
11
|
+
import { currentRequest, getWorkUnit } from '../../request/context';
|
|
12
|
+
import { realNow, trackPrerenderCacheFill } from '../../ppr';
|
|
13
|
+
import { cacheHandlerRevalidateTag } from './handler';
|
|
14
|
+
import { modernCacheUpdateTags } from './modern-handler';
|
|
15
|
+
import { escapeRegex } from '../../utils/source';
|
|
16
|
+
import {
|
|
17
|
+
prerenderErrorCollectionActive,
|
|
18
|
+
recordPrerenderError,
|
|
19
|
+
unstableCacheDynamicApiMessage,
|
|
20
|
+
useCacheDynamicApiMessage,
|
|
21
|
+
} from './build-prerender-errors';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Draft mode (a __prerender_bypass cookie on the active request) bypasses
|
|
25
|
+
* data-cache reads AND writes: every render sees fresh data and leaves the
|
|
26
|
+
* cached values untouched (Next semantics).
|
|
27
|
+
*/
|
|
28
|
+
export function draftModeBypassActive() {
|
|
29
|
+
return currentRequest()?.cookies?.has('__prerender_bypass') ?? false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Cross-instance singletons. The `'use cache'` transform pulls this module (and use-cache.ts) into the
|
|
33
|
+
// per-app server bundle, so an app that uses `use cache` ends up with a SECOND copy of revalidate.ts,
|
|
34
|
+
// distinct from the copy the pnext runtime loads from source. Two copies means two of every
|
|
35
|
+
// module-level Map/counter/ALS - the producer scope a cached render enters would be invisible to the
|
|
36
|
+
// runtime fetch patch, and a revalidatePath() written by one copy's registry would never be seen by the
|
|
37
|
+
// other's staleness reader. Anchor all process-wide state on globalThis so both copies share it.
|
|
38
|
+
const REVALIDATE_GLOBALS = Symbol.for('pnext.compat.revalidateGlobals');
|
|
39
|
+
const USE_CACHE_ENTRIES = Symbol.for('pnext.compat.useCacheEntries');
|
|
40
|
+
|
|
41
|
+
function revalidateGlobal<T>(name: string, create: () => T): T {
|
|
42
|
+
const root = globalThis as Record<PropertyKey, unknown>;
|
|
43
|
+
const container = (root[REVALIDATE_GLOBALS] ??= {}) as Record<string, unknown>;
|
|
44
|
+
if (!(name in container)) container[name] = create();
|
|
45
|
+
return container[name] as T;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const revalidatedPaths = revalidateGlobal('revalidatedPaths', () => new Map<string, number>());
|
|
49
|
+
const revalidatedPathReasons = revalidateGlobal(
|
|
50
|
+
'revalidatedPathReasons',
|
|
51
|
+
() => new Map<string, 'stale' | 'on-demand'>(),
|
|
52
|
+
);
|
|
53
|
+
const revalidatedTags = revalidateGlobal('revalidatedTags', () => new Map<string, number>());
|
|
54
|
+
// Monotonic counters stamped/sampled across the module. Held on a shared object
|
|
55
|
+
// (not module `let`s) so both module copies read and bump the same values:
|
|
56
|
+
// - tagSeq: stamped on every hard tag revalidation. In-memory cache entries
|
|
57
|
+
// ('use cache') compare their production-time sequence against it instead of
|
|
58
|
+
// wall-clock timestamps so read-your-writes works even when the updateTag and
|
|
59
|
+
// the re-read land in the same millisecond. The timestamp maps stay for
|
|
60
|
+
// prebuilt-HTML mtime comparisons, which cross request/process boundaries.
|
|
61
|
+
// - lastPathAt: wall-clock of the most recent path revalidation of ANY route;
|
|
62
|
+
// entries produced OUTSIDE a render (no owning route) fall back to it, so any
|
|
63
|
+
// revalidatePath invalidates them.
|
|
64
|
+
// - events: monotonic count of revalidation events (path or tag). Server
|
|
65
|
+
// actions sample it before/after an action to decide whether the client's
|
|
66
|
+
// view needs a refresh (Next invalidates its client router cache).
|
|
67
|
+
const counters = revalidateGlobal('counters', () => ({ tagSeq: 0, lastPathAt: 0, events: 0 }));
|
|
68
|
+
const revalidatedTagSeq = revalidateGlobal('revalidatedTagSeq', () => new Map<string, number>());
|
|
69
|
+
const revalidatedTagReasons = revalidateGlobal(
|
|
70
|
+
'revalidatedTagReasons',
|
|
71
|
+
() => new Map<string, 'stale' | 'on-demand'>(),
|
|
72
|
+
);
|
|
73
|
+
const staleTags = revalidateGlobal('staleTags', () => new Map<string, number>());
|
|
74
|
+
const dataCacheInvalidators = revalidateGlobal('dataCacheInvalidators', () => new Set<() => void>());
|
|
75
|
+
export type RevalidationInvalidation =
|
|
76
|
+
| { kind: 'path'; path: string }
|
|
77
|
+
| { kind: 'tag'; tag: string; stale: boolean };
|
|
78
|
+
|
|
79
|
+
const revalidationInvalidators = revalidateGlobal(
|
|
80
|
+
'revalidationInvalidators',
|
|
81
|
+
() => new Set<(invalidation: RevalidationInvalidation) => void>(),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
interface RevalidatableUseCacheEntry {
|
|
85
|
+
tags: string[];
|
|
86
|
+
route?: string;
|
|
87
|
+
storedAt: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function evictUseCacheEntries(invalidation: RevalidationInvalidation) {
|
|
91
|
+
const entries = (globalThis as Record<PropertyKey, unknown>)[USE_CACHE_ENTRIES] as
|
|
92
|
+
| Map<string, RevalidatableUseCacheEntry>
|
|
93
|
+
| undefined;
|
|
94
|
+
if (!entries || (invalidation.kind === 'tag' && invalidation.stale)) return;
|
|
95
|
+
for (const [key, entry] of entries) {
|
|
96
|
+
const matches =
|
|
97
|
+
invalidation.kind === 'tag'
|
|
98
|
+
? entry.tags.includes(invalidation.tag)
|
|
99
|
+
: pathScopeRevalidated(entry.route, entry.storedAt);
|
|
100
|
+
if (matches) entries.delete(key);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Pattern marks: revalidatePath('/x', 'layout') covers the whole subtree, and
|
|
104
|
+
// paths containing dynamic segments ('/blog/[author]') cover every match.
|
|
105
|
+
const revalidatedPatterns = revalidateGlobal(
|
|
106
|
+
'revalidatedPatterns',
|
|
107
|
+
() => [] as { regex: RegExp; at: number; reason: 'stale' | 'on-demand' }[],
|
|
108
|
+
);
|
|
109
|
+
const revalidatedPatternsLimit = 256;
|
|
110
|
+
|
|
111
|
+
export function revalidationEventCount() {
|
|
112
|
+
return counters.events;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Signal a client-router refresh without expiring the data cache. refresh()
|
|
117
|
+
* (next/cache) inside a server action re-renders the current route on the
|
|
118
|
+
* client; the action endpoint samples the event count to set the refresh
|
|
119
|
+
* header, so bumping the counter is enough.
|
|
120
|
+
*/
|
|
121
|
+
export function bumpRevalidationEvent() {
|
|
122
|
+
counters.events += 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function normalizeRevalidatePath(path: string) {
|
|
126
|
+
let pathname = path.split('?')[0] ?? path;
|
|
127
|
+
try {
|
|
128
|
+
pathname = decodeURI(pathname);
|
|
129
|
+
} catch {
|
|
130
|
+
// Keep malformed escape sequences unchanged.
|
|
131
|
+
}
|
|
132
|
+
const trimmed = pathname.replace(/\/+$/, '');
|
|
133
|
+
return trimmed === '' ? '/' : trimmed;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function markPathRevalidated(path: string, type?: 'page' | 'layout') {
|
|
137
|
+
counters.events += 1;
|
|
138
|
+
// fetch/data cache entries carry the route they were produced under (Next's
|
|
139
|
+
// implicit `_N_T_/route` soft tag). Revalidating a path only invalidates the
|
|
140
|
+
// entries owned by matching routes (see pathScopeRevalidated); route-less
|
|
141
|
+
// entries (produced outside a render) fall back to this global timestamp.
|
|
142
|
+
counters.lastPathAt = Date.now();
|
|
143
|
+
const normalized = normalizeRevalidatePath(path);
|
|
144
|
+
void modernCacheUpdateTags([`_N_T_${normalized}`]);
|
|
145
|
+
const reason = getWorkUnit()?.phase === 'action' ? 'stale' : 'on-demand';
|
|
146
|
+
if (normalized.includes('[') || type === 'layout') {
|
|
147
|
+
const source = normalized
|
|
148
|
+
.split('/')
|
|
149
|
+
.map(segment =>
|
|
150
|
+
/^\[?\[\.\.\./.test(segment) ? '.+' : segment.startsWith('[') ? '[^/]+' : escapeRegex(segment),
|
|
151
|
+
)
|
|
152
|
+
.join('/');
|
|
153
|
+
const base = normalized === '/' ? '' : source;
|
|
154
|
+
const regex =
|
|
155
|
+
type === 'layout' ? new RegExp(`^${base}(?:/.*)?$`) : new RegExp(`^${source}$`);
|
|
156
|
+
revalidatedPatterns.push({ regex, at: Date.now(), reason });
|
|
157
|
+
if (revalidatedPatterns.length > revalidatedPatternsLimit) revalidatedPatterns.shift();
|
|
158
|
+
const invalidation = { kind: 'path', path: normalized } as const;
|
|
159
|
+
evictUseCacheEntries(invalidation);
|
|
160
|
+
notifyRevalidationInvalidators(invalidation);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
revalidatedPaths.set(normalized, Date.now());
|
|
164
|
+
revalidatedPathReasons.set(normalized, reason);
|
|
165
|
+
const invalidation = { kind: 'path', path: normalized } as const;
|
|
166
|
+
evictUseCacheEntries(invalidation);
|
|
167
|
+
notifyRevalidationInvalidators(invalidation);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function markTagRevalidated(tag: string) {
|
|
171
|
+
counters.events += 1;
|
|
172
|
+
revalidatedTags.set(tag, Date.now());
|
|
173
|
+
revalidatedTagSeq.set(tag, ++counters.tagSeq);
|
|
174
|
+
revalidatedTagReasons.set(tag, getWorkUnit()?.phase === 'action' ? 'stale' : 'on-demand');
|
|
175
|
+
// Forward to a configured cache handler so a persistent backend drops the
|
|
176
|
+
// entry too (best-effort; the in-memory marks are authoritative).
|
|
177
|
+
void cacheHandlerRevalidateTag(tag);
|
|
178
|
+
void modernCacheUpdateTags([tag]);
|
|
179
|
+
const invalidation = { kind: 'tag', tag, stale: false } as const;
|
|
180
|
+
evictUseCacheEntries(invalidation);
|
|
181
|
+
notifyRevalidationInvalidators(invalidation);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function markTagStale(tag: string) {
|
|
185
|
+
// Deliberately NOT counted as a revalidation event: a profile-based revalidateTag is
|
|
186
|
+
// stale-while-revalidate - the client keeps showing its current (stale) view and must NOT be told to
|
|
187
|
+
// refresh after the action. Fresh data appears only on the next navigation/refresh, which serves stale
|
|
188
|
+
// and regenerates in the background.
|
|
189
|
+
staleTags.set(tag, Date.now());
|
|
190
|
+
void cacheHandlerRevalidateTag(tag);
|
|
191
|
+
void modernCacheUpdateTags([tag]);
|
|
192
|
+
notifyRevalidationInvalidators({ kind: 'tag', tag, stale: true });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Marks are integer Date.now() while file mtimes carry fractional
|
|
196
|
+
// milliseconds; truncate so a mark landing in the same millisecond as the
|
|
197
|
+
// write still counts as newer (worst case: one redundant regeneration).
|
|
198
|
+
function markedAfter(at: number | undefined, time: number) {
|
|
199
|
+
return at !== undefined && at >= Math.trunc(time) && at > time - 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function pathRevalidatedSince(path: string, time: number) {
|
|
203
|
+
const normalized = normalizeRevalidatePath(path);
|
|
204
|
+
if (markedAfter(revalidatedPaths.get(normalized), time)) return true;
|
|
205
|
+
return revalidatedPatterns.some(
|
|
206
|
+
pattern => markedAfter(pattern.at, time) && pattern.regex.test(normalized),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function pathRevalidationReasonSince(
|
|
211
|
+
path: string,
|
|
212
|
+
time: number,
|
|
213
|
+
): 'stale' | 'on-demand' | undefined {
|
|
214
|
+
const normalized = normalizeRevalidatePath(path);
|
|
215
|
+
if (markedAfter(revalidatedPaths.get(normalized), time)) {
|
|
216
|
+
return revalidatedPathReasons.get(normalized) ?? 'on-demand';
|
|
217
|
+
}
|
|
218
|
+
return revalidatedPatterns.find(
|
|
219
|
+
pattern => markedAfter(pattern.at, time) && pattern.regex.test(normalized),
|
|
220
|
+
)?.reason;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Whether a cache entry produced under `route` (its owning render's pathname,
|
|
225
|
+
* or undefined when produced outside any render) has been invalidated by a
|
|
226
|
+
* revalidatePath since it was stored at `time`. Route-scoped entries match
|
|
227
|
+
* only their own route (respecting layout/dynamic patterns); route-less
|
|
228
|
+
* entries are invalidated by any path revalidation.
|
|
229
|
+
*/
|
|
230
|
+
export function pathScopeRevalidated(route: string | undefined, time: number): boolean {
|
|
231
|
+
if (route === undefined) return markedAfter(counters.lastPathAt || undefined, time);
|
|
232
|
+
return pathRevalidatedSince(route, time);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function tagsRevalidatedSince(tags: readonly string[], time: number) {
|
|
236
|
+
return tags.some(tag => markedAfter(revalidatedTags.get(tag), time));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Current hard-tag-revalidation sequence, stamped on an entry at production. */
|
|
240
|
+
export function currentTagRevalidationSeq() {
|
|
241
|
+
return counters.tagSeq;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Identifies the process that produced a resume-data-cache record. Stamped into
|
|
246
|
+
* every RDC record at capture and compared at seed, because `tagSeq` only means
|
|
247
|
+
* something within one process (see rebaseRdcTagSeq).
|
|
248
|
+
*/
|
|
249
|
+
export function rdcProcessId(): string {
|
|
250
|
+
return revalidateGlobal(
|
|
251
|
+
'rdcProcessId',
|
|
252
|
+
() => `${process.pid}:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Rebase an RDC record's `tagSeq` onto this process's sequence.
|
|
258
|
+
*
|
|
259
|
+
* `tagSeq` counts hard tag revalidations WITHIN a process and resets to 0 on restart, so a sequence
|
|
260
|
+
* serialized by the build (or a previous server process) is meaningless here. A record from a FOREIGN
|
|
261
|
+
* process was produced before anything that happened in this one, so it rebases to 0: any updateTag()
|
|
262
|
+
* of its tags seen by this process invalidates it. Rebasing onto the CURRENT sequence instead hides
|
|
263
|
+
* revalidations that already happened - an RDC seed running on the post-action re-render would restamp
|
|
264
|
+
* the stale record as fresh, so updateTag() in a server action could never surface fresh data.
|
|
265
|
+
*
|
|
266
|
+
* A record this process itself captured keeps its own sequence (clamped, since the counter can be
|
|
267
|
+
* reset), so a shell regenerated right after a revalidation is not immediately re-invalidated by it.
|
|
268
|
+
*/
|
|
269
|
+
export function rebaseRdcTagSeq(record: { tagSeq: number; pid?: string }): number {
|
|
270
|
+
if (record.pid !== rdcProcessId()) return 0;
|
|
271
|
+
return Math.min(record.tagSeq, counters.tagSeq);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Whether any of `tags` was hard-revalidated after `seq` (the sequence sampled
|
|
276
|
+
* when the entry began producing). Sequence-based so it is immune to the
|
|
277
|
+
* millisecond-precision ambiguity that plagues `tagsRevalidatedSince` for
|
|
278
|
+
* in-memory entries: an updateTag() and an immediate re-read in the same
|
|
279
|
+
* request reliably see the invalidation exactly once.
|
|
280
|
+
*/
|
|
281
|
+
export function tagsRevalidatedAfterSeq(tags: readonly string[], seq: number) {
|
|
282
|
+
return tags.some(tag => (revalidatedTagSeq.get(tag) ?? 0) > seq);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function tagsRevalidationReasonSince(
|
|
286
|
+
tags: readonly string[],
|
|
287
|
+
time: number,
|
|
288
|
+
): 'stale' | 'on-demand' | undefined {
|
|
289
|
+
for (const tag of tags) {
|
|
290
|
+
if (markedAfter(revalidatedTags.get(tag), time)) {
|
|
291
|
+
return revalidatedTagReasons.get(tag) ?? 'on-demand';
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function tagsStaleSince(tags: readonly string[], time: number) {
|
|
298
|
+
return tags.some(tag => markedAfter(staleTags.get(tag), time));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
interface DataCacheEntry {
|
|
302
|
+
value: Promise<unknown>;
|
|
303
|
+
storedAt: number;
|
|
304
|
+
tags: readonly string[];
|
|
305
|
+
revalidateSeconds?: number;
|
|
306
|
+
/** A background stale-while-revalidate refresh is already in flight. */
|
|
307
|
+
refreshing?: boolean;
|
|
308
|
+
/** Pathname of the render that produced this entry (path-revalidation scope). */
|
|
309
|
+
route?: string;
|
|
310
|
+
/**
|
|
311
|
+
* The render cache-meta this entry was produced under: an on-demand
|
|
312
|
+
* regeneration (refreshFetches) re-produces each entry once per render, then
|
|
313
|
+
* repeated reads dedupe against it.
|
|
314
|
+
*/
|
|
315
|
+
refreshedIn?: object;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const dataCache = revalidateGlobal('dataCache', () => new Map<string, DataCacheEntry>());
|
|
319
|
+
const dataCacheLimit = 1024;
|
|
320
|
+
|
|
321
|
+
function dataCacheEntryExpired(entry: DataCacheEntry) {
|
|
322
|
+
return (
|
|
323
|
+
entry.revalidateSeconds !== undefined &&
|
|
324
|
+
realNow() - entry.storedAt >= entry.revalidateSeconds * 1000
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export interface DataCacheOptions {
|
|
329
|
+
tags?: string[];
|
|
330
|
+
revalidateSeconds?: number;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Memoize an async producer under a stable key. On-demand invalidation
|
|
335
|
+
* (revalidateTag / an on-demand-revalidating render) blocks and re-produces;
|
|
336
|
+
* TTL expiry serves the stale value while refreshing in the background
|
|
337
|
+
* (Next's stale-while-revalidate data-cache semantics). Rejected results are
|
|
338
|
+
* evicted so errors don't stick.
|
|
339
|
+
*/
|
|
340
|
+
export function cachedData(
|
|
341
|
+
key: string,
|
|
342
|
+
produce: () => Promise<unknown>,
|
|
343
|
+
options: DataCacheOptions = {},
|
|
344
|
+
): Promise<unknown> {
|
|
345
|
+
if (draftModeBypassActive()) return produce();
|
|
346
|
+
const entry = dataCache.get(key);
|
|
347
|
+
const meta = cacheMetaStorage.getStore();
|
|
348
|
+
// On-demand revalidation re-produces each entry ONCE per render (Next skips the cache read entirely
|
|
349
|
+
// under isOnDemandRevalidate), then dedupes via refreshedIn - but only entries the revalidation
|
|
350
|
+
// actually reached. See the matching gate in use-cache.ts: a still-marked route re-renders many times
|
|
351
|
+
// before its prebuilt HTML is written back.
|
|
352
|
+
const refresh =
|
|
353
|
+
meta?.refreshFetches === true &&
|
|
354
|
+
entry?.refreshedIn !== meta &&
|
|
355
|
+
entry !== undefined &&
|
|
356
|
+
(tagsRevalidatedSince(entry.tags, entry.storedAt) ||
|
|
357
|
+
pathScopeRevalidated(entry.route, entry.storedAt) ||
|
|
358
|
+
tagsStaleSince(entry.tags, entry.storedAt) ||
|
|
359
|
+
dataCacheEntryExpired(entry));
|
|
360
|
+
if (
|
|
361
|
+
entry &&
|
|
362
|
+
!refresh &&
|
|
363
|
+
!tagsRevalidatedSince(entry.tags, entry.storedAt) &&
|
|
364
|
+
!pathScopeRevalidated(entry.route, entry.storedAt)
|
|
365
|
+
) {
|
|
366
|
+
recordCacheTags(entry.tags);
|
|
367
|
+
if (entry.revalidateSeconds !== undefined) {
|
|
368
|
+
recordCacheRevalidate(entry.revalidateSeconds);
|
|
369
|
+
}
|
|
370
|
+
const tagStale = tagsStaleSince(entry.tags, entry.storedAt);
|
|
371
|
+
if (!tagStale && !dataCacheEntryExpired(entry)) return entry.value;
|
|
372
|
+
// ISR regeneration renders refetch expired entries inline.
|
|
373
|
+
if (cacheMetaStorage.getStore()?.blockingStaleFetches !== true) {
|
|
374
|
+
if (!entry.refreshing) {
|
|
375
|
+
entry.refreshing = true;
|
|
376
|
+
void storeDataCacheEntry(key, produce, options, entry);
|
|
377
|
+
}
|
|
378
|
+
return entry.value;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return storeDataCacheEntry(key, produce, options);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function storeDataCacheEntry(
|
|
385
|
+
key: string,
|
|
386
|
+
produce: () => Promise<unknown>,
|
|
387
|
+
options: DataCacheOptions,
|
|
388
|
+
serveStale?: DataCacheEntry,
|
|
389
|
+
): Promise<unknown> {
|
|
390
|
+
// Collect the tags of fetches/nested caches produced inside this entry so it
|
|
391
|
+
// inherits them (revalidateTag can then invalidate it), and propagate the
|
|
392
|
+
// aggregate up to an enclosing producer once settled.
|
|
393
|
+
const parentProducerTags = producerTagsStorage.getStore();
|
|
394
|
+
const producerTags = new Set<string>(options.tags ?? []);
|
|
395
|
+
const value = producerTagsStorage.run(producerTags, () =>
|
|
396
|
+
runInsideUnstableCacheProducer(produce),
|
|
397
|
+
);
|
|
398
|
+
// Same contract as the 'use cache' fill: a build prerender's task boundary
|
|
399
|
+
// waits for an in-flight data-cache fill rather than cutting it short, so
|
|
400
|
+
// cached IO still lands in the static shell (core ppr).
|
|
401
|
+
void trackPrerenderCacheFill(value);
|
|
402
|
+
const producingMeta = cacheMetaStorage.getStore();
|
|
403
|
+
const stored: DataCacheEntry = {
|
|
404
|
+
value,
|
|
405
|
+
storedAt: realNow(),
|
|
406
|
+
tags: options.tags ?? [],
|
|
407
|
+
revalidateSeconds: options.revalidateSeconds,
|
|
408
|
+
...(producingMeta !== undefined ? { refreshedIn: producingMeta } : {}),
|
|
409
|
+
...(serveStale?.route !== undefined
|
|
410
|
+
? { route: serveStale.route }
|
|
411
|
+
: producingMeta?.route !== undefined
|
|
412
|
+
? { route: producingMeta.route }
|
|
413
|
+
: {}),
|
|
414
|
+
};
|
|
415
|
+
const applyInheritedTags = () => {
|
|
416
|
+
stored.tags = [...producerTags];
|
|
417
|
+
if (parentProducerTags) for (const tag of producerTags) parentProducerTags.add(tag);
|
|
418
|
+
};
|
|
419
|
+
if (serveStale) {
|
|
420
|
+
// Background refresh: only replace the stale entry once the new value
|
|
421
|
+
// settles, so concurrent readers keep getting the stale value instantly.
|
|
422
|
+
value.then(
|
|
423
|
+
() => {
|
|
424
|
+
applyInheritedTags();
|
|
425
|
+
dataCache.set(key, stored);
|
|
426
|
+
},
|
|
427
|
+
() => {
|
|
428
|
+
serveStale.refreshing = false;
|
|
429
|
+
},
|
|
430
|
+
);
|
|
431
|
+
return value;
|
|
432
|
+
}
|
|
433
|
+
if (dataCache.size >= dataCacheLimit) {
|
|
434
|
+
const oldest = dataCache.keys().next().value;
|
|
435
|
+
if (oldest !== undefined) dataCache.delete(oldest);
|
|
436
|
+
}
|
|
437
|
+
dataCache.set(key, stored);
|
|
438
|
+
value.then(applyInheritedTags, () => {
|
|
439
|
+
if (dataCache.get(key)?.value === value) dataCache.delete(key);
|
|
440
|
+
});
|
|
441
|
+
return value;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function clearRevalidationState() {
|
|
445
|
+
revalidatedPaths.clear();
|
|
446
|
+
revalidatedPathReasons.clear();
|
|
447
|
+
revalidatedTags.clear();
|
|
448
|
+
revalidatedTagSeq.clear();
|
|
449
|
+
counters.tagSeq = 0;
|
|
450
|
+
revalidatedTagReasons.clear();
|
|
451
|
+
staleTags.clear();
|
|
452
|
+
revalidatedPatterns.length = 0;
|
|
453
|
+
counters.lastPathAt = 0;
|
|
454
|
+
dataCache.clear();
|
|
455
|
+
for (const invalidate of dataCacheInvalidators) invalidate();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function registerDataCacheInvalidator(invalidate: () => void): () => void {
|
|
459
|
+
dataCacheInvalidators.add(invalidate);
|
|
460
|
+
return () => {
|
|
461
|
+
dataCacheInvalidators.delete(invalidate);
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function registerRevalidationInvalidator(
|
|
466
|
+
invalidate: (invalidation: RevalidationInvalidation) => void,
|
|
467
|
+
): () => void {
|
|
468
|
+
revalidationInvalidators.add(invalidate);
|
|
469
|
+
return () => {
|
|
470
|
+
revalidationInvalidators.delete(invalidate);
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function notifyRevalidationInvalidators(invalidation: RevalidationInvalidation) {
|
|
475
|
+
for (const invalidate of revalidationInvalidators) invalidate(invalidation);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Cache-meta scope wrapped around a render. Data-cache consumers (patched
|
|
480
|
+
* fetch, unstable_cache, 'use cache') record the tags and revalidate periods
|
|
481
|
+
* they used so the surrounding prerender/ISR write can persist them, and read
|
|
482
|
+
* the route's `fetchCache` segment config for cache-mode defaults.
|
|
483
|
+
*/
|
|
484
|
+
export interface RenderCacheMeta {
|
|
485
|
+
tags: Set<string>;
|
|
486
|
+
revalidateSeconds?: number;
|
|
487
|
+
/**
|
|
488
|
+
* Effective `use cache` cacheLife expire/stale windows aggregated across the
|
|
489
|
+
* render's cache scopes (min of each). Recorded alongside revalidateSeconds so
|
|
490
|
+
* a build shell prerender can persist the route's SWR cache-control +
|
|
491
|
+
* x-nextjs-stale-time even when the work-unit stash (used by the live-render
|
|
492
|
+
* response finalizer) is not in scope during the async cache propagation.
|
|
493
|
+
*/
|
|
494
|
+
expireSeconds?: number;
|
|
495
|
+
staleSeconds?: number;
|
|
496
|
+
fetchCache?: string;
|
|
497
|
+
/** Route path label for error messages (e.g. invalid fetch revalidate). */
|
|
498
|
+
route?: string;
|
|
499
|
+
/** On-demand revalidation render: data caches must refetch and overwrite. */
|
|
500
|
+
refreshFetches?: boolean;
|
|
501
|
+
/**
|
|
502
|
+
* ISR regeneration render: TTL-expired data-cache entries refetch inline
|
|
503
|
+
* (blocking) instead of serving stale, so the regenerated page carries
|
|
504
|
+
* fresh data instead of lagging one generation behind.
|
|
505
|
+
*/
|
|
506
|
+
blockingStaleFetches?: boolean;
|
|
507
|
+
/**
|
|
508
|
+
* The render used an explicit no-store signal (fetch cache:'no-store'/
|
|
509
|
+
* 'no-cache', next.revalidate 0, or unstable_noStore()): the route must not
|
|
510
|
+
* be served from prebuilt output.
|
|
511
|
+
*/
|
|
512
|
+
noStore?: boolean;
|
|
513
|
+
/**
|
|
514
|
+
* Static-generation render (build prerender or ISR regeneration): a plain
|
|
515
|
+
* fetch with no explicit cache option defaults to caching (Next's
|
|
516
|
+
* force-cache-during-static-generation semantics) instead of per-request.
|
|
517
|
+
*/
|
|
518
|
+
prerender?: boolean;
|
|
519
|
+
/**
|
|
520
|
+
* True when the surrounding render is a route handler. Route handlers are
|
|
521
|
+
* dynamic by default (Next): an explicitly-uncached fetch (cache:'default')
|
|
522
|
+
* opts the handler out of prebuilt static output, whereas a page with the
|
|
523
|
+
* same fetch simply freezes at prerender time.
|
|
524
|
+
*/
|
|
525
|
+
handler?: boolean;
|
|
526
|
+
/** True when `dynamic = 'error'` must reject request API access. */
|
|
527
|
+
dynamicError?: boolean;
|
|
528
|
+
/**
|
|
529
|
+
* after() work queued during a static-generation render. There is no response close to flush against,
|
|
530
|
+
* so the render itself drains the queue once `produce` settles - Next's semantics: a build prerender
|
|
531
|
+
* waits for after() callbacks, and a rejection fails that page's prerender. Compat's after() enqueues;
|
|
532
|
+
* nothing in the cache layer knows what the tasks are.
|
|
533
|
+
*/
|
|
534
|
+
afterTasks?: (() => Promise<void>)[];
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const cacheMetaStorage = revalidateGlobal(
|
|
538
|
+
'cacheMetaStorage',
|
|
539
|
+
() => new AsyncLocalStorage<RenderCacheMeta>(),
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
export function currentRenderCacheMeta() {
|
|
543
|
+
return cacheMetaStorage.getStore();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* True while producing prebuilt static output - a build-time prerender or an ISR regeneration. In that
|
|
548
|
+
* context a plain fetch with no explicit cache option defaults to caching, matching Next's
|
|
549
|
+
* static-generation force-cache default.
|
|
550
|
+
*/
|
|
551
|
+
export function currentRenderIsStaticGeneration(): boolean {
|
|
552
|
+
const meta = cacheMetaStorage.getStore();
|
|
553
|
+
return meta?.prerender === true || meta?.blockingStaleFetches === true;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export function recordCacheTags(tags: readonly string[]) {
|
|
557
|
+
// Inside an unstable_cache producer, inner fetch/cache tags are inherited by
|
|
558
|
+
// the producing entry (Next semantics: an unstable_cache boundary collects the
|
|
559
|
+
// tags of every fetch made inside it, so revalidateTag invalidates the entry).
|
|
560
|
+
const producerTags = producerTagsStorage.getStore();
|
|
561
|
+
if (producerTags) for (const tag of tags) producerTags.add(tag);
|
|
562
|
+
const meta = cacheMetaStorage.getStore();
|
|
563
|
+
if (!meta) return;
|
|
564
|
+
for (const tag of tags) meta.tags.add(tag);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Hook into the active `use cache` producer scope (registered by use-cache.ts;
|
|
568
|
+
// held on the shared global container so both module copies see it). An
|
|
569
|
+
// explicit revalidate recorded while a `use cache` entry is producing lowers
|
|
570
|
+
// that ENTRY's cacheLife too, matching Next: a `fetch(..., { next:
|
|
571
|
+
// { revalidate: 1 } })` inside 'use cache' bounds the cache entry at 1s, not
|
|
572
|
+
// just the surrounding route.
|
|
573
|
+
const useCacheRevalidateSink = revalidateGlobal('useCacheRevalidateSink', () => ({
|
|
574
|
+
current: undefined as ((seconds: number) => void) | undefined,
|
|
575
|
+
}));
|
|
576
|
+
|
|
577
|
+
export function setUseCacheRevalidateSink(sink: (seconds: number) => void) {
|
|
578
|
+
useCacheRevalidateSink.current = sink;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export function recordCacheRevalidate(seconds: number) {
|
|
582
|
+
useCacheRevalidateSink.current?.(seconds);
|
|
583
|
+
const meta = cacheMetaStorage.getStore();
|
|
584
|
+
if (!meta) return;
|
|
585
|
+
meta.revalidateSeconds =
|
|
586
|
+
meta.revalidateSeconds === undefined ? seconds : Math.min(meta.revalidateSeconds, seconds);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** Record a `use cache` entry's expire window on the render meta (min-combined). */
|
|
590
|
+
export function recordCacheExpire(seconds: number) {
|
|
591
|
+
const meta = cacheMetaStorage.getStore();
|
|
592
|
+
if (!meta) return;
|
|
593
|
+
meta.expireSeconds =
|
|
594
|
+
meta.expireSeconds === undefined ? seconds : Math.min(meta.expireSeconds, seconds);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Record a `use cache` entry's stale window on the render meta (min-combined). */
|
|
598
|
+
export function recordCacheStale(seconds: number) {
|
|
599
|
+
const meta = cacheMetaStorage.getStore();
|
|
600
|
+
if (!meta) return;
|
|
601
|
+
meta.staleSeconds =
|
|
602
|
+
meta.staleSeconds === undefined ? seconds : Math.min(meta.staleSeconds, seconds);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export function recordCacheNoStore() {
|
|
606
|
+
// Inside a data-cache producer (unstable_cache / 'use cache') the cache
|
|
607
|
+
// boundary wins: no-store signals do not leak out to the route.
|
|
608
|
+
if (insideProducerStorage.getStore()) return;
|
|
609
|
+
const meta = cacheMetaStorage.getStore();
|
|
610
|
+
if (meta) meta.noStore = true;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const insideProducerStorage = revalidateGlobal(
|
|
614
|
+
'insideProducerStorage',
|
|
615
|
+
() => new AsyncLocalStorage<boolean>(),
|
|
616
|
+
);
|
|
617
|
+
const producerKindStorage = revalidateGlobal(
|
|
618
|
+
'producerKindStorage',
|
|
619
|
+
() => new AsyncLocalStorage<'use-cache' | 'unstable-cache'>(),
|
|
620
|
+
);
|
|
621
|
+
// Tags accumulated by fetches/nested caches inside the currently-producing
|
|
622
|
+
// unstable_cache entry (see recordCacheTags / storeDataCacheEntry).
|
|
623
|
+
const producerTagsStorage = revalidateGlobal(
|
|
624
|
+
'producerTagsStorage',
|
|
625
|
+
() => new AsyncLocalStorage<Set<string>>(),
|
|
626
|
+
);
|
|
627
|
+
// A dedicated marker for `unstable_cache` specifically (a subset of the general
|
|
628
|
+
// data-cache-producer scope, which `use cache` also enters). Root params read
|
|
629
|
+
// inside a `use cache` NESTED within an `unstable_cache` are E1140, which needs
|
|
630
|
+
// to tell the two producers apart — the general boolean cannot.
|
|
631
|
+
const insideUnstableCacheStorage = revalidateGlobal(
|
|
632
|
+
'insideUnstableCacheStorage',
|
|
633
|
+
() => new AsyncLocalStorage<boolean>(),
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
/** Mark fn as running inside a data-cache producer (see recordCacheNoStore). */
|
|
637
|
+
export function runInsideDataCacheProducer<T>(
|
|
638
|
+
fn: () => T,
|
|
639
|
+
kind: 'use-cache' | 'unstable-cache' = 'use-cache',
|
|
640
|
+
): T {
|
|
641
|
+
return producerKindStorage.run(kind, () => insideProducerStorage.run(true, fn));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Mark fn as running specifically inside an `unstable_cache` producer. */
|
|
645
|
+
export function runInsideUnstableCacheProducer<T>(fn: () => T): T {
|
|
646
|
+
return insideUnstableCacheStorage.run(true, () => runInsideDataCacheProducer(fn, 'unstable-cache'));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** True while executing inside an unstable_cache / 'use cache' producer. */
|
|
650
|
+
export function insideDataCacheProducer(): boolean {
|
|
651
|
+
return insideProducerStorage.getStore() === true;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** True while executing specifically inside an `unstable_cache` producer. */
|
|
655
|
+
export function insideUnstableCacheProducer(): boolean {
|
|
656
|
+
return insideUnstableCacheStorage.getStore() === true;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export function insidePublicUseCacheProducer(): boolean {
|
|
660
|
+
return producerKindStorage.getStore() === 'use-cache';
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Fail a build prerender when a dynamic request API (`cookies()`/`headers()`/
|
|
665
|
+
* `connection()`) is invoked inside a function cached with `unstable_cache()`.
|
|
666
|
+
* Called from the compat request-API seams (next/headers, next/server). Records
|
|
667
|
+
* Next's exact E838 error so the build aborts (register-usecache) and throws so
|
|
668
|
+
* the offending fill unwinds. A no-op outside an armed build prerender and
|
|
669
|
+
* outside an unstable_cache producer, so serving-runtime/dev calls are untouched
|
|
670
|
+
* (there, the same APIs simply read live request data).
|
|
671
|
+
*/
|
|
672
|
+
export function assertNoDynamicApiInsideCache(
|
|
673
|
+
api: 'cookies()' | 'headers()' | 'connection()',
|
|
674
|
+
): void {
|
|
675
|
+
if (!insideDataCacheProducer()) return;
|
|
676
|
+
const route = cacheMetaStorage.getStore()?.route ?? '';
|
|
677
|
+
const unstable = insideUnstableCacheProducer();
|
|
678
|
+
const message = unstable
|
|
679
|
+
? unstableCacheDynamicApiMessage(route, api)
|
|
680
|
+
: useCacheDynamicApiMessage(route, api);
|
|
681
|
+
// Build-time prerenders record the error for the build log; the throw itself
|
|
682
|
+
// is unconditional — Next rejects dynamic data inside a cache scope at
|
|
683
|
+
// request time too (500 / generic RSC error in prod).
|
|
684
|
+
if (prerenderErrorCollectionActive()) {
|
|
685
|
+
recordPrerenderError({
|
|
686
|
+
kind: unstable ? 'unstable-cache-dynamic' : 'use-cache-dynamic',
|
|
687
|
+
route,
|
|
688
|
+
consoleBlock: `Error: ${message}`,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
throw new Error(message);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export async function collectRenderCacheMeta<T>(
|
|
695
|
+
produce: () => Promise<T>,
|
|
696
|
+
options: {
|
|
697
|
+
fetchCache?: string;
|
|
698
|
+
refreshFetches?: boolean;
|
|
699
|
+
blockingStaleFetches?: boolean;
|
|
700
|
+
route?: string;
|
|
701
|
+
prerender?: boolean;
|
|
702
|
+
handler?: boolean;
|
|
703
|
+
dynamicError?: boolean;
|
|
704
|
+
} = {},
|
|
705
|
+
): Promise<{
|
|
706
|
+
value: T;
|
|
707
|
+
tags: string[];
|
|
708
|
+
revalidateSeconds?: number;
|
|
709
|
+
expireSeconds?: number;
|
|
710
|
+
staleSeconds?: number;
|
|
711
|
+
noStore?: boolean;
|
|
712
|
+
}> {
|
|
713
|
+
const meta: RenderCacheMeta = {
|
|
714
|
+
tags: new Set(),
|
|
715
|
+
fetchCache: options.fetchCache,
|
|
716
|
+
refreshFetches: options.refreshFetches,
|
|
717
|
+
blockingStaleFetches: options.blockingStaleFetches,
|
|
718
|
+
route: options.route,
|
|
719
|
+
prerender: options.prerender,
|
|
720
|
+
handler: options.handler,
|
|
721
|
+
dynamicError: options.dynamicError,
|
|
722
|
+
};
|
|
723
|
+
const value = await cacheMetaStorage.run(meta, produce);
|
|
724
|
+
await drainStaticAfterTasks(meta);
|
|
725
|
+
return {
|
|
726
|
+
value,
|
|
727
|
+
tags: [...meta.tags],
|
|
728
|
+
...(meta.revalidateSeconds !== undefined ? { revalidateSeconds: meta.revalidateSeconds } : {}),
|
|
729
|
+
...(meta.expireSeconds !== undefined ? { expireSeconds: meta.expireSeconds } : {}),
|
|
730
|
+
...(meta.staleSeconds !== undefined ? { staleSeconds: meta.staleSeconds } : {}),
|
|
731
|
+
...(meta.noStore ? { noStore: true } : {}),
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Run the after() tasks queued during a static-generation render, re-draining
|
|
737
|
+
* so a task can enqueue further tasks (nested after()). Tasks run inside the
|
|
738
|
+
* meta scope so nested after() re-enters this queue. During a build prerender
|
|
739
|
+
* a rejection fails the page like Next: print Next's prerender-error line for
|
|
740
|
+
* the route and rethrow so the build skips/fails the page. Runtime
|
|
741
|
+
* regeneration tasks are queued pre-wrapped to log-and-continue, so a
|
|
742
|
+
* rejection here is only possible on the prerender path.
|
|
743
|
+
*/
|
|
744
|
+
async function drainStaticAfterTasks(meta: RenderCacheMeta): Promise<void> {
|
|
745
|
+
while (meta.afterTasks?.length) {
|
|
746
|
+
const tasks = meta.afterTasks;
|
|
747
|
+
meta.afterTasks = [];
|
|
748
|
+
for (const task of tasks) {
|
|
749
|
+
try {
|
|
750
|
+
await cacheMetaStorage.run(meta, task);
|
|
751
|
+
} catch (error) {
|
|
752
|
+
console.error(
|
|
753
|
+
`Error occurred prerendering page "${meta.route ?? '/'}". Read more: https://nextjs.org/docs/messages/prerender-error`,
|
|
754
|
+
);
|
|
755
|
+
throw error;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|