@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bd6e11bc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -43
- package/dist/bin/rango.js +277 -99
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +2779 -1064
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +243 -21
- package/skills/caching/SKILL.md +155 -6
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +273 -53
- package/skills/middleware/SKILL.md +49 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +197 -6
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +242 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +88 -4
- package/skills/router-setup/SKILL.md +90 -5
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +716 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/__internal.ts +1 -1
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +91 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +102 -16
- package/src/browser/navigation-client.ts +164 -59
- package/src/browser/navigation-store.ts +75 -17
- package/src/browser/navigation-transaction.ts +21 -37
- package/src/browser/partial-update.ts +139 -38
- package/src/browser/prefetch/cache.ts +175 -15
- package/src/browser/prefetch/fetch.ts +180 -33
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +81 -9
- package/src/browser/react/NavigationProvider.tsx +110 -33
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +23 -64
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +43 -10
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +191 -74
- package/src/browser/scroll-restoration.ts +41 -14
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +31 -36
- package/src/browser/types.ts +57 -5
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -88
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +76 -49
- package/src/cache/cf/cf-cache-store.ts +501 -18
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +94 -238
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +65 -12
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +12 -5
- package/src/index.ts +61 -11
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +141 -80
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +435 -260
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +110 -34
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +113 -1
- package/src/router/error-handling.ts +1 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +77 -38
- package/src/router/intercept-resolution.ts +15 -22
- package/src/router/lazy-includes.ts +12 -9
- package/src/router/loader-resolution.ts +174 -22
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +128 -192
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +136 -106
- package/src/router/match-middleware/cache-store.ts +54 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +125 -10
- package/src/router/metrics.ts +7 -2
- package/src/router/middleware-types.ts +21 -34
- package/src/router/middleware.ts +103 -90
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +286 -0
- package/src/router/revalidation.ts +58 -2
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +77 -28
- package/src/router/router-options.ts +76 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +223 -24
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +466 -285
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +9 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +91 -23
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +440 -381
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +18 -2
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +41 -48
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +25 -37
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +17 -3
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +219 -67
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +277 -61
- package/src/server/cookie-store.ts +28 -4
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +204 -60
- package/src/ssr/index.tsx +9 -1
- package/src/static-handler.ts +19 -7
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +255 -0
- package/src/testing/render-route.tsx +565 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +179 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +183 -0
- package/src/types/cache-types.ts +4 -4
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +194 -72
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +37 -1
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +50 -9
- package/src/urls/path-helper.ts +63 -63
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +487 -44
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +34 -37
- package/src/vite/discovery/discover-routers.ts +105 -51
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +188 -93
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +46 -6
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +6 -0
- package/src/vite/plugin-types.ts +111 -72
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +55 -33
- package/src/vite/plugins/expose-id-utils.ts +24 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +544 -317
- package/src/vite/plugins/performance-tracks.ts +92 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +72 -3
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +265 -226
- package/src/vite/router-discovery.ts +920 -137
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +38 -5
- package/src/vite/utils/shared-utils.ts +109 -27
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -10,14 +10,21 @@ declare global {
|
|
|
10
10
|
/**
|
|
11
11
|
* Cloudflare Edge Cache Store
|
|
12
12
|
*
|
|
13
|
-
* Production cache store using Cloudflare's Cache API
|
|
14
|
-
*
|
|
13
|
+
* Production cache store using Cloudflare's Cache API (L1) with optional
|
|
14
|
+
* KV persistence (L2).
|
|
15
|
+
*
|
|
16
|
+
* L1 (Cache API): Per-colo, fast, ephemeral. Handles SWR atomically.
|
|
17
|
+
* L2 (KV): Global, persistent, ~50ms reads. Auto-warms cold colos.
|
|
18
|
+
*
|
|
19
|
+
* Read flow: L1 hit → serve | L1 miss → L2 hit → serve + promote to L1 | both miss → render
|
|
20
|
+
* Write flow: L1 write + L2 write (both via waitUntil)
|
|
15
21
|
*
|
|
16
22
|
* Features:
|
|
17
23
|
* - Extended TTL for SWR window (max-age = ttl + swr)
|
|
18
24
|
* - Staleness via x-edge-cache-stale-at header
|
|
19
|
-
* - Atomic REVALIDATING status for thundering herd prevention
|
|
25
|
+
* - Atomic REVALIDATING status for thundering herd prevention (L1 only)
|
|
20
26
|
* - Non-blocking writes via waitUntil
|
|
27
|
+
* - KV L2 for cross-colo cache persistence
|
|
21
28
|
*/
|
|
22
29
|
|
|
23
30
|
import type {
|
|
@@ -49,6 +56,15 @@ export const CACHE_STALE_AT_HEADER = "x-edge-cache-stale-at";
|
|
|
49
56
|
/** Header storing cache status: HIT | REVALIDATING */
|
|
50
57
|
export const CACHE_STATUS_HEADER = "x-edge-cache-status";
|
|
51
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Header stashing the route author's original Cache-Control on L1 document
|
|
61
|
+
* entries. putResponse/promoteResponseToL1 overwrite Cache-Control with a long
|
|
62
|
+
* `max-age` so the CF Cache API retains the entry across the whole SWR window;
|
|
63
|
+
* getResponse restores this original value before serving so the client and any
|
|
64
|
+
* upstream CDN see the author's intended directive, not the internal edge TTL.
|
|
65
|
+
*/
|
|
66
|
+
const CACHE_ORIG_CC_HEADER = "x-edge-cache-orig-cc";
|
|
67
|
+
|
|
52
68
|
/**
|
|
53
69
|
* Maximum age in seconds for REVALIDATING status before allowing new revalidation.
|
|
54
70
|
* After this period, a stale entry in REVALIDATING status will trigger revalidation again.
|
|
@@ -60,12 +76,71 @@ export const MAX_REVALIDATION_INTERVAL = 30;
|
|
|
60
76
|
// Types
|
|
61
77
|
// ============================================================================
|
|
62
78
|
|
|
79
|
+
// Re-exported from the canonical home so cf-cache-store consumers keep
|
|
80
|
+
// importing `ExecutionContext` from this module without a second interface
|
|
81
|
+
// drifting over time.
|
|
82
|
+
export type { ExecutionContext } from "../../types/request-scope.js";
|
|
83
|
+
import type { ExecutionContext } from "../../types/request-scope.js";
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Minimal Cloudflare KV Namespace interface.
|
|
87
|
+
* Avoids hard dependency on @cloudflare/workers-types.
|
|
88
|
+
*/
|
|
89
|
+
export interface KVNamespace {
|
|
90
|
+
get(key: string, options?: { type?: string }): Promise<any>;
|
|
91
|
+
put(
|
|
92
|
+
key: string,
|
|
93
|
+
value: string,
|
|
94
|
+
options?: { expirationTtl?: number },
|
|
95
|
+
): Promise<void>;
|
|
96
|
+
delete(key: string): Promise<void>;
|
|
97
|
+
}
|
|
98
|
+
|
|
63
99
|
/**
|
|
64
|
-
*
|
|
100
|
+
* KV envelope for segment cache entries.
|
|
101
|
+
* @internal
|
|
65
102
|
*/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
103
|
+
interface KVSegmentEnvelope {
|
|
104
|
+
/** Cached segment data */
|
|
105
|
+
d: CachedEntryData;
|
|
106
|
+
/** When entry becomes stale (ms epoch) */
|
|
107
|
+
s: number;
|
|
108
|
+
/** When entry hard-expires (ms epoch) */
|
|
109
|
+
e: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* KV envelope for function cache entries ("use cache").
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
interface KVItemEnvelope {
|
|
117
|
+
/** RSC-serialized return value */
|
|
118
|
+
v: string;
|
|
119
|
+
/** Handle data */
|
|
120
|
+
h?: Record<string, Record<string, unknown[]>>;
|
|
121
|
+
/** When entry becomes stale (ms epoch) */
|
|
122
|
+
s: number;
|
|
123
|
+
/** When entry hard-expires (ms epoch) */
|
|
124
|
+
e: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* KV envelope for document cache entries.
|
|
129
|
+
* @internal
|
|
130
|
+
*/
|
|
131
|
+
interface KVResponseEnvelope {
|
|
132
|
+
/** Response body as base64-encoded string (safe for binary payloads) */
|
|
133
|
+
b: string;
|
|
134
|
+
/** HTTP status code */
|
|
135
|
+
st: number;
|
|
136
|
+
/** HTTP status text */
|
|
137
|
+
stx: string;
|
|
138
|
+
/** Serialized headers as key-value pairs */
|
|
139
|
+
hd: [string, string][];
|
|
140
|
+
/** When entry becomes stale (ms epoch) */
|
|
141
|
+
s: number;
|
|
142
|
+
/** When entry hard-expires (ms epoch) */
|
|
143
|
+
e: number;
|
|
69
144
|
}
|
|
70
145
|
|
|
71
146
|
export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
@@ -98,11 +173,25 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
98
173
|
*/
|
|
99
174
|
ctx: ExecutionContext;
|
|
100
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Optional KV namespace for L2 cache persistence.
|
|
178
|
+
*
|
|
179
|
+
* When provided, KV acts as a global fallback behind the per-colo Cache API.
|
|
180
|
+
* On L1 miss, KV is checked and hits are promoted back to L1.
|
|
181
|
+
* On writes, data is persisted to both L1 and KV.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```typescript
|
|
185
|
+
* new CFCacheStore({ ctx: env.ctx, kv: env.CACHE_KV })
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
kv?: KVNamespace;
|
|
189
|
+
|
|
101
190
|
/**
|
|
102
191
|
* Cache version string override. When this changes, all cached entries are
|
|
103
192
|
* effectively invalidated (new keys won't match old entries).
|
|
104
193
|
*
|
|
105
|
-
* Defaults to the auto-generated VERSION from
|
|
194
|
+
* Defaults to the auto-generated VERSION from the `@rangojs/router:version` virtual module.
|
|
106
195
|
* Only set this if you need a custom versioning strategy.
|
|
107
196
|
*/
|
|
108
197
|
version?: string;
|
|
@@ -163,6 +252,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
163
252
|
private readonly baseUrl: string;
|
|
164
253
|
private readonly waitUntil?: (fn: () => Promise<void>) => void;
|
|
165
254
|
private readonly version?: string;
|
|
255
|
+
private readonly kv?: KVNamespace;
|
|
166
256
|
|
|
167
257
|
constructor(options: CFCacheStoreOptions<TEnv>) {
|
|
168
258
|
if (!options.ctx) {
|
|
@@ -179,6 +269,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
179
269
|
this.version = options.version ?? VERSION;
|
|
180
270
|
this.keyGenerator = options.keyGenerator;
|
|
181
271
|
this.waitUntil = (fn) => options.ctx.waitUntil(fn());
|
|
272
|
+
this.kv = options.kv;
|
|
182
273
|
}
|
|
183
274
|
|
|
184
275
|
/**
|
|
@@ -232,6 +323,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
232
323
|
return caches.default;
|
|
233
324
|
}
|
|
234
325
|
|
|
326
|
+
// ============================================================================
|
|
327
|
+
// Segment Cache Methods
|
|
328
|
+
// ============================================================================
|
|
329
|
+
|
|
235
330
|
/**
|
|
236
331
|
* Get cached entry data by key.
|
|
237
332
|
*
|
|
@@ -240,7 +335,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
240
335
|
* - If already REVALIDATING (and recent), returns shouldRevalidate: false
|
|
241
336
|
* - If fresh, returns shouldRevalidate: false
|
|
242
337
|
*
|
|
243
|
-
*
|
|
338
|
+
* On L1 miss, falls back to KV (L2) if configured.
|
|
339
|
+
* KV hits are promoted to L1 in the background.
|
|
244
340
|
*/
|
|
245
341
|
async get(key: string): Promise<CacheGetResult | null> {
|
|
246
342
|
try {
|
|
@@ -249,7 +345,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
249
345
|
const response = await cache.match(request);
|
|
250
346
|
|
|
251
347
|
if (!response) {
|
|
252
|
-
return
|
|
348
|
+
return this.kvGetSegment(key);
|
|
253
349
|
}
|
|
254
350
|
|
|
255
351
|
// Read status headers
|
|
@@ -292,6 +388,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
292
388
|
/**
|
|
293
389
|
* Store entry data with TTL and optional SWR window.
|
|
294
390
|
* Uses waitUntil for non-blocking write when available.
|
|
391
|
+
* When KV is configured, also persists to L2.
|
|
295
392
|
*/
|
|
296
393
|
async set(
|
|
297
394
|
key: string,
|
|
@@ -308,7 +405,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
308
405
|
const totalTtl = ttl + swrWindow;
|
|
309
406
|
const staleAt = Date.now() + ttl * 1000;
|
|
310
407
|
|
|
311
|
-
const
|
|
408
|
+
const body = JSON.stringify(data);
|
|
409
|
+
const response = new Response(body, {
|
|
312
410
|
headers: {
|
|
313
411
|
"Content-Type": "application/json",
|
|
314
412
|
"Cache-Control": `public, max-age=${totalTtl}`,
|
|
@@ -328,18 +426,35 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
328
426
|
// Blocking fallback
|
|
329
427
|
await putPromise;
|
|
330
428
|
}
|
|
429
|
+
|
|
430
|
+
// L2: persist to KV
|
|
431
|
+
this.kvSetSegment(key, data, staleAt, totalTtl, swrWindow);
|
|
331
432
|
} catch (error) {
|
|
332
433
|
console.error("[CFCacheStore] set failed:", error);
|
|
333
434
|
}
|
|
334
435
|
}
|
|
335
436
|
|
|
336
437
|
/**
|
|
337
|
-
* Delete a cached entry
|
|
438
|
+
* Delete a cached entry from L1 and L2.
|
|
338
439
|
*/
|
|
339
440
|
async delete(key: string): Promise<boolean> {
|
|
340
441
|
try {
|
|
341
442
|
const cache = await this.getCache();
|
|
342
|
-
|
|
443
|
+
const result = await cache.delete(this.keyToRequest(key));
|
|
444
|
+
|
|
445
|
+
// L2: delete from KV
|
|
446
|
+
if (this.kv && this.waitUntil) {
|
|
447
|
+
const kvKey = this.toKVKey(key);
|
|
448
|
+
this.waitUntil(async () => {
|
|
449
|
+
try {
|
|
450
|
+
await this.kv!.delete(kvKey);
|
|
451
|
+
} catch {
|
|
452
|
+
// KV delete failures are non-critical
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return result;
|
|
343
458
|
} catch (error) {
|
|
344
459
|
console.error("[CFCacheStore] delete failed:", error);
|
|
345
460
|
return false;
|
|
@@ -353,6 +468,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
353
468
|
/**
|
|
354
469
|
* Get a cached Response by key (for document-level caching).
|
|
355
470
|
* Returns the response and whether it should be revalidated (SWR).
|
|
471
|
+
* Falls back to KV (L2) on L1 miss.
|
|
356
472
|
*/
|
|
357
473
|
async getResponse(
|
|
358
474
|
key: string,
|
|
@@ -363,7 +479,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
363
479
|
const response = await cache.match(request);
|
|
364
480
|
|
|
365
481
|
if (!response || response.status !== 200) {
|
|
366
|
-
return
|
|
482
|
+
return this.kvGetResponse(key);
|
|
367
483
|
}
|
|
368
484
|
|
|
369
485
|
// Check staleness
|
|
@@ -371,7 +487,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
371
487
|
const isStale = staleAt > 0 && Date.now() > staleAt;
|
|
372
488
|
|
|
373
489
|
return {
|
|
374
|
-
response,
|
|
490
|
+
response: this.toClientResponse(response),
|
|
375
491
|
shouldRevalidate: isStale,
|
|
376
492
|
};
|
|
377
493
|
} catch (error) {
|
|
@@ -380,8 +496,33 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
380
496
|
}
|
|
381
497
|
}
|
|
382
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Strip internal edge headers and restore the author's Cache-Control before a
|
|
501
|
+
* cached document Response is served to a client. L1 entries carry the
|
|
502
|
+
* internal staleness/status headers and a rewritten Cache-Control; none of
|
|
503
|
+
* those should reach the browser or an upstream CDN.
|
|
504
|
+
*/
|
|
505
|
+
private toClientResponse(response: Response): Response {
|
|
506
|
+
const headers = new Headers(response.headers);
|
|
507
|
+
const originalCacheControl = headers.get(CACHE_ORIG_CC_HEADER);
|
|
508
|
+
if (originalCacheControl !== null) {
|
|
509
|
+
headers.set("Cache-Control", originalCacheControl);
|
|
510
|
+
} else {
|
|
511
|
+
headers.delete("Cache-Control");
|
|
512
|
+
}
|
|
513
|
+
headers.delete(CACHE_ORIG_CC_HEADER);
|
|
514
|
+
headers.delete(CACHE_STALE_AT_HEADER);
|
|
515
|
+
headers.delete(CACHE_STATUS_HEADER);
|
|
516
|
+
return new Response(response.body, {
|
|
517
|
+
status: response.status,
|
|
518
|
+
statusText: response.statusText,
|
|
519
|
+
headers,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
383
523
|
/**
|
|
384
524
|
* Store a Response with TTL and optional SWR window (for document-level caching).
|
|
525
|
+
* When KV is configured, also persists to L2.
|
|
385
526
|
*/
|
|
386
527
|
async putResponse(
|
|
387
528
|
key: string,
|
|
@@ -398,12 +539,25 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
398
539
|
const totalTtl = ttl + swrWindow;
|
|
399
540
|
const staleAt = Date.now() + ttl * 1000;
|
|
400
541
|
|
|
401
|
-
// Clone
|
|
542
|
+
// Clone body for potential KV write before consuming it for L1
|
|
543
|
+
const [l1Body, kvBody] = this.kv
|
|
544
|
+
? response.body
|
|
545
|
+
? response.body.tee()
|
|
546
|
+
: [null, null]
|
|
547
|
+
: [response.body, null];
|
|
548
|
+
|
|
549
|
+
// Clone and add cache headers. The author's Cache-Control is stashed and
|
|
550
|
+
// replaced with a long max-age so the CF Cache API holds the entry across
|
|
551
|
+
// the SWR window; getResponse restores the original before serving.
|
|
402
552
|
const headers = new Headers(response.headers);
|
|
553
|
+
const originalCacheControl = response.headers.get("Cache-Control");
|
|
554
|
+
if (originalCacheControl !== null) {
|
|
555
|
+
headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
|
|
556
|
+
}
|
|
403
557
|
headers.set("Cache-Control", `public, max-age=${totalTtl}`);
|
|
404
558
|
headers.set(CACHE_STALE_AT_HEADER, String(staleAt));
|
|
405
559
|
|
|
406
|
-
const toCache = new Response(
|
|
560
|
+
const toCache = new Response(l1Body, {
|
|
407
561
|
status: response.status,
|
|
408
562
|
statusText: response.statusText,
|
|
409
563
|
headers,
|
|
@@ -420,6 +574,36 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
420
574
|
// Blocking fallback
|
|
421
575
|
await putPromise;
|
|
422
576
|
}
|
|
577
|
+
|
|
578
|
+
// L2: persist to KV (KV requires expirationTtl >= 60s)
|
|
579
|
+
if (this.kv && this.waitUntil && totalTtl >= 60) {
|
|
580
|
+
const kvKey = this.toKVKey(`doc:${key}`);
|
|
581
|
+
const headersArray: [string, string][] = [];
|
|
582
|
+
response.headers.forEach((v, k) => headersArray.push([k, v]));
|
|
583
|
+
// Read body as ArrayBuffer and encode to base64 to preserve binary payloads
|
|
584
|
+
const bodyBuf = kvBody
|
|
585
|
+
? await new Response(kvBody).arrayBuffer()
|
|
586
|
+
: new ArrayBuffer(0);
|
|
587
|
+
const bodyBase64 = bufferToBase64(bodyBuf);
|
|
588
|
+
|
|
589
|
+
this.waitUntil(async () => {
|
|
590
|
+
try {
|
|
591
|
+
const envelope: KVResponseEnvelope = {
|
|
592
|
+
b: bodyBase64,
|
|
593
|
+
st: response.status,
|
|
594
|
+
stx: response.statusText,
|
|
595
|
+
hd: headersArray,
|
|
596
|
+
s: staleAt,
|
|
597
|
+
e: staleAt + swrWindow * 1000,
|
|
598
|
+
};
|
|
599
|
+
await this.kv!.put(kvKey, JSON.stringify(envelope), {
|
|
600
|
+
expirationTtl: totalTtl,
|
|
601
|
+
});
|
|
602
|
+
} catch (error) {
|
|
603
|
+
console.error("[CFCacheStore] KV putResponse failed:", error);
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
}
|
|
423
607
|
} catch (error) {
|
|
424
608
|
console.error("[CFCacheStore] putResponse failed:", error);
|
|
425
609
|
}
|
|
@@ -432,6 +616,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
432
616
|
/**
|
|
433
617
|
* Get a cached function result by key.
|
|
434
618
|
* Follows the same SWR pattern as get() for segment caching.
|
|
619
|
+
* Falls back to KV (L2) on L1 miss.
|
|
435
620
|
*/
|
|
436
621
|
async getItem(key: string): Promise<CacheItemResult | null> {
|
|
437
622
|
try {
|
|
@@ -439,7 +624,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
439
624
|
const request = this.keyToRequest(`fn:${key}`);
|
|
440
625
|
const response = await cache.match(request);
|
|
441
626
|
|
|
442
|
-
if (!response) return
|
|
627
|
+
if (!response) return this.kvGetItem(key);
|
|
443
628
|
|
|
444
629
|
const staleAt = Number(
|
|
445
630
|
response.headers.get(CACHE_STALE_AT_HEADER) ?? "0",
|
|
@@ -485,6 +670,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
485
670
|
|
|
486
671
|
/**
|
|
487
672
|
* Store a function result with TTL and optional SWR window.
|
|
673
|
+
* When KV is configured, also persists to L2.
|
|
488
674
|
*/
|
|
489
675
|
async setItem(
|
|
490
676
|
key: string,
|
|
@@ -519,11 +705,35 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
519
705
|
} else {
|
|
520
706
|
await putPromise;
|
|
521
707
|
}
|
|
708
|
+
|
|
709
|
+
// L2: persist to KV (KV requires expirationTtl >= 60s)
|
|
710
|
+
if (this.kv && this.waitUntil && totalTtl >= 60) {
|
|
711
|
+
const kvKey = this.toKVKey(`fn:${key}`);
|
|
712
|
+
this.waitUntil(async () => {
|
|
713
|
+
try {
|
|
714
|
+
const envelope: KVItemEnvelope = {
|
|
715
|
+
v: value,
|
|
716
|
+
h: options?.handles,
|
|
717
|
+
s: staleAt,
|
|
718
|
+
e: staleAt + swrWindow * 1000,
|
|
719
|
+
};
|
|
720
|
+
await this.kv!.put(kvKey, JSON.stringify(envelope), {
|
|
721
|
+
expirationTtl: totalTtl,
|
|
722
|
+
});
|
|
723
|
+
} catch (error) {
|
|
724
|
+
console.error("[CFCacheStore] KV setItem failed:", error);
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
}
|
|
522
728
|
} catch (error) {
|
|
523
729
|
console.error("[CFCacheStore] setItem failed:", error);
|
|
524
730
|
}
|
|
525
731
|
}
|
|
526
732
|
|
|
733
|
+
// ============================================================================
|
|
734
|
+
// Key Helpers
|
|
735
|
+
// ============================================================================
|
|
736
|
+
|
|
527
737
|
/**
|
|
528
738
|
* Convert string key to Request object for CF Cache API.
|
|
529
739
|
* Includes version in URL if specified (for cache invalidation on code changes).
|
|
@@ -537,4 +747,277 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
537
747
|
method: "GET",
|
|
538
748
|
});
|
|
539
749
|
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Convert string key to KV key string.
|
|
753
|
+
* Uses same version prefix as Cache API for consistent invalidation.
|
|
754
|
+
* @internal
|
|
755
|
+
*/
|
|
756
|
+
private toKVKey(key: string): string {
|
|
757
|
+
const versionPath = this.version ? `v/${this.version}/` : "";
|
|
758
|
+
return `${versionPath}${key}`;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// ============================================================================
|
|
762
|
+
// KV L2 Helpers
|
|
763
|
+
// ============================================================================
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* KV fallback for segment cache reads.
|
|
767
|
+
* Returns null if KV is not configured, entry is missing, or expired.
|
|
768
|
+
* Promotes hits to L1 via waitUntil.
|
|
769
|
+
* @internal
|
|
770
|
+
*/
|
|
771
|
+
private async kvGetSegment(key: string): Promise<CacheGetResult | null> {
|
|
772
|
+
if (!this.kv) return null;
|
|
773
|
+
|
|
774
|
+
try {
|
|
775
|
+
const kvKey = this.toKVKey(key);
|
|
776
|
+
const raw = await this.kv.get(kvKey, { type: "json" });
|
|
777
|
+
if (!raw) return null;
|
|
778
|
+
|
|
779
|
+
const envelope = raw as KVSegmentEnvelope;
|
|
780
|
+
const now = Date.now();
|
|
781
|
+
|
|
782
|
+
// Hard-expired — treat as miss
|
|
783
|
+
if (now > envelope.e) return null;
|
|
784
|
+
|
|
785
|
+
const shouldRevalidate = now > envelope.s;
|
|
786
|
+
|
|
787
|
+
// Promote to L1 in background
|
|
788
|
+
this.promoteSegmentToL1(key, envelope);
|
|
789
|
+
|
|
790
|
+
return { data: envelope.d, shouldRevalidate };
|
|
791
|
+
} catch (error) {
|
|
792
|
+
console.error("[CFCacheStore] KV get failed:", error);
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Write segment data to KV.
|
|
799
|
+
* @internal
|
|
800
|
+
*/
|
|
801
|
+
private kvSetSegment(
|
|
802
|
+
key: string,
|
|
803
|
+
data: CachedEntryData,
|
|
804
|
+
staleAt: number,
|
|
805
|
+
totalTtl: number,
|
|
806
|
+
swrWindow: number,
|
|
807
|
+
): void {
|
|
808
|
+
// KV requires expirationTtl >= 60s. Skip write for short-lived entries.
|
|
809
|
+
if (!this.kv || !this.waitUntil || totalTtl < 60) return;
|
|
810
|
+
|
|
811
|
+
const kvKey = this.toKVKey(key);
|
|
812
|
+
const expiresAt = staleAt + swrWindow * 1000;
|
|
813
|
+
|
|
814
|
+
this.waitUntil(async () => {
|
|
815
|
+
try {
|
|
816
|
+
const envelope: KVSegmentEnvelope = {
|
|
817
|
+
d: data,
|
|
818
|
+
s: staleAt,
|
|
819
|
+
e: expiresAt,
|
|
820
|
+
};
|
|
821
|
+
await this.kv!.put(kvKey, JSON.stringify(envelope), {
|
|
822
|
+
expirationTtl: totalTtl,
|
|
823
|
+
});
|
|
824
|
+
} catch (error) {
|
|
825
|
+
console.error("[CFCacheStore] KV set failed:", error);
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Promote segment data from KV to L1 Cache API.
|
|
832
|
+
* @internal
|
|
833
|
+
*/
|
|
834
|
+
private promoteSegmentToL1(key: string, envelope: KVSegmentEnvelope): void {
|
|
835
|
+
if (!this.waitUntil) return;
|
|
836
|
+
|
|
837
|
+
this.waitUntil(async () => {
|
|
838
|
+
try {
|
|
839
|
+
const now = Date.now();
|
|
840
|
+
const remainingTtl = Math.max(1, Math.floor((envelope.e - now) / 1000));
|
|
841
|
+
const cache = await this.getCache();
|
|
842
|
+
const request = this.keyToRequest(key);
|
|
843
|
+
|
|
844
|
+
const response = new Response(JSON.stringify(envelope.d), {
|
|
845
|
+
headers: {
|
|
846
|
+
"Content-Type": "application/json",
|
|
847
|
+
"Cache-Control": `public, max-age=${remainingTtl}`,
|
|
848
|
+
[CACHE_STALE_AT_HEADER]: String(envelope.s),
|
|
849
|
+
[CACHE_STATUS_HEADER]: "HIT",
|
|
850
|
+
},
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
await cache.put(request, response);
|
|
854
|
+
} catch (error) {
|
|
855
|
+
console.error("[CFCacheStore] L1 promote failed:", error);
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* KV fallback for function cache reads.
|
|
862
|
+
* @internal
|
|
863
|
+
*/
|
|
864
|
+
private async kvGetItem(key: string): Promise<CacheItemResult | null> {
|
|
865
|
+
if (!this.kv) return null;
|
|
866
|
+
|
|
867
|
+
try {
|
|
868
|
+
const kvKey = this.toKVKey(`fn:${key}`);
|
|
869
|
+
const raw = await this.kv.get(kvKey, { type: "json" });
|
|
870
|
+
if (!raw) return null;
|
|
871
|
+
|
|
872
|
+
const envelope = raw as KVItemEnvelope;
|
|
873
|
+
const now = Date.now();
|
|
874
|
+
|
|
875
|
+
if (now > envelope.e) return null;
|
|
876
|
+
|
|
877
|
+
const shouldRevalidate = now > envelope.s;
|
|
878
|
+
|
|
879
|
+
// Promote to L1
|
|
880
|
+
this.promoteItemToL1(key, envelope);
|
|
881
|
+
|
|
882
|
+
return {
|
|
883
|
+
value: envelope.v,
|
|
884
|
+
handles: envelope.h,
|
|
885
|
+
shouldRevalidate,
|
|
886
|
+
};
|
|
887
|
+
} catch (error) {
|
|
888
|
+
console.error("[CFCacheStore] KV getItem failed:", error);
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Promote function cache data from KV to L1.
|
|
895
|
+
* @internal
|
|
896
|
+
*/
|
|
897
|
+
private promoteItemToL1(key: string, envelope: KVItemEnvelope): void {
|
|
898
|
+
if (!this.waitUntil) return;
|
|
899
|
+
|
|
900
|
+
this.waitUntil(async () => {
|
|
901
|
+
try {
|
|
902
|
+
const now = Date.now();
|
|
903
|
+
const remainingTtl = Math.max(1, Math.floor((envelope.e - now) / 1000));
|
|
904
|
+
const cache = await this.getCache();
|
|
905
|
+
const request = this.keyToRequest(`fn:${key}`);
|
|
906
|
+
|
|
907
|
+
const body = JSON.stringify({ value: envelope.v, handles: envelope.h });
|
|
908
|
+
const response = new Response(body, {
|
|
909
|
+
headers: {
|
|
910
|
+
"Content-Type": "application/json",
|
|
911
|
+
"Cache-Control": `public, max-age=${remainingTtl}`,
|
|
912
|
+
[CACHE_STALE_AT_HEADER]: String(envelope.s),
|
|
913
|
+
[CACHE_STATUS_HEADER]: "HIT",
|
|
914
|
+
},
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
await cache.put(request, response);
|
|
918
|
+
} catch (error) {
|
|
919
|
+
console.error("[CFCacheStore] L1 item promote failed:", error);
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* KV fallback for document cache reads.
|
|
926
|
+
* @internal
|
|
927
|
+
*/
|
|
928
|
+
private async kvGetResponse(
|
|
929
|
+
key: string,
|
|
930
|
+
): Promise<{ response: Response; shouldRevalidate: boolean } | null> {
|
|
931
|
+
if (!this.kv) return null;
|
|
932
|
+
|
|
933
|
+
try {
|
|
934
|
+
const kvKey = this.toKVKey(`doc:${key}`);
|
|
935
|
+
const raw = await this.kv.get(kvKey, { type: "json" });
|
|
936
|
+
if (!raw) return null;
|
|
937
|
+
|
|
938
|
+
const envelope = raw as KVResponseEnvelope;
|
|
939
|
+
const now = Date.now();
|
|
940
|
+
|
|
941
|
+
if (now > envelope.e) return null;
|
|
942
|
+
|
|
943
|
+
const shouldRevalidate = now > envelope.s;
|
|
944
|
+
|
|
945
|
+
// Reconstruct Response (decode base64 → binary)
|
|
946
|
+
const headers = new Headers(envelope.hd);
|
|
947
|
+
const bodyBuffer = base64ToBuffer(envelope.b);
|
|
948
|
+
const response = new Response(bodyBuffer, {
|
|
949
|
+
status: envelope.st,
|
|
950
|
+
statusText: envelope.stx,
|
|
951
|
+
headers,
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
// Promote to L1
|
|
955
|
+
this.promoteResponseToL1(key, envelope);
|
|
956
|
+
|
|
957
|
+
return { response, shouldRevalidate };
|
|
958
|
+
} catch (error) {
|
|
959
|
+
console.error("[CFCacheStore] KV getResponse failed:", error);
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Promote document cache data from KV to L1.
|
|
966
|
+
* @internal
|
|
967
|
+
*/
|
|
968
|
+
private promoteResponseToL1(key: string, envelope: KVResponseEnvelope): void {
|
|
969
|
+
if (!this.waitUntil) return;
|
|
970
|
+
|
|
971
|
+
this.waitUntil(async () => {
|
|
972
|
+
try {
|
|
973
|
+
const now = Date.now();
|
|
974
|
+
const remainingTtl = Math.max(1, Math.floor((envelope.e - now) / 1000));
|
|
975
|
+
const cache = await this.getCache();
|
|
976
|
+
const request = this.keyToRequest(`doc:${key}`);
|
|
977
|
+
|
|
978
|
+
const headers = new Headers(envelope.hd);
|
|
979
|
+
const originalCacheControl = headers.get("Cache-Control");
|
|
980
|
+
if (originalCacheControl !== null) {
|
|
981
|
+
headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
|
|
982
|
+
}
|
|
983
|
+
headers.set("Cache-Control", `public, max-age=${remainingTtl}`);
|
|
984
|
+
headers.set(CACHE_STALE_AT_HEADER, String(envelope.s));
|
|
985
|
+
|
|
986
|
+
const bodyBuffer = base64ToBuffer(envelope.b);
|
|
987
|
+
const response = new Response(bodyBuffer, {
|
|
988
|
+
status: envelope.st,
|
|
989
|
+
statusText: envelope.stx,
|
|
990
|
+
headers,
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
await cache.put(request, response);
|
|
994
|
+
} catch (error) {
|
|
995
|
+
console.error("[CFCacheStore] L1 response promote failed:", error);
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// ============================================================================
|
|
1002
|
+
// Base64 Helpers (binary-safe response body encoding for KV)
|
|
1003
|
+
// ============================================================================
|
|
1004
|
+
|
|
1005
|
+
/** Encode ArrayBuffer to base64 string. */
|
|
1006
|
+
function bufferToBase64(buffer: ArrayBuffer): string {
|
|
1007
|
+
const bytes = new Uint8Array(buffer);
|
|
1008
|
+
let binary = "";
|
|
1009
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1010
|
+
binary += String.fromCharCode(bytes[i]!);
|
|
1011
|
+
}
|
|
1012
|
+
return btoa(binary);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/** Decode base64 string to ArrayBuffer. */
|
|
1016
|
+
function base64ToBuffer(base64: string): ArrayBuffer {
|
|
1017
|
+
const binary = atob(base64);
|
|
1018
|
+
const bytes = new Uint8Array(binary.length);
|
|
1019
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1020
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1021
|
+
}
|
|
1022
|
+
return bytes.buffer;
|
|
540
1023
|
}
|