@void/isr 0.21.9 → 0.23.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 +1 -1
- package/dist/core.d.mts +1 -1
- package/dist/core.mjs +9 -5
- package/dist/runtime.mjs +6 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ The package has three entry points:
|
|
|
20
20
|
- `@void/isr/core` owns target-neutral codecs, validation, TTL selection, request bypass rules, response construction, header sanitization, Void Pages data extraction, and query-variant normalization.
|
|
21
21
|
- `@void/isr/runtime` implements the fail-open, single-tenant Cloudflare KV and Cache API ladder used by direct Void deployments, including bounded variants from `routing.revalidateQueryAllowlist`.
|
|
22
22
|
|
|
23
|
-
The direct runtime applies ISR only to public page requests. API paths,
|
|
23
|
+
The direct runtime applies ISR only to public page requests. API paths, requests carrying `Cookie`, `Authorization`, or `Cf-Access-Jwt-Assertion` credentials, Range requests, and EventSource requests render live. A rendered response is shared only when it is a complete `200` response without `Cache-Control: private`, `no-store`, or `no-cache`, and without `Set-Cookie`. When a background revalidation returns one of those opt-outs, the runtime removes the old HTML, Pages JSON, and KV variants so previously public content cannot remain reachable.
|
|
24
24
|
|
|
25
25
|
| Symbol | Description |
|
|
26
26
|
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
package/dist/core.d.mts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* proven safe to share. Readers reject entries from earlier policies instead
|
|
5
5
|
* of replaying opaque bodies whose private headers were stripped at write time.
|
|
6
6
|
*/
|
|
7
|
-
export declare const SHARED_CACHE_POLICY_VERSION:
|
|
7
|
+
export declare const SHARED_CACHE_POLICY_VERSION: 3;
|
|
8
8
|
export declare const SHARED_CACHE_POLICY_HEADER = "x-void-shared-cache-policy";
|
|
9
9
|
/** Stamp an internal edge-cache representation with the policy that admitted it. */
|
|
10
10
|
export declare function markSharedCachePolicy(headers: Headers): void;
|
package/dist/core.mjs
CHANGED
|
@@ -5,15 +5,15 @@ import { matchLiteralPattern, matchSegmentPattern } from "@void/edge";
|
|
|
5
5
|
* proven safe to share. Readers reject entries from earlier policies instead
|
|
6
6
|
* of replaying opaque bodies whose private headers were stripped at write time.
|
|
7
7
|
*/
|
|
8
|
-
const SHARED_CACHE_POLICY_VERSION =
|
|
8
|
+
const SHARED_CACHE_POLICY_VERSION = 3;
|
|
9
9
|
const SHARED_CACHE_POLICY_HEADER = "x-void-shared-cache-policy";
|
|
10
10
|
/** Stamp an internal edge-cache representation with the policy that admitted it. */
|
|
11
11
|
function markSharedCachePolicy(headers) {
|
|
12
|
-
headers.set(SHARED_CACHE_POLICY_HEADER, String(
|
|
12
|
+
headers.set(SHARED_CACHE_POLICY_HEADER, String(3));
|
|
13
13
|
}
|
|
14
14
|
/** Reject opaque edge entries admitted before the current privacy policy. */
|
|
15
15
|
function hasCurrentSharedCachePolicy(response) {
|
|
16
|
-
return response.headers.get(SHARED_CACHE_POLICY_HEADER) === String(
|
|
16
|
+
return response.headers.get(SHARED_CACHE_POLICY_HEADER) === String(3);
|
|
17
17
|
}
|
|
18
18
|
const PAGE_DATA_SCRIPT_RE = /<script\b[^>]*\bid=(["'])__VOID_PAGE_DATA__\1[^>]*>([\s\S]*?)<\/script>/i;
|
|
19
19
|
const SKIP_HEADERS = /* @__PURE__ */ new Set([
|
|
@@ -89,7 +89,7 @@ function decodeIsrCacheEntry(value, metadata) {
|
|
|
89
89
|
const unpacked = unpackIsrValue(value);
|
|
90
90
|
if (!unpacked) return null;
|
|
91
91
|
if (!Number.isInteger(metadata.s) || metadata.s < 200 || metadata.s > 599) return null;
|
|
92
|
-
if (!Number.isFinite(metadata.c) || metadata.c <= 0 || !Number.isFinite(metadata.t) || metadata.t <= 0 || metadata.p !==
|
|
92
|
+
if (!Number.isFinite(metadata.c) || metadata.c <= 0 || !Number.isFinite(metadata.t) || metadata.t <= 0 || metadata.p !== 3) return null;
|
|
93
93
|
try {
|
|
94
94
|
new Headers(unpacked.headers);
|
|
95
95
|
} catch {
|
|
@@ -137,7 +137,7 @@ function acceptsEventStream(request) {
|
|
|
137
137
|
}
|
|
138
138
|
/** Return whether credentials or EventSource semantics require an ISR bypass. */
|
|
139
139
|
function shouldBypassIsr(request) {
|
|
140
|
-
return request.headers.has("Cookie") || request.headers.has("Authorization") || acceptsEventStream(request);
|
|
140
|
+
return request.headers.has("Cookie") || request.headers.has("Authorization") || request.headers.has("Cf-Access-Jwt-Assertion") || acceptsEventStream(request);
|
|
141
141
|
}
|
|
142
142
|
/** Only document navigations may read a framework's shared HTML cache. */
|
|
143
143
|
function isFrameworkDocumentRequest(request) {
|
|
@@ -153,6 +153,10 @@ function isFrameworkDocumentRequest(request) {
|
|
|
153
153
|
}
|
|
154
154
|
/** Return whether a response explicitly forbids storage in a shared cache. */
|
|
155
155
|
function hasSharedCacheOptOut(response) {
|
|
156
|
+
if (response.headers.get("Vary")?.split(",").some((name) => {
|
|
157
|
+
const header = name.trim().toLowerCase();
|
|
158
|
+
return header !== "" && header !== "x-voidpages";
|
|
159
|
+
})) return true;
|
|
156
160
|
if (response.headers.has("Set-Cookie")) return true;
|
|
157
161
|
const cacheControl = response.headers.get("Cache-Control");
|
|
158
162
|
if (!cacheControl) return false;
|
package/dist/runtime.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { SHARED_CACHE_POLICY_HEADER, SHARED_CACHE_POLICY_VERSION, buildVariantOr
|
|
|
3
3
|
import { matchSegmentPattern } from "@void/edge";
|
|
4
4
|
//#region src/runtime.ts
|
|
5
5
|
const MAX_ISR_VARIANTS_PER_PATH = 200;
|
|
6
|
+
const ISR_STALE_EVICT_GRACE_MS = 864e5;
|
|
6
7
|
const DIRECT_ISR_PROJECT_ID = "__void_direct__";
|
|
7
8
|
/** Prefix for the current direct deployment's host-scoped cache entries. */
|
|
8
9
|
function isrPrefix(deploymentId) {
|
|
@@ -91,7 +92,7 @@ async function writeIsrCache(env, host, pathname, response, ttl, variantUrl) {
|
|
|
91
92
|
s: status,
|
|
92
93
|
c: Date.now(),
|
|
93
94
|
t: ttl,
|
|
94
|
-
p:
|
|
95
|
+
p: 3
|
|
95
96
|
} });
|
|
96
97
|
} catch {}
|
|
97
98
|
await registerIsrHost(kv, id, host);
|
|
@@ -242,12 +243,12 @@ function cloneWithoutVoidPages(request) {
|
|
|
242
243
|
* stale, don't retry. An explicit cache opt-out (including `ttl === 0`)
|
|
243
244
|
* deletes the KV entry and both edge variants.
|
|
244
245
|
*/
|
|
245
|
-
async function revalidateInBackground(env, request, host, pathname, defaultTtl, keys, render, variantUrl, htmlOnly) {
|
|
246
|
+
async function revalidateInBackground(env, request, host, pathname, defaultTtl, keys, render, variantUrl, htmlOnly, staleSinceMs) {
|
|
246
247
|
try {
|
|
247
248
|
const response = await render(cloneWithoutVoidPages(request));
|
|
248
249
|
const ttl = resolveRevalidateTtl(response, defaultTtl);
|
|
249
250
|
const cache = edgeCache();
|
|
250
|
-
if (isEventStreamResponse(response)) {
|
|
251
|
+
if (response.status === 200 && isEventStreamResponse(response)) {
|
|
251
252
|
if (ttl === 0 || hasSharedCacheOptOut(response)) await deleteSharedIsrVariants(env, cache, host, pathname, keys, variantUrl);
|
|
252
253
|
return;
|
|
253
254
|
}
|
|
@@ -264,7 +265,7 @@ async function revalidateInBackground(env, request, host, pathname, defaultTtl,
|
|
|
264
265
|
headers
|
|
265
266
|
}));
|
|
266
267
|
await populateJsonEdgeCache(cache, keys.jsonKey, response, ttl, "HIT");
|
|
267
|
-
} else if (ttl === 0 || hasSharedCacheOptOut(response) || htmlOnly && !isHtmlResponse(response)) await deleteSharedIsrVariants(env, cache, host, pathname, keys, variantUrl);
|
|
268
|
+
} else if (response.status === 200 && (ttl === 0 || hasSharedCacheOptOut(response) || htmlOnly && !isHtmlResponse(response)) || (response.status === 404 || response.status === 410) && Date.now() - staleSinceMs > ISR_STALE_EVICT_GRACE_MS) await deleteSharedIsrVariants(env, cache, host, pathname, keys, variantUrl);
|
|
268
269
|
} catch (err) {
|
|
269
270
|
console.error("ISR revalidation failed:", err);
|
|
270
271
|
}
|
|
@@ -347,7 +348,7 @@ async function handleIsrInternal(request, env, ctx, options, render, htmlOnly) {
|
|
|
347
348
|
if (stale) ctx.waitUntil(revalidateInBackground(env, request, host, pathname, isrTtl, {
|
|
348
349
|
htmlKey,
|
|
349
350
|
jsonKey
|
|
350
|
-
}, render, variantUrl, htmlOnly));
|
|
351
|
+
}, render, variantUrl, htmlOnly, cached.createdAt + cached.ttl * 1e3));
|
|
351
352
|
const cacheResponse = isrCacheResponse(cached, stale);
|
|
352
353
|
if (!stale) ctx.waitUntil(safeCachePut(cache, htmlKey, cacheResponse.clone()));
|
|
353
354
|
return clientRevalidateResponse(cacheResponse);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@void/isr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/voidzero-dev/void.git",
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@cloudflare/workers-types": "^4.20260702.1",
|
|
32
|
-
"@void/edge": "0.
|
|
32
|
+
"@void/edge": "0.23.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"typescript": "7.0.2",
|
|
36
|
-
"vite-plus": "0.
|
|
36
|
+
"vite-plus": "1.0.0"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "vp pack",
|