@void/isr 0.27.0 → 0.28.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/dist/core.d.mts +4 -0
- package/dist/index.d.mts +26 -84
- package/dist/index.mjs +35 -133
- package/dist/runtime.d.mts +30 -82
- package/dist/runtime.mjs +32 -79
- package/package.json +2 -2
package/dist/core.d.mts
CHANGED
|
@@ -10,6 +10,10 @@ export declare const SHARED_CACHE_POLICY_HEADER = "x-void-shared-cache-policy";
|
|
|
10
10
|
export declare function markSharedCachePolicy(headers: Headers): void;
|
|
11
11
|
/** Reject opaque edge entries admitted before the current privacy policy. */
|
|
12
12
|
export declare function hasCurrentSharedCachePolicy(response: Response): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* KV metadata stored with every ISR cache entry. Writers should annotate it with
|
|
15
|
+
* `satisfies IsrCacheMetadata` so a renamed field fails typecheck.
|
|
16
|
+
*/
|
|
13
17
|
export type IsrCacheMetadata = Readonly<{
|
|
14
18
|
/** HTTP status code. */
|
|
15
19
|
s: number;
|
package/dist/index.d.mts
CHANGED
|
@@ -17,113 +17,55 @@ export declare function denormalizeIsrPath(normalized: string): string;
|
|
|
17
17
|
*/
|
|
18
18
|
export declare function canonicalIsrPathname(pathname: string): string;
|
|
19
19
|
/**
|
|
20
|
-
* Prefix for listing/purging ISR keys.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* empty segment (`<projectId>/isr/v3//`) because dispatch uses
|
|
24
|
-
* `routing.version ?? 'legacy'`, which allows empty strings and writes
|
|
25
|
-
* keys at `<projectId>/isr/v3//<path>`. Only `undefined` selects the
|
|
26
|
-
* legacy scan-all prefix (`<projectId>/isr/`).
|
|
20
|
+
* Prefix for listing/purging ISR keys. An empty `deploymentId` still yields the
|
|
21
|
+
* versioned prefix (`<projectId>/isr/v3//`), because writers may use an empty
|
|
22
|
+
* id. Only `undefined` selects every key (`<projectId>/isr/`).
|
|
27
23
|
*/
|
|
28
24
|
export declare function isrCachePrefix(projectId: string, deploymentId?: string): string;
|
|
29
25
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* optional-`deploymentId` behavior exactly (empty string yields the versioned
|
|
33
|
-
* prefix with an empty segment; only `undefined` selects the scan-all
|
|
34
|
-
* `<projectId>/isr/` prefix).
|
|
35
|
-
*
|
|
36
|
-
* Exists SOLELY for the host-scoping rollout: the purger sweeps this namespace
|
|
37
|
-
* to reap orphaned `v2` KV keys and purge their edge URLs. Current dispatch
|
|
38
|
-
* neither reads nor writes `v2` keys. Transition-only — remove once `v2`
|
|
39
|
-
* entries have aged out.
|
|
26
|
+
* Like `isrCachePrefix`, for hostless `v2` keys. The `isrLegacy*` helpers exist
|
|
27
|
+
* only to purge `v2` entries; remove them once those entries have aged out.
|
|
40
28
|
*/
|
|
41
29
|
export declare function isrLegacyCachePrefix(projectId: string, deploymentId?: string): string;
|
|
42
30
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* segment after the deployment id) so the same path served under different
|
|
47
|
-
* hostnames — e.g. a project slug and a custom domain, or two custom
|
|
48
|
-
* domains — never share a cache slot. `host` is the request hostname the
|
|
49
|
-
* dispatch worker saw (`new URL(request.url).hostname`).
|
|
31
|
+
* KV key for an ISR-cached page: `<proj>/isr/v3/<D>/<host>/<path>`. Deployment
|
|
32
|
+
* scoping keeps cached HTML from outliving its hashed assets; host scoping keeps
|
|
33
|
+
* different hostnames apart. `host` is the request hostname.
|
|
50
34
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* When `originalUrl` is provided, the request was served via a dispatch-side
|
|
55
|
-
* rewrite (`redirectRules`/`fallbackRules` with status 200). The original
|
|
56
|
-
* request URL is mixed into the key so a direct hit to `/en/docs` and a
|
|
57
|
-
* rewrite from `/docs` to `/en/docs` do not collide — user code can branch
|
|
58
|
-
* on `c.isRewritten()` / `c.originalUrl()`, and the cache must honor that.
|
|
59
|
-
* A missing `originalUrl` means "direct hit" and produces the unsuffixed key.
|
|
35
|
+
* Pass `originalUrl` for a rewritten request (or a query variant). It adds a
|
|
36
|
+
* `#rw=<token>` suffix, so a rewrite from `/docs` to `/en/docs` does not share
|
|
37
|
+
* an entry with a direct hit on `/en/docs`.
|
|
60
38
|
*/
|
|
61
39
|
export declare function isrCacheKey(projectId: string, deploymentId: string, host: string, pathname: string, originalUrl?: string | null): string;
|
|
62
|
-
/**
|
|
63
|
-
* Prefix used to list/purge every variant of a given pathname under a
|
|
64
|
-
* deployment — both the direct-hit key and any rewrite-variant keys.
|
|
65
|
-
*/
|
|
40
|
+
/** Prefix for listing/purging the `#rw=` variant keys of a pathname (not its direct-hit key). */
|
|
66
41
|
export declare function isrCacheVariantPrefix(projectId: string, deploymentId: string, host: string, pathname: string): string;
|
|
67
|
-
/**
|
|
68
|
-
* Build the exact LEGACY (pre-host-scoping) direct-hit key for a pathname under
|
|
69
|
-
* a deployment: `<projectId>/isr/v2/<deploymentId>/<normalizedPath>` — the
|
|
70
|
-
* hostless counterpart of `isrCacheKey` with no `<host>/` segment and no
|
|
71
|
-
* rewrite-variant suffix. Used by the purger to reap the orphaned `v2`
|
|
72
|
-
* direct-hit KV key for a requested path.
|
|
73
|
-
*
|
|
74
|
-
* Exists SOLELY for the host-scoping rollout — current dispatch neither writes
|
|
75
|
-
* nor reads hostless keys. Transition-only. Do not use it on the hot path.
|
|
76
|
-
*/
|
|
42
|
+
/** Hostless `v2` direct-hit key for a pathname (purge only; see `isrLegacyCachePrefix`). */
|
|
77
43
|
export declare function isrLegacyCacheKey(projectId: string, deploymentId: string, pathname: string): string;
|
|
44
|
+
/** Hostless `v2` rewrite-variant prefix for a pathname (purge only; see `isrLegacyCachePrefix`). */
|
|
45
|
+
export declare function isrLegacyCacheVariantPrefix(projectId: string, deploymentId: string, pathname: string): string;
|
|
78
46
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* `isrCacheVariantPrefix` but with NO `<host>/` segment. Those entries still
|
|
84
|
-
* persist during the host-scoping rollout (ISR puts set no `expirationTtl`),
|
|
85
|
-
* so the purger must scan this hostless `v2` prefix in addition to the
|
|
86
|
-
* host-scoped `v3` one to recover their variant tokens and purge the matching
|
|
87
|
-
* edge URLs.
|
|
88
|
-
*
|
|
89
|
-
* Built off `isrLegacyCachePrefix` (the `v2` namespace), NOT `isrCachePrefix`
|
|
90
|
-
* (now `v3`), so it targets the legacy hostless keys it is meant to sweep.
|
|
91
|
-
*
|
|
92
|
-
* Exists SOLELY for that rollout cleanup — current dispatch neither writes nor
|
|
93
|
-
* reads hostless keys. Transition-only. Do not use it on the hot path.
|
|
47
|
+
* Hash an original URL into a 16-hex-char variant token (FNV-1a 64-bit), used
|
|
48
|
+
* as both the KV-key suffix and the `__void_orig` edge-cache parameter. It is
|
|
49
|
+
* sync so it fits every read/write path, and opaque so KV never exposes
|
|
50
|
+
* original URLs or their query strings. Not cryptographic.
|
|
94
51
|
*/
|
|
95
|
-
export declare function isrLegacyCacheVariantPrefix(projectId: string, deploymentId: string, pathname: string): string;
|
|
96
52
|
export declare function hashOriginalUrl(originalUrl: string): string;
|
|
97
53
|
/**
|
|
98
|
-
* Split
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* The token is opaque (a hash); the original URL it was derived from is not
|
|
103
|
-
* recoverable. Callers that need to reproduce the matching edge-cache URL
|
|
104
|
-
* pass the token through `isrEdgeCacheUrlForToken`.
|
|
54
|
+
* Split an ISR KV key into its normalized path and its `#rw=` variant token
|
|
55
|
+
* (`null` for a direct hit). Pass the token to `isrEdgeCacheUrlForToken` to
|
|
56
|
+
* rebuild the matching edge-cache URL.
|
|
105
57
|
*/
|
|
106
58
|
export declare function splitIsrCacheKey(fullKey: string): {
|
|
107
59
|
normalizedPath: string;
|
|
108
60
|
variantToken: string | null;
|
|
109
61
|
};
|
|
110
62
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* produced by an older deploy. The `json` variant separates pages-protocol
|
|
115
|
-
* JSON responses from HTML for the same path+deployment.
|
|
116
|
-
*
|
|
117
|
-
* This MUST match the URL dispatch writes. Dispatch builds the URL from
|
|
118
|
-
* `new URL(request.url).hostname`; for standard-port HTTPS traffic (all CF
|
|
119
|
-
* edge traffic) that yields the same origin as the one produced here.
|
|
63
|
+
* Edge-cache (`caches.default`) URL for an ISR page. It carries the deployment
|
|
64
|
+
* id so a new deploy never reads older HTML, and the `json` variant keeps
|
|
65
|
+
* Pages-protocol JSON apart from HTML for the same path.
|
|
120
66
|
*/
|
|
121
67
|
export declare function isrEdgeCacheUrl(hostname: string, pathname: string, deploymentId: string, variant: "html" | "json", originalUrl?: string | null): string;
|
|
122
|
-
/**
|
|
123
|
-
* Like `isrEdgeCacheUrl`, but takes a pre-hashed variant token (from
|
|
124
|
-
* `splitIsrCacheKey`). Used by the purger, which only knows the hashed
|
|
125
|
-
* token: the original URL is not recoverable from a KV key.
|
|
126
|
-
*/
|
|
68
|
+
/** Like `isrEdgeCacheUrl`, but takes a variant token from `splitIsrCacheKey`. */
|
|
127
69
|
export declare function isrEdgeCacheUrlForToken(hostname: string, pathname: string, deploymentId: string, variant: "html" | "json", variantToken?: string | null): string;
|
|
128
70
|
//#endregion
|
|
129
71
|
export type { IsrCacheMetadata };
|
package/dist/index.mjs
CHANGED
|
@@ -1,32 +1,16 @@
|
|
|
1
1
|
//#region src/index.ts
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* the API purges KV keys and edge URLs that dispatch writes and reads.
|
|
3
|
+
* ISR cache keys and edge-cache URLs. Writers, readers, and purgers must build
|
|
4
|
+
* byte-identical keys, so all of them use these helpers.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
9
|
-
* including the empty string — to pass through unchanged. The helpers below
|
|
10
|
-
* preserve that shape so the API can compute the exact same key for purge.
|
|
6
|
+
* Key shapes: `v3` = `<proj>/isr/v3/<D>/<host>/<path>[#rw=<token>]`;
|
|
7
|
+
* `v2` = hostless `<proj>/isr/v2/<D>/<path>`, kept only to purge old entries.
|
|
11
8
|
*/
|
|
12
9
|
const ISR_CACHE_KEY_VERSION = "v3";
|
|
13
10
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* (
|
|
17
|
-
*
|
|
18
|
-
* The bump to `v3` was forced by a real aliasing hazard: under a single
|
|
19
|
-
* version, a host-scoped key `<D>/<host>/<path>` and a legacy hostless key
|
|
20
|
-
* `<D>/<host>/<path>` are byte-identical whenever the legacy path's leading
|
|
21
|
-
* segment happens to look like a hostname. Dotted leading path segments are
|
|
22
|
-
* legal (`[domain]` → `:domain`, `[...slug]` → `(.+)`), so a hostless key for
|
|
23
|
-
* the page `/example.com/about` collides exactly with the host-scoped key for
|
|
24
|
-
* `(host=example.com, /about)`. The stored metadata is only `{s,c,t}` (no
|
|
25
|
-
* path/host), so a reader cannot tell them apart and could serve one page's
|
|
26
|
-
* body for the other. Encoding the shape in the version removes the ambiguity:
|
|
27
|
-
* the version alone determines whether a `<host>` segment is present, so no
|
|
28
|
-
* runtime heuristic (e.g. "is the first segment an attached hostname?") is
|
|
29
|
-
* needed to classify a key. Package-internal, like `ISR_CACHE_KEY_VERSION`.
|
|
11
|
+
* Hostless key version. Host scoping needed a new version: otherwise a hostless
|
|
12
|
+
* key for `/example.com/about` would equal the host-scoped key for
|
|
13
|
+
* (`example.com`, `/about`).
|
|
30
14
|
*/
|
|
31
15
|
const ISR_LEGACY_CACHE_KEY_VERSION = "v2";
|
|
32
16
|
/**
|
|
@@ -62,125 +46,57 @@ function versionedIsrCachePrefix(projectId, version, deploymentId) {
|
|
|
62
46
|
return `${projectId}/isr/`;
|
|
63
47
|
}
|
|
64
48
|
/**
|
|
65
|
-
* Prefix for listing/purging ISR keys.
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* empty segment (`<projectId>/isr/v3//`) because dispatch uses
|
|
69
|
-
* `routing.version ?? 'legacy'`, which allows empty strings and writes
|
|
70
|
-
* keys at `<projectId>/isr/v3//<path>`. Only `undefined` selects the
|
|
71
|
-
* legacy scan-all prefix (`<projectId>/isr/`).
|
|
49
|
+
* Prefix for listing/purging ISR keys. An empty `deploymentId` still yields the
|
|
50
|
+
* versioned prefix (`<projectId>/isr/v3//`), because writers may use an empty
|
|
51
|
+
* id. Only `undefined` selects every key (`<projectId>/isr/`).
|
|
72
52
|
*/
|
|
73
53
|
function isrCachePrefix(projectId, deploymentId) {
|
|
74
54
|
return versionedIsrCachePrefix(projectId, ISR_CACHE_KEY_VERSION, deploymentId);
|
|
75
55
|
}
|
|
76
56
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* optional-`deploymentId` behavior exactly (empty string yields the versioned
|
|
80
|
-
* prefix with an empty segment; only `undefined` selects the scan-all
|
|
81
|
-
* `<projectId>/isr/` prefix).
|
|
82
|
-
*
|
|
83
|
-
* Exists SOLELY for the host-scoping rollout: the purger sweeps this namespace
|
|
84
|
-
* to reap orphaned `v2` KV keys and purge their edge URLs. Current dispatch
|
|
85
|
-
* neither reads nor writes `v2` keys. Transition-only — remove once `v2`
|
|
86
|
-
* entries have aged out.
|
|
57
|
+
* Like `isrCachePrefix`, for hostless `v2` keys. The `isrLegacy*` helpers exist
|
|
58
|
+
* only to purge `v2` entries; remove them once those entries have aged out.
|
|
87
59
|
*/
|
|
88
60
|
function isrLegacyCachePrefix(projectId, deploymentId) {
|
|
89
61
|
return versionedIsrCachePrefix(projectId, ISR_LEGACY_CACHE_KEY_VERSION, deploymentId);
|
|
90
62
|
}
|
|
91
|
-
/**
|
|
92
|
-
* Separator that marks the rewrite-variant suffix on an ISR cache key or
|
|
93
|
-
* the corresponding edge-cache URL parameter. `#` cannot appear in
|
|
94
|
-
* `url.pathname` (fragments are stripped before the request reaches the
|
|
95
|
-
* server) and does not survive `normalizeIsrPath`, so it unambiguously
|
|
96
|
-
* identifies the variant suffix.
|
|
97
|
-
*/
|
|
63
|
+
/** `#` never appears in a request pathname, so it unambiguously marks the variant suffix. */
|
|
98
64
|
const ISR_VARIANT_SEPARATOR = "#rw=";
|
|
99
65
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* segment after the deployment id) so the same path served under different
|
|
104
|
-
* hostnames — e.g. a project slug and a custom domain, or two custom
|
|
105
|
-
* domains — never share a cache slot. `host` is the request hostname the
|
|
106
|
-
* dispatch worker saw (`new URL(request.url).hostname`).
|
|
107
|
-
*
|
|
108
|
-
* Host-scoping is a `v3` key format: `<proj>/isr/v3/<D>/<host>/<path>`. The
|
|
109
|
-
* version bump is load-bearing, not cosmetic — see `ISR_LEGACY_CACHE_KEY_VERSION`.
|
|
66
|
+
* KV key for an ISR-cached page: `<proj>/isr/v3/<D>/<host>/<path>`. Deployment
|
|
67
|
+
* scoping keeps cached HTML from outliving its hashed assets; host scoping keeps
|
|
68
|
+
* different hostnames apart. `host` is the request hostname.
|
|
110
69
|
*
|
|
111
|
-
*
|
|
112
|
-
* rewrite
|
|
113
|
-
*
|
|
114
|
-
* rewrite from `/docs` to `/en/docs` do not collide — user code can branch
|
|
115
|
-
* on `c.isRewritten()` / `c.originalUrl()`, and the cache must honor that.
|
|
116
|
-
* A missing `originalUrl` means "direct hit" and produces the unsuffixed key.
|
|
70
|
+
* Pass `originalUrl` for a rewritten request (or a query variant). It adds a
|
|
71
|
+
* `#rw=<token>` suffix, so a rewrite from `/docs` to `/en/docs` does not share
|
|
72
|
+
* an entry with a direct hit on `/en/docs`.
|
|
117
73
|
*/
|
|
118
74
|
function isrCacheKey(projectId, deploymentId, host, pathname, originalUrl) {
|
|
119
75
|
const base = `${isrCachePrefix(projectId, deploymentId)}${host}/${normalizeIsrPath(pathname)}`;
|
|
120
76
|
if (!originalUrl) return base;
|
|
121
77
|
return `${base}${ISR_VARIANT_SEPARATOR}${hashOriginalUrl(originalUrl)}`;
|
|
122
78
|
}
|
|
123
|
-
/**
|
|
124
|
-
* Prefix used to list/purge every variant of a given pathname under a
|
|
125
|
-
* deployment — both the direct-hit key and any rewrite-variant keys.
|
|
126
|
-
*/
|
|
79
|
+
/** Prefix for listing/purging the `#rw=` variant keys of a pathname (not its direct-hit key). */
|
|
127
80
|
function isrCacheVariantPrefix(projectId, deploymentId, host, pathname) {
|
|
128
81
|
return `${isrCachePrefix(projectId, deploymentId)}${host}/${normalizeIsrPath(pathname)}${ISR_VARIANT_SEPARATOR}`;
|
|
129
82
|
}
|
|
130
|
-
/**
|
|
131
|
-
* Build the exact LEGACY (pre-host-scoping) direct-hit key for a pathname under
|
|
132
|
-
* a deployment: `<projectId>/isr/v2/<deploymentId>/<normalizedPath>` — the
|
|
133
|
-
* hostless counterpart of `isrCacheKey` with no `<host>/` segment and no
|
|
134
|
-
* rewrite-variant suffix. Used by the purger to reap the orphaned `v2`
|
|
135
|
-
* direct-hit KV key for a requested path.
|
|
136
|
-
*
|
|
137
|
-
* Exists SOLELY for the host-scoping rollout — current dispatch neither writes
|
|
138
|
-
* nor reads hostless keys. Transition-only. Do not use it on the hot path.
|
|
139
|
-
*/
|
|
83
|
+
/** Hostless `v2` direct-hit key for a pathname (purge only; see `isrLegacyCachePrefix`). */
|
|
140
84
|
function isrLegacyCacheKey(projectId, deploymentId, pathname) {
|
|
141
85
|
return `${isrLegacyCachePrefix(projectId, deploymentId)}${normalizeIsrPath(pathname)}`;
|
|
142
86
|
}
|
|
143
|
-
/**
|
|
144
|
-
* Prefix used to list/purge the pre-host-scoped ("legacy") rewrite variants of
|
|
145
|
-
* a given pathname under a deployment. Before ISR keys grew a `<host>` segment,
|
|
146
|
-
* variant keys were written as
|
|
147
|
-
* `<projectId>/isr/v2/<deploymentId>/<normalizedPath>#rw=<token>` — identical to
|
|
148
|
-
* `isrCacheVariantPrefix` but with NO `<host>/` segment. Those entries still
|
|
149
|
-
* persist during the host-scoping rollout (ISR puts set no `expirationTtl`),
|
|
150
|
-
* so the purger must scan this hostless `v2` prefix in addition to the
|
|
151
|
-
* host-scoped `v3` one to recover their variant tokens and purge the matching
|
|
152
|
-
* edge URLs.
|
|
153
|
-
*
|
|
154
|
-
* Built off `isrLegacyCachePrefix` (the `v2` namespace), NOT `isrCachePrefix`
|
|
155
|
-
* (now `v3`), so it targets the legacy hostless keys it is meant to sweep.
|
|
156
|
-
*
|
|
157
|
-
* Exists SOLELY for that rollout cleanup — current dispatch neither writes nor
|
|
158
|
-
* reads hostless keys. Transition-only. Do not use it on the hot path.
|
|
159
|
-
*/
|
|
87
|
+
/** Hostless `v2` rewrite-variant prefix for a pathname (purge only; see `isrLegacyCachePrefix`). */
|
|
160
88
|
function isrLegacyCacheVariantPrefix(projectId, deploymentId, pathname) {
|
|
161
89
|
return `${isrLegacyCachePrefix(projectId, deploymentId)}${normalizeIsrPath(pathname)}${ISR_VARIANT_SEPARATOR}`;
|
|
162
90
|
}
|
|
163
|
-
/**
|
|
164
|
-
* Hash an original-URL value into a fixed-length, opaque variant token.
|
|
165
|
-
* Used as both the KV-key suffix and the `__void_orig` query value, so the
|
|
166
|
-
* writer and purger paths see byte-identical tokens for the same URL.
|
|
167
|
-
*
|
|
168
|
-
* Properties:
|
|
169
|
-
* - Sync (Workers `crypto.subtle.digest` is async; we don't want to push
|
|
170
|
-
* async through every read/write path for this).
|
|
171
|
-
* - 16 hex chars (64 bits) — bounds the KV key length regardless of input.
|
|
172
|
-
* - Non-recoverable: KV is operator-readable, so the token must not reveal
|
|
173
|
-
* original URLs (including sensitive query strings like `?token=…`).
|
|
174
|
-
* - Not cryptographic: collision resistance is ~2^32 (birthday). Fine for
|
|
175
|
-
* the per-deployment, per-pathname variant set we ever store in one slot.
|
|
176
|
-
*
|
|
177
|
-
* Implementation: FNV-1a 64-bit over UTF-8 bytes. BigInt arithmetic for the
|
|
178
|
-
* multiply (no native u64 in JS), but inputs are short (URLs, sub-1KB) and
|
|
179
|
-
* this only runs once per ISR-touched request.
|
|
180
|
-
*/
|
|
181
91
|
const FNV_OFFSET_BASIS = 14695981039346656037n;
|
|
182
92
|
const FNV_PRIME = 1099511628211n;
|
|
183
93
|
const FNV_MASK = 18446744073709551615n;
|
|
94
|
+
/**
|
|
95
|
+
* Hash an original URL into a 16-hex-char variant token (FNV-1a 64-bit), used
|
|
96
|
+
* as both the KV-key suffix and the `__void_orig` edge-cache parameter. It is
|
|
97
|
+
* sync so it fits every read/write path, and opaque so KV never exposes
|
|
98
|
+
* original URLs or their query strings. Not cryptographic.
|
|
99
|
+
*/
|
|
184
100
|
function hashOriginalUrl(originalUrl) {
|
|
185
101
|
const bytes = new TextEncoder().encode(originalUrl);
|
|
186
102
|
let h = FNV_OFFSET_BASIS;
|
|
@@ -191,13 +107,9 @@ function hashOriginalUrl(originalUrl) {
|
|
|
191
107
|
return h.toString(16).padStart(16, "0");
|
|
192
108
|
}
|
|
193
109
|
/**
|
|
194
|
-
* Split
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* The token is opaque (a hash); the original URL it was derived from is not
|
|
199
|
-
* recoverable. Callers that need to reproduce the matching edge-cache URL
|
|
200
|
-
* pass the token through `isrEdgeCacheUrlForToken`.
|
|
110
|
+
* Split an ISR KV key into its normalized path and its `#rw=` variant token
|
|
111
|
+
* (`null` for a direct hit). Pass the token to `isrEdgeCacheUrlForToken` to
|
|
112
|
+
* rebuild the matching edge-cache URL.
|
|
201
113
|
*/
|
|
202
114
|
function splitIsrCacheKey(fullKey) {
|
|
203
115
|
const sepIdx = fullKey.indexOf(ISR_VARIANT_SEPARATOR);
|
|
@@ -211,24 +123,14 @@ function splitIsrCacheKey(fullKey) {
|
|
|
211
123
|
};
|
|
212
124
|
}
|
|
213
125
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* produced by an older deploy. The `json` variant separates pages-protocol
|
|
218
|
-
* JSON responses from HTML for the same path+deployment.
|
|
219
|
-
*
|
|
220
|
-
* This MUST match the URL dispatch writes. Dispatch builds the URL from
|
|
221
|
-
* `new URL(request.url).hostname`; for standard-port HTTPS traffic (all CF
|
|
222
|
-
* edge traffic) that yields the same origin as the one produced here.
|
|
126
|
+
* Edge-cache (`caches.default`) URL for an ISR page. It carries the deployment
|
|
127
|
+
* id so a new deploy never reads older HTML, and the `json` variant keeps
|
|
128
|
+
* Pages-protocol JSON apart from HTML for the same path.
|
|
223
129
|
*/
|
|
224
130
|
function isrEdgeCacheUrl(hostname, pathname, deploymentId, variant, originalUrl) {
|
|
225
131
|
return buildIsrEdgeCacheUrl(hostname, pathname, deploymentId, variant, originalUrl ? hashOriginalUrl(originalUrl) : null);
|
|
226
132
|
}
|
|
227
|
-
/**
|
|
228
|
-
* Like `isrEdgeCacheUrl`, but takes a pre-hashed variant token (from
|
|
229
|
-
* `splitIsrCacheKey`). Used by the purger, which only knows the hashed
|
|
230
|
-
* token: the original URL is not recoverable from a KV key.
|
|
231
|
-
*/
|
|
133
|
+
/** Like `isrEdgeCacheUrl`, but takes a variant token from `splitIsrCacheKey`. */
|
|
232
134
|
function isrEdgeCacheUrlForToken(hostname, pathname, deploymentId, variant, variantToken) {
|
|
233
135
|
return buildIsrEdgeCacheUrl(hostname, pathname, deploymentId, variant, variantToken ?? null);
|
|
234
136
|
}
|
package/dist/runtime.d.mts
CHANGED
|
@@ -4,36 +4,13 @@ import { canonicalIsrPathname, denormalizeIsrPath, normalizeIsrPath } from "./in
|
|
|
4
4
|
import { matchSegmentPattern } from "@void/edge";
|
|
5
5
|
//#region src/runtime.d.ts
|
|
6
6
|
/**
|
|
7
|
-
* Single-tenant ISR
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* caching ladder INTO the user worker, collapsed from multi-tenant to a single
|
|
13
|
-
* deployment:
|
|
14
|
-
*
|
|
15
|
-
* - No `projectId`, no cross-tenant metering.
|
|
16
|
-
* - No `#rw=` rewrite variant, no query-string variant fanout, no variant cap.
|
|
17
|
-
* - The deployment id is baked at build time as a compile-time literal and
|
|
18
|
-
* surfaced on `env.__VOID_ISR_DEPLOYMENT_ID`. The KV helpers read
|
|
19
|
-
* it from `env` (matching the `revalidate()` purge path in `runtime/isr.ts`),
|
|
20
|
-
* while `handleIsr` uses the `deploymentId` option for the edge-cache keys —
|
|
21
|
-
* the two are the same value.
|
|
22
|
-
*
|
|
23
|
-
* Fail-open contract: every `env.ISR_CACHE` (KV) and `caches.default` operation
|
|
24
|
-
* is wrapped so a misconfigured or erroring cache degrades to a live render,
|
|
25
|
-
* never a 500. Read/hit-path failures bail (caller renders live); write-path
|
|
26
|
-
* failures are swallowed.
|
|
27
|
-
*
|
|
28
|
-
* Worker-only: no `lib.dom` types — globals come from `@cloudflare/workers-types`.
|
|
29
|
-
*
|
|
30
|
-
* Ported from the hosted dispatch runtime and the shared `@void/isr` contract.
|
|
31
|
-
*/
|
|
32
|
-
/**
|
|
33
|
-
* Minimal env surface this module needs. Both fields are optional so a worker
|
|
34
|
-
* with no ISR binding (or a caller that only knows the deployment id) still
|
|
35
|
-
* typechecks and fails open at runtime.
|
|
7
|
+
* Single-tenant ISR cache for a direct Cloudflare Worker (Void apps and
|
|
8
|
+
* framework deploy wrappers). Every `ISR_CACHE` (KV) and `caches.default`
|
|
9
|
+
* operation fails open: read errors render live, write errors are ignored.
|
|
10
|
+
* KV helpers read the deployment id from `env.__VOID_ISR_DEPLOYMENT_ID`;
|
|
11
|
+
* edge-cache keys use the `deploymentId` option. Both hold the same value.
|
|
36
12
|
*/
|
|
13
|
+
/** Env fields ISR reads. Both are optional: without them ISR fails open. */
|
|
37
14
|
export interface IsrCacheEnv {
|
|
38
15
|
ISR_CACHE?: KVNamespace;
|
|
39
16
|
__VOID_ISR_DEPLOYMENT_ID?: string;
|
|
@@ -54,19 +31,15 @@ export declare const MAX_ISR_VARIANTS_PER_PATH = 200;
|
|
|
54
31
|
/** Prefix for the current direct deployment's host-scoped cache entries. */
|
|
55
32
|
export declare function isrPrefix(deploymentId: string): string;
|
|
56
33
|
/**
|
|
57
|
-
* KV
|
|
58
|
-
*
|
|
59
|
-
* `wrangler deploy` mints a new deployment id → cold cache), and one host's
|
|
60
|
-
* cached content never bleeds into another host served by the same worker
|
|
61
|
-
* (custom domain + workers.dev, apex + www, etc.).
|
|
34
|
+
* KV key for an ISR-cached page, scoped by deployment (a new deploy starts with
|
|
35
|
+
* a cold cache) and by host (custom domain, workers.dev, apex, and www stay apart).
|
|
62
36
|
*/
|
|
63
37
|
export declare function isrKey(deploymentId: string, host: string, pathname: string, variantUrl?: string | null): string;
|
|
64
38
|
export declare function isrVariantPrefix(deploymentId: string, host: string, pathname: string): string;
|
|
65
39
|
/**
|
|
66
|
-
* Host-registry key. Every host that
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* Scoped by deployment id so registrations from an old deploy do not leak.
|
|
40
|
+
* Host-registry key. Every host that writes cached content registers here, so a
|
|
41
|
+
* purge without a request host (cron or queue `revalidate({ paths })`) can find
|
|
42
|
+
* the hosts to purge.
|
|
70
43
|
*/
|
|
71
44
|
export declare function hostRegistryKey(deploymentId: string, host: string): string;
|
|
72
45
|
/**
|
|
@@ -80,68 +53,43 @@ export declare function htmlEdgeKey(host: string, pathname: string, id: string,
|
|
|
80
53
|
* the `&__void=json` marker.
|
|
81
54
|
*/
|
|
82
55
|
export declare function jsonEdgeKey(host: string, pathname: string, id: string, variantUrl?: string | null): string;
|
|
83
|
-
/**
|
|
84
|
-
* Read an ISR cache entry from KV. Returns null on cache miss, malformed
|
|
85
|
-
* metadata, or ANY KV error (fail-open → caller renders live). The KV key is
|
|
86
|
-
* deployment-scoped via `env.__VOID_ISR_DEPLOYMENT_ID` (see module header) and
|
|
87
|
-
* host-scoped via `host` so one host never reads another host's cached bytes.
|
|
88
|
-
*/
|
|
56
|
+
/** Read an ISR entry from KV. Returns null on a miss, a malformed entry, or any KV error. */
|
|
89
57
|
export declare function readIsrCache(env: IsrCacheEnv, host: string, pathname: string, variantUrl?: string | null): Promise<IsrCacheEntry | null>;
|
|
90
58
|
/**
|
|
91
|
-
* Write an ISR
|
|
92
|
-
*
|
|
93
|
-
* response (after kicking us off via `ctx.waitUntil`) cannot race the body
|
|
94
|
-
* away. Any KV error is swallowed (fail-open on the write path).
|
|
59
|
+
* Write an ISR entry to KV. The response is cloned before any `await`, so the
|
|
60
|
+
* caller can stream the original body to the client. KV errors are ignored.
|
|
95
61
|
*/
|
|
96
62
|
export declare function writeIsrCache(env: IsrCacheEnv, host: string, pathname: string, response: Response, ttl: number, variantUrl?: string | null): Promise<void>;
|
|
97
63
|
/**
|
|
98
|
-
* List every host
|
|
99
|
-
* the
|
|
100
|
-
* (cron / queue). Fail-open: a registry read error yields the hosts collected so
|
|
101
|
-
* far (or none) rather than throwing — degraded purge coverage, never a 500.
|
|
64
|
+
* List every host registered for a deployment. A registry read error returns
|
|
65
|
+
* the hosts collected so far instead of throwing.
|
|
102
66
|
*/
|
|
103
67
|
export declare function listIsrHosts(kv: KVNamespace, deploymentId: string): Promise<Array<string>>;
|
|
104
|
-
/**
|
|
105
|
-
* Delete an ISR cache entry from KV. Used when a worker permanently opts out
|
|
106
|
-
* via `x-revalidate: 0`. Any KV error is swallowed (fail-open).
|
|
107
|
-
*/
|
|
68
|
+
/** Delete an ISR entry from KV (for example after `x-revalidate: 0`). KV errors are ignored. */
|
|
108
69
|
export declare function deleteIsrCache(env: IsrCacheEnv, host: string, pathname: string, variantUrl?: string | null): Promise<void>;
|
|
109
70
|
export declare function clientRevalidateResponse(response: Response): Response;
|
|
110
71
|
export declare function isEventStreamResponse(response: Response): boolean;
|
|
111
72
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* (fail-open) so this is always safe to fire inside `ctx.waitUntil`.
|
|
73
|
+
* Fill the JSON edge cache from a response's page data (the `x-void-page-data`
|
|
74
|
+
* header or the `__VOID_PAGE_DATA__` script). Does nothing without page data;
|
|
75
|
+
* cache errors are ignored.
|
|
116
76
|
*/
|
|
117
77
|
export declare function populateJsonEdgeCache(cache: Cache | undefined, cacheKey: string, response: Response, ttl: number, state: "HIT" | "MISS"): Promise<void>;
|
|
118
|
-
/**
|
|
119
|
-
* Build a new Request with the `X-VoidPages` header removed. Headers on an
|
|
120
|
-
* existing Request are immutable, so we must clone into a fresh Request. ISR
|
|
121
|
-
* only runs for GET, so there is no body to preserve.
|
|
122
|
-
*/
|
|
78
|
+
/** Copy a request without its `X-VoidPages` header. */
|
|
123
79
|
export declare function cloneWithoutVoidPages(request: Request): Request;
|
|
124
80
|
/** Cache only framework document HTML; leave client data requests and other response types live. */
|
|
125
81
|
export declare function handleFrameworkIsr(request: Request, env: IsrCacheEnv, ctx: ExecutionContext, options: HandleIsrOptions, render: RenderFn): Promise<Response | null>;
|
|
126
82
|
/**
|
|
127
|
-
* Run the ISR
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* cache op failed open, or a cache miss for a URL carrying query params
|
|
132
|
-
* outside the allowlist).
|
|
83
|
+
* Run the ISR cache ladder for a request. Returns the response to send, or
|
|
84
|
+
* `null` when the caller should render live: the request is not eligible
|
|
85
|
+
* (not GET, `/api`, Range, `shouldBypassIsr`, or no TTL), a cache read failed,
|
|
86
|
+
* or a miss carries query params outside the allowlist.
|
|
133
87
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* miss renders live JSON and returns it — the JSON path never writes the edge
|
|
140
|
-
* cache. The JSON edge variant is warmed only as a side-effect of the HTML
|
|
141
|
-
* render path (cold colo re-renders JSON).
|
|
142
|
-
* - HTML path: L1 edge match → L2 KV. Hit serves `isrCacheResponse`
|
|
143
|
-
* (stale hits also queue a background revalidation). Miss renders live and,
|
|
144
|
-
* when cacheable, writes KV + JSON edge + HTML edge in the background.
|
|
88
|
+
* Pages-protocol (`X-VoidPages`) requests read only the JSON edge cache, and a
|
|
89
|
+
* miss renders live without caching; HTML renders fill the JSON variant. HTML
|
|
90
|
+
* requests read the edge cache, then KV. A stale hit is served while it
|
|
91
|
+
* revalidates in the background, and a cacheable miss fills KV and the edge
|
|
92
|
+
* cache in the background.
|
|
145
93
|
*/
|
|
146
94
|
export declare function handleIsr(request: Request, env: IsrCacheEnv, ctx: ExecutionContext, options: HandleIsrOptions, render: RenderFn): Promise<Response | null>;
|
|
147
95
|
//#endregion
|
package/dist/runtime.mjs
CHANGED
|
@@ -10,11 +10,8 @@ function isrPrefix(deploymentId) {
|
|
|
10
10
|
return isrCachePrefix(DIRECT_ISR_PROJECT_ID, deploymentId);
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* KV
|
|
14
|
-
*
|
|
15
|
-
* `wrangler deploy` mints a new deployment id → cold cache), and one host's
|
|
16
|
-
* cached content never bleeds into another host served by the same worker
|
|
17
|
-
* (custom domain + workers.dev, apex + www, etc.).
|
|
13
|
+
* KV key for an ISR-cached page, scoped by deployment (a new deploy starts with
|
|
14
|
+
* a cold cache) and by host (custom domain, workers.dev, apex, and www stay apart).
|
|
18
15
|
*/
|
|
19
16
|
function isrKey(deploymentId, host, pathname, variantUrl) {
|
|
20
17
|
return isrCacheKey(DIRECT_ISR_PROJECT_ID, deploymentId, host, pathname, variantUrl);
|
|
@@ -23,10 +20,9 @@ function isrVariantPrefix(deploymentId, host, pathname) {
|
|
|
23
20
|
return isrCacheVariantPrefix(DIRECT_ISR_PROJECT_ID, deploymentId, host, pathname);
|
|
24
21
|
}
|
|
25
22
|
/**
|
|
26
|
-
* Host-registry key. Every host that
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* Scoped by deployment id so registrations from an old deploy do not leak.
|
|
23
|
+
* Host-registry key. Every host that writes cached content registers here, so a
|
|
24
|
+
* purge without a request host (cron or queue `revalidate({ paths })`) can find
|
|
25
|
+
* the hosts to purge.
|
|
30
26
|
*/
|
|
31
27
|
function hostRegistryKey(deploymentId, host) {
|
|
32
28
|
return `void-isr-hosts/${deploymentId}/${host}`;
|
|
@@ -46,12 +42,7 @@ function htmlEdgeKey(host, pathname, id, variantUrl) {
|
|
|
46
42
|
function jsonEdgeKey(host, pathname, id, variantUrl) {
|
|
47
43
|
return isrEdgeCacheUrl(host, pathname, id, "json", variantUrl);
|
|
48
44
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Read an ISR cache entry from KV. Returns null on cache miss, malformed
|
|
51
|
-
* metadata, or ANY KV error (fail-open → caller renders live). The KV key is
|
|
52
|
-
* deployment-scoped via `env.__VOID_ISR_DEPLOYMENT_ID` (see module header) and
|
|
53
|
-
* host-scoped via `host` so one host never reads another host's cached bytes.
|
|
54
|
-
*/
|
|
45
|
+
/** Read an ISR entry from KV. Returns null on a miss, a malformed entry, or any KV error. */
|
|
55
46
|
async function readIsrCache(env, host, pathname, variantUrl) {
|
|
56
47
|
const kv = env.ISR_CACHE;
|
|
57
48
|
if (!kv) return null;
|
|
@@ -68,10 +59,8 @@ async function readIsrCache(env, host, pathname, variantUrl) {
|
|
|
68
59
|
return decodeIsrCacheEntry(value, metadata);
|
|
69
60
|
}
|
|
70
61
|
/**
|
|
71
|
-
* Write an ISR
|
|
72
|
-
*
|
|
73
|
-
* response (after kicking us off via `ctx.waitUntil`) cannot race the body
|
|
74
|
-
* away. Any KV error is swallowed (fail-open on the write path).
|
|
62
|
+
* Write an ISR entry to KV. The response is cloned before any `await`, so the
|
|
63
|
+
* caller can stream the original body to the client. KV errors are ignored.
|
|
75
64
|
*/
|
|
76
65
|
async function writeIsrCache(env, host, pathname, response, ttl, variantUrl) {
|
|
77
66
|
const kv = env.ISR_CACHE;
|
|
@@ -104,11 +93,7 @@ async function isVariantWriteAllowed(kv, deploymentId, host, pathname, key) {
|
|
|
104
93
|
});
|
|
105
94
|
return listed.keys.length < 200 || listed.keys.some((entry) => entry.name === key);
|
|
106
95
|
}
|
|
107
|
-
/**
|
|
108
|
-
* In-isolate guard: `${id}/${host}` values whose host-registry key this isolate
|
|
109
|
-
* has already written (or attempted). Bounds host registration to ~one KV write
|
|
110
|
-
* per (isolate, deployment, host) instead of one per page write.
|
|
111
|
-
*/
|
|
96
|
+
/** `${id}/${host}` pairs this isolate has registered: one registry write per pair, not per page. */
|
|
112
97
|
const registeredHosts = /* @__PURE__ */ new Set();
|
|
113
98
|
async function registerIsrHost(kv, deploymentId, host) {
|
|
114
99
|
const guardKey = `${deploymentId}/${host}`;
|
|
@@ -121,10 +106,8 @@ async function registerIsrHost(kv, deploymentId, host) {
|
|
|
121
106
|
}
|
|
122
107
|
}
|
|
123
108
|
/**
|
|
124
|
-
* List every host
|
|
125
|
-
* the
|
|
126
|
-
* (cron / queue). Fail-open: a registry read error yields the hosts collected so
|
|
127
|
-
* far (or none) rather than throwing — degraded purge coverage, never a 500.
|
|
109
|
+
* List every host registered for a deployment. A registry read error returns
|
|
110
|
+
* the hosts collected so far instead of throwing.
|
|
128
111
|
*/
|
|
129
112
|
async function listIsrHosts(kv, deploymentId) {
|
|
130
113
|
const prefix = `void-isr-hosts/${deploymentId}/`;
|
|
@@ -142,10 +125,7 @@ async function listIsrHosts(kv, deploymentId) {
|
|
|
142
125
|
} catch {}
|
|
143
126
|
return hosts;
|
|
144
127
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Delete an ISR cache entry from KV. Used when a worker permanently opts out
|
|
147
|
-
* via `x-revalidate: 0`. Any KV error is swallowed (fail-open).
|
|
148
|
-
*/
|
|
128
|
+
/** Delete an ISR entry from KV (for example after `x-revalidate: 0`). KV errors are ignored. */
|
|
149
129
|
async function deleteIsrCache(env, host, pathname, variantUrl) {
|
|
150
130
|
const kv = env.ISR_CACHE;
|
|
151
131
|
if (!kv) return;
|
|
@@ -199,10 +179,9 @@ function jsonCacheResponse(pageData, ttl, state) {
|
|
|
199
179
|
return response;
|
|
200
180
|
}
|
|
201
181
|
/**
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* (fail-open) so this is always safe to fire inside `ctx.waitUntil`.
|
|
182
|
+
* Fill the JSON edge cache from a response's page data (the `x-void-page-data`
|
|
183
|
+
* header or the `__VOID_PAGE_DATA__` script). Does nothing without page data;
|
|
184
|
+
* cache errors are ignored.
|
|
206
185
|
*/
|
|
207
186
|
async function populateJsonEdgeCache(cache, cacheKey, response, ttl, state) {
|
|
208
187
|
if (!cache) return;
|
|
@@ -213,19 +192,7 @@ async function populateJsonEdgeCache(cache, cacheKey, response, ttl, state) {
|
|
|
213
192
|
await cache.put(cacheKey, jsonCacheResponse(pageData, ttl, state));
|
|
214
193
|
} catch {}
|
|
215
194
|
}
|
|
216
|
-
/**
|
|
217
|
-
* The per-colo edge cache (`caches.default`), or `undefined` when the Cache API
|
|
218
|
-
* is absent/degraded. Accessed through a cast because this package has both
|
|
219
|
-
* `@types/node` and `@cloudflare/workers-types` in scope, and Node's ambient
|
|
220
|
-
* `CacheStorage` (which lacks `.default`) otherwise shadows the Workers one on
|
|
221
|
-
* the `caches` global.
|
|
222
|
-
*
|
|
223
|
-
* Non-throwing by contract: dereferencing `caches.default` can throw if the
|
|
224
|
-
* Cache API is missing (e.g. a non-worker runtime) or degraded. We swallow that
|
|
225
|
-
* here and return `undefined` so `handleIsr` never rejects before its fail-open
|
|
226
|
-
* guards run — the serving invariant is a degraded cache must render live, never
|
|
227
|
-
* 500. Every call site guards `undefined` (via `if (!cache)` or `cache?.`).
|
|
228
|
-
*/
|
|
195
|
+
/** `caches.default`, or `undefined` when reading it throws (for example, with no Cache API). */
|
|
229
196
|
function edgeCache() {
|
|
230
197
|
try {
|
|
231
198
|
return caches.default;
|
|
@@ -250,26 +217,20 @@ async function deleteSharedIsrVariants(env, cache, host, pathname, keys, variant
|
|
|
250
217
|
await safeCacheDelete(cache, keys.htmlKey);
|
|
251
218
|
await safeCacheDelete(cache, keys.jsonKey);
|
|
252
219
|
}
|
|
253
|
-
/**
|
|
254
|
-
* Build a new Request with the `X-VoidPages` header removed. Headers on an
|
|
255
|
-
* existing Request are immutable, so we must clone into a fresh Request. ISR
|
|
256
|
-
* only runs for GET, so there is no body to preserve.
|
|
257
|
-
*/
|
|
220
|
+
/** Copy a request without its `X-VoidPages` header. */
|
|
258
221
|
function cloneWithoutVoidPages(request) {
|
|
259
222
|
const headers = new Headers(request.headers);
|
|
260
223
|
headers.delete("X-VoidPages");
|
|
261
224
|
return new Request(request, { headers });
|
|
262
225
|
}
|
|
263
226
|
/**
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
* stale, don't retry. An explicit cache opt-out (including `ttl === 0`)
|
|
268
|
-
* deletes the KV entry and both edge variants.
|
|
227
|
+
* Re-render a stale page in the background and refresh KV and the edge cache.
|
|
228
|
+
* Errors are logged and the stale entry keeps serving. An explicit cache
|
|
229
|
+
* opt-out (including `ttl === 0`) deletes the KV entry and both edge variants.
|
|
269
230
|
*/
|
|
270
231
|
async function revalidateInBackground(env, request, host, pathname, defaultTtl, keys, render, variantUrl, htmlOnly, staleSinceMs) {
|
|
271
232
|
try {
|
|
272
|
-
const response = await render(
|
|
233
|
+
const response = await render(request);
|
|
273
234
|
const ttl = resolveRevalidateTtl(response, defaultTtl);
|
|
274
235
|
const cache = edgeCache();
|
|
275
236
|
if (response.status === 200 && isEventStreamResponse(response)) {
|
|
@@ -282,7 +243,7 @@ async function revalidateInBackground(env, request, host, pathname, defaultTtl,
|
|
|
282
243
|
status: response.status,
|
|
283
244
|
headers: sharedHtmlCacheHeaders(response, ttl, "HIT")
|
|
284
245
|
}));
|
|
285
|
-
await populateJsonEdgeCache(cache, keys.jsonKey, response, ttl, "HIT");
|
|
246
|
+
if (!htmlOnly) await populateJsonEdgeCache(cache, keys.jsonKey, response, ttl, "HIT");
|
|
286
247
|
} else if (isUncacheableRender(response, ttl, htmlOnly) || (response.status === 404 || response.status === 410) && Date.now() - staleSinceMs > ISR_STALE_EVICT_GRACE_MS) await deleteSharedIsrVariants(env, cache, host, pathname, keys, variantUrl);
|
|
287
248
|
} catch (err) {
|
|
288
249
|
console.error("ISR revalidation failed:", err);
|
|
@@ -294,24 +255,16 @@ async function handleFrameworkIsr(request, env, ctx, options, render) {
|
|
|
294
255
|
return handleIsrInternal(request, env, ctx, options, render, true);
|
|
295
256
|
}
|
|
296
257
|
/**
|
|
297
|
-
* Run the ISR
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
* cache op failed open, or a cache miss for a URL carrying query params
|
|
302
|
-
* outside the allowlist).
|
|
258
|
+
* Run the ISR cache ladder for a request. Returns the response to send, or
|
|
259
|
+
* `null` when the caller should render live: the request is not eligible
|
|
260
|
+
* (not GET, `/api`, Range, `shouldBypassIsr`, or no TTL), a cache read failed,
|
|
261
|
+
* or a miss carries query params outside the allowlist.
|
|
303
262
|
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
* miss renders live JSON and returns it — the JSON path never writes the edge
|
|
310
|
-
* cache. The JSON edge variant is warmed only as a side-effect of the HTML
|
|
311
|
-
* render path (cold colo re-renders JSON).
|
|
312
|
-
* - HTML path: L1 edge match → L2 KV. Hit serves `isrCacheResponse`
|
|
313
|
-
* (stale hits also queue a background revalidation). Miss renders live and,
|
|
314
|
-
* when cacheable, writes KV + JSON edge + HTML edge in the background.
|
|
263
|
+
* Pages-protocol (`X-VoidPages`) requests read only the JSON edge cache, and a
|
|
264
|
+
* miss renders live without caching; HTML renders fill the JSON variant. HTML
|
|
265
|
+
* requests read the edge cache, then KV. A stale hit is served while it
|
|
266
|
+
* revalidates in the background, and a cacheable miss fills KV and the edge
|
|
267
|
+
* cache in the background.
|
|
315
268
|
*/
|
|
316
269
|
async function handleIsr(request, env, ctx, options, render) {
|
|
317
270
|
return handleIsrInternal(request, env, ctx, options, render, false);
|
|
@@ -381,7 +334,7 @@ async function handleIsrInternal(request, env, ctx, options, render, htmlOnly) {
|
|
|
381
334
|
const ttl = resolveRevalidateTtl(response, isrTtl);
|
|
382
335
|
if (isStorableRender(response, ttl, htmlOnly)) {
|
|
383
336
|
ctx.waitUntil(writeIsrCache(env, host, pathname, response, ttl, variantUrl));
|
|
384
|
-
ctx.waitUntil(populateJsonEdgeCache(cache, jsonKey, response, ttl, "HIT"));
|
|
337
|
+
if (!htmlOnly) ctx.waitUntil(populateJsonEdgeCache(cache, jsonKey, response, ttl, "HIT"));
|
|
385
338
|
const cacheResponse = new Response(response.clone().body, {
|
|
386
339
|
status: response.status,
|
|
387
340
|
headers: sharedHtmlCacheHeaders(response, ttl, "HIT")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@void/isr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Incremental revalidation and cache helpers for Void runtimes.",
|
|
5
5
|
"homepage": "https://void.cloud",
|
|
6
6
|
"bugs": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@cloudflare/workers-types": "^5.20261009.1",
|
|
38
|
-
"@void/edge": "0.
|
|
38
|
+
"@void/edge": "0.28.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"typescript": "7.0.2",
|