@void/isr 0.20.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/index.mjs ADDED
@@ -0,0 +1,265 @@
1
+ //#region src/index.ts
2
+ /**
3
+ * Shared ISR key/URL helpers used by both the API (purge) and the dispatch
4
+ * worker (read/write). These functions MUST produce byte-identical output:
5
+ * the API purges KV keys and edge URLs that dispatch writes and reads.
6
+ *
7
+ * Dispatch derives the deployment id for its keys via
8
+ * `routing.version ?? 'legacy'`, which allows any non-nullish value —
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.
11
+ */
12
+ const ISR_CACHE_KEY_VERSION = "v3";
13
+ /**
14
+ * Previous key version. `v3` keys are ALWAYS host-scoped
15
+ * (`<proj>/isr/v3/<D>/<host>/<path>`); `v2` keys are ALWAYS hostless
16
+ * (`<proj>/isr/v2/<D>/<path>`) — the `<host>` segment did not exist yet.
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`.
30
+ */
31
+ const ISR_LEGACY_CACHE_KEY_VERSION = "v2";
32
+ /**
33
+ * Build the normalized path segment for an ISR-cached page.
34
+ * Root path `/` maps to `__index__` to avoid trailing-slash ambiguity.
35
+ */
36
+ function normalizeIsrPath(pathname) {
37
+ let normalized = pathname;
38
+ if (normalized === "/") normalized = "__index__";
39
+ else {
40
+ normalized = normalized.startsWith("/") ? normalized.slice(1) : normalized;
41
+ if (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
42
+ }
43
+ return normalized;
44
+ }
45
+ /**
46
+ * Inverse of `normalizeIsrPath`: turn a normalized segment back into a
47
+ * pathname starting with `/`.
48
+ */
49
+ function denormalizeIsrPath(normalized) {
50
+ return normalized === "__index__" ? "/" : `/${normalized}`;
51
+ }
52
+ /**
53
+ * Canonicalize an inbound pathname to the form used inside edge cache URLs.
54
+ * Equivalent to `denormalizeIsrPath(normalizeIsrPath(pathname))` — strips any
55
+ * trailing slash except for root.
56
+ */
57
+ function canonicalIsrPathname(pathname) {
58
+ return denormalizeIsrPath(normalizeIsrPath(pathname));
59
+ }
60
+ /**
61
+ * Prefix shared by every ISR key for a project at the current version —
62
+ * `<projectId>/isr/v3/`. Used internally by `isrCachePrefix`.
63
+ */
64
+ function isrCacheVersionPrefix(projectId) {
65
+ return `${projectId}/isr/${ISR_CACHE_KEY_VERSION}/`;
66
+ }
67
+ /**
68
+ * Prefix shared by every LEGACY (hostless) ISR key for a project —
69
+ * `<projectId>/isr/v2/`. Used internally by `isrLegacyCachePrefix`.
70
+ */
71
+ function isrLegacyCacheVersionPrefix(projectId) {
72
+ return `${projectId}/isr/${ISR_LEGACY_CACHE_KEY_VERSION}/`;
73
+ }
74
+ /**
75
+ * Prefix for listing/purging ISR keys.
76
+ *
77
+ * `deploymentId === ''` must still produce the versioned prefix with an
78
+ * empty segment (`<projectId>/isr/v3//`) because dispatch uses
79
+ * `routing.version ?? 'legacy'`, which allows empty strings and writes
80
+ * keys at `<projectId>/isr/v3//<path>`. Only `undefined` selects the
81
+ * legacy scan-all prefix (`<projectId>/isr/`).
82
+ */
83
+ function isrCachePrefix(projectId, deploymentId) {
84
+ if (deploymentId !== void 0) return `${isrCacheVersionPrefix(projectId)}${deploymentId}/`;
85
+ return `${projectId}/isr/`;
86
+ }
87
+ /**
88
+ * Prefix for listing/purging LEGACY (pre-host-scoping) ISR keys — the `v2`
89
+ * namespace `<projectId>/isr/v2/<deploymentId>/`. Mirrors `isrCachePrefix`'s
90
+ * optional-`deploymentId` behavior exactly (empty string yields the versioned
91
+ * prefix with an empty segment; only `undefined` selects the scan-all
92
+ * `<projectId>/isr/` prefix).
93
+ *
94
+ * Exists SOLELY for the host-scoping rollout: the purger sweeps this namespace
95
+ * to reap orphaned `v2` KV keys and purge their edge URLs. Current dispatch
96
+ * neither reads nor writes `v2` keys. Transition-only — remove once `v2`
97
+ * entries have aged out.
98
+ */
99
+ function isrLegacyCachePrefix(projectId, deploymentId) {
100
+ if (deploymentId !== void 0) return `${isrLegacyCacheVersionPrefix(projectId)}${deploymentId}/`;
101
+ return `${projectId}/isr/`;
102
+ }
103
+ /**
104
+ * Separator that marks the rewrite-variant suffix on an ISR cache key or
105
+ * the corresponding edge-cache URL parameter. `#` cannot appear in
106
+ * `url.pathname` (fragments are stripped before the request reaches the
107
+ * server) and does not survive `normalizeIsrPath`, so it unambiguously
108
+ * identifies the variant suffix.
109
+ */
110
+ const ISR_VARIANT_SEPARATOR = "#rw=";
111
+ /**
112
+ * Build the cache key for an ISR-cached page.
113
+ * ISR is deployment-scoped so cached HTML cannot outlive the hashed assets
114
+ * referenced by the deployment that produced it, and host-scoped (`<host>`
115
+ * segment after the deployment id) so the same path served under different
116
+ * hostnames — e.g. a project slug and a custom domain, or two custom
117
+ * domains — never share a cache slot. `host` is the request hostname the
118
+ * dispatch worker saw (`new URL(request.url).hostname`).
119
+ *
120
+ * Host-scoping is a `v3` key format: `<proj>/isr/v3/<D>/<host>/<path>`. The
121
+ * version bump is load-bearing, not cosmetic — see `ISR_LEGACY_CACHE_KEY_VERSION`.
122
+ * Under a single version, this key would be byte-identical to a legacy hostless
123
+ * key for a page whose leading path segment looks like a hostname (e.g.
124
+ * `(host=example.com, /about)` vs the hostless page `/example.com/about`, both
125
+ * `<D>/example.com/about`). Dotted leading segments are legal, so the collision
126
+ * is reachable. `v3` keys are ALWAYS host-scoped and `v2` keys ALWAYS hostless,
127
+ * so the version segment alone disambiguates the two shapes.
128
+ *
129
+ * When `originalUrl` is provided, the request was served via a dispatch-side
130
+ * rewrite (`redirectRules`/`fallbackRules` with status 200). The original
131
+ * request URL is mixed into the key so a direct hit to `/en/docs` and a
132
+ * rewrite from `/docs` to `/en/docs` do not collide — user code can branch
133
+ * on `c.isRewritten()` / `c.originalUrl()`, and the cache must honor that.
134
+ * A missing `originalUrl` means "direct hit" and produces the legacy
135
+ * rewriteless key (backward-compatible with entries written before this
136
+ * feature landed).
137
+ */
138
+ function isrCacheKey(projectId, deploymentId, host, pathname, originalUrl) {
139
+ const base = `${isrCachePrefix(projectId, deploymentId)}${host}/${normalizeIsrPath(pathname)}`;
140
+ if (!originalUrl) return base;
141
+ return `${base}${ISR_VARIANT_SEPARATOR}${hashOriginalUrl(originalUrl)}`;
142
+ }
143
+ /**
144
+ * Prefix used to list/purge every variant of a given pathname under a
145
+ * deployment — both the direct-hit key and any rewrite-variant keys.
146
+ */
147
+ function isrCacheVariantPrefix(projectId, deploymentId, host, pathname) {
148
+ return `${isrCachePrefix(projectId, deploymentId)}${host}/${normalizeIsrPath(pathname)}${ISR_VARIANT_SEPARATOR}`;
149
+ }
150
+ /**
151
+ * Build the exact LEGACY (pre-host-scoping) direct-hit key for a pathname under
152
+ * a deployment: `<projectId>/isr/v2/<deploymentId>/<normalizedPath>` — the
153
+ * hostless counterpart of `isrCacheKey` with no `<host>/` segment and no
154
+ * rewrite-variant suffix. Used by the purger to reap the orphaned `v2`
155
+ * direct-hit KV key for a requested path.
156
+ *
157
+ * Exists SOLELY for the host-scoping rollout — current dispatch neither writes
158
+ * nor reads hostless keys. Transition-only. Do not use it on the hot path.
159
+ */
160
+ function isrLegacyCacheKey(projectId, deploymentId, pathname) {
161
+ return `${isrLegacyCachePrefix(projectId, deploymentId)}${normalizeIsrPath(pathname)}`;
162
+ }
163
+ /**
164
+ * Prefix used to list/purge the pre-host-scoped ("legacy") rewrite variants of
165
+ * a given pathname under a deployment. Before ISR keys grew a `<host>` segment,
166
+ * variant keys were written as
167
+ * `<projectId>/isr/v2/<deploymentId>/<normalizedPath>#rw=<token>` — identical to
168
+ * `isrCacheVariantPrefix` but with NO `<host>/` segment. Those entries still
169
+ * persist during the host-scoping rollout (ISR puts set no `expirationTtl`),
170
+ * so the purger must scan this hostless `v2` prefix in addition to the
171
+ * host-scoped `v3` one to recover their variant tokens and purge the matching
172
+ * edge URLs.
173
+ *
174
+ * Built off `isrLegacyCachePrefix` (the `v2` namespace), NOT `isrCachePrefix`
175
+ * (now `v3`), so it targets the legacy hostless keys it is meant to sweep.
176
+ *
177
+ * Exists SOLELY for that rollout cleanup — current dispatch neither writes nor
178
+ * reads hostless keys. Transition-only. Do not use it on the hot path.
179
+ */
180
+ function isrLegacyCacheVariantPrefix(projectId, deploymentId, pathname) {
181
+ return `${isrLegacyCachePrefix(projectId, deploymentId)}${normalizeIsrPath(pathname)}${ISR_VARIANT_SEPARATOR}`;
182
+ }
183
+ /**
184
+ * Hash an original-URL value into a fixed-length, opaque variant token.
185
+ * Used as both the KV-key suffix and the `__void_orig` query value, so the
186
+ * writer and purger paths see byte-identical tokens for the same URL.
187
+ *
188
+ * Properties:
189
+ * - Sync (Workers `crypto.subtle.digest` is async; we don't want to push
190
+ * async through every read/write path for this).
191
+ * - 16 hex chars (64 bits) — bounds the KV key length regardless of input.
192
+ * - Non-recoverable: KV is operator-readable, and the previous base64url
193
+ * encoding let any operator with list access read the original URLs
194
+ * (including sensitive query strings like `?token=…`, `?email=…`).
195
+ * - Not cryptographic: collision resistance is ~2^32 (birthday). Fine for
196
+ * the per-deployment, per-pathname variant set we ever store in one slot.
197
+ *
198
+ * Implementation: FNV-1a 64-bit over UTF-8 bytes. BigInt arithmetic for the
199
+ * multiply (no native u64 in JS), but inputs are short (URLs, sub-1KB) and
200
+ * this only runs once per ISR-touched request.
201
+ */
202
+ const FNV_OFFSET_BASIS = 14695981039346656037n;
203
+ const FNV_PRIME = 1099511628211n;
204
+ const FNV_MASK = 18446744073709551615n;
205
+ function hashOriginalUrl(originalUrl) {
206
+ const bytes = new TextEncoder().encode(originalUrl);
207
+ let h = FNV_OFFSET_BASIS;
208
+ for (let i = 0; i < bytes.length; i++) {
209
+ h = (h ^ BigInt(bytes[i])) & FNV_MASK;
210
+ h = h * FNV_PRIME & FNV_MASK;
211
+ }
212
+ return h.toString(16).padStart(16, "0");
213
+ }
214
+ /**
215
+ * Split a fully-qualified ISR KV key into the normalized pathname segment
216
+ * and the optional rewrite-variant token. Returns `null` token when the
217
+ * key has no `#rw=` suffix (direct hit).
218
+ *
219
+ * The token is opaque (a hash); the original URL it was derived from is not
220
+ * recoverable. Callers that need to reproduce the matching edge-cache URL
221
+ * pass the token through `isrEdgeCacheUrlForToken`.
222
+ */
223
+ function splitIsrCacheKey(fullKey) {
224
+ const sepIdx = fullKey.indexOf(ISR_VARIANT_SEPARATOR);
225
+ if (sepIdx === -1) return {
226
+ normalizedPath: fullKey,
227
+ variantToken: null
228
+ };
229
+ return {
230
+ normalizedPath: fullKey.slice(0, sepIdx),
231
+ variantToken: fullKey.slice(sepIdx + 4)
232
+ };
233
+ }
234
+ /**
235
+ * Build the edge cache URL that dispatch uses as its `caches.default` key
236
+ * (wrapped in a GET `Request`). The public URL stays clean; the internal
237
+ * cache key carries the deployment id so a new deploy never reads HTML
238
+ * produced by an older deploy. The `json` variant separates pages-protocol
239
+ * JSON responses from HTML for the same path+deployment.
240
+ *
241
+ * This MUST match the URL dispatch writes. Dispatch builds the URL from
242
+ * `new URL(request.url).hostname`; for standard-port HTTPS traffic (all CF
243
+ * edge traffic) that yields the same origin as the one produced here.
244
+ */
245
+ function isrEdgeCacheUrl(hostname, pathname, deploymentId, variant, originalUrl) {
246
+ return buildIsrEdgeCacheUrl(hostname, pathname, deploymentId, variant, originalUrl ? hashOriginalUrl(originalUrl) : null);
247
+ }
248
+ /**
249
+ * Like `isrEdgeCacheUrl`, but takes a pre-hashed variant token (from
250
+ * `splitIsrCacheKey`). Used by the purger, which only knows the hashed
251
+ * token: the original URL is not recoverable from a KV key.
252
+ */
253
+ function isrEdgeCacheUrlForToken(hostname, pathname, deploymentId, variant, variantToken) {
254
+ return buildIsrEdgeCacheUrl(hostname, pathname, deploymentId, variant, variantToken ?? null);
255
+ }
256
+ function buildIsrEdgeCacheUrl(hostname, pathname, deploymentId, variant, variantToken) {
257
+ const normalizedPathname = canonicalIsrPathname(pathname);
258
+ const url = new URL(`https://${hostname}${normalizedPathname}`);
259
+ if (variant === "json") url.searchParams.set("__void", "json");
260
+ url.searchParams.set("__void_isr", deploymentId);
261
+ if (variantToken) url.searchParams.set("__void_orig", variantToken);
262
+ return url.toString();
263
+ }
264
+ //#endregion
265
+ export { canonicalIsrPathname, denormalizeIsrPath, hashOriginalUrl, isrCacheKey, isrCachePrefix, isrCacheVariantPrefix, isrEdgeCacheUrl, isrEdgeCacheUrlForToken, isrLegacyCacheKey, isrLegacyCachePrefix, isrLegacyCacheVariantPrefix, normalizeIsrPath, splitIsrCacheKey };
@@ -0,0 +1,145 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ import { IsrCacheEntry, IsrCacheMetadata, QueryAllowlist, SHARED_CACHE_POLICY_HEADER, SHARED_CACHE_POLICY_VERSION, extractPageDataFromHtml, hasCurrentSharedCachePolicy, hasSharedCacheOptOut, isSharedCacheableResponse, isStale, isrCacheResponse, markSharedCachePolicy, normalizePageDataForJsonCache, packIsrValue, resolvePathTtl, resolveRevalidateTtl, serializeHeaders, shouldBypassIsr, unpackIsrValue } from "./core.mjs";
3
+ import { canonicalIsrPathname, denormalizeIsrPath, normalizeIsrPath } from "./index.mjs";
4
+ import { matchSegmentPattern } from "@void/edge";
5
+ //#region src/runtime.d.ts
6
+ /**
7
+ * Single-tenant ISR (incremental static regeneration) cache ladder for the
8
+ * self-hosted Void worker (`vite build && wrangler deploy`).
9
+ *
10
+ * On managed Void, ISR lives in the dispatch worker that sits IN FRONT of the
11
+ * user worker. Self-host has no dispatch worker, so this module ports that
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` (Task 7). 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.
36
+ */
37
+ export interface IsrCacheEnv {
38
+ ISR_CACHE?: KVNamespace;
39
+ __VOID_ISR_DEPLOYMENT_ID?: string;
40
+ }
41
+ /** Renders a live response for a request (worker dispatch + asset serving). */
42
+ export type RenderFn = (request: Request) => Promise<Response>;
43
+ export type HandleIsrOptions = Readonly<{
44
+ revalidate: number | Record<string, number> | undefined;
45
+ pages?: ReadonlyArray<{
46
+ pattern: string;
47
+ revalidate?: number;
48
+ }>;
49
+ pathRevalidate?: Record<string, number>;
50
+ deploymentId: string;
51
+ queryAllowlist?: QueryAllowlist;
52
+ }>;
53
+ export declare const MAX_ISR_VARIANTS_PER_PATH = 200;
54
+ /** Prefix for the current direct deployment's host-scoped cache entries. */
55
+ export declare function isrPrefix(deploymentId: string): string;
56
+ /**
57
+ * KV cache key for an ISR-cached page. Deployment- AND host-scoped: cached HTML
58
+ * cannot outlive the hashed assets of the deployment that produced it (a fresh
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.).
62
+ */
63
+ export declare function isrKey(deploymentId: string, host: string, pathname: string, variantUrl?: string | null): string;
64
+ export declare function isrVariantPrefix(deploymentId: string, host: string, pathname: string): string;
65
+ /**
66
+ * Host-registry key. Every host that has written cached content registers itself
67
+ * under `void-isr-hosts/<id>/<host>` (value `''`) so a purge with no request host
68
+ * (cron / queue `revalidate({ paths })`) can enumerate the hosts to purge.
69
+ * Scoped by deployment id so registrations from an old deploy do not leak.
70
+ */
71
+ export declare function hostRegistryKey(deploymentId: string, host: string): string;
72
+ /**
73
+ * Edge-cache (`caches.default`) key for the HTML variant. The public URL stays
74
+ * clean; this internal key carries the deployment id so a new deploy never
75
+ * reads HTML produced by an older deploy.
76
+ */
77
+ export declare function htmlEdgeKey(host: string, pathname: string, id: string, variantUrl?: string | null): string;
78
+ /**
79
+ * Edge-cache key for the JSON (pages-protocol) variant. Separated from HTML by
80
+ * the `&__void=json` marker.
81
+ */
82
+ 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
+ */
89
+ export declare function readIsrCache(env: IsrCacheEnv, host: string, pathname: string, variantUrl?: string | null): Promise<IsrCacheEntry | null>;
90
+ /**
91
+ * Write an ISR cache entry to KV. The `response.clone()` is taken SYNCHRONOUSLY
92
+ * before any `await` so a caller that consumes `response.body` for the client
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).
95
+ */
96
+ export declare function writeIsrCache(env: IsrCacheEnv, host: string, pathname: string, response: Response, ttl: number, variantUrl?: string | null): Promise<void>;
97
+ /**
98
+ * List every host that has registered cached content for a deployment. Used by
99
+ * the on-demand `revalidate({ paths })` purge when there is no request host
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.
102
+ */
103
+ 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
+ */
108
+ export declare function deleteIsrCache(env: IsrCacheEnv, host: string, pathname: string, variantUrl?: string | null): Promise<void>;
109
+ export declare function clientRevalidateResponse(response: Response): Response;
110
+ export declare function isEventStreamResponse(response: Response): boolean;
111
+ /**
112
+ * Populate the JSON edge cache from a response's embedded page data (the
113
+ * `x-void-page-data` header or the `__VOID_PAGE_DATA__` inline script). No-ops
114
+ * when there is no extractable page data. Any cache error is swallowed
115
+ * (fail-open) so this is always safe to fire inside `ctx.waitUntil`.
116
+ */
117
+ 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
+ */
123
+ export declare function cloneWithoutVoidPages(request: Request): Request;
124
+ /**
125
+ * Run the ISR caching ladder for a request.
126
+ *
127
+ * Returns a cached/rendered `Response` when ISR applies, or `null` when the
128
+ * caller should render live itself (request not ISR-eligible, or a read/hit
129
+ * cache op failed open).
130
+ *
131
+ * Ladder:
132
+ * - Bail (`null`) if not GET, an API path, a Range request, `shouldBypassIsr`,
133
+ * or the path has no ISR TTL. Range requests bail BEFORE any cache read/write so they
134
+ * render live (correct 206) and never touch the shared full-response entry.
135
+ * - JSON (X-VoidPages) path: edge match → hit serves `clientRevalidateResponse`;
136
+ * miss renders live JSON and returns it — the JSON path never writes the edge
137
+ * cache. The JSON edge variant is warmed only as a side-effect of the HTML
138
+ * render path (cold colo re-renders JSON).
139
+ * - HTML path: L1 edge match → L2 KV. Hit serves `isrCacheResponse`
140
+ * (stale hits also queue a background revalidation). Miss renders live and,
141
+ * when cacheable, writes KV + JSON edge + HTML edge in the background.
142
+ */
143
+ export declare function handleIsr(request: Request, env: IsrCacheEnv, ctx: ExecutionContext, options: HandleIsrOptions, render: RenderFn): Promise<Response | null>;
144
+ //#endregion
145
+ export { type IsrCacheEntry, type IsrCacheMetadata, SHARED_CACHE_POLICY_HEADER, SHARED_CACHE_POLICY_VERSION, canonicalIsrPathname, denormalizeIsrPath, extractPageDataFromHtml, hasCurrentSharedCachePolicy, hasSharedCacheOptOut, isSharedCacheableResponse, isStale, isrCacheResponse, markSharedCachePolicy, matchSegmentPattern as matchLiteralPattern, normalizeIsrPath, normalizePageDataForJsonCache, packIsrValue, resolvePathTtl, resolveRevalidateTtl, serializeHeaders, shouldBypassIsr, unpackIsrValue };