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