@nifrajs/web 4.0.2 → 5.1.0
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 +19 -0
- package/dist/build-vite.d.ts +19 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +31 -10
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +8 -10
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +90 -31
- package/dist/build.js.map +1 -1
- package/dist/cdn.d.ts +8 -0
- package/dist/cdn.d.ts.map +1 -1
- package/dist/cdn.js +14 -15
- package/dist/cdn.js.map +1 -1
- package/dist/client.d.ts +6 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +46 -18
- package/dist/client.js.map +1 -1
- package/dist/deferred.d.ts +2 -2
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +28 -7
- package/dist/deferred.js.map +1 -1
- package/dist/dev-feed.d.ts.map +1 -1
- package/dist/dev-feed.js +24 -5
- package/dist/dev-feed.js.map +1 -1
- package/dist/dev-port.d.ts +3 -2
- package/dist/dev-port.d.ts.map +1 -1
- package/dist/dev-port.js +2 -6
- package/dist/dev-port.js.map +1 -1
- package/dist/dev-session.d.ts.map +1 -1
- package/dist/dev-session.js +4 -14
- package/dist/dev-session.js.map +1 -1
- package/dist/dev.d.ts +7 -17
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +13 -1
- package/dist/dev.js.map +1 -1
- package/dist/draft.d.ts +8 -5
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +30 -13
- package/dist/draft.js.map +1 -1
- package/dist/fonts-google.d.ts.map +1 -1
- package/dist/fonts-google.js +7 -0
- package/dist/fonts-google.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/internal/codegen.d.ts +1 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +56 -29
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/disposable-dir.d.ts +8 -0
- package/dist/internal/disposable-dir.d.ts.map +1 -0
- package/dist/internal/disposable-dir.js +25 -0
- package/dist/internal/disposable-dir.js.map +1 -0
- package/dist/internal/entry-chunks.d.ts +9 -0
- package/dist/internal/entry-chunks.d.ts.map +1 -0
- package/dist/internal/entry-chunks.js +15 -0
- package/dist/internal/entry-chunks.js.map +1 -0
- package/dist/internal/evidence-traversal.d.ts +12 -0
- package/dist/internal/evidence-traversal.d.ts.map +1 -0
- package/dist/internal/evidence-traversal.js +48 -0
- package/dist/internal/evidence-traversal.js.map +1 -0
- package/dist/internal/head-merge.d.ts +6 -0
- package/dist/internal/head-merge.d.ts.map +1 -1
- package/dist/internal/head-merge.js +9 -0
- package/dist/internal/head-merge.js.map +1 -1
- package/dist/internal/js-string.d.ts +2 -0
- package/dist/internal/js-string.d.ts.map +1 -1
- package/dist/internal/js-string.js +5 -1
- package/dist/internal/js-string.js.map +1 -1
- package/dist/internal/mount-shadow.js +1 -1
- package/dist/internal/mount-shadow.js.map +1 -1
- package/dist/internal/output-guard.d.ts +7 -0
- package/dist/internal/output-guard.d.ts.map +1 -1
- package/dist/internal/output-guard.js +80 -8
- package/dist/internal/output-guard.js.map +1 -1
- package/dist/internal/package-entries.d.ts +34 -0
- package/dist/internal/package-entries.d.ts.map +1 -0
- package/dist/internal/package-entries.js +324 -0
- package/dist/internal/package-entries.js.map +1 -0
- package/dist/internal/page-execution.d.ts +6 -0
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +437 -114
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +15 -2
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/private-env.d.ts +8 -0
- package/dist/internal/private-env.d.ts.map +1 -1
- package/dist/internal/private-env.js +25 -10
- package/dist/internal/private-env.js.map +1 -1
- package/dist/internal/progressive-cookies.d.ts +14 -0
- package/dist/internal/progressive-cookies.d.ts.map +1 -0
- package/dist/internal/progressive-cookies.js +43 -0
- package/dist/internal/progressive-cookies.js.map +1 -0
- package/dist/internal/public-env.d.ts +2 -0
- package/dist/internal/public-env.d.ts.map +1 -0
- package/dist/internal/public-env.js +13 -0
- package/dist/internal/public-env.js.map +1 -0
- package/dist/internal/render-document.d.ts +5 -44
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +51 -63
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/response-controls.d.ts +2 -0
- package/dist/internal/response-controls.d.ts.map +1 -1
- package/dist/internal/response-controls.js +26 -16
- package/dist/internal/response-controls.js.map +1 -1
- package/dist/internal/revive-dates.d.ts +16 -0
- package/dist/internal/revive-dates.d.ts.map +1 -0
- package/dist/internal/revive-dates.js +32 -0
- package/dist/internal/revive-dates.js.map +1 -0
- package/dist/internal/route-path.d.ts +14 -0
- package/dist/internal/route-path.d.ts.map +1 -0
- package/dist/internal/route-path.js +19 -0
- package/dist/internal/route-path.js.map +1 -0
- package/dist/internal/secret-scan.d.ts.map +1 -1
- package/dist/internal/secret-scan.js +190 -20
- package/dist/internal/secret-scan.js.map +1 -1
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +33 -5
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/web-app.d.ts +25 -18
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +81 -24
- package/dist/internal/web-app.js.map +1 -1
- package/dist/internal/wire-date.d.ts +3 -0
- package/dist/internal/wire-date.d.ts.map +1 -0
- package/dist/internal/wire-date.js +14 -0
- package/dist/internal/wire-date.js.map +1 -0
- package/dist/internal/without-query.d.ts +4 -0
- package/dist/internal/without-query.d.ts.map +1 -0
- package/dist/internal/without-query.js +9 -0
- package/dist/internal/without-query.js.map +1 -0
- package/dist/internal/zone-graph.d.ts +4 -0
- package/dist/internal/zone-graph.d.ts.map +1 -1
- package/dist/internal/zone-graph.js +71 -20
- package/dist/internal/zone-graph.js.map +1 -1
- package/dist/isr.d.ts +59 -3
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +311 -41
- package/dist/isr.js.map +1 -1
- package/dist/llms-txt.d.ts +2 -0
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +29 -3
- package/dist/llms-txt.js.map +1 -1
- package/dist/manifest.d.ts +37 -4
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +4 -0
- package/dist/manifest.js.map +1 -1
- package/dist/open-resolved.d.ts +18 -0
- package/dist/open-resolved.d.ts.map +1 -0
- package/dist/open-resolved.js +62 -0
- package/dist/open-resolved.js.map +1 -0
- package/dist/plugins/kit.d.ts +1 -0
- package/dist/plugins/kit.d.ts.map +1 -1
- package/dist/plugins/kit.js +26 -11
- package/dist/plugins/kit.js.map +1 -1
- package/dist/plugins/vite-leak-guard.d.ts +9 -1
- package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
- package/dist/plugins/vite-leak-guard.js +84 -7
- package/dist/plugins/vite-leak-guard.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
- package/dist/plugins/vite-zone-guard.js +17 -21
- package/dist/plugins/vite-zone-guard.js.map +1 -1
- package/dist/plugins/zone-guard.d.ts.map +1 -1
- package/dist/plugins/zone-guard.js +44 -3
- package/dist/plugins/zone-guard.js.map +1 -1
- package/dist/prerender.d.ts +5 -1
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +41 -19
- package/dist/prerender.js.map +1 -1
- package/dist/public-dir.d.ts +8 -1
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +17 -6
- package/dist/public-dir.js.map +1 -1
- package/dist/query.d.ts +13 -4
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +89 -26
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +25 -2
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +1 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-types.d.ts.map +1 -1
- package/dist/route-types.js +20 -9
- package/dist/route-types.js.map +1 -1
- package/dist/router.d.ts +9 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +47 -50
- package/dist/router.js.map +1 -1
- package/dist/seo.d.ts +49 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +48 -0
- package/dist/seo.js.map +1 -0
- package/dist/server-components/build-info.d.ts +3 -0
- package/dist/server-components/build-info.d.ts.map +1 -0
- package/dist/server-components/build-info.js +7 -0
- package/dist/server-components/build-info.js.map +1 -0
- package/dist/server-components/build-target.d.ts +12 -0
- package/dist/server-components/build-target.d.ts.map +1 -0
- package/dist/server-components/build-target.js +153 -0
- package/dist/server-components/build-target.js.map +1 -0
- package/dist/server-components/build.d.ts +24 -0
- package/dist/server-components/build.d.ts.map +1 -0
- package/dist/server-components/build.js +366 -0
- package/dist/server-components/build.js.map +1 -0
- package/dist/server-components/bundle-proof.d.ts +3 -0
- package/dist/server-components/bundle-proof.d.ts.map +1 -0
- package/dist/server-components/bundle-proof.js +23 -0
- package/dist/server-components/bundle-proof.js.map +1 -0
- package/dist/server-components/client.d.ts +46 -0
- package/dist/server-components/client.d.ts.map +1 -0
- package/dist/server-components/client.js +594 -0
- package/dist/server-components/client.js.map +1 -0
- package/dist/server-components/definition.d.ts +11 -0
- package/dist/server-components/definition.d.ts.map +1 -0
- package/dist/server-components/definition.js +94 -0
- package/dist/server-components/definition.js.map +1 -0
- package/dist/server-components/dev-http.d.ts +5 -0
- package/dist/server-components/dev-http.d.ts.map +1 -0
- package/dist/server-components/dev-http.js +71 -0
- package/dist/server-components/dev-http.js.map +1 -0
- package/dist/server-components/error.d.ts +9 -0
- package/dist/server-components/error.d.ts.map +1 -0
- package/dist/server-components/error.js +24 -0
- package/dist/server-components/error.js.map +1 -0
- package/dist/server-components/frames.d.ts +7 -0
- package/dist/server-components/frames.d.ts.map +1 -0
- package/dist/server-components/frames.js +169 -0
- package/dist/server-components/frames.js.map +1 -0
- package/dist/server-components/html-policy.d.ts +6 -0
- package/dist/server-components/html-policy.d.ts.map +1 -0
- package/dist/server-components/html-policy.js +75 -0
- package/dist/server-components/html-policy.js.map +1 -0
- package/dist/server-components/html.d.ts +4 -0
- package/dist/server-components/html.d.ts.map +1 -0
- package/dist/server-components/html.js +67 -0
- package/dist/server-components/html.js.map +1 -0
- package/dist/server-components/islands.d.ts +44 -0
- package/dist/server-components/islands.d.ts.map +1 -0
- package/dist/server-components/islands.js +42 -0
- package/dist/server-components/islands.js.map +1 -0
- package/dist/server-components/native.d.ts +4 -0
- package/dist/server-components/native.d.ts.map +1 -0
- package/dist/server-components/native.js +81 -0
- package/dist/server-components/native.js.map +1 -0
- package/dist/server-components/portal.d.ts +6 -0
- package/dist/server-components/portal.d.ts.map +1 -0
- package/dist/server-components/portal.js +35 -0
- package/dist/server-components/portal.js.map +1 -0
- package/dist/server-components/props.d.ts +3 -0
- package/dist/server-components/props.d.ts.map +1 -0
- package/dist/server-components/props.js +14 -0
- package/dist/server-components/props.js.map +1 -0
- package/dist/server-components/protocol.d.ts +44 -0
- package/dist/server-components/protocol.d.ts.map +1 -0
- package/dist/server-components/protocol.js +139 -0
- package/dist/server-components/protocol.js.map +1 -0
- package/dist/server-components/range.d.ts +24 -0
- package/dist/server-components/range.d.ts.map +1 -0
- package/dist/server-components/range.js +263 -0
- package/dist/server-components/range.js.map +1 -0
- package/dist/server-components/registry.d.ts +6 -0
- package/dist/server-components/registry.d.ts.map +1 -0
- package/dist/server-components/registry.js +30 -0
- package/dist/server-components/registry.js.map +1 -0
- package/dist/server-components/response.d.ts +4 -0
- package/dist/server-components/response.d.ts.map +1 -0
- package/dist/server-components/response.js +32 -0
- package/dist/server-components/response.js.map +1 -0
- package/dist/server-components/router-integration.d.ts +9 -0
- package/dist/server-components/router-integration.d.ts.map +1 -0
- package/dist/server-components/router-integration.js +29 -0
- package/dist/server-components/router-integration.js.map +1 -0
- package/dist/server-components/runtime.d.ts +14 -0
- package/dist/server-components/runtime.d.ts.map +1 -0
- package/dist/server-components/runtime.js +276 -0
- package/dist/server-components/runtime.js.map +1 -0
- package/dist/server-components/scheduler.d.ts +14 -0
- package/dist/server-components/scheduler.d.ts.map +1 -0
- package/dist/server-components/scheduler.js +74 -0
- package/dist/server-components/scheduler.js.map +1 -0
- package/dist/server-components/schema.d.ts +4 -0
- package/dist/server-components/schema.d.ts.map +1 -0
- package/dist/server-components/schema.js +77 -0
- package/dist/server-components/schema.js.map +1 -0
- package/dist/server-components/server-entry.d.ts +12 -0
- package/dist/server-components/server-entry.d.ts.map +1 -0
- package/dist/server-components/server-entry.js +90 -0
- package/dist/server-components/server-entry.js.map +1 -0
- package/dist/server-components/source.d.ts +15 -0
- package/dist/server-components/source.d.ts.map +1 -0
- package/dist/server-components/source.js +269 -0
- package/dist/server-components/source.js.map +1 -0
- package/dist/server-components/state.d.ts +4 -0
- package/dist/server-components/state.d.ts.map +1 -0
- package/dist/server-components/state.js +123 -0
- package/dist/server-components/state.js.map +1 -0
- package/dist/server-components/stream.d.ts +6 -0
- package/dist/server-components/stream.d.ts.map +1 -0
- package/dist/server-components/stream.js +70 -0
- package/dist/server-components/stream.js.map +1 -0
- package/dist/server-components/trusted-html.d.ts +3 -0
- package/dist/server-components/trusted-html.d.ts.map +1 -0
- package/dist/server-components/trusted-html.js +14 -0
- package/dist/server-components/trusted-html.js.map +1 -0
- package/dist/server-components/types.d.ts +107 -0
- package/dist/server-components/types.d.ts.map +1 -0
- package/dist/server-components/types.js +2 -0
- package/dist/server-components/types.js.map +1 -0
- package/dist/server-components/typescript.d.ts +11 -0
- package/dist/server-components/typescript.d.ts.map +1 -0
- package/dist/server-components/typescript.js +28 -0
- package/dist/server-components/typescript.js.map +1 -0
- package/dist/server-components/value.d.ts +5 -0
- package/dist/server-components/value.d.ts.map +1 -0
- package/dist/server-components/value.js +77 -0
- package/dist/server-components/value.js.map +1 -0
- package/dist/server-components/vite.d.ts +12 -0
- package/dist/server-components/vite.d.ts.map +1 -0
- package/dist/server-components/vite.js +197 -0
- package/dist/server-components/vite.js.map +1 -0
- package/dist/server-components.d.ts +5 -0
- package/dist/server-components.d.ts.map +1 -0
- package/dist/server-components.js +4 -0
- package/dist/server-components.js.map +1 -0
- package/dist/service-worker.d.ts +8 -1
- package/dist/service-worker.d.ts.map +1 -1
- package/dist/service-worker.js +20 -6
- package/dist/service-worker.js.map +1 -1
- package/dist/vite.d.ts +12 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +7 -1
- package/dist/vite.js.map +1 -1
- package/dist/zones.d.ts +64 -2
- package/dist/zones.d.ts.map +1 -1
- package/dist/zones.js +331 -28
- package/dist/zones.js.map +1 -1
- package/package.json +102 -4
- package/src/build-vite.ts +31 -9
- package/src/build.ts +109 -31
- package/src/cdn.ts +18 -12
- package/src/client.ts +48 -17
- package/src/deferred.ts +27 -9
- package/src/dev-feed.ts +24 -5
- package/src/dev-port.ts +4 -10
- package/src/dev-session.ts +4 -13
- package/src/dev.ts +29 -1
- package/src/draft.ts +35 -14
- package/src/fonts-google.ts +10 -0
- package/src/index.ts +5 -6
- package/src/internal/codegen.ts +60 -29
- package/src/internal/disposable-dir.ts +24 -0
- package/src/internal/entry-chunks.ts +18 -0
- package/src/internal/evidence-traversal.ts +57 -0
- package/src/internal/head-merge.ts +16 -0
- package/src/internal/js-string.ts +6 -1
- package/src/internal/mount-shadow.ts +1 -1
- package/src/internal/output-guard.ts +88 -7
- package/src/internal/package-entries.ts +331 -0
- package/src/internal/page-execution.ts +573 -114
- package/src/internal/parity.ts +15 -2
- package/src/internal/private-env.ts +27 -13
- package/src/internal/progressive-cookies.ts +60 -0
- package/src/internal/public-env.ts +14 -0
- package/src/internal/render-document.ts +66 -89
- package/src/internal/response-controls.ts +31 -14
- package/src/internal/revive-dates.ts +35 -0
- package/src/internal/route-path.ts +24 -0
- package/src/internal/secret-scan.ts +190 -18
- package/src/internal/server-boundary.ts +29 -5
- package/src/internal/web-app.ts +126 -42
- package/src/internal/wire-date.ts +16 -0
- package/src/internal/without-query.ts +8 -0
- package/src/internal/zone-graph.ts +78 -25
- package/src/isr.ts +364 -45
- package/src/llms-txt.ts +32 -3
- package/src/manifest.ts +38 -4
- package/src/open-resolved.ts +64 -0
- package/src/plugins/kit.ts +31 -11
- package/src/plugins/vite-leak-guard.ts +104 -8
- package/src/plugins/vite-zone-guard.ts +21 -19
- package/src/plugins/zone-guard.ts +41 -2
- package/src/prerender.ts +39 -18
- package/src/public-dir.ts +27 -8
- package/src/query.ts +107 -26
- package/src/render-seam.ts +30 -2
- package/src/route-types.ts +34 -7
- package/src/router.ts +66 -59
- package/src/seo.ts +68 -0
- package/src/server-components/build-info.ts +11 -0
- package/src/server-components/build-target.ts +194 -0
- package/src/server-components/build.ts +419 -0
- package/src/server-components/bundle-proof.ts +33 -0
- package/src/server-components/client.ts +663 -0
- package/src/server-components/definition.ts +125 -0
- package/src/server-components/dev-http.ts +75 -0
- package/src/server-components/error.ts +24 -0
- package/src/server-components/frames.ts +181 -0
- package/src/server-components/html-policy.ts +94 -0
- package/src/server-components/html.ts +81 -0
- package/src/server-components/islands.ts +111 -0
- package/src/server-components/native.ts +101 -0
- package/src/server-components/portal.ts +42 -0
- package/src/server-components/props.ts +19 -0
- package/src/server-components/protocol.ts +167 -0
- package/src/server-components/range.ts +313 -0
- package/src/server-components/registry.ts +34 -0
- package/src/server-components/response.ts +44 -0
- package/src/server-components/router-integration.ts +40 -0
- package/src/server-components/runtime.ts +376 -0
- package/src/server-components/scheduler.ts +84 -0
- package/src/server-components/schema.ts +69 -0
- package/src/server-components/server-entry.ts +98 -0
- package/src/server-components/source.ts +324 -0
- package/src/server-components/state.ts +143 -0
- package/src/server-components/stream.ts +71 -0
- package/src/server-components/trusted-html.ts +21 -0
- package/src/server-components/types.ts +138 -0
- package/src/server-components/typescript.ts +39 -0
- package/src/server-components/value.ts +82 -0
- package/src/server-components/vite.ts +242 -0
- package/src/server-components.ts +32 -0
- package/src/service-worker.ts +24 -7
- package/src/vite.ts +8 -1
- package/src/zones.ts +418 -32
package/dist/isr.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* This module is the store primitive; the SWR wrapper (`withISR`) builds on it next.
|
|
8
8
|
*/
|
|
9
|
+
import { isAuthenticatedRequest, writableResponse } from "@nifrajs/core/server";
|
|
10
|
+
import { readBoundedBytes, TransportCodecError } from "@nifrajs/core/transport-codec";
|
|
9
11
|
import { isDraftEnabled } from "./draft.js";
|
|
10
12
|
import { assertTokenSecret, timingSafeEqual } from "./internal/timing-safe-equal.js";
|
|
11
13
|
const ISR_TAG_PATTERN = /^[A-Za-z][A-Za-z0-9._:/-]{0,127}$/;
|
|
@@ -81,6 +83,10 @@ export class MemoryCacheStore {
|
|
|
81
83
|
cache = new Map();
|
|
82
84
|
max;
|
|
83
85
|
tagEpoch = new Map();
|
|
86
|
+
// Purge order: `fence` reads it; each purge advances it and records where, per key and per tag.
|
|
87
|
+
order = 0;
|
|
88
|
+
purgedKeys = new Map();
|
|
89
|
+
purgedTags = new Map();
|
|
84
90
|
constructor(options = {}) {
|
|
85
91
|
const isProd = typeof process !== "undefined" && process.env?.NODE_ENV === "production";
|
|
86
92
|
if (options.allowInProduction !== true && isProd) {
|
|
@@ -113,8 +119,13 @@ export class MemoryCacheStore {
|
|
|
113
119
|
}
|
|
114
120
|
return Promise.resolve(value);
|
|
115
121
|
}
|
|
122
|
+
fence() {
|
|
123
|
+
return this.order;
|
|
124
|
+
}
|
|
116
125
|
set(key, value) {
|
|
117
126
|
const tags = normalizeTags(value.tags);
|
|
127
|
+
if (value.fence !== undefined && this.purgedAfter(key, tags, value.fence))
|
|
128
|
+
return Promise.resolve();
|
|
118
129
|
const tagEpochs = {};
|
|
119
130
|
for (const tag of tags)
|
|
120
131
|
tagEpochs[tag] = this.tagEpoch.get(tag) ?? "0";
|
|
@@ -131,12 +142,14 @@ export class MemoryCacheStore {
|
|
|
131
142
|
}
|
|
132
143
|
delete(key) {
|
|
133
144
|
this.cache.delete(key);
|
|
145
|
+
this.markPurged(this.purgedKeys, key);
|
|
134
146
|
this.pruneTagEpochs();
|
|
135
147
|
return Promise.resolve();
|
|
136
148
|
}
|
|
137
149
|
invalidateTag(tag) {
|
|
138
150
|
normalizeTag(tag);
|
|
139
151
|
this.tagEpoch.set(tag, crypto.randomUUID());
|
|
152
|
+
this.markPurged(this.purgedTags, tag);
|
|
140
153
|
// This store is bounded and local, so eagerly remove matching rows. It keeps invalidation
|
|
141
154
|
// immediate and lets us discard epoch keys that no retained entry references anymore.
|
|
142
155
|
for (const [key, row] of this.cache) {
|
|
@@ -146,6 +159,22 @@ export class MemoryCacheStore {
|
|
|
146
159
|
this.pruneTagEpochs();
|
|
147
160
|
return Promise.resolve();
|
|
148
161
|
}
|
|
162
|
+
purgedAfter(key, tags, fence) {
|
|
163
|
+
if ((this.purgedKeys.get(key) ?? 0) > fence)
|
|
164
|
+
return true;
|
|
165
|
+
return tags.some((tag) => (this.purgedTags.get(tag) ?? 0) > fence);
|
|
166
|
+
}
|
|
167
|
+
markPurged(marks, name) {
|
|
168
|
+
marks.delete(name);
|
|
169
|
+
marks.set(name, ++this.order);
|
|
170
|
+
// Bounded like the entries: a mark matters only to a render still in flight.
|
|
171
|
+
while (marks.size > this.max) {
|
|
172
|
+
const oldest = marks.keys().next().value;
|
|
173
|
+
if (oldest === undefined)
|
|
174
|
+
break;
|
|
175
|
+
marks.delete(oldest);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
149
178
|
pruneTagEpochs() {
|
|
150
179
|
if (this.tagEpoch.size === 0)
|
|
151
180
|
return;
|
|
@@ -169,12 +198,21 @@ const isCachedResponse = (value) => {
|
|
|
169
198
|
if (typeof value !== "object" || value === null)
|
|
170
199
|
return false;
|
|
171
200
|
const v = value;
|
|
201
|
+
// Everything a replay hands to `new Response` must be legal there, or a corrupt record would throw
|
|
202
|
+
// on every hit instead of reading as a miss.
|
|
172
203
|
if (typeof v.body !== "string" ||
|
|
173
204
|
typeof v.status !== "number" ||
|
|
205
|
+
!Number.isInteger(v.status) ||
|
|
206
|
+
v.status < 200 ||
|
|
207
|
+
v.status > 599 ||
|
|
174
208
|
typeof v.storedAt !== "number" ||
|
|
209
|
+
!Number.isFinite(v.storedAt) ||
|
|
175
210
|
typeof v.revalidate !== "number" ||
|
|
211
|
+
!Number.isFinite(v.revalidate) ||
|
|
212
|
+
v.revalidate < 0 ||
|
|
176
213
|
typeof v.headers !== "object" ||
|
|
177
|
-
v.headers === null
|
|
214
|
+
v.headers === null ||
|
|
215
|
+
Array.isArray(v.headers)) {
|
|
178
216
|
return false;
|
|
179
217
|
}
|
|
180
218
|
if (v.tags !== undefined) {
|
|
@@ -187,13 +225,55 @@ const isCachedResponse = (value) => {
|
|
|
187
225
|
return false;
|
|
188
226
|
}
|
|
189
227
|
}
|
|
190
|
-
|
|
228
|
+
const headers = {};
|
|
229
|
+
for (const [name, headerValue] of Object.entries(v.headers)) {
|
|
191
230
|
if (typeof headerValue !== "string")
|
|
192
231
|
return false;
|
|
232
|
+
headers[name] = headerValue;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
new Headers(headers);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return false;
|
|
193
239
|
}
|
|
194
240
|
return true;
|
|
195
241
|
};
|
|
196
242
|
const KV_TAG_EPOCH_PREFIX = "__nifra_isr_tag_v1:";
|
|
243
|
+
const KV_PURGED_AT = "__nifraPurgedAt";
|
|
244
|
+
/** When a KV page key was purged, from the marker `delete` leaves; `-Infinity` for a page or nothing. */
|
|
245
|
+
function kvPurgedAt(raw) {
|
|
246
|
+
if (raw === null || !raw.includes(KV_PURGED_AT))
|
|
247
|
+
return Number.NEGATIVE_INFINITY;
|
|
248
|
+
try {
|
|
249
|
+
const parsed = JSON.parse(raw);
|
|
250
|
+
if (typeof parsed === "object" && parsed !== null && KV_PURGED_AT in parsed) {
|
|
251
|
+
const at = parsed[KV_PURGED_AT];
|
|
252
|
+
if (typeof at === "number")
|
|
253
|
+
return at;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Not a marker.
|
|
258
|
+
}
|
|
259
|
+
return Number.NEGATIVE_INFINITY;
|
|
260
|
+
}
|
|
261
|
+
/** When a tag epoch was written (`<time>:<uuid>`); an epoch from before epochs carried a time is older
|
|
262
|
+
* than any render. */
|
|
263
|
+
function kvEpochTime(epoch) {
|
|
264
|
+
const at = epoch.indexOf(":");
|
|
265
|
+
const time = at === -1 ? Number.NaN : Number(epoch.slice(0, at));
|
|
266
|
+
return Number.isFinite(time) ? time : Number.NEGATIVE_INFINITY;
|
|
267
|
+
}
|
|
268
|
+
let lastKvTime = 0;
|
|
269
|
+
/** Wall-clock ms, strictly increasing per process so a purge orders after a render it follows. Not
|
|
270
|
+
* `timeOrigin + performance.now()`: that clock stops while the host sleeps, so a purge from a process
|
|
271
|
+
* that slept would order before renders other instances began earlier. */
|
|
272
|
+
const kvClock = () => {
|
|
273
|
+
const now = Date.now();
|
|
274
|
+
lastKvTime = now > lastKvTime ? now : lastKvTime + 0.001;
|
|
275
|
+
return lastKvTime;
|
|
276
|
+
};
|
|
197
277
|
/**
|
|
198
278
|
* A {@link CacheStore} backed by a **Cloudflare Workers KV** namespace (or any {@link KVNamespaceLike}
|
|
199
279
|
* binding) - the production-grade shared/durable store ISR wants: cached pages and on-demand purges
|
|
@@ -256,29 +336,42 @@ export class KVCacheStore {
|
|
|
256
336
|
const { __nifraTagEpochs: _ignored, ...value } = parsed;
|
|
257
337
|
return value;
|
|
258
338
|
}
|
|
339
|
+
/** Wall-clock time, so purges on any instance order against it. KV is eventually consistent, so the
|
|
340
|
+
* fence holds only for purges this location has already seen. */
|
|
341
|
+
fence() {
|
|
342
|
+
return kvClock();
|
|
343
|
+
}
|
|
259
344
|
async set(key, value) {
|
|
260
|
-
const
|
|
345
|
+
const { fence, ...entry } = value;
|
|
346
|
+
if (fence !== undefined && kvPurgedAt(await this.kv.get(key)) > fence)
|
|
347
|
+
return;
|
|
348
|
+
const tags = normalizeTags(entry.tags);
|
|
261
349
|
if (tags.length === 0) {
|
|
262
|
-
await this.kv.put(key, JSON.stringify(
|
|
350
|
+
await this.kv.put(key, JSON.stringify(entry), this.putOptions);
|
|
263
351
|
return;
|
|
264
352
|
}
|
|
265
353
|
const epochs = Object.fromEntries(await Promise.all(tags.map(async (tag) => [tag, (await this.kv.get(this.tagKey(tag))) ?? "0"])));
|
|
354
|
+
if (fence !== undefined && Object.values(epochs).some((epoch) => kvEpochTime(epoch) > fence)) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
266
357
|
const stored = {
|
|
267
|
-
...
|
|
358
|
+
...entry,
|
|
268
359
|
tags,
|
|
269
360
|
__nifraTagEpochs: epochs,
|
|
270
361
|
};
|
|
271
362
|
await this.kv.put(key, JSON.stringify(stored), this.putOptions);
|
|
272
363
|
}
|
|
364
|
+
/** Leaves a marker rather than removing the key, so `set` can refuse a render that began before
|
|
365
|
+
* the purge. The marker reads as a miss. */
|
|
273
366
|
async delete(key) {
|
|
274
|
-
await this.kv.
|
|
367
|
+
await this.kv.put(key, JSON.stringify({ [KV_PURGED_AT]: kvClock() }), this.putOptions);
|
|
275
368
|
}
|
|
276
369
|
async invalidateTag(tag) {
|
|
277
370
|
normalizeTag(tag);
|
|
278
371
|
// Epoch invalidation avoids KV key enumeration and remains bounded for a tag with millions of
|
|
279
372
|
// pages. Epoch keys intentionally have no expiration: an expired epoch would resurrect entries
|
|
280
|
-
// written before the invalidation when their page key still exists.
|
|
281
|
-
await this.kv.put(this.tagKey(tag), crypto.randomUUID());
|
|
373
|
+
// written before the invalidation when their page key still exists. The time orders it for `set`.
|
|
374
|
+
await this.kv.put(this.tagKey(tag), `${kvClock()}:${crypto.randomUUID()}`);
|
|
282
375
|
}
|
|
283
376
|
tagKey(tag) {
|
|
284
377
|
return `${KV_TAG_EPOCH_PREFIX}${encodeURIComponent(tag)}`;
|
|
@@ -310,18 +403,30 @@ export function withoutCacheChannel(res) {
|
|
|
310
403
|
if (!res.headers.has(ISR_REVALIDATE_HEADER) && !res.headers.has(ISR_REVALIDATE_TAGS_HEADER)) {
|
|
311
404
|
return res;
|
|
312
405
|
}
|
|
406
|
+
const target = writableResponse(res);
|
|
407
|
+
target.headers.delete(ISR_REVALIDATE_HEADER);
|
|
408
|
+
target.headers.delete(ISR_REVALIDATE_TAGS_HEADER);
|
|
409
|
+
return target;
|
|
410
|
+
}
|
|
411
|
+
const STORE_FAILED = Symbol("nifra.isr.store-failed");
|
|
412
|
+
const STORE_REPORT_INTERVAL_MS = 60_000;
|
|
413
|
+
const STORE_REPORT_DETAIL_CHARS = 200;
|
|
414
|
+
/** The error's own words with the cache key cut out, since a store may name the key it failed on. */
|
|
415
|
+
function storeErrorDetail(error, key) {
|
|
416
|
+
const text = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
417
|
+
let path;
|
|
313
418
|
try {
|
|
314
|
-
|
|
315
|
-
res.headers.delete(ISR_REVALIDATE_TAGS_HEADER);
|
|
316
|
-
return res;
|
|
419
|
+
path = new URL(key).pathname;
|
|
317
420
|
}
|
|
318
421
|
catch {
|
|
319
|
-
|
|
320
|
-
const headers = new Headers(res.headers);
|
|
321
|
-
headers.delete(ISR_REVALIDATE_HEADER);
|
|
322
|
-
headers.delete(ISR_REVALIDATE_TAGS_HEADER);
|
|
323
|
-
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
|
|
422
|
+
path = undefined;
|
|
324
423
|
}
|
|
424
|
+
let redacted = text.split(key).join("<key>");
|
|
425
|
+
if (path !== undefined && path.length > 1)
|
|
426
|
+
redacted = redacted.split(path).join("<key>");
|
|
427
|
+
return redacted.length > STORE_REPORT_DETAIL_CHARS
|
|
428
|
+
? `${redacted.slice(0, STORE_REPORT_DETAIL_CHARS)}...`
|
|
429
|
+
: redacted;
|
|
325
430
|
}
|
|
326
431
|
/** `createWebApp`'s marker for how its documents meet a CSP (see `DOCUMENT_POLICY` in ./csp.ts). Read
|
|
327
432
|
* through the global symbol registry so a cache wrapper never imports the CSP module. */
|
|
@@ -376,7 +481,18 @@ const cacheControlHas = (headers, names) => {
|
|
|
376
481
|
return names.some((name) => directives.includes(name));
|
|
377
482
|
};
|
|
378
483
|
const hasSetCookie = (headers) => headers.has("set-cookie") || (headers.getSetCookie?.().length ?? 0) > 0;
|
|
379
|
-
|
|
484
|
+
/** Whether every cookie in a `Cookie` header is named in `names`. */
|
|
485
|
+
const cookieNamesWithin = (header, names) => {
|
|
486
|
+
for (const pair of header.split(";")) {
|
|
487
|
+
const name = pair.split("=", 1)[0]?.trim() ?? "";
|
|
488
|
+
if (name !== "" && !names.has(name))
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
return true;
|
|
492
|
+
};
|
|
493
|
+
// An authentication stage may read any header, so a request it accepted is private whatever carried
|
|
494
|
+
// the credential. The mark is set on the request the app was handed, which is why this runs after it.
|
|
495
|
+
const requestCarriesPrivateState = (req) => req.headers.has("authorization") || req.headers.has("cookie") || isAuthenticatedRequest(req);
|
|
380
496
|
const responseIsExplicitlyPublic = (res) => cacheControlHas(res.headers, ["public"]);
|
|
381
497
|
// `x-nifra-data` alone is not a variation the URL key misses: a navigation data request never reads
|
|
382
498
|
// or writes the store, so every stored entry is the document.
|
|
@@ -394,7 +510,8 @@ const responseDeclaresVary = (res) => {
|
|
|
394
510
|
/**
|
|
395
511
|
* Whether `res` may be stored by a shared cache and served to anyone requesting `req`'s URL: a
|
|
396
512
|
* full-document `GET` 200 `text/html` with no Set-Cookie, no `private`/`no-store`, no `Vary` beyond
|
|
397
|
-
* the data header, and, for a request carrying a cookie or Authorization
|
|
513
|
+
* the data header, and, for a request carrying a cookie or Authorization or accepted by an
|
|
514
|
+
* authentication stage, an explicit `public`.
|
|
398
515
|
* The one rule both `withISR` and `@nifrajs/web/cdn` cache by.
|
|
399
516
|
*/
|
|
400
517
|
export const isCacheablePage = (req, res) => {
|
|
@@ -471,7 +588,21 @@ function sanitizeCachedHeaders(headers) {
|
|
|
471
588
|
}
|
|
472
589
|
return out;
|
|
473
590
|
}
|
|
474
|
-
|
|
591
|
+
/**
|
|
592
|
+
* Whether a regeneration's answer withdraws the stored page: it is gone or moved, or, rendered for a
|
|
593
|
+
* request with no cookie or Authorization, refused (401, 403) or declared `private`/`no-store`. A
|
|
594
|
+
* signed-in visitor's own 403 or private copy says nothing about the shared page. A 5xx or 429 keeps
|
|
595
|
+
* serving the stale copy.
|
|
596
|
+
*/
|
|
597
|
+
const isInvalidationResponse = (req, res) => {
|
|
598
|
+
if (res.status === 404 || res.status === 410 || (res.status >= 300 && res.status < 400))
|
|
599
|
+
return true;
|
|
600
|
+
if (requestCarriesPrivateState(req))
|
|
601
|
+
return false;
|
|
602
|
+
return (res.status === 401 ||
|
|
603
|
+
res.status === 403 ||
|
|
604
|
+
(res.status >= 200 && res.status < 300 && cacheControlHas(res.headers, ["private", "no-store"])));
|
|
605
|
+
};
|
|
475
606
|
/**
|
|
476
607
|
* Wrap a nifra app with **Incremental Static Regeneration**: a cacheable page is served from
|
|
477
608
|
* {@link CacheStore} when fresh, served **stale while a fresh copy regenerates in the background**
|
|
@@ -486,8 +617,77 @@ const isInvalidationResponse = (res) => res.status === 404 || res.status === 410
|
|
|
486
617
|
export function withISR(app, options) {
|
|
487
618
|
const { store, now } = options;
|
|
488
619
|
assertRevalidate(options.revalidate, "revalidate");
|
|
620
|
+
const storeTimeoutMs = options.storeTimeoutMs;
|
|
621
|
+
if (storeTimeoutMs !== undefined && !(Number.isFinite(storeTimeoutMs) && storeTimeoutMs > 0)) {
|
|
622
|
+
throw new RangeError("[nifra/web] ISR storeTimeoutMs must be a positive finite number");
|
|
623
|
+
}
|
|
624
|
+
const lastReport = new Map();
|
|
625
|
+
const reportStoreError = (op, key, error) => {
|
|
626
|
+
try {
|
|
627
|
+
if (options.onStoreError !== undefined) {
|
|
628
|
+
options.onStoreError(error, { op });
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const last = lastReport.get(op);
|
|
632
|
+
const at = now();
|
|
633
|
+
if (last !== undefined && at - last < STORE_REPORT_INTERVAL_MS)
|
|
634
|
+
return;
|
|
635
|
+
lastReport.set(op, at);
|
|
636
|
+
console.error(`[nifra/web] ISR store ${op} failed; serving without the cache (${storeErrorDetail(error, key)})`);
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
// A failing reporter must not turn a served page into an error.
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
// Runs one store call; a throw, a rejection or a timeout is reported and answered with STORE_FAILED.
|
|
643
|
+
const attempt = async (op, key, call) => {
|
|
644
|
+
let timer;
|
|
645
|
+
try {
|
|
646
|
+
const pending = call();
|
|
647
|
+
if (storeTimeoutMs === undefined)
|
|
648
|
+
return await pending;
|
|
649
|
+
const timeout = new Promise((_, reject) => {
|
|
650
|
+
timer = setTimeout(() => {
|
|
651
|
+
const error = new Error(`ISR store ${op} exceeded storeTimeoutMs (${storeTimeoutMs} ms)`);
|
|
652
|
+
error.name = "TimeoutError";
|
|
653
|
+
reject(error);
|
|
654
|
+
}, storeTimeoutMs);
|
|
655
|
+
});
|
|
656
|
+
return await Promise.race([pending, timeout]);
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
reportStoreError(op, key, error);
|
|
660
|
+
return STORE_FAILED;
|
|
661
|
+
}
|
|
662
|
+
finally {
|
|
663
|
+
if (timer !== undefined)
|
|
664
|
+
clearTimeout(timer);
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
// A store without purge order, or one whose fence failed, stores the render unfenced.
|
|
668
|
+
const fenceFor = async (key) => {
|
|
669
|
+
const { fence } = store;
|
|
670
|
+
if (fence === undefined)
|
|
671
|
+
return undefined;
|
|
672
|
+
const taken = await attempt("fence", key, async () => fence.call(store));
|
|
673
|
+
return taken === STORE_FAILED ? undefined : taken;
|
|
674
|
+
};
|
|
489
675
|
const keyOf = options.key ?? requestKeyOf(urlKeyOf(options.query));
|
|
490
676
|
const draftSecret = options.draftSecret;
|
|
677
|
+
const anonymousCookies = new Set(options.anonymousCookies ?? []);
|
|
678
|
+
// A request whose cookies are all anonymous is judged for storage as if it carried none.
|
|
679
|
+
const judged = (req) => {
|
|
680
|
+
if (anonymousCookies.size === 0 ||
|
|
681
|
+
req.headers.has("authorization") ||
|
|
682
|
+
isAuthenticatedRequest(req))
|
|
683
|
+
return req;
|
|
684
|
+
const cookie = req.headers.get("cookie");
|
|
685
|
+
if (cookie === null || !cookieNamesWithin(cookie, anonymousCookies))
|
|
686
|
+
return req;
|
|
687
|
+
const headers = new Headers(req.headers);
|
|
688
|
+
headers.delete("cookie");
|
|
689
|
+
return new Request(req.url, { method: req.method, headers });
|
|
690
|
+
};
|
|
491
691
|
const regenerating = new Set();
|
|
492
692
|
openCacheChannel(app);
|
|
493
693
|
// An outer cache layer (`withCdn`) opens this wrapper's own channel: cached responses then carry how
|
|
@@ -531,10 +731,12 @@ export function withISR(app, options) {
|
|
|
531
731
|
const safeSeconds = Number.isFinite(seconds) && seconds >= 0 ? seconds : options.revalidate;
|
|
532
732
|
return safeSeconds * 1000;
|
|
533
733
|
};
|
|
534
|
-
const render = async (req, platform, key) => {
|
|
734
|
+
const render = async (req, platform, key, remember, stored) => {
|
|
735
|
+
const fence = await fenceFor(key);
|
|
535
736
|
const res = await app.fetch(req, platform);
|
|
536
|
-
if (!isCacheablePage(req, res)) {
|
|
537
|
-
|
|
737
|
+
if (!isCacheablePage(judged(req), res)) {
|
|
738
|
+
if (remember)
|
|
739
|
+
rememberUncacheable(key, res);
|
|
538
740
|
return withoutCacheChannel(res);
|
|
539
741
|
}
|
|
540
742
|
uncacheable.delete(key);
|
|
@@ -547,14 +749,44 @@ export function withISR(app, options) {
|
|
|
547
749
|
storedAt: now(),
|
|
548
750
|
revalidate: ttlMs(res),
|
|
549
751
|
...(tags.length === 0 ? {} : { tags }),
|
|
752
|
+
fence,
|
|
550
753
|
};
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
754
|
+
// A failed write still answers with the page, and still hands it to concurrent misses: whether it
|
|
755
|
+
// may be shared was decided above, not by the store.
|
|
756
|
+
await attempt("set", key, () => store.set(key, entry));
|
|
757
|
+
stored?.(entry);
|
|
758
|
+
return fresh(entry);
|
|
759
|
+
};
|
|
760
|
+
const fresh = (entry) => {
|
|
761
|
+
const res = new Response(entry.body, {
|
|
762
|
+
status: entry.status,
|
|
554
763
|
headers: { ...entry.headers, [ISR_STATUS_HEADER]: "miss" },
|
|
555
764
|
});
|
|
556
|
-
advertise(
|
|
557
|
-
return
|
|
765
|
+
advertise(res.headers, entry.revalidate, entry.tags ?? []);
|
|
766
|
+
return res;
|
|
767
|
+
};
|
|
768
|
+
// Cold misses in flight per key. Concurrent misses wait for the first one's stored entry instead
|
|
769
|
+
// of each rendering the page; a render that is not stored (private, failed) sends each waiter to
|
|
770
|
+
// render for itself, so a waiter never receives a page that was not safe to share.
|
|
771
|
+
const coldRenders = new Map();
|
|
772
|
+
const renderCold = async (req, platform, key) => {
|
|
773
|
+
const pending = coldRenders.get(key);
|
|
774
|
+
if (pending !== undefined) {
|
|
775
|
+
const entry = await pending;
|
|
776
|
+
return entry === undefined ? render(req, platform, key, true) : fresh(entry);
|
|
777
|
+
}
|
|
778
|
+
const shared = Promise.withResolvers();
|
|
779
|
+
coldRenders.set(key, shared.promise);
|
|
780
|
+
let entry;
|
|
781
|
+
try {
|
|
782
|
+
return await render(req, platform, key, true, (stored) => {
|
|
783
|
+
entry = stored;
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
finally {
|
|
787
|
+
coldRenders.delete(key);
|
|
788
|
+
shared.resolve(entry);
|
|
789
|
+
}
|
|
558
790
|
};
|
|
559
791
|
// Background regeneration (single-flight per key) - a failed regen keeps the stale entry; the next
|
|
560
792
|
// stale hit retries. Never throws (it runs detached / under waitUntil).
|
|
@@ -563,23 +795,27 @@ export function withISR(app, options) {
|
|
|
563
795
|
return;
|
|
564
796
|
regenerating.add(key);
|
|
565
797
|
try {
|
|
798
|
+
const fence = await fenceFor(key);
|
|
566
799
|
const res = await app.fetch(req, platform);
|
|
567
|
-
if (isCacheablePage(req, res)) {
|
|
800
|
+
if (isCacheablePage(judged(req), res)) {
|
|
568
801
|
const tags = tagsOf(res);
|
|
569
|
-
|
|
802
|
+
const entry = {
|
|
570
803
|
body: await res.text(),
|
|
571
804
|
status: res.status,
|
|
572
805
|
headers: headersOf(res),
|
|
573
806
|
storedAt: now(),
|
|
574
807
|
revalidate: ttlMs(res),
|
|
575
808
|
...(tags.length === 0 ? {} : { tags }),
|
|
576
|
-
|
|
809
|
+
fence,
|
|
810
|
+
};
|
|
811
|
+
await attempt("set", key, () => store.set(key, entry));
|
|
577
812
|
}
|
|
578
|
-
else if (isInvalidationResponse(res)) {
|
|
579
|
-
// A
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
await
|
|
813
|
+
else if (isInvalidationResponse(judged(req), res)) {
|
|
814
|
+
// A withdrawal is authoritative. Keeping the old entry would make a withdrawn page stay
|
|
815
|
+
// publicly visible forever, because every stale request would retry the same non-cacheable
|
|
816
|
+
// response while continuing to serve the old body.
|
|
817
|
+
await res.body?.cancel();
|
|
818
|
+
await attempt("delete", key, () => store.delete(key));
|
|
583
819
|
}
|
|
584
820
|
else {
|
|
585
821
|
await res.body?.cancel();
|
|
@@ -596,7 +832,14 @@ export function withISR(app, options) {
|
|
|
596
832
|
// A data-mode soft-nav GET bypasses the cache entirely: entries are full HTML documents keyed
|
|
597
833
|
// by URL, and serving one to a loader-data fetch would hand the client HTML where it expects
|
|
598
834
|
// the loader payload. (The write path already refuses to cache data-mode responses.)
|
|
599
|
-
const
|
|
835
|
+
const privateRoute = Reflect.get(app, Symbol.for("nifra.web.private-route"));
|
|
836
|
+
const ownsPrivate = typeof privateRoute === "function" && privateRoute(req) === true;
|
|
837
|
+
const key = req.method === "GET" &&
|
|
838
|
+
req.headers.get("x-nifra-data") === null &&
|
|
839
|
+
!req.headers.has("x-nifra-component") &&
|
|
840
|
+
!ownsPrivate
|
|
841
|
+
? keyOf(req)
|
|
842
|
+
: null;
|
|
600
843
|
if (key === null)
|
|
601
844
|
return withoutCacheChannel(await app.fetch(req, platform));
|
|
602
845
|
// Draft/preview: an editor (valid signed cookie) always renders fresh and is never cached, so
|
|
@@ -604,13 +847,17 @@ export function withISR(app, options) {
|
|
|
604
847
|
if (draftSecret !== undefined && (await isDraftEnabled(req, draftSecret))) {
|
|
605
848
|
return withoutCacheChannel(await app.fetch(req, platform));
|
|
606
849
|
}
|
|
850
|
+
// A cookie or Authorization request may be personalized: under the default key it never reads
|
|
851
|
+
// the store, and its private render never marks the URL uncacheable for everyone else.
|
|
852
|
+
const personal = options.key === undefined && requestCarriesPrivateState(judged(req));
|
|
607
853
|
const until = uncacheable.get(key);
|
|
608
854
|
if (until !== undefined) {
|
|
609
855
|
if (now() < until)
|
|
610
|
-
return render(req, platform, key);
|
|
856
|
+
return render(req, platform, key, !personal);
|
|
611
857
|
uncacheable.delete(key);
|
|
612
858
|
}
|
|
613
|
-
const
|
|
859
|
+
const read = personal ? undefined : await attempt("get", key, () => store.get(key));
|
|
860
|
+
const hit = read === STORE_FAILED ? undefined : read;
|
|
614
861
|
if (hit !== undefined) {
|
|
615
862
|
if (now() - hit.storedAt < hit.revalidate)
|
|
616
863
|
return served(hit, "hit");
|
|
@@ -622,7 +869,7 @@ export function withISR(app, options) {
|
|
|
622
869
|
void task.catch(() => { });
|
|
623
870
|
return served(hit, "stale");
|
|
624
871
|
}
|
|
625
|
-
return render(req, platform, key);
|
|
872
|
+
return personal ? render(req, platform, key, false) : renderCold(req, platform, key);
|
|
626
873
|
};
|
|
627
874
|
Object.defineProperty(handler, CACHE_CHANNEL, {
|
|
628
875
|
value: () => {
|
|
@@ -635,6 +882,8 @@ const jsonError = (status, error) => Response.json({ ok: false, error }, { statu
|
|
|
635
882
|
/** Most paths and tags one revalidation request may name, so a leaked token cannot fan out. */
|
|
636
883
|
export const MAX_REVALIDATE_PATHS = 100;
|
|
637
884
|
export const MAX_REVALIDATE_TAGS = MAX_ISR_TAGS;
|
|
885
|
+
// Room for a full batch of long paths; a purge body past it is refused before it is buffered.
|
|
886
|
+
const MAX_REVALIDATE_BODY_BYTES = 1024 * 1024;
|
|
638
887
|
const stringList = (value) => {
|
|
639
888
|
if (value === undefined)
|
|
640
889
|
return undefined;
|
|
@@ -648,7 +897,7 @@ const stringList = (value) => {
|
|
|
648
897
|
* `?path=/blog/x`, `?tag=products`, a JSON `{ "path": "/blog/x" }` / `{ "tag": "products" }` body, or a
|
|
649
898
|
* batch `{ "paths": [...], "tags": [...] }` of at most 100 paths and 32 tags.
|
|
650
899
|
* The token is checked in **constant time** (wrong/missing → `401`); malformed targets → `400`;
|
|
651
|
-
* non-POST → `405`. A store without tag support returns `501` for tag requests. Mount it on a nifra route, e.g.
|
|
900
|
+
* a JSON body over 1 MiB → `413`; non-POST → `405`. A store without tag support returns `501` for tag requests. Mount it on a nifra route, e.g.
|
|
652
901
|
* `app.post("/__nifra/revalidate", (c) => handler(c.req))`.
|
|
653
902
|
*/
|
|
654
903
|
export function revalidateEndpoint(options) {
|
|
@@ -671,7 +920,17 @@ export function revalidateEndpoint(options) {
|
|
|
671
920
|
let tag = url.searchParams.get("tag");
|
|
672
921
|
let batch;
|
|
673
922
|
if (path === null && tag === null) {
|
|
674
|
-
|
|
923
|
+
let body = null;
|
|
924
|
+
try {
|
|
925
|
+
const bytes = await readBoundedBytes(new Response(req.body, { headers: req.headers }), {
|
|
926
|
+
maxBytes: MAX_REVALIDATE_BODY_BYTES,
|
|
927
|
+
});
|
|
928
|
+
body = JSON.parse(new TextDecoder().decode(bytes));
|
|
929
|
+
}
|
|
930
|
+
catch (error) {
|
|
931
|
+
if (error instanceof TransportCodecError)
|
|
932
|
+
return jsonError(413, "body_too_large");
|
|
933
|
+
}
|
|
675
934
|
if (typeof body === "object" && body !== null) {
|
|
676
935
|
const candidate = body;
|
|
677
936
|
if (candidate.paths !== undefined || candidate.tags !== undefined) {
|
|
@@ -723,6 +982,17 @@ export function revalidateEndpoint(options) {
|
|
|
723
982
|
if (tags.length > 0 && invalidateTag === undefined) {
|
|
724
983
|
return jsonError(501, "tag_invalidation_not_supported");
|
|
725
984
|
}
|
|
985
|
+
if (tags.length > 0 && options.dataCaches !== undefined) {
|
|
986
|
+
try {
|
|
987
|
+
for (const cache of options.dataCaches) {
|
|
988
|
+
for (const candidate of tags)
|
|
989
|
+
await cache.invalidateTag(candidate);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
catch {
|
|
993
|
+
return Response.json({ ok: false, error: "data_cache_invalidation_failed" }, { status: 500 });
|
|
994
|
+
}
|
|
995
|
+
}
|
|
726
996
|
for (const key of keys)
|
|
727
997
|
await options.store.delete(key);
|
|
728
998
|
for (const candidate of tags)
|