@rangojs/router 0.27.0 → 0.29.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/bin/rango.js +1 -1
- package/dist/types/__internal.d.ts +7 -1
- package/dist/types/browser/action-fence.d.ts +2 -1
- package/dist/types/browser/event-controller.d.ts +8 -2
- package/dist/types/browser/history-state.d.ts +3 -9
- package/dist/types/browser/navigation-store.d.ts +8 -6
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
- package/dist/types/browser/react/use-action.d.ts +7 -2
- package/dist/types/browser/react/use-handle.d.ts +19 -16
- package/dist/types/browser/react/use-link-status.d.ts +5 -2
- package/dist/types/browser/segment-reconciler.d.ts +32 -4
- package/dist/types/browser/types.d.ts +24 -5
- package/dist/types/browser/wire-protocol.d.ts +26 -0
- package/dist/types/cache/cache-key-utils.d.ts +11 -5
- package/dist/types/cache/cache-scope.d.ts +34 -22
- package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
- package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
- package/dist/types/cache/handle-snapshot.d.ts +9 -4
- package/dist/types/cache/segment-codec.d.ts +14 -0
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/taint.d.ts +5 -3
- package/dist/types/cache/types.d.ts +7 -3
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
- package/dist/types/prefetch-markers.d.ts +5 -14
- package/dist/types/prerender/store.d.ts +31 -0
- package/dist/types/prerender/warm-request.d.ts +3 -3
- package/dist/types/prerender.d.ts +2 -2
- package/dist/types/route-name.d.ts +5 -0
- package/dist/types/router/error-handling.d.ts +11 -5
- package/dist/types/router/instrument.d.ts +4 -3
- package/dist/types/router/intercept-resolution.d.ts +25 -9
- package/dist/types/router/match-context.d.ts +6 -11
- package/dist/types/router/match-handlers.d.ts +2 -6
- package/dist/types/router/match-middleware/background-revalidation.d.ts +13 -22
- package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
- package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
- package/dist/types/router/navigation-snapshot.d.ts +7 -0
- package/dist/types/router/prerender-match.d.ts +21 -6
- package/dist/types/router/preview-match.d.ts +2 -7
- package/dist/types/router/request-classification.d.ts +5 -0
- package/dist/types/router/router-context.d.ts +7 -46
- package/dist/types/router/router-interfaces.d.ts +8 -14
- package/dist/types/router/segment-resolution/helpers.d.ts +19 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
- package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
- package/dist/types/router/segment-resolution/resolution-pass.d.ts +95 -0
- package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +2 -0
- package/dist/types/router/segment-resolution.d.ts +1 -2
- package/dist/types/router/segment-wrappers.d.ts +11 -48
- package/dist/types/router/telemetry.d.ts +2 -0
- package/dist/types/router/types.d.ts +11 -1
- package/dist/types/rsc/full-payload.d.ts +2 -2
- package/dist/types/rsc/handler-context.d.ts +6 -6
- package/dist/types/rsc/json-route-result.d.ts +5 -9
- package/dist/types/rsc/request-stages.d.ts +203 -0
- package/dist/types/rsc/response-cache-serve.d.ts +7 -12
- package/dist/types/rsc/response-route-handler.d.ts +2 -2
- package/dist/types/rsc/routine-plan.d.ts +1 -1
- package/dist/types/rsc/server-action.d.ts +2 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
- package/dist/types/rsc/shell-capture.d.ts +132 -15
- package/dist/types/rsc/shell-serve.d.ts +37 -12
- package/dist/types/server/context.d.ts +9 -8
- package/dist/types/server/handle-store.d.ts +16 -0
- package/dist/types/server/render-pass.d.ts +72 -0
- package/dist/types/server/request-context.d.ts +53 -104
- package/dist/types/server/rewrite-trail.d.ts +2 -2
- package/dist/types/testing/dispatch.d.ts +35 -18
- package/dist/types/testing/render-route.d.ts +43 -8
- package/dist/types/testing/serve-shell-request.d.ts +8 -1
- package/dist/types/types/cache-types.d.ts +6 -3
- package/dist/types/types/error-types.d.ts +30 -3
- package/dist/types/types/handler-context.d.ts +9 -1
- package/dist/types/types/segments.d.ts +1 -1
- package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
- package/dist/types/vite/discovery/build-env.d.ts +22 -0
- package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
- package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
- package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
- package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +12 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
- package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +8 -47
- package/dist/types/vite/temp-rsc-server.d.ts +39 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
- package/dist/types/wire-params.d.ts +39 -0
- package/dist/vite/index.js +3822 -3727
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +8 -5
- package/skills/caching/SKILL.md +37 -18
- package/skills/client-urls/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +13 -1
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/hooks/SKILL.md +1 -1
- package/skills/hooks/data.md +8 -1
- package/skills/hooks/handle-and-actions.md +33 -2
- package/skills/hooks/navigation.md +121 -15
- package/skills/hooks/state.md +6 -4
- package/skills/hooks/urls.md +16 -5
- package/skills/intercept/SKILL.md +23 -7
- package/skills/middleware/SKILL.md +18 -0
- package/skills/observability/SKILL.md +30 -0
- package/skills/ppr/SKILL.md +18 -3
- package/skills/prerender/SKILL.md +39 -15
- package/skills/rango/SKILL.md +5 -1
- package/skills/server-actions/SKILL.md +15 -1
- package/skills/testing/client-components.md +25 -18
- package/skills/testing/response-routes.md +6 -4
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/use-cache/SKILL.md +46 -8
- package/skills/view-transitions/SKILL.md +1 -1
- package/src/__internal.ts +7 -1
- package/src/browser/action-fence.ts +2 -1
- package/src/browser/connection-warmup.ts +3 -1
- package/src/browser/event-controller.ts +37 -13
- package/src/browser/history-state.ts +6 -12
- package/src/browser/navigation-bridge.ts +59 -26
- package/src/browser/navigation-client.ts +20 -25
- package/src/browser/navigation-store.ts +25 -27
- package/src/browser/navigation-transaction.ts +4 -1
- package/src/browser/network-error-handler.ts +4 -0
- package/src/browser/partial-update.ts +33 -6
- package/src/browser/prefetch/cache.ts +2 -1
- package/src/browser/prefetch/fetch.ts +8 -16
- package/src/browser/prefetch/queue.ts +4 -4
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/filter-segment-order.ts +2 -2
- package/src/browser/react/use-action.ts +24 -48
- package/src/browser/react/use-handle.ts +22 -18
- package/src/browser/react/use-link-status.ts +48 -60
- package/src/browser/react/use-navigation.ts +92 -41
- package/src/browser/rsc-router.tsx +1 -0
- package/src/browser/segment-reconciler.ts +109 -53
- package/src/browser/server-action-bridge.ts +81 -61
- package/src/browser/types.ts +25 -5
- package/src/browser/wire-protocol.ts +43 -0
- package/src/cache/cache-key-utils.ts +11 -5
- package/src/cache/cache-runtime.ts +132 -75
- package/src/cache/cache-scope.ts +110 -64
- package/src/cache/cf/cf-cache-constants.ts +2 -2
- package/src/cache/cf/cf-cache-store.ts +486 -874
- package/src/cache/cf/cf-kv-utils.ts +1 -1
- package/src/cache/document-cache.ts +64 -46
- package/src/cache/handle-snapshot.ts +19 -7
- package/src/cache/read-through-swr.ts +20 -18
- package/src/cache/segment-codec.ts +55 -0
- package/src/cache/shell-snapshot.ts +2 -2
- package/src/cache/tag-invalidation.ts +6 -2
- package/src/cache/taint.ts +5 -3
- package/src/cache/types.ts +7 -3
- package/src/cache/vercel/vercel-cache-store.ts +243 -310
- package/src/handles/pushed-by.ts +1 -1
- package/src/prefetch-markers.ts +5 -16
- package/src/prerender/store.ts +76 -0
- package/src/prerender/warm-request.ts +8 -4
- package/src/prerender.ts +2 -2
- package/src/route-name.ts +8 -0
- package/src/router/error-handling.ts +22 -10
- package/src/router/instrument.ts +4 -3
- package/src/router/intercept-resolution.ts +82 -75
- package/src/router/loader-resolution.ts +5 -4
- package/src/router/match-api.ts +5 -5
- package/src/router/match-context.ts +23 -12
- package/src/router/match-handlers.ts +8 -20
- package/src/router/match-middleware/background-revalidation.ts +46 -57
- package/src/router/match-middleware/cache-lookup.ts +677 -726
- package/src/router/match-middleware/cache-store.ts +4 -8
- package/src/router/match-middleware/intercept-resolution.ts +3 -23
- package/src/router/match-middleware/segment-resolution.ts +34 -61
- package/src/router/middleware.ts +69 -11
- package/src/router/navigation-snapshot.ts +14 -4
- package/src/router/prerender-match.ts +100 -44
- package/src/router/preview-match.ts +6 -41
- package/src/router/request-classification.ts +13 -7
- package/src/router/route-snapshot.ts +2 -5
- package/src/router/router-context.ts +10 -95
- package/src/router/router-interfaces.ts +8 -14
- package/src/router/segment-resolution/helpers.ts +49 -13
- package/src/router/segment-resolution/loader-cache.ts +15 -9
- package/src/router/segment-resolution/loader-mask.ts +5 -5
- package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
- package/src/router/segment-resolution/resolution-pass.ts +139 -0
- package/src/router/segment-resolution/revalidation.ts +939 -789
- package/src/router/segment-resolution/streamed-handler-boundary.ts +3 -0
- package/src/router/segment-resolution.ts +4 -13
- package/src/router/segment-wrappers.ts +51 -269
- package/src/router/telemetry.ts +5 -0
- package/src/router/types.ts +11 -1
- package/src/router.ts +19 -17
- package/src/rsc/full-payload.ts +6 -5
- package/src/rsc/handler-context.ts +8 -14
- package/src/rsc/handler.ts +126 -507
- package/src/rsc/json-route-result.ts +5 -9
- package/src/rsc/loader-fetch.ts +6 -5
- package/src/rsc/progressive-enhancement.ts +5 -0
- package/src/rsc/request-stages.ts +759 -0
- package/src/rsc/response-cache-serve.ts +10 -14
- package/src/rsc/response-route-handler.ts +13 -5
- package/src/rsc/rewrite-passes.ts +2 -1
- package/src/rsc/routine-plan.ts +1 -1
- package/src/rsc/rsc-rendering.ts +164 -349
- package/src/rsc/server-action.ts +87 -113
- package/src/rsc/shell-capture-constants.ts +1 -1
- package/src/rsc/shell-capture.ts +275 -227
- package/src/rsc/shell-serve.ts +121 -18
- package/src/rsc/ssr-setup.ts +5 -4
- package/src/segment-system.tsx +3 -3
- package/src/server/context.ts +22 -25
- package/src/server/handle-store.ts +55 -0
- package/src/server/render-pass.ts +291 -0
- package/src/server/request-context.ts +75 -115
- package/src/server/rewrite-trail.ts +6 -4
- package/src/ssr/index.tsx +1 -1
- package/src/testing/dispatch.ts +226 -710
- package/src/testing/e2e/page-helpers.ts +2 -2
- package/src/testing/render-route.tsx +199 -13
- package/src/testing/serve-shell-request.ts +36 -40
- package/src/types/cache-types.ts +6 -3
- package/src/types/error-types.ts +30 -3
- package/src/types/handler-context.ts +12 -1
- package/src/types/segments.ts +1 -1
- package/src/vite/discovery/build-discovery-phase.ts +204 -0
- package/src/vite/discovery/build-env.ts +148 -0
- package/src/vite/discovery/bundle-scan-phase.ts +128 -0
- package/src/vite/discovery/dev-prerender-cache.ts +8 -3
- package/src/vite/discovery/dev-server-phase.ts +1776 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +1 -1
- package/src/vite/discovery/prerender-collection.ts +24 -39
- package/src/vite/discovery/state.ts +15 -0
- package/src/vite/plugins/version-plugin.ts +1 -1
- package/src/vite/plugins/virtual-entries.ts +1 -1
- package/src/vite/router-discovery.ts +29 -2428
- package/src/vite/temp-rsc-server.ts +223 -0
- package/src/vite/utils/prerender-utils.ts +47 -0
- package/src/wire-params.ts +40 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
- package/src/router/segment-resolution/fresh.ts +0 -945
|
@@ -16,7 +16,7 @@ declare global {
|
|
|
16
16
|
*
|
|
17
17
|
* Read flow: L1 hit → serve | L1 miss → L2 hit → serve + promote to L1 | both miss → render
|
|
18
18
|
* With `kvFallThrough`, an L1 copy a tag marker rejects reads as
|
|
19
|
-
* an L1 miss: L2 is checked against the same markers (see
|
|
19
|
+
* an L1 miss: L2 is checked against the same markers (see readL1(),
|
|
20
20
|
* "Tag invalidation"). Off by default: the rejected copy is a miss.
|
|
21
21
|
* Write flow: L1 write + L2 write (both via waitUntil)
|
|
22
22
|
*
|
|
@@ -485,6 +485,234 @@ interface KVResponseEnvelope {
|
|
|
485
485
|
ta?: number;
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
+
type CFReadOp = "get" | "getItem" | "getResponse";
|
|
489
|
+
|
|
490
|
+
/** The deadlines every KV envelope carries (ms epoch). */
|
|
491
|
+
interface KVDeadlines {
|
|
492
|
+
s: number;
|
|
493
|
+
e: number;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* A family read of CFCacheStore (`get`, `getItem`, `getResponse`, through
|
|
498
|
+
* readL1 and readKv): its keys, its KV envelope check, what an L1 or KV hit
|
|
499
|
+
* serves and the L1 entry a KV hit is promoted to. `kvMethod` names the KV
|
|
500
|
+
* read in errors and evictions.
|
|
501
|
+
*/
|
|
502
|
+
interface CFReadFamily<
|
|
503
|
+
E extends KVDeadlines,
|
|
504
|
+
B,
|
|
505
|
+
V,
|
|
506
|
+
Err extends CacheReadError | null,
|
|
507
|
+
> {
|
|
508
|
+
family: "segment" | "item" | "response";
|
|
509
|
+
debugOp: CFReadOp;
|
|
510
|
+
/** What a failed read returns. */
|
|
511
|
+
readError: Err;
|
|
512
|
+
prefix: "" | "fn:" | "doc:";
|
|
513
|
+
version: KeyVersion;
|
|
514
|
+
kvMethod: string;
|
|
515
|
+
promoteLabel: string;
|
|
516
|
+
/** The corrupt-L1 label of a read that claims the revalidation. */
|
|
517
|
+
staleLabel: string;
|
|
518
|
+
/** Report a tag-marker-namespace key and miss (isReservedSegmentKey). */
|
|
519
|
+
rejectsReservedKeys: boolean;
|
|
520
|
+
validate(envelope: E): boolean;
|
|
521
|
+
/** An L1 hit's value: its parsed body, or the document's client Response. */
|
|
522
|
+
fromL1(body: B, shouldRevalidate: boolean, tags?: string[]): V;
|
|
523
|
+
tags(envelope: E): { tags?: string[]; taggedAt?: number };
|
|
524
|
+
/** A KV hit's value; throws when the envelope does not rebuild. */
|
|
525
|
+
fromKv(envelope: E, shouldRevalidate: boolean): V;
|
|
526
|
+
/**
|
|
527
|
+
* A KV hit's L1 entry: its body and headers, with promoteToL1's
|
|
528
|
+
* Cache-Control, the envelope's deadlines and its tag headers.
|
|
529
|
+
*/
|
|
530
|
+
toL1(
|
|
531
|
+
envelope: E,
|
|
532
|
+
cacheControl: string,
|
|
533
|
+
tagHeaders: Record<string, string>,
|
|
534
|
+
): Response;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Merge the internal tag headers (tagHeaderEntries) onto an existing Headers
|
|
539
|
+
* instance. The from-scratch paths spread them into an object-literal init;
|
|
540
|
+
* the document put/promote paths build a Headers first, so they .set() each
|
|
541
|
+
* entry instead — except `Cache-Tag`, which is APPENDED: a document author
|
|
542
|
+
* may have set their own Cache-Tag, and clobbering it would break their CDN
|
|
543
|
+
* purging. Append produces the comma-merged list CF expects.
|
|
544
|
+
*/
|
|
545
|
+
function setTagHeaders(
|
|
546
|
+
headers: Headers,
|
|
547
|
+
tagHeaders: Record<string, string>,
|
|
548
|
+
): void {
|
|
549
|
+
for (const [name, value] of Object.entries(tagHeaders)) {
|
|
550
|
+
if (name === "Cache-Tag") headers.append(name, value);
|
|
551
|
+
else headers.set(name, value);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* A JSON family's promoted L1 entry, marked HIT as set() marks it. A plain
|
|
557
|
+
* header record: building a Headers and set()-ing each entry costs the KV-hit
|
|
558
|
+
* path a second validation pass per header.
|
|
559
|
+
*/
|
|
560
|
+
function jsonL1Entry(
|
|
561
|
+
body: string,
|
|
562
|
+
e: KVDeadlines,
|
|
563
|
+
cacheControl: string,
|
|
564
|
+
tagHeaders: Record<string, string>,
|
|
565
|
+
): Response {
|
|
566
|
+
return new Response(body, {
|
|
567
|
+
headers: {
|
|
568
|
+
"Content-Type": "application/json",
|
|
569
|
+
"Cache-Control": cacheControl,
|
|
570
|
+
[CACHE_STALE_AT_HEADER]: String(e.s),
|
|
571
|
+
[CACHE_EXPIRES_AT_HEADER]: String(e.e),
|
|
572
|
+
[CACHE_STATUS_HEADER]: "HIT",
|
|
573
|
+
...tagHeaders,
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const SEGMENT_READ: CFReadFamily<
|
|
579
|
+
KVSegmentEnvelope,
|
|
580
|
+
CachedEntryData,
|
|
581
|
+
CacheGetResult,
|
|
582
|
+
CacheReadError
|
|
583
|
+
> = {
|
|
584
|
+
family: "segment",
|
|
585
|
+
debugOp: "get",
|
|
586
|
+
// A KV read TIMEOUT stays a miss: a bounded-latency degrade of a
|
|
587
|
+
// likely-healthy read, unlike a failed one.
|
|
588
|
+
readError: CACHE_READ_ERROR,
|
|
589
|
+
prefix: "",
|
|
590
|
+
version: "data",
|
|
591
|
+
kvMethod: "kvGetSegment",
|
|
592
|
+
promoteLabel: "[CFCacheStore] promoteSegmentToL1",
|
|
593
|
+
staleLabel: "get(revalidating)",
|
|
594
|
+
rejectsReservedKeys: true,
|
|
595
|
+
validate: (e) =>
|
|
596
|
+
typeof e.e === "number" && typeof e.s === "number" && e.d != null,
|
|
597
|
+
tags: (e) => e.d,
|
|
598
|
+
fromL1: (data, shouldRevalidate) => ({ data, shouldRevalidate }),
|
|
599
|
+
fromKv: (e, shouldRevalidate) => ({ data: e.d, shouldRevalidate }),
|
|
600
|
+
toL1: (e, cacheControl, tagHeaders) =>
|
|
601
|
+
jsonL1Entry(JSON.stringify(e.d), e, cacheControl, tagHeaders),
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
const ITEM_READ: CFReadFamily<
|
|
605
|
+
KVItemEnvelope,
|
|
606
|
+
{ value: string; handles?: string },
|
|
607
|
+
CacheItemResult,
|
|
608
|
+
null
|
|
609
|
+
> = {
|
|
610
|
+
family: "item",
|
|
611
|
+
debugOp: "getItem",
|
|
612
|
+
readError: null,
|
|
613
|
+
prefix: "fn:",
|
|
614
|
+
version: "data",
|
|
615
|
+
kvMethod: "kvGetItem",
|
|
616
|
+
promoteLabel: "[CFCacheStore] promoteItemToL1",
|
|
617
|
+
staleLabel: "getItem",
|
|
618
|
+
rejectsReservedKeys: false,
|
|
619
|
+
validate: (e) =>
|
|
620
|
+
typeof e.v === "string" &&
|
|
621
|
+
typeof e.e === "number" &&
|
|
622
|
+
typeof e.s === "number",
|
|
623
|
+
tags: (e) => ({ tags: e.t, taggedAt: e.ta }),
|
|
624
|
+
fromL1: (data, shouldRevalidate, tags) => ({
|
|
625
|
+
value: data.value,
|
|
626
|
+
handles: data.handles,
|
|
627
|
+
shouldRevalidate,
|
|
628
|
+
tags,
|
|
629
|
+
}),
|
|
630
|
+
fromKv: (e, shouldRevalidate) => ({
|
|
631
|
+
value: e.v,
|
|
632
|
+
handles: e.h,
|
|
633
|
+
shouldRevalidate,
|
|
634
|
+
tags: e.t,
|
|
635
|
+
}),
|
|
636
|
+
toL1: (e, cacheControl, tagHeaders) =>
|
|
637
|
+
jsonL1Entry(
|
|
638
|
+
JSON.stringify({ value: e.v, handles: e.h }),
|
|
639
|
+
e,
|
|
640
|
+
cacheControl,
|
|
641
|
+
tagHeaders,
|
|
642
|
+
),
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
const RESPONSE_READ: CFReadFamily<
|
|
646
|
+
KVResponseEnvelope,
|
|
647
|
+
Response,
|
|
648
|
+
{ response: Response; shouldRevalidate: boolean },
|
|
649
|
+
null
|
|
650
|
+
> = {
|
|
651
|
+
family: "response",
|
|
652
|
+
debugOp: "getResponse",
|
|
653
|
+
readError: null,
|
|
654
|
+
prefix: "doc:",
|
|
655
|
+
version: "document",
|
|
656
|
+
kvMethod: "kvGetResponse",
|
|
657
|
+
promoteLabel: "[CFCacheStore] promoteResponseToL1",
|
|
658
|
+
// Never read: the document returns before the body read.
|
|
659
|
+
staleLabel: "getResponse",
|
|
660
|
+
rejectsReservedKeys: false,
|
|
661
|
+
validate: (e) =>
|
|
662
|
+
typeof e.b === "string" &&
|
|
663
|
+
typeof e.st === "number" &&
|
|
664
|
+
typeof e.e === "number" &&
|
|
665
|
+
typeof e.s === "number" &&
|
|
666
|
+
// stx is optional but, if present, must be a string (feeds Response).
|
|
667
|
+
(e.stx === undefined || typeof e.stx === "string") &&
|
|
668
|
+
// hd must be an array of [name, value] string tuples; a malformed
|
|
669
|
+
// shape would otherwise throw in `new Headers(hd)`. Validate it here
|
|
670
|
+
// so a faulty envelope is a fail-open MISS, never a thrown read.
|
|
671
|
+
Array.isArray(e.hd) &&
|
|
672
|
+
e.hd.every(
|
|
673
|
+
(entry) =>
|
|
674
|
+
Array.isArray(entry) &&
|
|
675
|
+
entry.length === 2 &&
|
|
676
|
+
typeof entry[0] === "string" &&
|
|
677
|
+
typeof entry[1] === "string",
|
|
678
|
+
),
|
|
679
|
+
tags: (e) => ({ tags: e.t, taggedAt: e.ta }),
|
|
680
|
+
fromL1: (response, shouldRevalidate) => ({ response, shouldRevalidate }),
|
|
681
|
+
// Corrupt/partial base64 throws in atob, an out-of-range `st` in new
|
|
682
|
+
// Response: a faulty entry, which readKv evicts. Per-client signals a stale
|
|
683
|
+
// envelope may carry are stripped (Finding #3), in place so the promotion
|
|
684
|
+
// re-seeds from the filtered `hd`.
|
|
685
|
+
fromKv: (e, shouldRevalidate) => {
|
|
686
|
+
e.hd = e.hd.filter(([name]) => !isPerClientSignalHeader(name));
|
|
687
|
+
return {
|
|
688
|
+
response: new Response(base64ToBuffer(e.b), {
|
|
689
|
+
status: e.st,
|
|
690
|
+
statusText: e.stx,
|
|
691
|
+
headers: new Headers(e.hd),
|
|
692
|
+
}),
|
|
693
|
+
shouldRevalidate,
|
|
694
|
+
};
|
|
695
|
+
},
|
|
696
|
+
// `hd` is client-facing: the internal headers are re-attached and the
|
|
697
|
+
// author's Cache-Control is stashed for toClientResponse, as putResponse does.
|
|
698
|
+
toL1: (e, cacheControl, tagHeaders) => {
|
|
699
|
+
const headers = new Headers(e.hd);
|
|
700
|
+
const originalCacheControl = headers.get("Cache-Control");
|
|
701
|
+
if (originalCacheControl !== null) {
|
|
702
|
+
headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
|
|
703
|
+
}
|
|
704
|
+
headers.set("Cache-Control", cacheControl);
|
|
705
|
+
headers.set(CACHE_STALE_AT_HEADER, String(e.s));
|
|
706
|
+
headers.set(CACHE_EXPIRES_AT_HEADER, String(e.e));
|
|
707
|
+
setTagHeaders(headers, tagHeaders);
|
|
708
|
+
return new Response(base64ToBuffer(e.b), {
|
|
709
|
+
status: e.st,
|
|
710
|
+
statusText: e.stx,
|
|
711
|
+
headers,
|
|
712
|
+
});
|
|
713
|
+
},
|
|
714
|
+
};
|
|
715
|
+
|
|
488
716
|
// ============================================================================
|
|
489
717
|
// CFCacheStore Implementation
|
|
490
718
|
// ============================================================================
|
|
@@ -936,8 +1164,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
936
1164
|
// A FAST json() rejection (a corrupt body, or a foreign 200 non-JSON
|
|
937
1165
|
// response that collided on this key) is caught at the thunk and turned into
|
|
938
1166
|
// a miss, so the caller falls through to L2/KV exactly like a body-timeout
|
|
939
|
-
// -- instead of escaping to
|
|
940
|
-
//
|
|
1167
|
+
// -- instead of escaping to readL1()'s outer catch, which returns the
|
|
1168
|
+
// family's read error WITHOUT ever consulting KV. The catch lives here, not in
|
|
941
1169
|
// readWithTimeout, so the L2/KV tier keeps propagating a genuine kv.get
|
|
942
1170
|
// rejection to its own error sink. The `errored` flag lets the caller emit a
|
|
943
1171
|
// distinct "body-error" debug outcome rather than masquerading as a timeout.
|
|
@@ -967,7 +1195,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
967
1195
|
* the faulty per-colo entry ONLY when that fall-through found no good copy.
|
|
968
1196
|
*
|
|
969
1197
|
* The conditional evict is the load-bearing detail: when KV DOES serve a copy,
|
|
970
|
-
*
|
|
1198
|
+
* readKv has already scheduled a same-key promote (`cache.put`); an eager
|
|
971
1199
|
* `cache.delete` here would race that put with no CF Cache API ordering
|
|
972
1200
|
* guarantee and could clobber the freshly-restored entry. So in that case we
|
|
973
1201
|
* lean on #558's heal-by-overwrite (the non-suppressed fall-through promotes /
|
|
@@ -1011,7 +1239,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1011
1239
|
|
|
1012
1240
|
/**
|
|
1013
1241
|
* Re-put a stale L1 entry marked REVALIDATING, so concurrent requests serve it
|
|
1014
|
-
* without each triggering a revalidation.
|
|
1242
|
+
* without each triggering a revalidation (readL1). A JSON family re-puts its
|
|
1243
|
+
* parsed body as a string, without a status text as set() writes it; the
|
|
1244
|
+
* document re-puts a clone (`source`) whose original body still streams to
|
|
1245
|
+
* the client.
|
|
1015
1246
|
*
|
|
1016
1247
|
* The write is NON-BLOCKING (waitUntil) and best-effort by design:
|
|
1017
1248
|
* - It runs in waitUntil, so it never adds the put latency to the served stale
|
|
@@ -1034,61 +1265,26 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1034
1265
|
private markRevalidating(
|
|
1035
1266
|
cache: Cache,
|
|
1036
1267
|
request: Request,
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1268
|
+
source: Response,
|
|
1269
|
+
body: BodyInit | null = source.body,
|
|
1270
|
+
statusText = source.statusText,
|
|
1040
1271
|
): void {
|
|
1041
1272
|
const reputNow = Date.now();
|
|
1042
|
-
const headers = new Headers(
|
|
1273
|
+
const headers = new Headers(source.headers);
|
|
1043
1274
|
headers.set(CACHE_STATUS_HEADER, "REVALIDATING");
|
|
1044
1275
|
headers.set(CACHE_REVALIDATING_AT_HEADER, String(reputNow));
|
|
1045
1276
|
headers.set("Cache-Control", remainingCacheControl(headers, reputNow));
|
|
1046
|
-
const markerResponse = new Response(body, {
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
await cache.put(request, markerResponse);
|
|
1050
|
-
} catch {
|
|
1051
|
-
// Best-effort: a failed marker write must not affect the served read;
|
|
1052
|
-
// the entry simply re-arms on the next stale read.
|
|
1053
|
-
}
|
|
1054
|
-
};
|
|
1055
|
-
if (this.waitUntil) this.waitUntil(write);
|
|
1056
|
-
else void write();
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
/**
|
|
1060
|
-
* Document-tier counterpart of markRevalidating for getResponse's herd guard.
|
|
1061
|
-
* The segment/item tiers JSON-parse the body, so they re-put with a string
|
|
1062
|
-
* body; document bodies are streamed verbatim, so we re-put with a CLONED
|
|
1063
|
-
* response body (`response.clone()`) supplied by the caller -- the original
|
|
1064
|
-
* body still streams to the client while the marker carries the clone. Same
|
|
1065
|
-
* REVALIDATING status header, same revalidating-at stamp, same
|
|
1066
|
-
* remainingCacheControl re-put math as markRevalidating, so the document tier
|
|
1067
|
-
* suppresses concurrent revalidation for the identical MAX_REVALIDATION_INTERVAL
|
|
1068
|
-
* window the segment tier does. Best-effort and non-blocking: a failed marker
|
|
1069
|
-
* write must not affect the served stale read.
|
|
1070
|
-
* @internal
|
|
1071
|
-
*/
|
|
1072
|
-
private markResponseRevalidating(
|
|
1073
|
-
cache: Cache,
|
|
1074
|
-
request: Request,
|
|
1075
|
-
clonedResponse: Response,
|
|
1076
|
-
): void {
|
|
1077
|
-
const reputNow = Date.now();
|
|
1078
|
-
const headers = new Headers(clonedResponse.headers);
|
|
1079
|
-
headers.set(CACHE_STATUS_HEADER, "REVALIDATING");
|
|
1080
|
-
headers.set(CACHE_REVALIDATING_AT_HEADER, String(reputNow));
|
|
1081
|
-
headers.set("Cache-Control", remainingCacheControl(headers, reputNow));
|
|
1082
|
-
const markerResponse = new Response(clonedResponse.body, {
|
|
1083
|
-
status: clonedResponse.status,
|
|
1084
|
-
statusText: clonedResponse.statusText,
|
|
1277
|
+
const markerResponse = new Response(body, {
|
|
1278
|
+
status: source.status,
|
|
1279
|
+
statusText,
|
|
1085
1280
|
headers,
|
|
1086
1281
|
});
|
|
1087
1282
|
const write = async (): Promise<void> => {
|
|
1088
1283
|
try {
|
|
1089
1284
|
await cache.put(request, markerResponse);
|
|
1090
1285
|
} catch {
|
|
1091
|
-
// Best-effort:
|
|
1286
|
+
// Best-effort: a failed marker write must not affect the served read;
|
|
1287
|
+
// the entry simply re-arms on the next stale read.
|
|
1092
1288
|
}
|
|
1093
1289
|
};
|
|
1094
1290
|
if (this.waitUntil) this.waitUntil(write);
|
|
@@ -1133,84 +1329,89 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1133
1329
|
}
|
|
1134
1330
|
|
|
1135
1331
|
/**
|
|
1136
|
-
* Get cached entry data by key.
|
|
1137
|
-
*
|
|
1138
|
-
|
|
1332
|
+
* Get cached entry data by key (readL1). On L1 miss, falls back to KV (L2)
|
|
1333
|
+
* if configured; KV hits are promoted to L1 in the background.
|
|
1334
|
+
*/
|
|
1335
|
+
get(key: string): Promise<CacheGetResult | null | CacheReadError> {
|
|
1336
|
+
return this.readL1(key, SEGMENT_READ);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* The family's debug sink for one read; the document family is outside the
|
|
1341
|
+
* debug surface (op is only get/getItem). Undefined when debug is off, so
|
|
1342
|
+
* the hot path allocates no event.
|
|
1343
|
+
* @internal
|
|
1344
|
+
*/
|
|
1345
|
+
private debugSink(
|
|
1346
|
+
op: CFReadOp,
|
|
1347
|
+
key: string,
|
|
1348
|
+
): ((event: Omit<CFCacheReadDebugEvent, "op" | "key">) => void) | undefined {
|
|
1349
|
+
return this.debug && op !== "getResponse"
|
|
1350
|
+
? (event) => this.emitDebug({ op, key, ...event })
|
|
1351
|
+
: undefined;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* The L1 read of a family, which handles SWR atomically:
|
|
1139
1356
|
* - If stale and not already revalidating, marks as REVALIDATING and returns shouldRevalidate: true
|
|
1140
1357
|
* - If already REVALIDATING (and recent), returns shouldRevalidate: false
|
|
1141
1358
|
* - If fresh, returns shouldRevalidate: false
|
|
1142
1359
|
*
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1360
|
+
* Every degraded L1 read falls through to KV (readKv). The family methods
|
|
1361
|
+
* return it unwrapped: an async wrapper adds two promises and two microtask
|
|
1362
|
+
* ticks to every read.
|
|
1363
|
+
* @internal
|
|
1145
1364
|
*/
|
|
1146
|
-
async
|
|
1147
|
-
|
|
1365
|
+
private async readL1<
|
|
1366
|
+
E extends KVDeadlines,
|
|
1367
|
+
V,
|
|
1368
|
+
Err extends CacheReadError | null,
|
|
1369
|
+
>(key: string, f: CFReadFamily<E, unknown, V, Err>): Promise<V | null | Err> {
|
|
1370
|
+
if (f.rejectsReservedKeys && this.isReservedSegmentKey(key, "cache-read"))
|
|
1371
|
+
return null;
|
|
1372
|
+
const debug = this.debugSink(f.debugOp, key);
|
|
1148
1373
|
try {
|
|
1149
1374
|
const cache = await this.getCache();
|
|
1150
|
-
const request = this.keyToRequest(key,
|
|
1151
|
-
const matchStart = Date.now();
|
|
1375
|
+
const request = this.keyToRequest(f.prefix + key, f.version);
|
|
1376
|
+
const matchStart = debug ? Date.now() : 0;
|
|
1152
1377
|
const {
|
|
1153
1378
|
response,
|
|
1154
1379
|
timedOut,
|
|
1155
1380
|
error: matchError,
|
|
1156
1381
|
} = await this.matchWithTimeout(cache, request);
|
|
1157
|
-
const matchMs = Date.now() - matchStart;
|
|
1382
|
+
const matchMs = debug ? Date.now() - matchStart : 0;
|
|
1158
1383
|
|
|
1159
1384
|
if (!response) {
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
// surface CACHE_READ_ERROR instead of a replayable miss (the PPR
|
|
1167
|
-
// seeded fallback must render uncached, not substitute the doc
|
|
1168
|
-
// record for a partition the store could not actually read).
|
|
1385
|
+
// A match REJECTION is reported as cache-read and still degrades to
|
|
1386
|
+
// L2/KV, but a null KV result is not proof of absence under it: the
|
|
1387
|
+
// only real signal this read produced is the failure, so it returns
|
|
1388
|
+
// the family's read error (CACHE_READ_ERROR for segments). An
|
|
1389
|
+
// abandoned slow match keeps the fail-open latency-budget policy.
|
|
1390
|
+
if (matchError)
|
|
1169
1391
|
reportCacheError(
|
|
1170
1392
|
matchError,
|
|
1171
1393
|
"cache-read",
|
|
1172
|
-
|
|
1394
|
+
`[CFCacheStore] ${f.debugOp} L1 match`,
|
|
1173
1395
|
);
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
const kvResult = await this.kvGetSegment(key);
|
|
1185
|
-
return kvResult ?? CACHE_READ_ERROR;
|
|
1186
|
-
}
|
|
1187
|
-
// An abandoned slow match (timedOut) keeps the fail-open latency-budget
|
|
1188
|
-
// policy: degrade to L2/KV, and a KV null stays a miss.
|
|
1189
|
-
if (this.debug)
|
|
1190
|
-
this.emitDebug({
|
|
1191
|
-
op: "get",
|
|
1192
|
-
key,
|
|
1193
|
-
outcome: timedOut ? "match-timeout" : "l1-miss",
|
|
1194
|
-
matchMs,
|
|
1195
|
-
});
|
|
1196
|
-
return this.kvGetSegment(key);
|
|
1396
|
+
debug?.({
|
|
1397
|
+
outcome: matchError
|
|
1398
|
+
? "match-error"
|
|
1399
|
+
: timedOut
|
|
1400
|
+
? "match-timeout"
|
|
1401
|
+
: "l1-miss",
|
|
1402
|
+
matchMs,
|
|
1403
|
+
});
|
|
1404
|
+
if (!matchError) return this.readKv(key, f);
|
|
1405
|
+
return (await this.readKv(key, f)) ?? f.readError;
|
|
1197
1406
|
}
|
|
1198
1407
|
|
|
1199
1408
|
// A non-200 entry (a cached error response, or a foreign response that
|
|
1200
|
-
// landed on this key) is not valid
|
|
1201
|
-
//
|
|
1409
|
+
// landed on this key) is not valid data: a miss. Its KV read suppresses
|
|
1410
|
+
// revalidation, so a broken L1 entry hit concurrently serves KV-stale,
|
|
1411
|
+
// not a herd (the document read never did).
|
|
1202
1412
|
if (response.status !== 200) {
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
op: "get",
|
|
1206
|
-
key,
|
|
1207
|
-
outcome: "non-200",
|
|
1208
|
-
status: response.status,
|
|
1209
|
-
matchMs,
|
|
1210
|
-
});
|
|
1211
|
-
// Degraded fall-through: suppress revalidation so a broken L1 entry hit
|
|
1212
|
-
// concurrently serves KV-stale, not a herd. See kvGetSegment.
|
|
1213
|
-
return this.kvGetSegment(key, { suppressRevalidate: true });
|
|
1413
|
+
debug?.({ outcome: "non-200", status: response.status, matchMs });
|
|
1414
|
+
return this.readKv(key, f, f.family !== "response");
|
|
1214
1415
|
}
|
|
1215
1416
|
|
|
1216
1417
|
// Tag invalidation: a copy whose tags were invalidated after it was
|
|
@@ -1218,39 +1419,31 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1218
1419
|
// colo re-renders. With it, it reads as an L1 miss, because the tiers can
|
|
1219
1420
|
// diverge: another colo may have written KV after the marker (its own
|
|
1220
1421
|
// re-render, or a router.prerender() warm after updateTag()), and this
|
|
1221
|
-
// colo then serves that entry instead of rendering it again.
|
|
1222
|
-
//
|
|
1223
|
-
//
|
|
1224
|
-
//
|
|
1225
|
-
//
|
|
1226
|
-
// invalidated the tag itself keeps the plain miss (declinesKvFallThrough).
|
|
1422
|
+
// colo then serves that entry instead of rendering it again. readKv
|
|
1423
|
+
// checks it against the same markers (this request's memo: no second
|
|
1424
|
+
// marker read). The request that invalidated the tag itself keeps the
|
|
1425
|
+
// plain miss (declinesKvFallThrough). The marker-resolution tail is
|
|
1426
|
+
// measured only under debug.
|
|
1227
1427
|
const tagInfo = this.readTagInfo(response.headers);
|
|
1228
|
-
|
|
1229
|
-
// when debug is on, so the hot path pays nothing. It is the serial read
|
|
1230
|
-
// that sits between matchMs and bodyReadMs for a tagged entry.
|
|
1231
|
-
const markerStart = this.debug ? Date.now() : 0;
|
|
1428
|
+
const markerStart = debug ? Date.now() : 0;
|
|
1232
1429
|
const invalidated = await this.isL1Invalidated(
|
|
1233
1430
|
tagInfo.tags,
|
|
1234
1431
|
tagInfo.taggedAt,
|
|
1235
1432
|
response.headers,
|
|
1236
1433
|
);
|
|
1237
|
-
const markerMs =
|
|
1434
|
+
const markerMs = debug ? Date.now() - markerStart : undefined;
|
|
1238
1435
|
if (invalidated) {
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
matchMs,
|
|
1246
|
-
markerMs,
|
|
1247
|
-
});
|
|
1436
|
+
debug?.({
|
|
1437
|
+
outcome: "tag-invalidated",
|
|
1438
|
+
status: response.status,
|
|
1439
|
+
matchMs,
|
|
1440
|
+
markerMs,
|
|
1441
|
+
});
|
|
1248
1442
|
return this.declinesKvFallThrough(tagInfo.tags, tagInfo.taggedAt)
|
|
1249
1443
|
? null
|
|
1250
|
-
: this.
|
|
1444
|
+
: this.readKv(key, f);
|
|
1251
1445
|
}
|
|
1252
1446
|
|
|
1253
|
-
// Read status headers
|
|
1254
1447
|
const status = response.headers.get(CACHE_STATUS_HEADER);
|
|
1255
1448
|
const staleAt = Number(
|
|
1256
1449
|
response.headers.get(CACHE_STALE_AT_HEADER) ?? "0",
|
|
@@ -1258,7 +1451,6 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1258
1451
|
const revalidatingAt = Number(
|
|
1259
1452
|
response.headers.get(CACHE_REVALIDATING_AT_HEADER) ?? "0",
|
|
1260
1453
|
);
|
|
1261
|
-
|
|
1262
1454
|
const now = Date.now();
|
|
1263
1455
|
const isStale = staleAt > 0 && now > staleAt;
|
|
1264
1456
|
// Recency comes from our explicit revalidating-at stamp, not CF's `Age`
|
|
@@ -1268,19 +1460,37 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1268
1460
|
status === "REVALIDATING" &&
|
|
1269
1461
|
revalidatingAt > 0 &&
|
|
1270
1462
|
now - revalidatingAt < MAX_REVALIDATION_INTERVAL * 1000;
|
|
1463
|
+
const shouldRevalidate = isStale && !isRevalidating;
|
|
1464
|
+
|
|
1465
|
+
if (f.family === "response") {
|
|
1466
|
+
// The document body streams through verbatim, so its marker re-put
|
|
1467
|
+
// carries a clone. Integrity relies on the Cache API: cache.put
|
|
1468
|
+
// stores a response atomically or fails, so a truncated body is not
|
|
1469
|
+
// served back; buffering + hashing it would defeat streaming and add
|
|
1470
|
+
// a full read to every hit.
|
|
1471
|
+
if (shouldRevalidate)
|
|
1472
|
+
this.markRevalidating(cache, request, response.clone());
|
|
1473
|
+
return f.fromL1(this.toClientResponse(response), shouldRevalidate);
|
|
1474
|
+
}
|
|
1271
1475
|
|
|
1272
|
-
//
|
|
1273
|
-
//
|
|
1274
|
-
//
|
|
1275
|
-
|
|
1476
|
+
// Read the body under the edge-read budget BEFORE writing the
|
|
1477
|
+
// REVALIDATING marker: CF can resolve match() fast but stall the body
|
|
1478
|
+
// stream, and reading first bounds the stall and skips marking an entry
|
|
1479
|
+
// that could not be read.
|
|
1480
|
+
const bodyStart = debug ? Date.now() : 0;
|
|
1481
|
+
const {
|
|
1482
|
+
value: body,
|
|
1483
|
+
errored,
|
|
1484
|
+
error,
|
|
1485
|
+
} = await this.readJsonWithTimeout(response);
|
|
1486
|
+
const bodyReadMs = debug ? Date.now() - bodyStart : 0;
|
|
1487
|
+
// The informational-only `age` header is read only under debug.
|
|
1488
|
+
const debugL1 = debug
|
|
1276
1489
|
? (
|
|
1277
1490
|
outcome: CFCacheReadDebugEvent["outcome"],
|
|
1278
|
-
bodyReadMs: number,
|
|
1279
1491
|
shouldRevalidate?: boolean,
|
|
1280
1492
|
) =>
|
|
1281
|
-
|
|
1282
|
-
op: "get",
|
|
1283
|
-
key,
|
|
1493
|
+
debug({
|
|
1284
1494
|
outcome,
|
|
1285
1495
|
status: response.status,
|
|
1286
1496
|
cacheStatus: status,
|
|
@@ -1295,88 +1505,44 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1295
1505
|
bodyReadMs,
|
|
1296
1506
|
})
|
|
1297
1507
|
: undefined;
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
errored,
|
|
1305
|
-
error,
|
|
1306
|
-
} = await this.readJsonWithTimeout<CachedEntryData>(response);
|
|
1307
|
-
const bodyReadMs = Date.now() - bodyStart;
|
|
1308
|
-
if (data === undefined) {
|
|
1309
|
-
debugRead?.(errored ? "body-error" : "body-timeout", bodyReadMs);
|
|
1310
|
-
// A body-ERROR (corrupt/foreign body) self-heals via healCorruptL1:
|
|
1311
|
-
// report cache-corrupt, fall through to L2/KV (which overwrites the
|
|
1312
|
-
// bad entry), and evict only if KV had no good copy to promote. A
|
|
1313
|
-
// body-TIMEOUT is a degraded read of a likely-valid entry: leave it
|
|
1314
|
-
// intact and suppress revalidation so a stalling colo cannot herd.
|
|
1315
|
-
if (errored)
|
|
1316
|
-
return this.healCorruptL1(cache, request, error, "get", () =>
|
|
1317
|
-
this.kvGetSegment(key, { suppressRevalidate: false }),
|
|
1318
|
-
);
|
|
1319
|
-
return this.kvGetSegment(key, { suppressRevalidate: true });
|
|
1320
|
-
}
|
|
1321
|
-
debugRead?.(
|
|
1322
|
-
isRevalidating ? "l1-revalidating-guarded" : "l1-fresh",
|
|
1323
|
-
bodyReadMs,
|
|
1324
|
-
false,
|
|
1325
|
-
);
|
|
1326
|
-
return { data, shouldRevalidate: false };
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
// Case 2: Stale and needs revalidation.
|
|
1330
|
-
// Read the body under the edge-read budget BEFORE writing the REVALIDATING
|
|
1331
|
-
// marker. CF can resolve match() fast but stall the body stream; the prior
|
|
1332
|
-
// approach teed the stream and awaited cache.put(b1) first, which blocked
|
|
1333
|
-
// on that same stalled stream so the read budget could never fire on a
|
|
1334
|
-
// stale hit. Reading first bounds the stall and lets us skip marking an
|
|
1335
|
-
// entry we could not even read.
|
|
1336
|
-
const bodyStart = Date.now();
|
|
1337
|
-
const {
|
|
1338
|
-
value: data,
|
|
1339
|
-
errored,
|
|
1340
|
-
error,
|
|
1341
|
-
} = await this.readJsonWithTimeout<CachedEntryData>(response);
|
|
1342
|
-
const bodyReadMs = Date.now() - bodyStart;
|
|
1343
|
-
if (data === undefined) {
|
|
1344
|
-
debugRead?.(errored ? "body-error" : "body-timeout", bodyReadMs);
|
|
1345
|
-
// Heal + conditionally evict a body-error, suppress a body-timeout; see
|
|
1346
|
-
// Case 1.
|
|
1508
|
+
if (body === undefined) {
|
|
1509
|
+
debugL1?.(errored ? "body-error" : "body-timeout");
|
|
1510
|
+
// A body-ERROR (corrupt/foreign body) self-heals: healCorruptL1
|
|
1511
|
+
// reports it and evicts only if KV had no good copy to promote. A
|
|
1512
|
+
// body-TIMEOUT is a degraded read of a likely-valid entry: leave it
|
|
1513
|
+
// and suppress revalidation so a stalling colo cannot herd.
|
|
1347
1514
|
if (errored)
|
|
1348
1515
|
return this.healCorruptL1(
|
|
1349
1516
|
cache,
|
|
1350
1517
|
request,
|
|
1351
1518
|
error,
|
|
1352
|
-
|
|
1353
|
-
() => this.
|
|
1519
|
+
shouldRevalidate ? f.staleLabel : f.debugOp,
|
|
1520
|
+
() => this.readKv(key, f),
|
|
1354
1521
|
);
|
|
1355
|
-
return this.
|
|
1522
|
+
return this.readKv(key, f, true);
|
|
1356
1523
|
}
|
|
1357
1524
|
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1525
|
+
if (shouldRevalidate)
|
|
1526
|
+
this.markRevalidating(
|
|
1527
|
+
cache,
|
|
1528
|
+
request,
|
|
1529
|
+
response,
|
|
1530
|
+
JSON.stringify(body),
|
|
1531
|
+
"",
|
|
1532
|
+
);
|
|
1533
|
+
debugL1?.(
|
|
1534
|
+
shouldRevalidate
|
|
1535
|
+
? "l1-stale-revalidate"
|
|
1536
|
+
: isRevalidating
|
|
1537
|
+
? "l1-revalidating-guarded"
|
|
1538
|
+
: "l1-fresh",
|
|
1539
|
+
shouldRevalidate,
|
|
1368
1540
|
);
|
|
1369
|
-
|
|
1370
|
-
debugRead?.("l1-stale-revalidate", bodyReadMs, true);
|
|
1371
|
-
return { data, shouldRevalidate: true };
|
|
1541
|
+
return f.fromL1(body, shouldRevalidate, tagInfo.tags);
|
|
1372
1542
|
} catch (error) {
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
if (this.debug) this.emitDebug({ op: "get", key, outcome: "error" });
|
|
1377
|
-
// Distinct from a miss so the PPR replay composition renders uncached
|
|
1378
|
-
// instead of substituting the seeded doc record (CACHE_READ_ERROR).
|
|
1379
|
-
return CACHE_READ_ERROR;
|
|
1543
|
+
reportCacheError(error, "cache-read", `[CFCacheStore] ${f.debugOp}`);
|
|
1544
|
+
debug?.({ outcome: "error" });
|
|
1545
|
+
return f.readError;
|
|
1380
1546
|
}
|
|
1381
1547
|
}
|
|
1382
1548
|
|
|
@@ -1497,104 +1663,14 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1497
1663
|
// ============================================================================
|
|
1498
1664
|
|
|
1499
1665
|
/**
|
|
1500
|
-
* Get a cached Response by key (for document-level caching).
|
|
1501
|
-
*
|
|
1502
|
-
*
|
|
1666
|
+
* Get a cached Response by key (for document-level caching): see readL1.
|
|
1667
|
+
* Its body streams to the client unread, outside the debug surface. Falls
|
|
1668
|
+
* back to KV (L2) on L1 miss.
|
|
1503
1669
|
*/
|
|
1504
|
-
|
|
1670
|
+
getResponse(
|
|
1505
1671
|
key: string,
|
|
1506
1672
|
): Promise<{ response: Response; shouldRevalidate: boolean } | null> {
|
|
1507
|
-
|
|
1508
|
-
const cache = await this.getCache();
|
|
1509
|
-
const request = this.keyToRequest(`doc:${key}`, "document");
|
|
1510
|
-
// The document path is outside the debug surface (op is only get/getItem),
|
|
1511
|
-
// so the match-timeout flag is not surfaced as an event here -- though
|
|
1512
|
-
// matchWithTimeout still warns on a slow match. A miss or timeout falls
|
|
1513
|
-
// through to the KV document path and then render.
|
|
1514
|
-
const { response, error: matchError } = await this.matchWithTimeout(
|
|
1515
|
-
cache,
|
|
1516
|
-
request,
|
|
1517
|
-
);
|
|
1518
|
-
|
|
1519
|
-
if (!response || response.status !== 200) {
|
|
1520
|
-
// A transient L1 match rejection (matchError set; only ever set when
|
|
1521
|
-
// response is undefined) is surfaced as cache-read before degrading to
|
|
1522
|
-
// L2/KV -- matching get()/getItem(). A genuine miss or a non-200 hit
|
|
1523
|
-
// carries no matchError and reports nothing.
|
|
1524
|
-
if (matchError)
|
|
1525
|
-
reportCacheError(
|
|
1526
|
-
matchError,
|
|
1527
|
-
"cache-read",
|
|
1528
|
-
"[CFCacheStore] getResponse L1 match",
|
|
1529
|
-
);
|
|
1530
|
-
return this.kvGetResponse(key);
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
// Tag invalidation check: a rejected copy reads as an L1 miss (see get()).
|
|
1534
|
-
const tagInfo = this.readTagInfo(response.headers);
|
|
1535
|
-
if (
|
|
1536
|
-
await this.isL1Invalidated(
|
|
1537
|
-
tagInfo.tags,
|
|
1538
|
-
tagInfo.taggedAt,
|
|
1539
|
-
response.headers,
|
|
1540
|
-
)
|
|
1541
|
-
) {
|
|
1542
|
-
return this.declinesKvFallThrough(tagInfo.tags, tagInfo.taggedAt)
|
|
1543
|
-
? null
|
|
1544
|
-
: this.kvGetResponse(key);
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
// Check staleness
|
|
1548
|
-
const staleAt = Number(response.headers.get(CACHE_STALE_AT_HEADER) || 0);
|
|
1549
|
-
const now = Date.now();
|
|
1550
|
-
const isStale = staleAt > 0 && now > staleAt;
|
|
1551
|
-
|
|
1552
|
-
// Thundering-herd guard, mirroring the segment (get) and item (getItem)
|
|
1553
|
-
// tiers. Without it, every concurrent stale reader returned
|
|
1554
|
-
// shouldRevalidate=true and document-cache.ts scheduled a fresh render for
|
|
1555
|
-
// each one. Recency comes from our own revalidating-at stamp, not CF's Age
|
|
1556
|
-
// header (see CACHE_REVALIDATING_AT_HEADER); an absent/zero stamp counts as
|
|
1557
|
-
// "not recent" so a dropped revalidation re-arms instead of pinning.
|
|
1558
|
-
const status = response.headers.get(CACHE_STATUS_HEADER);
|
|
1559
|
-
const revalidatingAt = Number(
|
|
1560
|
-
response.headers.get(CACHE_REVALIDATING_AT_HEADER) ?? "0",
|
|
1561
|
-
);
|
|
1562
|
-
const isRevalidating =
|
|
1563
|
-
status === "REVALIDATING" &&
|
|
1564
|
-
revalidatingAt > 0 &&
|
|
1565
|
-
now - revalidatingAt < MAX_REVALIDATION_INTERVAL * 1000;
|
|
1566
|
-
|
|
1567
|
-
// L1 document bodies are streamed through verbatim - unlike the segment/
|
|
1568
|
-
// item tiers (which JSON-parse and so structurally detect corruption) and
|
|
1569
|
-
// the KV doc tier (validated in kvGetResponse, KV being the real partial-
|
|
1570
|
-
// read vector). Integrity here relies on the Cache API: cache.put stores a
|
|
1571
|
-
// response atomically or fails, so a truncated body is not served back. We
|
|
1572
|
-
// deliberately do NOT buffer+hash the body to re-verify it: that would
|
|
1573
|
-
// defeat streaming the document and add a full read to every cache hit.
|
|
1574
|
-
|
|
1575
|
-
if (isStale && !isRevalidating) {
|
|
1576
|
-
// First stale reader within the window: mark REVALIDATING (non-blocking,
|
|
1577
|
-
// best-effort) so concurrent readers below see the guard and suppress,
|
|
1578
|
-
// then return shouldRevalidate=true so this caller revalidates. Clone the
|
|
1579
|
-
// matched response for the marker since its original body must still
|
|
1580
|
-
// stream to the client.
|
|
1581
|
-
this.markResponseRevalidating(cache, request, response.clone());
|
|
1582
|
-
return {
|
|
1583
|
-
response: this.toClientResponse(response),
|
|
1584
|
-
shouldRevalidate: true,
|
|
1585
|
-
};
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
// Fresh, or stale-but-already-REVALIDATING: serve without scheduling a
|
|
1589
|
-
// (re-)revalidation. A recent marker already has a render in flight.
|
|
1590
|
-
return {
|
|
1591
|
-
response: this.toClientResponse(response),
|
|
1592
|
-
shouldRevalidate: false,
|
|
1593
|
-
};
|
|
1594
|
-
} catch (error) {
|
|
1595
|
-
reportCacheError(error, "cache-read", "[CFCacheStore] getResponse");
|
|
1596
|
-
return null;
|
|
1597
|
-
}
|
|
1673
|
+
return this.readL1(key, RESPONSE_READ);
|
|
1598
1674
|
}
|
|
1599
1675
|
|
|
1600
1676
|
/**
|
|
@@ -1684,7 +1760,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1684
1760
|
// toClientResponse before serving.
|
|
1685
1761
|
headers.set(CACHE_EXPIRES_AT_HEADER, String(staleAt + swrWindow * 1000));
|
|
1686
1762
|
// Internal tag headers (stripped by toClientResponse before serving).
|
|
1687
|
-
|
|
1763
|
+
setTagHeaders(headers, this.tagHeaderEntries(tags, taggedAt));
|
|
1688
1764
|
|
|
1689
1765
|
const toCache = new Response(l1Body, {
|
|
1690
1766
|
status: response.status,
|
|
@@ -1756,185 +1832,11 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1756
1832
|
|
|
1757
1833
|
/**
|
|
1758
1834
|
* Get a cached function result by key.
|
|
1759
|
-
* Follows the same SWR pattern as get() for segment caching.
|
|
1835
|
+
* Follows the same SWR pattern as get() for segment caching (readL1).
|
|
1760
1836
|
* Falls back to KV (L2) on L1 miss.
|
|
1761
1837
|
*/
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
const cache = await this.getCache();
|
|
1765
|
-
const request = this.keyToRequest(`fn:${key}`, "data");
|
|
1766
|
-
const matchStart = Date.now();
|
|
1767
|
-
const {
|
|
1768
|
-
response,
|
|
1769
|
-
timedOut,
|
|
1770
|
-
error: matchError,
|
|
1771
|
-
} = await this.matchWithTimeout(cache, request);
|
|
1772
|
-
const matchMs = Date.now() - matchStart;
|
|
1773
|
-
|
|
1774
|
-
if (!response) {
|
|
1775
|
-
// Transient match error reported cache-read; still degrades to L2/KV.
|
|
1776
|
-
if (matchError)
|
|
1777
|
-
reportCacheError(
|
|
1778
|
-
matchError,
|
|
1779
|
-
"cache-read",
|
|
1780
|
-
"[CFCacheStore] getItem L1 match",
|
|
1781
|
-
);
|
|
1782
|
-
if (this.debug)
|
|
1783
|
-
this.emitDebug({
|
|
1784
|
-
op: "getItem",
|
|
1785
|
-
key,
|
|
1786
|
-
// match-error (rejection) vs l1-miss (absence); see get().
|
|
1787
|
-
outcome: matchError
|
|
1788
|
-
? "match-error"
|
|
1789
|
-
: timedOut
|
|
1790
|
-
? "match-timeout"
|
|
1791
|
-
: "l1-miss",
|
|
1792
|
-
matchMs,
|
|
1793
|
-
});
|
|
1794
|
-
return this.kvGetItem(key);
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
// Non-200 entry is not a valid cached function result; treat as a miss.
|
|
1798
|
-
if (response.status !== 200) {
|
|
1799
|
-
if (this.debug)
|
|
1800
|
-
this.emitDebug({
|
|
1801
|
-
op: "getItem",
|
|
1802
|
-
key,
|
|
1803
|
-
outcome: "non-200",
|
|
1804
|
-
status: response.status,
|
|
1805
|
-
matchMs,
|
|
1806
|
-
});
|
|
1807
|
-
// Degraded fall-through: suppress revalidation so a broken L1 entry hit
|
|
1808
|
-
// concurrently serves KV-stale instead of spawning a herd (see get()).
|
|
1809
|
-
return this.kvGetItem(key, { suppressRevalidate: true });
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
// Tag invalidation check: a rejected copy reads as an L1 miss (see
|
|
1813
|
-
// get()). Measure the marker-resolution tail only under debug.
|
|
1814
|
-
const tagInfo = this.readTagInfo(response.headers);
|
|
1815
|
-
const markerStart = this.debug ? Date.now() : 0;
|
|
1816
|
-
const invalidated = await this.isL1Invalidated(
|
|
1817
|
-
tagInfo.tags,
|
|
1818
|
-
tagInfo.taggedAt,
|
|
1819
|
-
response.headers,
|
|
1820
|
-
);
|
|
1821
|
-
const markerMs = this.debug ? Date.now() - markerStart : undefined;
|
|
1822
|
-
if (invalidated) {
|
|
1823
|
-
if (this.debug)
|
|
1824
|
-
this.emitDebug({
|
|
1825
|
-
op: "getItem",
|
|
1826
|
-
key,
|
|
1827
|
-
outcome: "tag-invalidated",
|
|
1828
|
-
status: response.status,
|
|
1829
|
-
matchMs,
|
|
1830
|
-
markerMs,
|
|
1831
|
-
});
|
|
1832
|
-
return this.declinesKvFallThrough(tagInfo.tags, tagInfo.taggedAt)
|
|
1833
|
-
? null
|
|
1834
|
-
: this.kvGetItem(key);
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
const staleAt = Number(
|
|
1838
|
-
response.headers.get(CACHE_STALE_AT_HEADER) ?? "0",
|
|
1839
|
-
);
|
|
1840
|
-
const status = response.headers.get(CACHE_STATUS_HEADER);
|
|
1841
|
-
const revalidatingAt = Number(
|
|
1842
|
-
response.headers.get(CACHE_REVALIDATING_AT_HEADER) ?? "0",
|
|
1843
|
-
);
|
|
1844
|
-
|
|
1845
|
-
const now = Date.now();
|
|
1846
|
-
const isStale = staleAt > 0 && now > staleAt;
|
|
1847
|
-
// Recency from our explicit stamp, not CF's `Age` header (see get()).
|
|
1848
|
-
const isRevalidating =
|
|
1849
|
-
status === "REVALIDATING" &&
|
|
1850
|
-
revalidatingAt > 0 &&
|
|
1851
|
-
now - revalidatingAt < MAX_REVALIDATION_INTERVAL * 1000;
|
|
1852
|
-
|
|
1853
|
-
// Single emitter for the post-header L1 outcomes (see get()). Undefined
|
|
1854
|
-
// when debug is off, so the event object is never allocated on the hot
|
|
1855
|
-
// path; the informational-only `age` header is read lazily inside.
|
|
1856
|
-
const debugRead = this.debug
|
|
1857
|
-
? (
|
|
1858
|
-
outcome: CFCacheReadDebugEvent["outcome"],
|
|
1859
|
-
bodyReadMs: number,
|
|
1860
|
-
shouldRevalidate?: boolean,
|
|
1861
|
-
) =>
|
|
1862
|
-
this.emitDebug({
|
|
1863
|
-
op: "getItem",
|
|
1864
|
-
key,
|
|
1865
|
-
outcome,
|
|
1866
|
-
status: response.status,
|
|
1867
|
-
cacheStatus: status,
|
|
1868
|
-
staleAt,
|
|
1869
|
-
revalidatingAt,
|
|
1870
|
-
ageHeader: response.headers.get("age"),
|
|
1871
|
-
isStale,
|
|
1872
|
-
isRevalidating,
|
|
1873
|
-
shouldRevalidate,
|
|
1874
|
-
matchMs,
|
|
1875
|
-
markerMs,
|
|
1876
|
-
bodyReadMs,
|
|
1877
|
-
})
|
|
1878
|
-
: undefined;
|
|
1879
|
-
|
|
1880
|
-
const bodyStart = Date.now();
|
|
1881
|
-
const {
|
|
1882
|
-
value: data,
|
|
1883
|
-
errored,
|
|
1884
|
-
error,
|
|
1885
|
-
} = await this.readJsonWithTimeout<{
|
|
1886
|
-
value: string;
|
|
1887
|
-
handles?: string;
|
|
1888
|
-
}>(response);
|
|
1889
|
-
const bodyReadMs = Date.now() - bodyStart;
|
|
1890
|
-
if (data === undefined) {
|
|
1891
|
-
debugRead?.(errored ? "body-error" : "body-timeout", bodyReadMs);
|
|
1892
|
-
// Heal + conditionally evict a body-error, suppress a body-timeout; see
|
|
1893
|
-
// get().
|
|
1894
|
-
if (errored)
|
|
1895
|
-
return this.healCorruptL1(cache, request, error, "getItem", () =>
|
|
1896
|
-
this.kvGetItem(key, { suppressRevalidate: false }),
|
|
1897
|
-
);
|
|
1898
|
-
return this.kvGetItem(key, { suppressRevalidate: true });
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
if (!isStale || isRevalidating) {
|
|
1902
|
-
debugRead?.(
|
|
1903
|
-
isRevalidating ? "l1-revalidating-guarded" : "l1-fresh",
|
|
1904
|
-
bodyReadMs,
|
|
1905
|
-
false,
|
|
1906
|
-
);
|
|
1907
|
-
return {
|
|
1908
|
-
value: data.value,
|
|
1909
|
-
handles: data.handles,
|
|
1910
|
-
shouldRevalidate: false,
|
|
1911
|
-
tags: tagInfo.tags,
|
|
1912
|
-
};
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
// Stale and needs revalidation -- mark REVALIDATING (non-blocking,
|
|
1916
|
-
// best-effort, remaining-ttl) and return the stale value. See get() /
|
|
1917
|
-
// markRevalidating for the full rationale.
|
|
1918
|
-
this.markRevalidating(
|
|
1919
|
-
cache,
|
|
1920
|
-
request,
|
|
1921
|
-
response.headers,
|
|
1922
|
-
200,
|
|
1923
|
-
JSON.stringify(data),
|
|
1924
|
-
);
|
|
1925
|
-
|
|
1926
|
-
debugRead?.("l1-stale-revalidate", bodyReadMs, true);
|
|
1927
|
-
return {
|
|
1928
|
-
value: data.value,
|
|
1929
|
-
handles: data.handles,
|
|
1930
|
-
shouldRevalidate: true,
|
|
1931
|
-
tags: tagInfo.tags,
|
|
1932
|
-
};
|
|
1933
|
-
} catch (error) {
|
|
1934
|
-
reportCacheError(error, "cache-read", "[CFCacheStore] getItem");
|
|
1935
|
-
if (this.debug) this.emitDebug({ op: "getItem", key, outcome: "error" });
|
|
1936
|
-
return null;
|
|
1937
|
-
}
|
|
1838
|
+
getItem(key: string): Promise<CacheItemResult | null> {
|
|
1839
|
+
return this.readL1(key, ITEM_READ);
|
|
1938
1840
|
}
|
|
1939
1841
|
|
|
1940
1842
|
/**
|
|
@@ -2340,7 +2242,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2340
2242
|
* caller's `tagHints`: cfTagHints may have evicted the key while the memo
|
|
2341
2243
|
* still holds it. A shell a tag marker rejects is dropped and read from
|
|
2342
2244
|
* the store too, which may hold a capture from after the marker (see
|
|
2343
|
-
*
|
|
2245
|
+
* readL1(), "Tag invalidation"), unless the request's own mask rejected it
|
|
2344
2246
|
* (declinesKvFallThrough).
|
|
2345
2247
|
*/
|
|
2346
2248
|
private async readMemoizedShell(
|
|
@@ -3222,27 +3124,6 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
3222
3124
|
return true;
|
|
3223
3125
|
}
|
|
3224
3126
|
|
|
3225
|
-
/**
|
|
3226
|
-
* Merge the internal tag headers onto an existing Headers instance. The
|
|
3227
|
-
* from-scratch paths spread tagHeaderEntries() into an object-literal init;
|
|
3228
|
-
* the document put/promote paths build a Headers first, so they .set() each
|
|
3229
|
-
* entry instead — except `Cache-Tag`, which is APPENDED: a document author
|
|
3230
|
-
* may have set their own Cache-Tag, and clobbering it would break their CDN
|
|
3231
|
-
* purging. Append produces the comma-merged list CF expects.
|
|
3232
|
-
*/
|
|
3233
|
-
private setTagHeaders(
|
|
3234
|
-
headers: Headers,
|
|
3235
|
-
tags: string[] | undefined,
|
|
3236
|
-
taggedAt: number | undefined,
|
|
3237
|
-
): void {
|
|
3238
|
-
for (const [name, value] of Object.entries(
|
|
3239
|
-
this.tagHeaderEntries(tags, taggedAt),
|
|
3240
|
-
)) {
|
|
3241
|
-
if (name === "Cache-Tag") headers.append(name, value);
|
|
3242
|
-
else headers.set(name, value);
|
|
3243
|
-
}
|
|
3244
|
-
}
|
|
3245
|
-
|
|
3246
3127
|
/** Read an entry's tags/taggedAt back from its headers. */
|
|
3247
3128
|
private readTagInfo(headers: Headers): {
|
|
3248
3129
|
tags?: string[];
|
|
@@ -4024,254 +3905,102 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
4024
3905
|
// ============================================================================
|
|
4025
3906
|
|
|
4026
3907
|
/**
|
|
4027
|
-
* KV
|
|
4028
|
-
*
|
|
4029
|
-
*
|
|
3908
|
+
* The KV (L2) read behind an L1 miss, for every family: a missing,
|
|
3909
|
+
* timed-out, corrupt (kvGetOrEvict reports and evicts it), expired or
|
|
3910
|
+
* tag-invalidated envelope is a miss, a read failure is the family's read
|
|
3911
|
+
* error, and a hit is promoted to L1 in the background.
|
|
3912
|
+
*
|
|
3913
|
+
* A degraded L1 fall-through (body-timeout / non-200) passes
|
|
3914
|
+
* `suppressRevalidate`: KV has no REVALIDATING herd guard, so N concurrent
|
|
3915
|
+
* degraded reads would otherwise each spawn a render exactly when the colo
|
|
3916
|
+
* is already struggling. The stale data is still served and promoted; only
|
|
3917
|
+
* the revalidation is withheld.
|
|
4030
3918
|
* @internal
|
|
4031
3919
|
*/
|
|
4032
|
-
private async
|
|
3920
|
+
private async readKv<
|
|
3921
|
+
E extends KVDeadlines,
|
|
3922
|
+
V,
|
|
3923
|
+
Err extends CacheReadError | null,
|
|
3924
|
+
>(
|
|
4033
3925
|
key: string,
|
|
4034
|
-
|
|
4035
|
-
|
|
3926
|
+
f: CFReadFamily<E, unknown, V, Err>,
|
|
3927
|
+
suppressRevalidate = false,
|
|
3928
|
+
): Promise<V | null | Err> {
|
|
4036
3929
|
if (!this.kv) return null;
|
|
4037
|
-
|
|
3930
|
+
const debug = this.debugSink(f.debugOp, key);
|
|
4038
3931
|
try {
|
|
4039
|
-
const kvKey = await
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
// from a genuine kv-miss so the degradation is visible on wrangler tail.
|
|
4050
|
-
if (this.debug)
|
|
4051
|
-
this.emitDebug({ op: "get", key, outcome: "kv-timeout" });
|
|
4052
|
-
return null;
|
|
4053
|
-
}
|
|
3932
|
+
const kvKey = await (f.family === "response"
|
|
3933
|
+
? this.toDocKVKey(key)
|
|
3934
|
+
: this.toKVKey(f.prefix + key, f.version));
|
|
3935
|
+
const { value: envelope, timedOut } = await this.kvGetOrEvict(
|
|
3936
|
+
kvKey,
|
|
3937
|
+
f.validate,
|
|
3938
|
+
f.kvMethod,
|
|
3939
|
+
);
|
|
3940
|
+
// An abandoned slow read is distinct from a genuine kv-miss, so the
|
|
3941
|
+
// degradation is visible on wrangler tail.
|
|
4054
3942
|
if (!envelope) {
|
|
4055
|
-
|
|
4056
|
-
// kvGetOrEvict. Either way a miss.
|
|
4057
|
-
if (this.debug) this.emitDebug({ op: "get", key, outcome: "kv-miss" });
|
|
3943
|
+
debug?.({ outcome: timedOut ? "kv-timeout" : "kv-miss" });
|
|
4058
3944
|
return null;
|
|
4059
3945
|
}
|
|
4060
|
-
|
|
4061
3946
|
const now = Date.now();
|
|
4062
|
-
|
|
4063
|
-
// Hard-expired — treat as miss
|
|
3947
|
+
// Hard-expired: a miss.
|
|
4064
3948
|
if (now > envelope.e) {
|
|
4065
|
-
|
|
3949
|
+
debug?.({ outcome: "kv-miss" });
|
|
4066
3950
|
return null;
|
|
4067
3951
|
}
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
if (
|
|
4071
|
-
|
|
4072
|
-
) {
|
|
4073
|
-
if (this.debug)
|
|
4074
|
-
this.emitDebug({ op: "get", key, outcome: "tag-invalidated" });
|
|
3952
|
+
// KV entries answer to tag invalidation as L1 copies do.
|
|
3953
|
+
const { tags, taggedAt } = f.tags(envelope);
|
|
3954
|
+
if (await this.isGloballyInvalidated(tags, taggedAt)) {
|
|
3955
|
+
debug?.({ outcome: "tag-invalidated" });
|
|
4075
3956
|
return null;
|
|
4076
3957
|
}
|
|
4077
|
-
|
|
4078
|
-
// When this is a degraded L1 fall-through (body-timeout / non-200), the
|
|
4079
|
-
// caller asks us to suppress revalidation: KV has no REVALIDATING herd
|
|
4080
|
-
// guard, so N concurrent degraded reads would otherwise each spawn a
|
|
4081
|
-
// render exactly when the colo is already struggling. We still serve the
|
|
4082
|
-
// stale data and still promote to L1; only the revalidation is withheld.
|
|
4083
3958
|
const stale = now > envelope.s;
|
|
4084
|
-
const shouldRevalidate = stale && !
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
? "kv-fresh"
|
|
4095
|
-
: opts?.suppressRevalidate
|
|
4096
|
-
? "kv-stale-suppressed"
|
|
4097
|
-
: "kv-stale",
|
|
4098
|
-
shouldRevalidate,
|
|
4099
|
-
});
|
|
4100
|
-
return { data: envelope.d, shouldRevalidate };
|
|
4101
|
-
} catch (error) {
|
|
4102
|
-
reportCacheError(error, "cache-read", "[CFCacheStore] kvGetSegment");
|
|
4103
|
-
if (this.debug) this.emitDebug({ op: "get", key, outcome: "error" });
|
|
4104
|
-
// A KV failure is NOT proof of absence: returning null classified it a
|
|
4105
|
-
// real miss and let the PPR seeded fallback substitute the doc record
|
|
4106
|
-
// for a key partition the store could not actually read. Same sentinel
|
|
4107
|
-
// as get()'s own catch — lookupRouteDetailed classifies it `error` and
|
|
4108
|
-
// the render stays uncached. (A kvGetOrEvict TIMEOUT above stays null
|
|
4109
|
-
// by design: it is a bounded-latency degrade of a likely-healthy read,
|
|
4110
|
-
// and serving the equivalent seeded record there is the fallback
|
|
4111
|
-
// working as intended, not a masked failure.)
|
|
4112
|
-
return CACHE_READ_ERROR;
|
|
4113
|
-
}
|
|
4114
|
-
}
|
|
4115
|
-
|
|
4116
|
-
/**
|
|
4117
|
-
* Write segment data to KV.
|
|
4118
|
-
*
|
|
4119
|
-
* `dataJson` is the already-serialized CachedEntryData body also stored in
|
|
4120
|
-
* L1 — embedded as envelope.d without a second JSON.stringify walk.
|
|
4121
|
-
* @internal
|
|
4122
|
-
*/
|
|
4123
|
-
private kvSetSegment(
|
|
4124
|
-
key: string,
|
|
4125
|
-
dataJson: string,
|
|
4126
|
-
staleAt: number,
|
|
4127
|
-
totalTtl: number,
|
|
4128
|
-
swrWindow: number,
|
|
4129
|
-
): void {
|
|
4130
|
-
// KV requires expirationTtl >= 60s. Skip write for short-lived entries.
|
|
4131
|
-
if (!this.kv || !this.waitUntil || totalTtl < 60) return;
|
|
4132
|
-
|
|
4133
|
-
const expiresAt = staleAt + swrWindow * 1000;
|
|
4134
|
-
// Same wire shape as JSON.stringify({ d, s, e }) — dataJson is already
|
|
4135
|
-
// valid JSON for CachedEntryData, so embedding it avoids re-walking the tree.
|
|
4136
|
-
const envelopeJson = `{"d":${dataJson},"s":${staleAt},"e":${expiresAt}}`;
|
|
4137
|
-
|
|
4138
|
-
this.waitUntil(() =>
|
|
4139
|
-
reportingAsync(
|
|
4140
|
-
async () =>
|
|
4141
|
-
this.kv!.put(await this.toKVKey(key, "data"), envelopeJson, {
|
|
4142
|
-
expirationTtl: totalTtl,
|
|
4143
|
-
}),
|
|
4144
|
-
"cache-write",
|
|
4145
|
-
"[CFCacheStore] kvSetSegment",
|
|
4146
|
-
),
|
|
4147
|
-
);
|
|
4148
|
-
}
|
|
4149
|
-
|
|
4150
|
-
/**
|
|
4151
|
-
* Promote segment data from KV to L1 Cache API.
|
|
4152
|
-
* @internal
|
|
4153
|
-
*/
|
|
4154
|
-
private promoteSegmentToL1(key: string, envelope: KVSegmentEnvelope): void {
|
|
4155
|
-
if (!this.waitUntil) return;
|
|
4156
|
-
|
|
4157
|
-
this.waitUntil(() =>
|
|
4158
|
-
reportingAsync(
|
|
4159
|
-
async () => {
|
|
4160
|
-
const now = Date.now();
|
|
4161
|
-
const remainingTtl = Math.max(
|
|
4162
|
-
1,
|
|
4163
|
-
Math.floor((envelope.e - now) / 1000),
|
|
4164
|
-
);
|
|
4165
|
-
const cache = await this.getCache();
|
|
4166
|
-
const request = this.keyToRequest(key, "data");
|
|
4167
|
-
|
|
4168
|
-
const response = new Response(JSON.stringify(envelope.d), {
|
|
4169
|
-
headers: {
|
|
4170
|
-
"Content-Type": "application/json",
|
|
4171
|
-
"Cache-Control": `public, max-age=${remainingTtl}`,
|
|
4172
|
-
[CACHE_STALE_AT_HEADER]: String(envelope.s),
|
|
4173
|
-
// Carry the hard-expiry deadline so a promoted entry that later
|
|
4174
|
-
// goes stale re-puts with the correct remaining ttl (see set()).
|
|
4175
|
-
[CACHE_EXPIRES_AT_HEADER]: String(envelope.e),
|
|
4176
|
-
[CACHE_STATUS_HEADER]: "HIT",
|
|
4177
|
-
// Preserve tags across KV->L1 promotion so the promoted entry
|
|
4178
|
-
// stays tag-invalidatable.
|
|
4179
|
-
...this.tagHeaderEntries(envelope.d.tags, envelope.d.taggedAt),
|
|
4180
|
-
},
|
|
4181
|
-
});
|
|
4182
|
-
|
|
4183
|
-
await cache.put(request, response);
|
|
4184
|
-
},
|
|
4185
|
-
"cache-write",
|
|
4186
|
-
"[CFCacheStore] promoteSegmentToL1",
|
|
4187
|
-
),
|
|
4188
|
-
);
|
|
4189
|
-
}
|
|
4190
|
-
|
|
4191
|
-
/**
|
|
4192
|
-
* KV fallback for function cache reads.
|
|
4193
|
-
* @internal
|
|
4194
|
-
*/
|
|
4195
|
-
private async kvGetItem(
|
|
4196
|
-
key: string,
|
|
4197
|
-
opts?: { suppressRevalidate?: boolean },
|
|
4198
|
-
): Promise<CacheItemResult | null> {
|
|
4199
|
-
if (!this.kv) return null;
|
|
4200
|
-
|
|
4201
|
-
try {
|
|
4202
|
-
const kvKey = await this.toKVKey(`fn:${key}`, "data");
|
|
4203
|
-
const { value: envelope, timedOut } =
|
|
4204
|
-
await this.kvGetOrEvict<KVItemEnvelope>(
|
|
4205
|
-
kvKey,
|
|
4206
|
-
(e) =>
|
|
4207
|
-
typeof e.v === "string" &&
|
|
4208
|
-
typeof e.e === "number" &&
|
|
4209
|
-
typeof e.s === "number",
|
|
4210
|
-
"kvGetItem",
|
|
3959
|
+
const shouldRevalidate = stale && !suppressRevalidate;
|
|
3960
|
+
let value: V;
|
|
3961
|
+
try {
|
|
3962
|
+
value = f.fromKv(envelope, shouldRevalidate);
|
|
3963
|
+
} catch (error) {
|
|
3964
|
+
// Only a document envelope can fail to rebuild (RESPONSE_READ).
|
|
3965
|
+
reportCacheError(
|
|
3966
|
+
error,
|
|
3967
|
+
"cache-corrupt",
|
|
3968
|
+
`[CFCacheStore] ${f.kvMethod}: corrupt response envelope, evicting`,
|
|
4211
3969
|
);
|
|
4212
|
-
|
|
4213
|
-
if (this.debug)
|
|
4214
|
-
this.emitDebug({ op: "getItem", key, outcome: "kv-timeout" });
|
|
4215
|
-
return null;
|
|
4216
|
-
}
|
|
4217
|
-
if (!envelope) {
|
|
4218
|
-
if (this.debug)
|
|
4219
|
-
this.emitDebug({ op: "getItem", key, outcome: "kv-miss" });
|
|
3970
|
+
this.scheduleKvEvict(kvKey, f.kvMethod);
|
|
4220
3971
|
return null;
|
|
4221
3972
|
}
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
}
|
|
4230
|
-
|
|
4231
|
-
// Tag invalidation check (also covers the KV tier, not just L1).
|
|
4232
|
-
if (await this.isGloballyInvalidated(envelope.t, envelope.ta)) {
|
|
4233
|
-
if (this.debug)
|
|
4234
|
-
this.emitDebug({ op: "getItem", key, outcome: "tag-invalidated" });
|
|
4235
|
-
return null;
|
|
4236
|
-
}
|
|
4237
|
-
|
|
4238
|
-
// Degraded fall-through suppresses revalidation (no KV herd guard); see
|
|
4239
|
-
// kvGetSegment. Still serves stale and still promotes.
|
|
4240
|
-
const stale = now > envelope.s;
|
|
4241
|
-
const shouldRevalidate = stale && !opts?.suppressRevalidate;
|
|
4242
|
-
|
|
4243
|
-
// Promote to L1
|
|
4244
|
-
this.promoteItemToL1(key, envelope);
|
|
4245
|
-
|
|
4246
|
-
if (this.debug)
|
|
4247
|
-
this.emitDebug({
|
|
4248
|
-
op: "getItem",
|
|
4249
|
-
key,
|
|
4250
|
-
outcome: !stale
|
|
4251
|
-
? "kv-fresh"
|
|
4252
|
-
: opts?.suppressRevalidate
|
|
4253
|
-
? "kv-stale-suppressed"
|
|
4254
|
-
: "kv-stale",
|
|
4255
|
-
shouldRevalidate,
|
|
4256
|
-
});
|
|
4257
|
-
return {
|
|
4258
|
-
value: envelope.v,
|
|
4259
|
-
handles: envelope.h,
|
|
3973
|
+
this.promoteToL1(key, f, envelope);
|
|
3974
|
+
debug?.({
|
|
3975
|
+
outcome: !stale
|
|
3976
|
+
? "kv-fresh"
|
|
3977
|
+
: suppressRevalidate
|
|
3978
|
+
? "kv-stale-suppressed"
|
|
3979
|
+
: "kv-stale",
|
|
4260
3980
|
shouldRevalidate,
|
|
4261
|
-
|
|
4262
|
-
|
|
3981
|
+
});
|
|
3982
|
+
return value;
|
|
4263
3983
|
} catch (error) {
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
3984
|
+
// A failed KV read (5xx/429/network, left intact), or a promotion that
|
|
3985
|
+
// throws (ctx.waitUntil).
|
|
3986
|
+
reportCacheError(error, "cache-read", `[CFCacheStore] ${f.kvMethod}`);
|
|
3987
|
+
debug?.({ outcome: "error" });
|
|
3988
|
+
return f.readError;
|
|
4267
3989
|
}
|
|
4268
3990
|
}
|
|
4269
3991
|
|
|
4270
3992
|
/**
|
|
4271
|
-
* Promote
|
|
3993
|
+
* Promote a KV hit to L1 in the background, under its remaining ttl, with
|
|
3994
|
+
* its deadlines (so a promoted entry that later goes stale re-puts with the
|
|
3995
|
+
* correct remaining ttl, see set()) and its tags (so it stays
|
|
3996
|
+
* tag-invalidatable). The family builds the entry (toL1).
|
|
4272
3997
|
* @internal
|
|
4273
3998
|
*/
|
|
4274
|
-
private
|
|
3999
|
+
private promoteToL1<E extends KVDeadlines>(
|
|
4000
|
+
key: string,
|
|
4001
|
+
f: CFReadFamily<E, unknown, unknown, CacheReadError | null>,
|
|
4002
|
+
envelope: E,
|
|
4003
|
+
): void {
|
|
4275
4004
|
if (!this.waitUntil) return;
|
|
4276
4005
|
|
|
4277
4006
|
this.waitUntil(() =>
|
|
@@ -4283,170 +4012,53 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
4283
4012
|
Math.floor((envelope.e - now) / 1000),
|
|
4284
4013
|
);
|
|
4285
4014
|
const cache = await this.getCache();
|
|
4286
|
-
const request = this.keyToRequest(
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
[CACHE_STALE_AT_HEADER]: String(envelope.s),
|
|
4297
|
-
// Carry the hard-expiry deadline; see promoteSegmentToL1 / set().
|
|
4298
|
-
[CACHE_EXPIRES_AT_HEADER]: String(envelope.e),
|
|
4299
|
-
[CACHE_STATUS_HEADER]: "HIT",
|
|
4300
|
-
// Preserve tags across KV->L1 promotion (the item tier previously
|
|
4301
|
-
// dropped them, permanently disabling tag invalidation here).
|
|
4302
|
-
...this.tagHeaderEntries(envelope.t, envelope.ta),
|
|
4303
|
-
},
|
|
4304
|
-
});
|
|
4305
|
-
|
|
4306
|
-
await cache.put(request, response);
|
|
4015
|
+
const request = this.keyToRequest(f.prefix + key, f.version);
|
|
4016
|
+
const { tags, taggedAt } = f.tags(envelope);
|
|
4017
|
+
await cache.put(
|
|
4018
|
+
request,
|
|
4019
|
+
f.toL1(
|
|
4020
|
+
envelope,
|
|
4021
|
+
`public, max-age=${remainingTtl}`,
|
|
4022
|
+
this.tagHeaderEntries(tags, taggedAt),
|
|
4023
|
+
),
|
|
4024
|
+
);
|
|
4307
4025
|
},
|
|
4308
4026
|
"cache-write",
|
|
4309
|
-
|
|
4027
|
+
f.promoteLabel,
|
|
4310
4028
|
),
|
|
4311
4029
|
);
|
|
4312
4030
|
}
|
|
4313
4031
|
|
|
4314
4032
|
/**
|
|
4315
|
-
*
|
|
4033
|
+
* Write segment data to KV.
|
|
4034
|
+
*
|
|
4035
|
+
* `dataJson` is the already-serialized CachedEntryData body also stored in
|
|
4036
|
+
* L1 — embedded as envelope.d without a second JSON.stringify walk.
|
|
4316
4037
|
* @internal
|
|
4317
4038
|
*/
|
|
4318
|
-
private
|
|
4039
|
+
private kvSetSegment(
|
|
4319
4040
|
key: string,
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
// budget) but emits no kv-timeout event, so its absence from the debug
|
|
4328
|
-
// stream is expected. A null envelope is a miss -- missing key, a budget
|
|
4329
|
-
// timeout, or a corrupt entry already evicted + reported by kvGetOrEvict.
|
|
4330
|
-
const { value: envelope } = await this.kvGetOrEvict<KVResponseEnvelope>(
|
|
4331
|
-
kvKey,
|
|
4332
|
-
(e) =>
|
|
4333
|
-
typeof e.b === "string" &&
|
|
4334
|
-
typeof e.st === "number" &&
|
|
4335
|
-
typeof e.e === "number" &&
|
|
4336
|
-
typeof e.s === "number" &&
|
|
4337
|
-
// stx is optional but, if present, must be a string (feeds Response).
|
|
4338
|
-
(e.stx === undefined || typeof e.stx === "string") &&
|
|
4339
|
-
// hd must be an array of [name, value] string tuples; a malformed
|
|
4340
|
-
// shape would otherwise throw in `new Headers(hd)`. Validate it here
|
|
4341
|
-
// so a faulty envelope is a fail-open MISS, never a thrown read.
|
|
4342
|
-
Array.isArray(e.hd) &&
|
|
4343
|
-
e.hd.every(
|
|
4344
|
-
(entry) =>
|
|
4345
|
-
Array.isArray(entry) &&
|
|
4346
|
-
entry.length === 2 &&
|
|
4347
|
-
typeof entry[0] === "string" &&
|
|
4348
|
-
typeof entry[1] === "string",
|
|
4349
|
-
),
|
|
4350
|
-
"kvGetResponse",
|
|
4351
|
-
);
|
|
4352
|
-
if (!envelope) return null;
|
|
4353
|
-
|
|
4354
|
-
const now = Date.now();
|
|
4355
|
-
|
|
4356
|
-
if (now > envelope.e) return null;
|
|
4357
|
-
|
|
4358
|
-
// Tag invalidation check (also covers the KV tier, not just L1).
|
|
4359
|
-
if (await this.isGloballyInvalidated(envelope.t, envelope.ta)) {
|
|
4360
|
-
return null;
|
|
4361
|
-
}
|
|
4362
|
-
|
|
4363
|
-
const shouldRevalidate = now > envelope.s;
|
|
4364
|
-
|
|
4365
|
-
// Reconstruct Response: decode base64 -> binary, rebuild headers/status.
|
|
4366
|
-
// Corrupt/partial base64 throws in atob; malformed `hd` or an out-of-range
|
|
4367
|
-
// `st` throws in new Headers/new Response. Any of these is a faulty entry,
|
|
4368
|
-
// so evict it and miss rather than re-failing every read until TTL.
|
|
4369
|
-
let response: Response;
|
|
4370
|
-
try {
|
|
4371
|
-
// Finding #3 (read side): strip per-client signals a stale envelope may
|
|
4372
|
-
// carry. Inside the try so a malformed `hd` evicts (not throws through);
|
|
4373
|
-
// mutates `hd` in place so promoteResponseToL1 re-seeds from it too.
|
|
4374
|
-
envelope.hd = envelope.hd.filter(
|
|
4375
|
-
([name]) => !isPerClientSignalHeader(name),
|
|
4376
|
-
);
|
|
4377
|
-
const bodyBuffer = base64ToBuffer(envelope.b);
|
|
4378
|
-
const headers = new Headers(envelope.hd);
|
|
4379
|
-
response = new Response(bodyBuffer, {
|
|
4380
|
-
status: envelope.st,
|
|
4381
|
-
statusText: envelope.stx,
|
|
4382
|
-
headers,
|
|
4383
|
-
});
|
|
4384
|
-
} catch (error) {
|
|
4385
|
-
reportCacheError(
|
|
4386
|
-
error,
|
|
4387
|
-
"cache-corrupt",
|
|
4388
|
-
"[CFCacheStore] kvGetResponse: corrupt response envelope, evicting",
|
|
4389
|
-
);
|
|
4390
|
-
this.scheduleKvEvict(kvKey, "kvGetResponse");
|
|
4391
|
-
return null;
|
|
4392
|
-
}
|
|
4393
|
-
|
|
4394
|
-
// Promote to L1
|
|
4395
|
-
this.promoteResponseToL1(key, envelope);
|
|
4396
|
-
|
|
4397
|
-
return { response, shouldRevalidate };
|
|
4398
|
-
} catch (error) {
|
|
4399
|
-
reportCacheError(error, "cache-read", "[CFCacheStore] kvGetResponse");
|
|
4400
|
-
return null;
|
|
4401
|
-
}
|
|
4402
|
-
}
|
|
4041
|
+
dataJson: string,
|
|
4042
|
+
staleAt: number,
|
|
4043
|
+
totalTtl: number,
|
|
4044
|
+
swrWindow: number,
|
|
4045
|
+
): void {
|
|
4046
|
+
// KV requires expirationTtl >= 60s. Skip write for short-lived entries.
|
|
4047
|
+
if (!this.kv || !this.waitUntil || totalTtl < 60) return;
|
|
4403
4048
|
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
private promoteResponseToL1(key: string, envelope: KVResponseEnvelope): void {
|
|
4409
|
-
if (!this.waitUntil) return;
|
|
4049
|
+
const expiresAt = staleAt + swrWindow * 1000;
|
|
4050
|
+
// Same wire shape as JSON.stringify({ d, s, e }) — dataJson is already
|
|
4051
|
+
// valid JSON for CachedEntryData, so embedding it avoids re-walking the tree.
|
|
4052
|
+
const envelopeJson = `{"d":${dataJson},"s":${staleAt},"e":${expiresAt}}`;
|
|
4410
4053
|
|
|
4411
4054
|
this.waitUntil(() =>
|
|
4412
4055
|
reportingAsync(
|
|
4413
|
-
async () =>
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
Math.floor((envelope.e - now) / 1000),
|
|
4418
|
-
);
|
|
4419
|
-
const cache = await this.getCache();
|
|
4420
|
-
const request = this.keyToRequest(`doc:${key}`, "document");
|
|
4421
|
-
|
|
4422
|
-
const headers = new Headers(envelope.hd);
|
|
4423
|
-
const originalCacheControl = headers.get("Cache-Control");
|
|
4424
|
-
if (originalCacheControl !== null) {
|
|
4425
|
-
headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
|
|
4426
|
-
}
|
|
4427
|
-
headers.set("Cache-Control", `public, max-age=${remainingTtl}`);
|
|
4428
|
-
headers.set(CACHE_STALE_AT_HEADER, String(envelope.s));
|
|
4429
|
-
// Carry the hard-expiry deadline so the document herd guard's
|
|
4430
|
-
// markResponseRevalidating re-put can compute the remaining window
|
|
4431
|
-
// (matches promoteSegmentToL1/promoteItemToL1); without it a stale
|
|
4432
|
-
// re-put would floor to max-age=1 and churn the KV-promoted twin.
|
|
4433
|
-
headers.set(CACHE_EXPIRES_AT_HEADER, String(envelope.e));
|
|
4434
|
-
// Re-attach the internal tag headers (envelope.hd is client-facing
|
|
4435
|
-
// and intentionally excludes them) so the promoted entry stays
|
|
4436
|
-
// invalidatable.
|
|
4437
|
-
this.setTagHeaders(headers, envelope.t, envelope.ta);
|
|
4438
|
-
|
|
4439
|
-
const bodyBuffer = base64ToBuffer(envelope.b);
|
|
4440
|
-
const response = new Response(bodyBuffer, {
|
|
4441
|
-
status: envelope.st,
|
|
4442
|
-
statusText: envelope.stx,
|
|
4443
|
-
headers,
|
|
4444
|
-
});
|
|
4445
|
-
|
|
4446
|
-
await cache.put(request, response);
|
|
4447
|
-
},
|
|
4056
|
+
async () =>
|
|
4057
|
+
this.kv!.put(await this.toKVKey(key, "data"), envelopeJson, {
|
|
4058
|
+
expirationTtl: totalTtl,
|
|
4059
|
+
}),
|
|
4448
4060
|
"cache-write",
|
|
4449
|
-
"[CFCacheStore]
|
|
4061
|
+
"[CFCacheStore] kvSetSegment",
|
|
4450
4062
|
),
|
|
4451
4063
|
);
|
|
4452
4064
|
}
|