@void/isr 0.20.3 → 0.21.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 CHANGED
@@ -53,6 +53,8 @@ export declare function isrCacheResponse(entry: IsrCacheEntry, stale: boolean, n
53
53
  export declare function resolveRevalidateTtl(response: Response, defaultTtl: number): number;
54
54
  /** Return whether credentials or EventSource semantics require an ISR bypass. */
55
55
  export declare function shouldBypassIsr(request: Request): boolean;
56
+ /** Only document navigations may read a framework's shared HTML cache. */
57
+ export declare function isFrameworkDocumentRequest(request: Request): boolean;
56
58
  /** Return whether a response explicitly forbids storage in a shared cache. */
57
59
  export declare function hasSharedCacheOptOut(response: Response): boolean;
58
60
  /** Return whether a complete render may be persisted in the shared ISR cache. */
package/dist/core.mjs CHANGED
@@ -139,6 +139,18 @@ function acceptsEventStream(request) {
139
139
  function shouldBypassIsr(request) {
140
140
  return request.headers.has("Cookie") || request.headers.has("Authorization") || acceptsEventStream(request);
141
141
  }
142
+ /** Only document navigations may read a framework's shared HTML cache. */
143
+ function isFrameworkDocumentRequest(request) {
144
+ if (request.method !== "GET" || request.headers.has("X-VoidPages")) return false;
145
+ if (new URL(request.url).pathname.startsWith("/__void/")) return false;
146
+ if (!request.headers.get("accept")?.split(",").some((value) => value.trim().toLowerCase().split(";", 1)[0] === "text/html")) return false;
147
+ const mode = request.headers.get("sec-fetch-mode")?.toLowerCase();
148
+ if (mode && mode !== "navigate") return false;
149
+ const destination = request.headers.get("sec-fetch-dest")?.toLowerCase();
150
+ if (destination && destination !== "document") return false;
151
+ if (request.headers.has("rsc") || request.headers.has("x-nextjs-data")) return false;
152
+ return true;
153
+ }
142
154
  /** Return whether a response explicitly forbids storage in a shared cache. */
143
155
  function hasSharedCacheOptOut(response) {
144
156
  if (response.headers.has("Set-Cookie")) return true;
@@ -197,4 +209,4 @@ function extractPageDataFromHtml(html) {
197
209
  return match ? normalizePageDataForJsonCache(match[2]) : null;
198
210
  }
199
211
  //#endregion
200
- export { SHARED_CACHE_POLICY_HEADER, SHARED_CACHE_POLICY_VERSION, buildVariantOriginalUrl, decodeIsrCacheEntry, exactQueryAllowlist, extractPageDataFromHtml, hasCurrentSharedCachePolicy, hasSharedCacheOptOut, isSharedCacheableResponse, isStale, isrCacheResponse, markSharedCachePolicy, normalizePageDataForJsonCache, packIsrValue, resolvePageTtl, resolvePathTtl, resolveRevalidateTtl, serializeHeaders, shouldBypassIsr, unpackIsrValue };
212
+ export { SHARED_CACHE_POLICY_HEADER, SHARED_CACHE_POLICY_VERSION, buildVariantOriginalUrl, decodeIsrCacheEntry, exactQueryAllowlist, extractPageDataFromHtml, hasCurrentSharedCachePolicy, hasSharedCacheOptOut, isFrameworkDocumentRequest, isSharedCacheableResponse, isStale, isrCacheResponse, markSharedCachePolicy, normalizePageDataForJsonCache, packIsrValue, resolvePageTtl, resolvePathTtl, resolveRevalidateTtl, serializeHeaders, shouldBypassIsr, unpackIsrValue };
@@ -5,7 +5,7 @@ import { matchSegmentPattern } from "@void/edge";
5
5
  //#region src/runtime.d.ts
6
6
  /**
7
7
  * Single-tenant ISR (incremental static regeneration) cache ladder for the
8
- * self-hosted Void worker (`vite build && wrangler deploy`).
8
+ * direct Cloudflare Worker (Void apps and framework deploy wrappers).
9
9
  *
10
10
  * On managed Void, ISR lives in the dispatch worker that sits IN FRONT of the
11
11
  * user worker. Self-host has no dispatch worker, so this module ports that
@@ -121,6 +121,8 @@ export declare function populateJsonEdgeCache(cache: Cache | undefined, cacheKey
121
121
  * only runs for GET, so there is no body to preserve.
122
122
  */
123
123
  export declare function cloneWithoutVoidPages(request: Request): Request;
124
+ /** Cache only framework document HTML; leave client data requests and other response types live. */
125
+ export declare function handleFrameworkIsr(request: Request, env: IsrCacheEnv, ctx: ExecutionContext, options: HandleIsrOptions, render: RenderFn): Promise<Response | null>;
124
126
  /**
125
127
  * Run the ISR caching ladder for a request.
126
128
  *
package/dist/runtime.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { canonicalIsrPathname, denormalizeIsrPath, isrCacheKey, isrCachePrefix, isrCacheVariantPrefix, isrEdgeCacheUrl, normalizeIsrPath } from "./index.mjs";
2
- import { SHARED_CACHE_POLICY_HEADER, SHARED_CACHE_POLICY_VERSION, buildVariantOriginalUrl, decodeIsrCacheEntry, extractPageDataFromHtml, hasCurrentSharedCachePolicy, hasSharedCacheOptOut, isSharedCacheableResponse, isStale, isrCacheResponse, markSharedCachePolicy, normalizePageDataForJsonCache, packIsrValue, resolvePageTtl, resolvePathTtl, resolveRevalidateTtl, serializeHeaders, shouldBypassIsr, unpackIsrValue } from "./core.mjs";
2
+ import { SHARED_CACHE_POLICY_HEADER, SHARED_CACHE_POLICY_VERSION, buildVariantOriginalUrl, decodeIsrCacheEntry, extractPageDataFromHtml, hasCurrentSharedCachePolicy, hasSharedCacheOptOut, isFrameworkDocumentRequest, isSharedCacheableResponse, isStale, isrCacheResponse, markSharedCachePolicy, normalizePageDataForJsonCache, packIsrValue, resolvePageTtl, resolvePathTtl, resolveRevalidateTtl, serializeHeaders, shouldBypassIsr, unpackIsrValue } from "./core.mjs";
3
3
  import { matchSegmentPattern } from "@void/edge";
4
4
  //#region src/runtime.ts
5
5
  const MAX_ISR_VARIANTS_PER_PATH = 200;
@@ -242,7 +242,7 @@ function cloneWithoutVoidPages(request) {
242
242
  * stale, don't retry. An explicit cache opt-out (including `ttl === 0`)
243
243
  * deletes the KV entry and both edge variants.
244
244
  */
245
- async function revalidateInBackground(env, request, host, pathname, defaultTtl, keys, render, variantUrl) {
245
+ async function revalidateInBackground(env, request, host, pathname, defaultTtl, keys, render, variantUrl, htmlOnly) {
246
246
  try {
247
247
  const response = await render(cloneWithoutVoidPages(request));
248
248
  const ttl = resolveRevalidateTtl(response, defaultTtl);
@@ -251,7 +251,7 @@ async function revalidateInBackground(env, request, host, pathname, defaultTtl,
251
251
  if (ttl === 0 || hasSharedCacheOptOut(response)) await deleteSharedIsrVariants(env, cache, host, pathname, keys, variantUrl);
252
252
  return;
253
253
  }
254
- if (ttl > 0 && isSharedCacheableResponse(response)) {
254
+ if (ttl > 0 && isSharedCacheableResponse(response) && (!htmlOnly || isHtmlResponse(response))) {
255
255
  await writeIsrCache(env, host, pathname, response, ttl, variantUrl);
256
256
  const headers = new Headers(response.headers);
257
257
  headers.set("x-isr-cache", "HIT");
@@ -264,11 +264,19 @@ async function revalidateInBackground(env, request, host, pathname, defaultTtl,
264
264
  headers
265
265
  }));
266
266
  await populateJsonEdgeCache(cache, keys.jsonKey, response, ttl, "HIT");
267
- } else if (ttl === 0 || hasSharedCacheOptOut(response)) await deleteSharedIsrVariants(env, cache, host, pathname, keys, variantUrl);
267
+ } else if (ttl === 0 || hasSharedCacheOptOut(response) || htmlOnly && !isHtmlResponse(response)) await deleteSharedIsrVariants(env, cache, host, pathname, keys, variantUrl);
268
268
  } catch (err) {
269
269
  console.error("ISR revalidation failed:", err);
270
270
  }
271
271
  }
272
+ function isHtmlResponse(response) {
273
+ return response.headers.get("content-type")?.toLowerCase().split(";", 1)[0]?.trim() === "text/html";
274
+ }
275
+ /** Cache only framework document HTML; leave client data requests and other response types live. */
276
+ async function handleFrameworkIsr(request, env, ctx, options, render) {
277
+ if (!isFrameworkDocumentRequest(request)) return null;
278
+ return handleIsrInternal(request, env, ctx, options, render, true);
279
+ }
272
280
  /**
273
281
  * Run the ISR caching ladder for a request.
274
282
  *
@@ -289,6 +297,9 @@ async function revalidateInBackground(env, request, host, pathname, defaultTtl,
289
297
  * when cacheable, writes KV + JSON edge + HTML edge in the background.
290
298
  */
291
299
  async function handleIsr(request, env, ctx, options, render) {
300
+ return handleIsrInternal(request, env, ctx, options, render, false);
301
+ }
302
+ async function handleIsrInternal(request, env, ctx, options, render, htmlOnly) {
292
303
  const { revalidate, pages, pathRevalidate, deploymentId, queryAllowlist } = options;
293
304
  if (request.method !== "GET") return null;
294
305
  if (request.headers.has("range")) return null;
@@ -336,7 +347,7 @@ async function handleIsr(request, env, ctx, options, render) {
336
347
  if (stale) ctx.waitUntil(revalidateInBackground(env, request, host, pathname, isrTtl, {
337
348
  htmlKey,
338
349
  jsonKey
339
- }, render, variantUrl));
350
+ }, render, variantUrl, htmlOnly));
340
351
  const cacheResponse = isrCacheResponse(cached, stale);
341
352
  if (!stale) ctx.waitUntil(safeCachePut(cache, htmlKey, cacheResponse.clone()));
342
353
  return clientRevalidateResponse(cacheResponse);
@@ -346,18 +357,20 @@ async function handleIsr(request, env, ctx, options, render) {
346
357
  const response = await render(request);
347
358
  if (isEventStreamResponse(response)) return response;
348
359
  const ttl = resolveRevalidateTtl(response, isrTtl);
349
- if (ttl > 0 && isSharedCacheableResponse(response)) {
360
+ if (ttl > 0 && isSharedCacheableResponse(response) && (!htmlOnly || isHtmlResponse(response))) {
350
361
  ctx.waitUntil(writeIsrCache(env, host, pathname, response, ttl, variantUrl));
351
- ctx.waitUntil(populateJsonEdgeCache(cache, jsonKey, response, ttl, "MISS"));
362
+ ctx.waitUntil(populateJsonEdgeCache(cache, jsonKey, response, ttl, "HIT"));
352
363
  const cacheHeaders = new Headers(response.headers);
353
364
  cacheHeaders.set("Cache-Control", `public, s-maxage=${ttl}, max-age=0, must-revalidate`);
354
365
  cacheHeaders.set("x-isr-cache", "MISS");
355
366
  markSharedCachePolicy(cacheHeaders);
356
367
  cacheHeaders.delete("x-void-page-data");
357
368
  cacheHeaders.delete("set-cookie");
369
+ const edgeHeaders = new Headers(cacheHeaders);
370
+ edgeHeaders.set("x-isr-cache", "HIT");
358
371
  const cacheResponse = new Response(response.clone().body, {
359
372
  status: response.status,
360
- headers: cacheHeaders
373
+ headers: edgeHeaders
361
374
  });
362
375
  ctx.waitUntil(safeCachePut(cache, htmlKey, cacheResponse));
363
376
  const clientHeaders = new Headers(cacheHeaders);
@@ -368,11 +381,11 @@ async function handleIsr(request, env, ctx, options, render) {
368
381
  headers: clientHeaders
369
382
  });
370
383
  }
371
- if (response.status === 200 && (ttl === 0 || hasSharedCacheOptOut(response))) ctx.waitUntil(deleteSharedIsrVariants(env, cache, host, pathname, {
384
+ if (response.status === 200 && (ttl === 0 || hasSharedCacheOptOut(response) || htmlOnly && !isHtmlResponse(response))) ctx.waitUntil(deleteSharedIsrVariants(env, cache, host, pathname, {
372
385
  htmlKey,
373
386
  jsonKey
374
387
  }, variantUrl));
375
388
  return response;
376
389
  }
377
390
  //#endregion
378
- export { MAX_ISR_VARIANTS_PER_PATH, SHARED_CACHE_POLICY_HEADER, SHARED_CACHE_POLICY_VERSION, canonicalIsrPathname, clientRevalidateResponse, cloneWithoutVoidPages, deleteIsrCache, denormalizeIsrPath, extractPageDataFromHtml, handleIsr, hasCurrentSharedCachePolicy, hasSharedCacheOptOut, hostRegistryKey, htmlEdgeKey, isEventStreamResponse, isSharedCacheableResponse, isStale, isrCacheResponse, isrKey, isrPrefix, isrVariantPrefix, jsonEdgeKey, listIsrHosts, markSharedCachePolicy, matchSegmentPattern as matchLiteralPattern, normalizeIsrPath, normalizePageDataForJsonCache, packIsrValue, populateJsonEdgeCache, readIsrCache, resolvePathTtl, resolveRevalidateTtl, serializeHeaders, shouldBypassIsr, unpackIsrValue, writeIsrCache };
391
+ export { MAX_ISR_VARIANTS_PER_PATH, SHARED_CACHE_POLICY_HEADER, SHARED_CACHE_POLICY_VERSION, canonicalIsrPathname, clientRevalidateResponse, cloneWithoutVoidPages, deleteIsrCache, denormalizeIsrPath, extractPageDataFromHtml, handleFrameworkIsr, handleIsr, hasCurrentSharedCachePolicy, hasSharedCacheOptOut, hostRegistryKey, htmlEdgeKey, isEventStreamResponse, isSharedCacheableResponse, isStale, isrCacheResponse, isrKey, isrPrefix, isrVariantPrefix, jsonEdgeKey, listIsrHosts, markSharedCachePolicy, matchSegmentPattern as matchLiteralPattern, normalizeIsrPath, normalizePageDataForJsonCache, packIsrValue, populateJsonEdgeCache, readIsrCache, resolvePathTtl, resolveRevalidateTtl, serializeHeaders, shouldBypassIsr, unpackIsrValue, writeIsrCache };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@void/isr",
3
- "version": "0.20.3",
3
+ "version": "0.21.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/voidzero-dev/void.git",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@cloudflare/workers-types": "^4.20260702.1",
32
- "@void/edge": "0.20.3"
32
+ "@void/edge": "0.21.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "typescript": "7.0.2",