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