@rangojs/router 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/event-controller.d.ts +3 -3
- package/dist/types/browser/history-state.d.ts +18 -0
- package/dist/types/browser/navigation-transaction.d.ts +20 -2
- package/dist/types/browser/partial-update.d.ts +16 -1
- package/dist/types/browser/prefetch/cache.d.ts +6 -6
- package/dist/types/browser/react/Link.d.ts +19 -9
- package/dist/types/browser/react/context.d.ts +2 -2
- package/dist/types/browser/react/location-state-shared.d.ts +95 -56
- package/dist/types/browser/react/location-state.d.ts +1 -1
- package/dist/types/browser/transition-when.d.ts +80 -0
- package/dist/types/browser/types.d.ts +104 -9
- package/dist/types/build/generate-manifest.d.ts +8 -0
- package/dist/types/cache/background-task.d.ts +24 -2
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/cache-key-utils.d.ts +48 -2
- package/dist/types/cache/cache-policy.d.ts +18 -1
- package/dist/types/cache/cache-runtime.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +192 -26
- package/dist/types/cache/cache-tag.d.ts +116 -11
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +224 -72
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +19 -0
- package/dist/types/cache/document-cache.d.ts +10 -0
- package/dist/types/cache/handle-snapshot.d.ts +41 -8
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/invalidation-order.d.ts +23 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/request-tag-mask.d.ts +54 -0
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +141 -105
- package/dist/types/cache/tag-invalidation.d.ts +54 -3
- package/dist/types/cache/types.d.ts +267 -96
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
- package/dist/types/client-urls/navigation.d.ts +28 -2
- package/dist/types/client-urls/server-projection.d.ts +5 -2
- package/dist/types/client-urls/types.d.ts +5 -5
- package/dist/types/client.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/route-definition/helpers-types.d.ts +11 -11
- package/dist/types/route-name.d.ts +7 -0
- package/dist/types/router/error-handling.d.ts +1 -1
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/intercept-resolution.d.ts +2 -0
- package/dist/types/router/match-context.d.ts +5 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
- package/dist/types/router/middleware-types.d.ts +6 -1
- package/dist/types/router/router-options.d.ts +4 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
- package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
- package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
- package/dist/types/router/transition-when.d.ts +24 -10
- package/dist/types/rsc/attach-transition-when.d.ts +18 -0
- package/dist/types/rsc/full-payload.d.ts +21 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/response-cache-serve.d.ts +15 -12
- package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
- package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
- package/dist/types/rsc/shell-capture.d.ts +202 -74
- package/dist/types/rsc/shell-serve.d.ts +144 -50
- package/dist/types/rsc/types.d.ts +9 -0
- package/dist/types/segment-system.d.ts +7 -0
- package/dist/types/server/context.d.ts +155 -33
- package/dist/types/server/cookie-store.d.ts +33 -0
- package/dist/types/server/handle-store.d.ts +52 -1
- package/dist/types/server/request-context.d.ts +210 -63
- package/dist/types/server/request-headers.d.ts +44 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +9 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/index.d.ts +3 -2
- package/dist/types/testing/internal/action-id.d.ts +7 -0
- package/dist/types/testing/location-state-key.d.ts +24 -0
- package/dist/types/testing/render-route.d.ts +17 -1
- package/dist/types/testing/run-loader.d.ts +18 -3
- package/dist/types/testing/run-transition-when.d.ts +44 -69
- package/dist/types/testing/serve-shell-request.d.ts +133 -0
- package/dist/types/testing/shell-status.d.ts +44 -19
- package/dist/types/transition-when-ref.d.ts +79 -0
- package/dist/types/types/cache-types.d.ts +15 -3
- package/dist/types/types/handler-context.d.ts +14 -1
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/segments.d.ts +86 -71
- package/dist/types/urls/path-helper-types.d.ts +3 -4
- package/dist/types/urls/pattern-types.d.ts +21 -18
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1005 -105
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +17 -7
- package/skills/cache-guide/SKILL.md +175 -76
- package/skills/caching/SKILL.md +361 -33
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +12 -7
- package/skills/cloudflare/SKILL.md +15 -0
- package/skills/comparison/references/framework-comparison.md +52 -37
- package/skills/defer-hydration/SKILL.md +50 -30
- package/skills/deployment-caching/SKILL.md +5 -2
- package/skills/document-cache/SKILL.md +32 -2
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/SKILL.md +3 -1
- package/skills/hooks/handle-and-actions.md +3 -1
- package/skills/hooks/navigation.md +23 -2
- package/skills/hooks/state.md +101 -7
- package/skills/intercept/SKILL.md +29 -12
- package/skills/loader/SKILL.md +197 -44
- package/skills/middleware/SKILL.md +34 -9
- package/skills/migrate-nextjs/SKILL.md +68 -37
- package/skills/migrate-react-router/SKILL.md +2 -1
- package/skills/migrate-react-router/component-migration.md +3 -1
- package/skills/migrate-react-router/data-and-actions.md +3 -1
- package/skills/migrate-react-router/route-mapping.md +7 -4
- package/skills/observability/SKILL.md +65 -0
- package/skills/parallel/SKILL.md +13 -8
- package/skills/ppr/SKILL.md +715 -371
- package/skills/prerender/SKILL.md +30 -17
- package/skills/rango/SKILL.md +72 -34
- package/skills/route/SKILL.md +14 -7
- package/skills/router-setup/SKILL.md +4 -2
- package/skills/scripts/SKILL.md +8 -1
- package/skills/server-actions/SKILL.md +11 -0
- package/skills/shell-manifest/SKILL.md +21 -12
- package/skills/testing/SKILL.md +49 -43
- package/skills/testing/cache-prerender.md +15 -14
- package/skills/testing/client-components.md +45 -3
- package/skills/testing/loader.md +19 -1
- package/skills/testing/middleware.md +1 -0
- package/skills/testing/predicates.md +49 -49
- package/skills/testing/render-handler.md +1 -0
- package/skills/testing/serve-shell-request.md +202 -0
- package/skills/testing/server-actions.md +9 -3
- package/skills/theme/SKILL.md +56 -4
- package/skills/typesafety/env-and-bindings.md +18 -2
- package/skills/use-cache/SKILL.md +91 -23
- package/skills/vercel/SKILL.md +14 -2
- package/skills/view-transitions/SKILL.md +150 -29
- package/src/__augment-tests__/augmented.check.ts +23 -0
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/event-controller.ts +4 -4
- package/src/browser/history-state.ts +75 -1
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +77 -18
- package/src/browser/navigation-client.ts +4 -3
- package/src/browser/navigation-store.ts +43 -1
- package/src/browser/navigation-transaction.ts +31 -11
- package/src/browser/partial-update.ts +146 -35
- package/src/browser/prefetch/cache.ts +6 -6
- package/src/browser/prefetch/fetch.ts +2 -2
- package/src/browser/react/Link.tsx +38 -33
- package/src/browser/react/NavigationProvider.tsx +2 -2
- package/src/browser/react/context.ts +2 -2
- package/src/browser/react/location-state-shared.ts +241 -49
- package/src/browser/react/location-state.ts +5 -2
- package/src/browser/rsc-router.tsx +9 -0
- package/src/browser/server-action-bridge.ts +84 -15
- package/src/browser/transition-when.ts +205 -0
- package/src/browser/types.ts +124 -10
- package/src/build/generate-manifest.ts +77 -1
- package/src/cache/background-task.ts +55 -2
- package/src/cache/cache-exec-scope.ts +48 -1
- package/src/cache/cache-key-utils.ts +62 -2
- package/src/cache/cache-policy.ts +31 -4
- package/src/cache/cache-runtime.ts +209 -129
- package/src/cache/cache-scope.ts +573 -69
- package/src/cache/cache-tag.ts +475 -19
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +1094 -376
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +55 -40
- package/src/cache/document-cache.ts +53 -7
- package/src/cache/handle-snapshot.ts +53 -9
- package/src/cache/index.ts +2 -0
- package/src/cache/invalidation-order.ts +84 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/request-tag-mask.ts +147 -0
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +249 -277
- package/src/cache/tag-invalidation.ts +109 -18
- package/src/cache/types.ts +274 -102
- package/src/cache/vercel/vercel-cache-store.ts +561 -74
- package/src/client-urls/client-root.tsx +8 -4
- package/src/client-urls/client-urls.ts +8 -6
- package/src/client-urls/navigation.ts +55 -4
- package/src/client-urls/server-projection.ts +31 -10
- package/src/client-urls/types.ts +12 -5
- package/src/client.tsx +2 -0
- package/src/host/cookie-handler.ts +2 -1
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +3 -0
- package/src/loader-redirect.tsx +7 -3
- package/src/prerender/build-shell-capture.ts +173 -110
- package/src/route-definition/dsl-helpers.ts +6 -3
- package/src/route-definition/helpers-types.ts +11 -11
- package/src/route-name.ts +12 -0
- package/src/router/content-negotiation.ts +4 -1
- package/src/router/error-handling.ts +5 -2
- package/src/router/handler-context.ts +42 -5
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +30 -8
- package/src/router/loader-resolution.ts +89 -33
- package/src/router/logging.ts +5 -3
- package/src/router/match-api.ts +89 -42
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +14 -29
- package/src/router/match-middleware/background-revalidation.ts +3 -5
- package/src/router/match-middleware/cache-lookup.ts +52 -19
- package/src/router/match-middleware/cache-store.ts +36 -10
- package/src/router/match-result.ts +1 -0
- package/src/router/middleware-types.ts +6 -1
- package/src/router/middleware.ts +20 -4
- package/src/router/navigation-snapshot.ts +6 -5
- package/src/router/prerender-match.ts +10 -0
- package/src/router/request-classification.ts +7 -4
- package/src/router/router-options.ts +4 -1
- package/src/router/segment-resolution/fresh.ts +18 -13
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +366 -70
- package/src/router/segment-resolution/loader-mask.ts +3 -4
- package/src/router/segment-resolution/loader-snapshot.ts +215 -126
- package/src/router/segment-resolution/mask-nested.ts +161 -42
- package/src/router/segment-resolution/revalidation.ts +27 -13
- package/src/router/segment-resolution/view-transition-default.ts +6 -21
- package/src/router/telemetry.ts +6 -3
- package/src/router/transition-when.ts +34 -52
- package/src/router.ts +2 -1
- package/src/rsc/attach-transition-when.ts +47 -0
- package/src/rsc/full-payload.ts +47 -3
- package/src/rsc/handler.ts +24 -10
- package/src/rsc/loader-fetch.ts +2 -1
- package/src/rsc/manifest-init.ts +4 -0
- package/src/rsc/origin-guard.ts +8 -5
- package/src/rsc/progressive-enhancement.ts +10 -32
- package/src/rsc/response-cache-serve.ts +69 -78
- package/src/rsc/response-route-handler.ts +2 -2
- package/src/rsc/rsc-rendering.ts +712 -338
- package/src/rsc/server-action.ts +8 -20
- package/src/rsc/shell-build-manifest.ts +49 -29
- package/src/rsc/shell-capture-constants.ts +182 -12
- package/src/rsc/shell-capture.ts +1126 -612
- package/src/rsc/shell-serve.ts +331 -85
- package/src/rsc/ssr-setup.ts +3 -2
- package/src/rsc/types.ts +9 -0
- package/src/segment-system.tsx +40 -7
- package/src/server/context.ts +328 -72
- package/src/server/cookie-store.ts +184 -133
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +364 -100
- package/src/server/request-headers.ts +134 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +96 -37
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/testing/dispatch.ts +4 -7
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/internal/action-id.ts +25 -0
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/location-state-key.ts +45 -0
- package/src/testing/render-handler.ts +1 -1
- package/src/testing/render-route.tsx +109 -16
- package/src/testing/run-client-revalidate.ts +3 -19
- package/src/testing/run-loader.ts +67 -5
- package/src/testing/run-transition-when.ts +109 -172
- package/src/testing/serve-shell-request.ts +381 -0
- package/src/testing/shell-status.ts +73 -47
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/transition-when-ref.ts +220 -0
- package/src/types/cache-types.ts +15 -3
- package/src/types/handler-context.ts +14 -1
- package/src/types/index.ts +3 -0
- package/src/types/segments.ts +94 -91
- package/src/urls/path-helper-types.ts +3 -4
- package/src/urls/pattern-types.ts +21 -18
- package/src/vite/discovery/discover-routers.ts +3 -0
- package/src/vite/discovery/shell-prerender-phase.ts +9 -2
- package/src/vite/discovery/virtual-module-codegen.ts +16 -6
- package/src/vite/plugins/transition-when-hoist.ts +1064 -0
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +89 -9
- package/dist/types/rsc/transition-gate.d.ts +0 -23
- package/src/rsc/transition-gate.ts +0 -86
|
@@ -27,6 +27,7 @@ declare global {
|
|
|
27
27
|
* - KV L2 for cross-colo cache persistence
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
import { requestHeaders } from "../../server/request-headers.js";
|
|
30
31
|
import type {
|
|
31
32
|
SegmentCacheStore,
|
|
32
33
|
CachedEntryData,
|
|
@@ -35,6 +36,12 @@ import type {
|
|
|
35
36
|
CacheItemResult,
|
|
36
37
|
CacheItemOptions,
|
|
37
38
|
ShellCacheEntry,
|
|
39
|
+
ShellDocumentRead,
|
|
40
|
+
ShellDocumentReadOptions,
|
|
41
|
+
ShellEntryHead,
|
|
42
|
+
ShellReadStats,
|
|
43
|
+
ShellSnapshotFailure,
|
|
44
|
+
ShellSnapshotRecord,
|
|
38
45
|
CacheReadError,
|
|
39
46
|
} from "../types.js";
|
|
40
47
|
import { CACHE_READ_ERROR } from "../types.js";
|
|
@@ -55,7 +62,30 @@ import {
|
|
|
55
62
|
} from "../cache-policy.js";
|
|
56
63
|
import { reportCacheError, reportingAsync } from "../cache-error.js";
|
|
57
64
|
import type { CacheErrorCategory } from "../cache-error.js";
|
|
58
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
bufferToBase64,
|
|
67
|
+
base64ToBuffer,
|
|
68
|
+
base64ToBytes,
|
|
69
|
+
bytesToBase64,
|
|
70
|
+
} from "./cf-base64.js";
|
|
71
|
+
import {
|
|
72
|
+
ShellFrameReader,
|
|
73
|
+
encodeShellFrame,
|
|
74
|
+
encodeShellSnapshot,
|
|
75
|
+
parseShellSnapshot,
|
|
76
|
+
shellFrameToText,
|
|
77
|
+
type ShellFrameHead,
|
|
78
|
+
} from "./cf-shell-frame.js";
|
|
79
|
+
import {
|
|
80
|
+
ShellMemo,
|
|
81
|
+
RecentTagInvalidations,
|
|
82
|
+
DEFAULT_SHELL_MEMO_MS,
|
|
83
|
+
freshReadsWindowMs,
|
|
84
|
+
isShellFresh,
|
|
85
|
+
resolveShellMemoOptions,
|
|
86
|
+
shellHasAnyTag,
|
|
87
|
+
type ResolvedShellMemoOptions,
|
|
88
|
+
} from "../shell-memo.js";
|
|
59
89
|
import {
|
|
60
90
|
KV_KEY_PRESERVED_PREFIX_BYTES,
|
|
61
91
|
KV_MAX_KEY_BYTES,
|
|
@@ -70,7 +100,22 @@ import {
|
|
|
70
100
|
TAG_MARKER_ABSENT,
|
|
71
101
|
getTagMarkerMemo,
|
|
72
102
|
getTagMarkerInflight,
|
|
103
|
+
getShellMarkerReads,
|
|
73
104
|
} from "./cf-tag-marker-memo.js";
|
|
105
|
+
import {
|
|
106
|
+
gateMarkerRead,
|
|
107
|
+
maskRequestTags,
|
|
108
|
+
maskedForRequest,
|
|
109
|
+
} from "../request-tag-mask.js";
|
|
110
|
+
import {
|
|
111
|
+
TagMarkerMemo,
|
|
112
|
+
TagNameHints,
|
|
113
|
+
hintedTags,
|
|
114
|
+
freshReadsRequired,
|
|
115
|
+
recordMarkerRow,
|
|
116
|
+
DEFAULT_CF_MARKER_FRESH_MS,
|
|
117
|
+
DEFAULT_CF_MARKER_MAX_STALE_MS,
|
|
118
|
+
} from "../isolate-tag-memo.js";
|
|
74
119
|
import { createCloudflareZonePurge } from "./cf-zone-purge.js";
|
|
75
120
|
|
|
76
121
|
// ============================================================================
|
|
@@ -94,6 +139,7 @@ import {
|
|
|
94
139
|
EDGE_LOOKUP_TIMEOUT_MS,
|
|
95
140
|
EDGE_READ_TIMEOUT_MS,
|
|
96
141
|
KV_READ_TIMEOUT_MS,
|
|
142
|
+
SHELL_SNAPSHOT_READ_MIN_TIMEOUT_MS,
|
|
97
143
|
} from "./cf-cache-constants.js";
|
|
98
144
|
|
|
99
145
|
// Re-export the public constants so consumers/tests importing them from
|
|
@@ -267,57 +313,119 @@ interface KVItemEnvelope {
|
|
|
267
313
|
}
|
|
268
314
|
|
|
269
315
|
/**
|
|
270
|
-
*
|
|
271
|
-
*
|
|
316
|
+
* Key namespace of the shell family. `shell2:` holds the prelude-first frame
|
|
317
|
+
* (cf-shell-frame.ts); `shell:` held the JSON envelope. Every deploy retires
|
|
318
|
+
* the old entries anyway (buildVersion gate); the new namespace also keeps
|
|
319
|
+
* new code from reporting an old body as corrupt and a rollback from parsing
|
|
320
|
+
* a frame. Old keys age out by their TTL.
|
|
272
321
|
*/
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
322
|
+
const SHELL_KEY_PREFIX = "shell2:";
|
|
323
|
+
|
|
324
|
+
/** A shell snapshot read's timeout, and its warning's consequence text. */
|
|
325
|
+
interface SnapshotReadBudget {
|
|
326
|
+
ms: number;
|
|
327
|
+
consequence?: string;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** A memoized fresh shell read (shell-memo.ts). */
|
|
331
|
+
interface CFShellMemoValue {
|
|
332
|
+
head: ShellFrameHead;
|
|
333
|
+
prelude: Uint8Array;
|
|
334
|
+
snapshot: ShellSnapshotRecord[] | undefined;
|
|
335
|
+
/** L1 response headers: the KV-less purge-mode marker check reads them. */
|
|
336
|
+
headers?: Headers;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** The per-isolate shell memo shared by every CFCacheStore in the isolate. */
|
|
340
|
+
const cfShellMemo = new ShellMemo<CFShellMemoValue>();
|
|
341
|
+
/** Per-isolate tag-marker values (isolate-tag-memo.ts), keyed per namespace. */
|
|
342
|
+
const cfMarkerMemo = new TagMarkerMemo();
|
|
343
|
+
/** Per-isolate shell-key -> tag-name hints for the marker prefetch. */
|
|
344
|
+
const cfTagHints = new TagNameHints();
|
|
345
|
+
|
|
346
|
+
/** Tags this isolate invalidated recently: they keep shells out of cfShellMemo. */
|
|
347
|
+
const recentShellInvalidations = new RecentTagInvalidations();
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Whether cfShellMemo may keep or serve this shell: still fresh (a stale one
|
|
351
|
+
* stays with the store read, which schedules the recapture), and not tagged
|
|
352
|
+
* at or before an invalidation this isolate is running or just ran.
|
|
353
|
+
*/
|
|
354
|
+
function isMemoizableShell(head: ShellFrameHead): boolean {
|
|
355
|
+
return (
|
|
356
|
+
isShellFresh(head.s, head.e) &&
|
|
357
|
+
!recentShellInvalidations.covers(head.t, head.ta)
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* @internal Reset the per-isolate shell and marker memos and the once-per-
|
|
363
|
+
* isolate warnings (tests).
|
|
364
|
+
*/
|
|
365
|
+
export function resetCFShellMemoForTests(): void {
|
|
366
|
+
cfShellMemo.clear();
|
|
367
|
+
recentShellInvalidations.clear();
|
|
368
|
+
cfMarkerMemo.clear();
|
|
369
|
+
cfTagHints.clear();
|
|
370
|
+
warnedNoKvReadInvalidation.clear();
|
|
371
|
+
warnedTagInvalidationTtlFloor.clear();
|
|
372
|
+
warnedShellTagsNoEviction.clear();
|
|
373
|
+
warnedNoTagInvalidationTtl.clear();
|
|
374
|
+
warnedCacheTagHeaderOverflow.clear();
|
|
375
|
+
warnedCacheTagOverflowUncacheable.clear();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** openShellFrame outcome: the head and prelude, or why the read failed. */
|
|
379
|
+
type OpenedShellFrame =
|
|
380
|
+
| {
|
|
381
|
+
status: "ok";
|
|
382
|
+
head: ShellFrameHead;
|
|
383
|
+
prelude: Uint8Array;
|
|
384
|
+
reader: ShellFrameReader;
|
|
385
|
+
}
|
|
386
|
+
| { status: "corrupt"; error: Error; head?: undefined }
|
|
387
|
+
| { status: "expired"; head: ShellFrameHead }
|
|
388
|
+
| { status: "timeout"; head?: undefined }
|
|
389
|
+
| { status: "invalidated"; head?: undefined };
|
|
390
|
+
|
|
391
|
+
/** The `[CFCacheStore][shell]` trace's timings, from a read's stats. */
|
|
392
|
+
function debugTimings(
|
|
393
|
+
stats: ShellReadStats | undefined,
|
|
394
|
+
): Pick<CFShellDebugDetails, "matchMs" | "readMs" | "bodyReadMs" | "markerMs"> {
|
|
395
|
+
if (!stats) return {};
|
|
396
|
+
if (stats.tier === "memo")
|
|
397
|
+
return { markerMs: Math.round(stats.markerMs ?? 0) };
|
|
398
|
+
const round = (ms: number | undefined) =>
|
|
399
|
+
ms === undefined ? undefined : Math.round(ms);
|
|
400
|
+
const bodyReadMs =
|
|
401
|
+
stats.headMs === undefined && stats.preludeMs === undefined
|
|
402
|
+
? undefined
|
|
403
|
+
: (stats.headMs ?? 0) + (stats.preludeMs ?? 0);
|
|
404
|
+
return {
|
|
405
|
+
...(stats.tier === "kv"
|
|
406
|
+
? { readMs: round(stats.matchMs) }
|
|
407
|
+
: { matchMs: round(stats.matchMs) }),
|
|
408
|
+
bodyReadMs: round(bodyReadMs),
|
|
409
|
+
markerMs: round(stats.markerMs),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** The public entry shape of a frame head (no prelude, no snapshot). */
|
|
414
|
+
function shellHeadToEntry(head: ShellFrameHead): ShellEntryHead {
|
|
415
|
+
return {
|
|
416
|
+
...(head.po !== undefined ? { postponed: head.po } : {}),
|
|
417
|
+
reactVersion: head.rv,
|
|
418
|
+
buildVersion: head.bv,
|
|
419
|
+
initialTheme: head.i,
|
|
420
|
+
docKey: head.dk,
|
|
421
|
+
prunedRecords: head.pr,
|
|
422
|
+
navigationOnly: head.no,
|
|
423
|
+
createdAt: head.c,
|
|
424
|
+
};
|
|
318
425
|
}
|
|
319
426
|
|
|
320
427
|
type CFShellDebugOutcome =
|
|
428
|
+
| "memo-hit"
|
|
321
429
|
| "l1-hit"
|
|
322
430
|
| "l1-miss"
|
|
323
431
|
| "kv-hit"
|
|
@@ -329,13 +437,12 @@ type CFShellDebugOutcome =
|
|
|
329
437
|
| "write-invalidated";
|
|
330
438
|
|
|
331
439
|
interface CFShellDebugDetails {
|
|
332
|
-
tier?: "l1" | "kv";
|
|
440
|
+
tier?: "memo" | "l1" | "kv";
|
|
333
441
|
reason?:
|
|
334
442
|
| "absent"
|
|
335
443
|
| "timeout"
|
|
336
444
|
| "error"
|
|
337
445
|
| "non-200"
|
|
338
|
-
| "corrupt"
|
|
339
446
|
| "malformed"
|
|
340
447
|
| "expired"
|
|
341
448
|
| "unavailable";
|
|
@@ -349,41 +456,6 @@ interface CFShellDebugDetails {
|
|
|
349
456
|
remainingTtl?: number;
|
|
350
457
|
}
|
|
351
458
|
|
|
352
|
-
/** Validate the coupled PPR shell envelope before any field reaches resume. */
|
|
353
|
-
function isShellEnvelope(value: unknown): value is CFShellEnvelope {
|
|
354
|
-
if (value == null || typeof value !== "object") return false;
|
|
355
|
-
const envelope = value as Partial<CFShellEnvelope>;
|
|
356
|
-
return (
|
|
357
|
-
// Document half: required unless the envelope is navigationOnly (`no`),
|
|
358
|
-
// which stores neither field. Tolerate a legacy navigationOnly envelope
|
|
359
|
-
// that still carries them.
|
|
360
|
-
(typeof envelope.p === "string" ||
|
|
361
|
-
(envelope.p === undefined && envelope.no === true)) &&
|
|
362
|
-
(envelope.po === null ||
|
|
363
|
-
typeof envelope.po === "string" ||
|
|
364
|
-
(envelope.po === undefined && envelope.no === true)) &&
|
|
365
|
-
typeof envelope.rv === "string" &&
|
|
366
|
-
(envelope.bv === undefined || typeof envelope.bv === "string") &&
|
|
367
|
-
typeof envelope.c === "number" &&
|
|
368
|
-
Number.isFinite(envelope.c) &&
|
|
369
|
-
typeof envelope.s === "number" &&
|
|
370
|
-
Number.isFinite(envelope.s) &&
|
|
371
|
-
typeof envelope.e === "number" &&
|
|
372
|
-
Number.isFinite(envelope.e) &&
|
|
373
|
-
(envelope.t === undefined ||
|
|
374
|
-
(Array.isArray(envelope.t) &&
|
|
375
|
-
envelope.t.every((tag) => typeof tag === "string"))) &&
|
|
376
|
-
(envelope.ta === undefined ||
|
|
377
|
-
(typeof envelope.ta === "number" && Number.isFinite(envelope.ta))) &&
|
|
378
|
-
(envelope.i === undefined || typeof envelope.i === "string") &&
|
|
379
|
-
(envelope.sn === undefined || Array.isArray(envelope.sn)) &&
|
|
380
|
-
(envelope.dk === undefined || typeof envelope.dk === "string") &&
|
|
381
|
-
(envelope.lh === undefined || typeof envelope.lh === "boolean") &&
|
|
382
|
-
(envelope.tw === undefined || envelope.tw === true) &&
|
|
383
|
-
(envelope.no === undefined || envelope.no === true)
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
459
|
/**
|
|
388
460
|
* KV envelope for document cache entries.
|
|
389
461
|
* @internal
|
|
@@ -426,6 +498,22 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
426
498
|
private readonly edgeLookupTimeoutMs: number;
|
|
427
499
|
private readonly edgeReadTimeoutMs: number;
|
|
428
500
|
private readonly kvReadTimeoutMs: number;
|
|
501
|
+
/**
|
|
502
|
+
* The shell snapshot read's budgets. `read` (getShell: partial replay, the
|
|
503
|
+
* testing helpers) has nothing committed, so a slow snapshot is a quick
|
|
504
|
+
* miss at kvReadTimeoutMs. `document` (readShellDocument, the serve path)
|
|
505
|
+
* has sent the prelude and cannot finish without the snapshot's doc
|
|
506
|
+
* record; a timeout there reloads the page, so it waits at least
|
|
507
|
+
* SHELL_SNAPSHOT_READ_MIN_TIMEOUT_MS. `kvReadTimeoutMs <= 0` keeps both
|
|
508
|
+
* unbounded.
|
|
509
|
+
*/
|
|
510
|
+
private readonly snapshotBudgets: {
|
|
511
|
+
read: SnapshotReadBudget;
|
|
512
|
+
document: SnapshotReadBudget;
|
|
513
|
+
};
|
|
514
|
+
private readonly memo: ResolvedShellMemoOptions;
|
|
515
|
+
/** @internal SegmentCacheStore.freshReadsWindowMs */
|
|
516
|
+
readonly freshReadsWindowMs: number;
|
|
429
517
|
private readonly debug?: (event: CFCacheReadDebugEvent) => void;
|
|
430
518
|
private readonly kv?: KVNamespace;
|
|
431
519
|
/** True when constructed without KV: no durable tag history (see ctor). */
|
|
@@ -476,6 +564,28 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
476
564
|
options.kvReadTimeoutMs,
|
|
477
565
|
KV_READ_TIMEOUT_MS,
|
|
478
566
|
);
|
|
567
|
+
this.snapshotBudgets = {
|
|
568
|
+
read: { ms: this.kvReadTimeoutMs },
|
|
569
|
+
document:
|
|
570
|
+
this.kvReadTimeoutMs > 0
|
|
571
|
+
? {
|
|
572
|
+
ms: Math.max(
|
|
573
|
+
this.kvReadTimeoutMs,
|
|
574
|
+
SHELL_SNAPSHOT_READ_MIN_TIMEOUT_MS,
|
|
575
|
+
),
|
|
576
|
+
consequence: "the HIT reloads the page into a cache-miss render",
|
|
577
|
+
}
|
|
578
|
+
: { ms: this.kvReadTimeoutMs },
|
|
579
|
+
};
|
|
580
|
+
this.memo = resolveShellMemoOptions(options.memo, {
|
|
581
|
+
markerFreshMs: DEFAULT_CF_MARKER_FRESH_MS,
|
|
582
|
+
markerMaxStaleMs: DEFAULT_CF_MARKER_MAX_STALE_MS,
|
|
583
|
+
});
|
|
584
|
+
// Without KV there are no markers, so no marker memo to outwait.
|
|
585
|
+
this.freshReadsWindowMs = freshReadsWindowMs(
|
|
586
|
+
this.memo,
|
|
587
|
+
Boolean(options.kv),
|
|
588
|
+
);
|
|
479
589
|
this.debug =
|
|
480
590
|
options.debug === true
|
|
481
591
|
? (event) =>
|
|
@@ -487,7 +597,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
487
597
|
this.waitUntil = (fn) => options.ctx.waitUntil(fn());
|
|
488
598
|
this.kv = options.kv;
|
|
489
599
|
// Without KV, isTagsInvalidatedSince has no durable history — it answers
|
|
490
|
-
// from the per-request
|
|
600
|
+
// from the per-request mask at best. Runtime shells tolerate that (purge
|
|
491
601
|
// eviction + ttl/swr bound the staleness), but an immutable TAGGED
|
|
492
602
|
// build-manifest shell must not serve on such a store: nothing could ever
|
|
493
603
|
// evict it (purge cannot delete a build asset), so the manifest gate
|
|
@@ -602,17 +712,23 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
602
712
|
}
|
|
603
713
|
}
|
|
604
714
|
|
|
605
|
-
/**
|
|
715
|
+
/**
|
|
716
|
+
* Build-time-gated shell tier trace for deployed cross-colo diagnostics.
|
|
717
|
+
* A read's `stats` print as its timings, after the other details and before
|
|
718
|
+
* `expiresAt`.
|
|
719
|
+
*/
|
|
606
720
|
private debugShell(
|
|
607
721
|
key: string,
|
|
608
722
|
outcome: CFShellDebugOutcome,
|
|
609
|
-
details: CFShellDebugDetails = {},
|
|
723
|
+
details: CFShellDebugDetails & { stats?: ShellReadStats } = {},
|
|
610
724
|
): void {
|
|
611
725
|
if (!INTERNAL_RANGO_DEBUG) return;
|
|
612
726
|
const request = _getRequestContext()?.request as
|
|
613
727
|
| (Request & { cf?: { colo?: unknown } })
|
|
614
728
|
| undefined;
|
|
615
|
-
const ray = request
|
|
729
|
+
const ray = request
|
|
730
|
+
? (requestHeaders(request).get("cf-ray") ?? undefined)
|
|
731
|
+
: undefined;
|
|
616
732
|
const cfColo = request?.cf?.colo;
|
|
617
733
|
const colo =
|
|
618
734
|
typeof cfColo === "string"
|
|
@@ -620,6 +736,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
620
736
|
: ray?.includes("-")
|
|
621
737
|
? ray.slice(ray.lastIndexOf("-") + 1)
|
|
622
738
|
: undefined;
|
|
739
|
+
const { stats, expiresAt, ...rest } = details;
|
|
623
740
|
console.log(
|
|
624
741
|
`[CFCacheStore][shell] ${JSON.stringify({
|
|
625
742
|
key,
|
|
@@ -627,7 +744,9 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
627
744
|
at: Date.now(),
|
|
628
745
|
ray,
|
|
629
746
|
colo,
|
|
630
|
-
...
|
|
747
|
+
...rest,
|
|
748
|
+
...debugTimings(stats),
|
|
749
|
+
expiresAt,
|
|
631
750
|
})}`,
|
|
632
751
|
);
|
|
633
752
|
}
|
|
@@ -723,6 +842,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
723
842
|
read: () => Promise<T>,
|
|
724
843
|
budgetMs: number,
|
|
725
844
|
label: string,
|
|
845
|
+
consequence = "treating as miss",
|
|
726
846
|
): Promise<{ value: T | undefined; timedOut: boolean }> {
|
|
727
847
|
if (budgetMs <= 0) return { value: await read(), timedOut: false };
|
|
728
848
|
|
|
@@ -741,7 +861,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
741
861
|
]);
|
|
742
862
|
if (result.timedOut) {
|
|
743
863
|
console.warn(
|
|
744
|
-
`[CFCacheStore] ${label} exceeded ${budgetMs}ms;
|
|
864
|
+
`[CFCacheStore] ${label} exceeded ${budgetMs}ms; ${consequence}`,
|
|
745
865
|
);
|
|
746
866
|
return { value: undefined, timedOut: true };
|
|
747
867
|
}
|
|
@@ -1246,7 +1366,9 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1246
1366
|
|
|
1247
1367
|
/**
|
|
1248
1368
|
* Store entry data with TTL and optional SWR window.
|
|
1249
|
-
*
|
|
1369
|
+
* Resolves once the L1 write lands, per the SegmentCacheStore.set contract
|
|
1370
|
+
* (also held by waitUntil when available); the KV write stays in the
|
|
1371
|
+
* background.
|
|
1250
1372
|
* When KV is configured, also persists to L2.
|
|
1251
1373
|
*/
|
|
1252
1374
|
async set(
|
|
@@ -1297,20 +1419,21 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1297
1419
|
|
|
1298
1420
|
const putPromise = cache.put(request, response);
|
|
1299
1421
|
|
|
1422
|
+
let l1Write: Promise<void> | undefined;
|
|
1300
1423
|
if (this.waitUntil) {
|
|
1301
|
-
//
|
|
1302
|
-
//
|
|
1424
|
+
// These store-level background tasks intentionally omit the
|
|
1425
|
+
// reportingAsync ctx argument: the store is a request-agnostic
|
|
1303
1426
|
// singleton and this.waitUntil is the execution context's, not a single
|
|
1304
1427
|
// request's, so a failure is reported console-loud only (it cannot be
|
|
1305
1428
|
// attributed to one request's onError). The request-scoped tag verbs
|
|
1306
1429
|
// (revalidateTag / stale-revalidation) DO thread their captured ctx.
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
"[CFCacheStore] L1 write",
|
|
1312
|
-
),
|
|
1430
|
+
const put = reportingAsync(
|
|
1431
|
+
() => putPromise,
|
|
1432
|
+
"cache-write",
|
|
1433
|
+
"[CFCacheStore] L1 write",
|
|
1313
1434
|
);
|
|
1435
|
+
this.waitUntil(() => put);
|
|
1436
|
+
l1Write = put;
|
|
1314
1437
|
} else {
|
|
1315
1438
|
// Blocking fallback
|
|
1316
1439
|
await putPromise;
|
|
@@ -1318,6 +1441,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1318
1441
|
|
|
1319
1442
|
// L2: persist to KV (reuses `body` as envelope.d)
|
|
1320
1443
|
this.kvSetSegment(key, body, staleAt, totalTtl, swrWindow);
|
|
1444
|
+
|
|
1445
|
+
// The set() contract (SegmentCacheStore.set, #957): resolve once L1 is
|
|
1446
|
+
// readable.
|
|
1447
|
+
await l1Write;
|
|
1321
1448
|
} catch (error) {
|
|
1322
1449
|
reportCacheError(error, "cache-write", "[CFCacheStore] set");
|
|
1323
1450
|
}
|
|
@@ -1897,7 +2024,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1897
2024
|
// serves its own shell from the Cache API. Tag eviction then needs purge
|
|
1898
2025
|
// mode (tagPurge) — shell L1 entries carry the same namespaced Cache-Tag
|
|
1899
2026
|
// tokens as the data families, read-your-own-writes comes from the
|
|
1900
|
-
// per-request
|
|
2027
|
+
// per-request tag mask, and the capture resurrection race narrows to
|
|
1901
2028
|
// cross-request timing bounded by ttl+swr (the data families' documented
|
|
1902
2029
|
// purge-mode stance). KV-less WITHOUT tagPurge still caches: freshness is
|
|
1903
2030
|
// ttl/swr only, and a tagged write warns once that invalidation cannot
|
|
@@ -1927,7 +2054,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1927
2054
|
/**
|
|
1928
2055
|
* Generation gate for shell writes and the capture scheduler
|
|
1929
2056
|
* (isTagsInvalidatedSince). With KV it is the durable marker cascade.
|
|
1930
|
-
* Without KV there are no markers: in purge mode the per-request
|
|
2057
|
+
* Without KV there are no markers: in purge mode the per-request mask is
|
|
1931
2058
|
* the only signal — a capture racing THIS request's updateTag() is still
|
|
1932
2059
|
* rejected (read-your-own-writes), while cross-request races are bounded
|
|
1933
2060
|
* by ttl+swr exactly like the data families' purge-mode writes. Without
|
|
@@ -1941,38 +2068,100 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1941
2068
|
if (this.kv) return this.isGloballyInvalidated(tags, since);
|
|
1942
2069
|
if (!this.tagPurge || !Array.isArray(tags) || tags.length === 0 || !since)
|
|
1943
2070
|
return false;
|
|
1944
|
-
|
|
1945
|
-
if (!ctx) return false;
|
|
1946
|
-
const memo = getTagMarkerMemo(ctx, this);
|
|
1947
|
-
for (const tag of tags) {
|
|
1948
|
-
const marker = memo.get(tag);
|
|
1949
|
-
if (marker != null && marker >= since) return true;
|
|
1950
|
-
}
|
|
1951
|
-
return false;
|
|
2071
|
+
return maskedForRequest(_getRequestContext(), this, tags, since);
|
|
1952
2072
|
}
|
|
1953
2073
|
|
|
1954
2074
|
/**
|
|
1955
|
-
* Get a cached PPR shell entry
|
|
1956
|
-
*
|
|
1957
|
-
*
|
|
1958
|
-
*
|
|
1959
|
-
*
|
|
2075
|
+
* Get a cached PPR shell entry: the public custom-store contract, built on
|
|
2076
|
+
* readShellDocument. It waits for the snapshot and re-encodes the prelude to
|
|
2077
|
+
* the base64 `ShellCacheEntry.prelude` the contract carries (native
|
|
2078
|
+
* `toBase64` in workerd). Partial replay and the testing helpers read
|
|
2079
|
+
* through here; the document serve path reads readShellDocument directly.
|
|
2080
|
+
* A snapshot that failed to read is a miss here: with no committed response
|
|
2081
|
+
* to protect, an entry without its pins is worse than none.
|
|
1960
2082
|
*/
|
|
1961
2083
|
async getShell(
|
|
1962
2084
|
key: string,
|
|
1963
2085
|
): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
|
|
2086
|
+
const read = await this.readShellDocumentWithin(
|
|
2087
|
+
key,
|
|
2088
|
+
undefined,
|
|
2089
|
+
this.snapshotBudgets.read,
|
|
2090
|
+
);
|
|
2091
|
+
if (!read) return null;
|
|
2092
|
+
const snapshot = await read.snapshot;
|
|
2093
|
+
if (await read.snapshotFailure) return null;
|
|
2094
|
+
// A frame with no snapshot bytes carries no records.
|
|
2095
|
+
const entry: ShellCacheEntry = { ...read.entry, snapshot: snapshot ?? [] };
|
|
2096
|
+
if (!entry.navigationOnly) entry.prelude = bytesToBase64(read.prelude);
|
|
2097
|
+
return { entry, shouldRevalidate: read.shouldRevalidate };
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
/**
|
|
2101
|
+
* @internal Prelude-first read of a shell (SegmentCacheStore
|
|
2102
|
+
* .readShellDocument). Cache API first, then KV; both tiers hold one
|
|
2103
|
+
* cf-shell-frame body, so the prelude, postponed state, snapshot, versions,
|
|
2104
|
+
* and generation metadata cannot mix. Before it resolves it reads only the
|
|
2105
|
+
* head and the prelude's bytes, and the tag-marker read runs alongside the
|
|
2106
|
+
* prelude read (it starts as soon as the head is parsed, and is awaited
|
|
2107
|
+
* before this resolves, so an invalidated shell is never returned). The
|
|
2108
|
+
* snapshot is the rest of the same body, read on its own promise, bounded
|
|
2109
|
+
* by the document read's budget (`snapshotBudgets.document`; getShell uses
|
|
2110
|
+
* `snapshotBudgets.read`). SWR is a plain staleness flag; the capture
|
|
2111
|
+
* scheduler's in-flight set is the recapture stampede guard.
|
|
2112
|
+
*/
|
|
2113
|
+
async readShellDocument(
|
|
2114
|
+
key: string,
|
|
2115
|
+
options?: ShellDocumentReadOptions,
|
|
2116
|
+
): Promise<ShellDocumentRead | null> {
|
|
2117
|
+
return this.readShellDocumentWithin(
|
|
2118
|
+
key,
|
|
2119
|
+
options,
|
|
2120
|
+
this.snapshotBudgets.document,
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
private async readShellDocumentWithin(
|
|
2125
|
+
key: string,
|
|
2126
|
+
options: ShellDocumentReadOptions | undefined,
|
|
2127
|
+
snapshotBudget: SnapshotReadBudget,
|
|
2128
|
+
): Promise<ShellDocumentRead | null> {
|
|
2129
|
+
const memoKey = this.shellMemoKey(key);
|
|
2130
|
+
const ctx = _getRequestContext();
|
|
2131
|
+
// A request after the same user's updateTag() reads past the isolate
|
|
2132
|
+
// memos (isolate-tag-memo.ts freshReadsRequired).
|
|
2133
|
+
const freshReads = freshReadsRequired(ctx);
|
|
2134
|
+
const memoized = freshReads
|
|
2135
|
+
? undefined
|
|
2136
|
+
: cfShellMemo.get(memoKey, this.memo.shellMs);
|
|
2137
|
+
if (memoized) {
|
|
2138
|
+
return this.readMemoizedShell(
|
|
2139
|
+
key,
|
|
2140
|
+
memoKey,
|
|
2141
|
+
memoized,
|
|
2142
|
+
options,
|
|
2143
|
+
snapshotBudget,
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
const stats = this.shellReadStats("l1");
|
|
2147
|
+
const l1StartedAt = stats ? performance.now() : 0;
|
|
2148
|
+
const kvFallback = (reason: string): Promise<ShellDocumentRead | null> =>
|
|
2149
|
+
this.kvReadShellDocument(key, snapshotBudget, stats, l1StartedAt, reason);
|
|
2150
|
+
if (stats && freshReads) stats.freshReads = true;
|
|
2151
|
+
this.prefetchShellMarkers(memoKey, options?.tagHints, stats);
|
|
2152
|
+
if (stats && this.memo.shellMs > 0) {
|
|
2153
|
+
stats.memo = { hit: false, bytes: cfShellMemo.size };
|
|
2154
|
+
}
|
|
1964
2155
|
try {
|
|
1965
2156
|
const cache = await this.getCache();
|
|
1966
|
-
const request = this.keyToRequest(
|
|
1967
|
-
const matchStartedAt =
|
|
2157
|
+
const request = this.keyToRequest(`${SHELL_KEY_PREFIX}${key}`);
|
|
2158
|
+
const matchStartedAt = stats ? performance.now() : 0;
|
|
1968
2159
|
const {
|
|
1969
2160
|
response,
|
|
1970
2161
|
timedOut,
|
|
1971
2162
|
error: matchError,
|
|
1972
2163
|
} = await this.matchWithTimeout(cache, request);
|
|
1973
|
-
|
|
1974
|
-
? Date.now() - matchStartedAt
|
|
1975
|
-
: undefined;
|
|
2164
|
+
if (stats) stats.matchMs = performance.now() - matchStartedAt;
|
|
1976
2165
|
|
|
1977
2166
|
if (!response) {
|
|
1978
2167
|
if (matchError) {
|
|
@@ -1982,116 +2171,434 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1982
2171
|
"[CFCacheStore] getShell L1 match",
|
|
1983
2172
|
);
|
|
1984
2173
|
}
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
});
|
|
1989
|
-
return this.kvGetShell(key);
|
|
2174
|
+
const reason = matchError ? "error" : timedOut ? "timeout" : "absent";
|
|
2175
|
+
this.debugShell(key, "l1-miss", { reason, stats });
|
|
2176
|
+
return kvFallback(reason);
|
|
1990
2177
|
}
|
|
1991
|
-
if (response.status !== 200) {
|
|
2178
|
+
if (response.status !== 200 || !response.body) {
|
|
1992
2179
|
this.debugShell(key, "l1-miss", {
|
|
1993
2180
|
reason: "non-200",
|
|
1994
2181
|
status: response.status,
|
|
1995
|
-
|
|
2182
|
+
stats,
|
|
1996
2183
|
});
|
|
1997
|
-
return
|
|
2184
|
+
return kvFallback("non-200");
|
|
1998
2185
|
}
|
|
1999
2186
|
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
this.
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
}
|
|
2019
|
-
if (!isShellEnvelope(value)) {
|
|
2187
|
+
// Unlike other L1 families, shells with KV always check the durable
|
|
2188
|
+
// generation marker — see the capture-start/purge race documented
|
|
2189
|
+
// above. Without KV there are no markers: L1-only shells adopt the
|
|
2190
|
+
// data families' purge-mode read semantics (a hit that survived the
|
|
2191
|
+
// purge is trusted; the per-request mask covers this request's own
|
|
2192
|
+
// updateTag() writes; entries a purge cannot reach fall back to the
|
|
2193
|
+
// marker check, which fails open KV-less).
|
|
2194
|
+
const opened = await this.openShellFrame(
|
|
2195
|
+
response.body,
|
|
2196
|
+
this.edgeReadTimeoutMs,
|
|
2197
|
+
"edge cache body read",
|
|
2198
|
+
(head) =>
|
|
2199
|
+
this.kv
|
|
2200
|
+
? this.isGloballyInvalidated(head.t, head.ta, true)
|
|
2201
|
+
: this.isL1Invalidated(head.t, head.ta, response.headers),
|
|
2202
|
+
stats,
|
|
2203
|
+
);
|
|
2204
|
+
if (opened.status === "corrupt") {
|
|
2020
2205
|
this.debugShell(key, "l1-miss", {
|
|
2021
2206
|
reason: "malformed",
|
|
2022
|
-
|
|
2023
|
-
bodyReadMs,
|
|
2207
|
+
stats,
|
|
2024
2208
|
});
|
|
2025
2209
|
return this.healCorruptL1(
|
|
2026
2210
|
cache,
|
|
2027
2211
|
request,
|
|
2028
|
-
|
|
2212
|
+
opened.error,
|
|
2029
2213
|
"getShell",
|
|
2030
|
-
() =>
|
|
2214
|
+
() => kvFallback("malformed"),
|
|
2031
2215
|
);
|
|
2032
2216
|
}
|
|
2033
|
-
|
|
2034
|
-
const now = Date.now();
|
|
2035
|
-
if (now > value.e) {
|
|
2217
|
+
if (opened.status === "timeout" || opened.status === "expired") {
|
|
2036
2218
|
this.debugShell(key, "l1-miss", {
|
|
2037
|
-
reason:
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
expiresAt: value.e,
|
|
2219
|
+
reason: opened.status,
|
|
2220
|
+
stats,
|
|
2221
|
+
expiresAt: opened.head?.e,
|
|
2041
2222
|
});
|
|
2042
|
-
return
|
|
2223
|
+
return kvFallback(opened.status);
|
|
2043
2224
|
}
|
|
2044
|
-
|
|
2045
|
-
// Unlike other L1 families, shells with KV always check the durable
|
|
2046
|
-
// generation marker — see the capture-start/purge race documented
|
|
2047
|
-
// above. Without KV there are no markers: L1-only shells adopt the
|
|
2048
|
-
// data families' purge-mode read semantics (a hit that survived the
|
|
2049
|
-
// purge is trusted; the per-request memo masks this request's own
|
|
2050
|
-
// updateTag() writes; entries a purge cannot reach fall back to the
|
|
2051
|
-
// marker check, which fails open KV-less).
|
|
2052
|
-
const markerStartedAt = INTERNAL_RANGO_DEBUG ? Date.now() : 0;
|
|
2053
|
-
const invalidated = this.kv
|
|
2054
|
-
? await this.isGloballyInvalidated(value.t, value.ta)
|
|
2055
|
-
: await this.isL1Invalidated(value.t, value.ta, response.headers);
|
|
2056
|
-
const markerMs = INTERNAL_RANGO_DEBUG
|
|
2057
|
-
? Date.now() - markerStartedAt
|
|
2058
|
-
: undefined;
|
|
2059
|
-
if (invalidated) {
|
|
2225
|
+
if (opened.status === "invalidated") {
|
|
2060
2226
|
this.debugShell(key, "marker-invalidated", {
|
|
2061
2227
|
tier: "l1",
|
|
2062
|
-
|
|
2063
|
-
bodyReadMs,
|
|
2064
|
-
markerMs,
|
|
2228
|
+
stats,
|
|
2065
2229
|
});
|
|
2066
2230
|
return null;
|
|
2067
2231
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2232
|
+
const read = this.shellDocumentRead(
|
|
2233
|
+
key,
|
|
2234
|
+
opened,
|
|
2235
|
+
snapshotBudget,
|
|
2236
|
+
stats,
|
|
2237
|
+
response.headers,
|
|
2238
|
+
);
|
|
2239
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
2240
|
+
this.debugShell(key, "l1-hit", {
|
|
2241
|
+
freshness: read.shouldRevalidate ? "stale" : "fresh",
|
|
2242
|
+
stats,
|
|
2243
|
+
expiresAt: opened.head.e,
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
return read;
|
|
2081
2247
|
} catch (error) {
|
|
2082
2248
|
reportCacheError(error, "cache-read", "[CFCacheStore] getShell");
|
|
2083
2249
|
this.debugShell(key, "l1-miss", { reason: "error" });
|
|
2084
|
-
return
|
|
2250
|
+
return kvFallback("error");
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
/** @internal SegmentCacheStore.dropShellMemo */
|
|
2255
|
+
dropShellMemo(key: string): void {
|
|
2256
|
+
cfShellMemo.delete(this.shellMemoKey(key));
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
/**
|
|
2260
|
+
* Start the marker reads of the tags this shell key carried last time (and
|
|
2261
|
+
* the route's static tags) alongside the entry read (readShellTagMarker),
|
|
2262
|
+
* so the marker check after the head reuses them. With KV only: without it
|
|
2263
|
+
* there are no markers to read. A wrong hint costs a wasted read; the check
|
|
2264
|
+
* itself uses the head's tags.
|
|
2265
|
+
*/
|
|
2266
|
+
private prefetchShellMarkers(
|
|
2267
|
+
memoKey: string,
|
|
2268
|
+
routeTags: readonly string[] | undefined,
|
|
2269
|
+
stats: ShellReadStats | undefined,
|
|
2270
|
+
): void {
|
|
2271
|
+
if (!this.kv) return;
|
|
2272
|
+
const tags = hintedTags(cfTagHints.get(memoKey), routeTags);
|
|
2273
|
+
if (tags.length === 0) return;
|
|
2274
|
+
if (stats) {
|
|
2275
|
+
stats.markerHinted = tags;
|
|
2276
|
+
stats.markerHintStartedAt = performance.now();
|
|
2277
|
+
}
|
|
2278
|
+
for (const tag of tags) this.readShellTagMarker(tag).catch(() => {});
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
/**
|
|
2282
|
+
* A HIT: remember the entry's tags for the next read's hints, and fill the
|
|
2283
|
+
* marker row. The shell-read marker values stay in their own record: the
|
|
2284
|
+
* request's data reads (the tail's, or a partial navigation's matchPartial
|
|
2285
|
+
* on the same context) read their markers as before.
|
|
2286
|
+
*/
|
|
2287
|
+
private recordShellHit(
|
|
2288
|
+
key: string,
|
|
2289
|
+
tags: string[] | undefined,
|
|
2290
|
+
stats: ShellReadStats | undefined,
|
|
2291
|
+
): void {
|
|
2292
|
+
cfTagHints.remember(this.shellMemoKey(key), tags);
|
|
2293
|
+
if (!stats) return;
|
|
2294
|
+
const ctx = _getRequestContext();
|
|
2295
|
+
if (ctx) {
|
|
2296
|
+
recordMarkerRow(stats, tags, getShellMarkerReads(ctx, this).outcomes);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
/** The isolate marker memo's key for a tag: markers are per namespace and version. */
|
|
2301
|
+
private markerMemoKey(tag: string): string {
|
|
2302
|
+
return `${this.namespace ?? ""}\u0000${this.version ?? ""}\u0000${tag}`;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
/** The memo key: one per namespace, build version, base URL, and shell key. */
|
|
2306
|
+
private shellMemoKey(key: string): string {
|
|
2307
|
+
return `${this.namespace ?? ""}\u0000${this.keyToRequest(`${SHELL_KEY_PREFIX}${key}`).url}`;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
/**
|
|
2311
|
+
* Serve a memoized shell (shell-memo.ts). The tag-marker check is the only
|
|
2312
|
+
* store I/O left, so it is started first and awaited before anything is
|
|
2313
|
+
* returned: nothing of an invalidated shell reaches the response. A shell
|
|
2314
|
+
* that went stale since it was memoized, or that predates a tag this
|
|
2315
|
+
* isolate is invalidating or just invalidated (RecentTagInvalidations), is
|
|
2316
|
+
* dropped and read from the store (SWR recapture scheduling and each mode's
|
|
2317
|
+
* invalidation semantics stay with the store read). That read keeps the
|
|
2318
|
+
* caller's `tagHints`: cfTagHints may have evicted the key while the memo
|
|
2319
|
+
* still holds it.
|
|
2320
|
+
*/
|
|
2321
|
+
private async readMemoizedShell(
|
|
2322
|
+
key: string,
|
|
2323
|
+
memoKey: string,
|
|
2324
|
+
memoized: CFShellMemoValue,
|
|
2325
|
+
options: ShellDocumentReadOptions | undefined,
|
|
2326
|
+
snapshotBudget: SnapshotReadBudget,
|
|
2327
|
+
): Promise<ShellDocumentRead | null> {
|
|
2328
|
+
const { head } = memoized;
|
|
2329
|
+
if (!isMemoizableShell(head)) {
|
|
2330
|
+
cfShellMemo.delete(memoKey);
|
|
2331
|
+
return this.readShellDocumentWithin(key, options, snapshotBudget);
|
|
2332
|
+
}
|
|
2333
|
+
const stats = this.shellReadStats("memo");
|
|
2334
|
+
const markerStartedAt = stats ? performance.now() : 0;
|
|
2335
|
+
const invalidated = await (this.kv
|
|
2336
|
+
? this.isGloballyInvalidated(head.t, head.ta, true)
|
|
2337
|
+
: this.isL1Invalidated(
|
|
2338
|
+
head.t,
|
|
2339
|
+
head.ta,
|
|
2340
|
+
memoized.headers ?? new Headers(),
|
|
2341
|
+
));
|
|
2342
|
+
if (stats) {
|
|
2343
|
+
// Nothing else to wait for: the marker IS the memo hit's critical path.
|
|
2344
|
+
stats.markerMs = stats.markerWaitMs = performance.now() - markerStartedAt;
|
|
2345
|
+
stats.tags = head.t?.length ?? 0;
|
|
2346
|
+
stats.preludeBytes = memoized.prelude.length;
|
|
2347
|
+
}
|
|
2348
|
+
if (invalidated) {
|
|
2349
|
+
cfShellMemo.delete(memoKey);
|
|
2350
|
+
this.debugShell(key, "marker-invalidated", {
|
|
2351
|
+
tier: "memo",
|
|
2352
|
+
stats,
|
|
2353
|
+
});
|
|
2354
|
+
return null;
|
|
2355
|
+
}
|
|
2356
|
+
if (stats) stats.memo = { hit: true, bytes: cfShellMemo.size };
|
|
2357
|
+
this.recordShellHit(key, head.t, stats);
|
|
2358
|
+
this.debugShell(key, "memo-hit", {
|
|
2359
|
+
freshness: "fresh",
|
|
2360
|
+
stats,
|
|
2361
|
+
expiresAt: head.e,
|
|
2362
|
+
});
|
|
2363
|
+
return {
|
|
2364
|
+
entry: shellHeadToEntry(head),
|
|
2365
|
+
prelude: memoized.prelude,
|
|
2366
|
+
shouldRevalidate: false,
|
|
2367
|
+
snapshot: Promise.resolve(memoized.snapshot),
|
|
2368
|
+
...(stats ? { stats } : {}),
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
/**
|
|
2373
|
+
* Stats for one shell read, allocated only when the request collects
|
|
2374
|
+
* `debugPerformance` metrics or the build has the internal shell trace on,
|
|
2375
|
+
* so a normal HIT takes no timestamps for them.
|
|
2376
|
+
*/
|
|
2377
|
+
private shellReadStats(
|
|
2378
|
+
tier: ShellReadStats["tier"],
|
|
2379
|
+
): ShellReadStats | undefined {
|
|
2380
|
+
return INTERNAL_RANGO_DEBUG || _getRequestContext()?._metricsStore
|
|
2381
|
+
? { tier }
|
|
2382
|
+
: undefined;
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
/**
|
|
2386
|
+
* Read a frame's head and prelude under one budget, starting the
|
|
2387
|
+
* tag-marker read (`isInvalidated`) the moment the head is parsed so it
|
|
2388
|
+
* overlaps the prelude read, then await it. Expired heads are rejected
|
|
2389
|
+
* before the marker read starts.
|
|
2390
|
+
*/
|
|
2391
|
+
private async openShellFrame(
|
|
2392
|
+
body: ReadableStream<Uint8Array>,
|
|
2393
|
+
budgetMs: number,
|
|
2394
|
+
label: string,
|
|
2395
|
+
isInvalidated: (head: ShellFrameHead) => Promise<boolean>,
|
|
2396
|
+
stats: ShellReadStats | undefined,
|
|
2397
|
+
): Promise<OpenedShellFrame> {
|
|
2398
|
+
const reader = new ShellFrameReader(body);
|
|
2399
|
+
let marker: Promise<boolean> | undefined;
|
|
2400
|
+
let markerSettledAt = 0;
|
|
2401
|
+
const headStartedAt = stats ? performance.now() : 0;
|
|
2402
|
+
const { value, timedOut } = await this.readWithTimeout(
|
|
2403
|
+
async (): Promise<OpenedShellFrame> => {
|
|
2404
|
+
const head = await reader.readHead();
|
|
2405
|
+
if (!head) {
|
|
2406
|
+
return {
|
|
2407
|
+
status: "corrupt",
|
|
2408
|
+
error: new Error("malformed/partial shell frame head"),
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
const markerStartedAt = stats ? performance.now() : 0;
|
|
2412
|
+
if (stats) {
|
|
2413
|
+
stats.headMs = markerStartedAt - headStartedAt;
|
|
2414
|
+
stats.headBytes = reader.headBytes;
|
|
2415
|
+
stats.tags = head.t?.length ?? 0;
|
|
2416
|
+
if (stats.markerHintStartedAt !== undefined) {
|
|
2417
|
+
stats.markerLeadMs = markerStartedAt - stats.markerHintStartedAt;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
if (Date.now() > head.e) return { status: "expired", head };
|
|
2421
|
+
marker = isInvalidated(head).then((invalidated) => {
|
|
2422
|
+
if (stats) {
|
|
2423
|
+
markerSettledAt = performance.now();
|
|
2424
|
+
stats.markerMs = markerSettledAt - markerStartedAt;
|
|
2425
|
+
}
|
|
2426
|
+
return invalidated;
|
|
2427
|
+
});
|
|
2428
|
+
marker.catch(() => {});
|
|
2429
|
+
const prelude = await reader.take(head.pl);
|
|
2430
|
+
if (!prelude) {
|
|
2431
|
+
return {
|
|
2432
|
+
status: "corrupt",
|
|
2433
|
+
error: new Error("shell frame ends inside the prelude"),
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
2436
|
+
if (stats) {
|
|
2437
|
+
stats.preludeMs = performance.now() - markerStartedAt;
|
|
2438
|
+
stats.preludeBytes = prelude.length;
|
|
2439
|
+
}
|
|
2440
|
+
return { status: "ok", head, prelude, reader };
|
|
2441
|
+
},
|
|
2442
|
+
budgetMs,
|
|
2443
|
+
label,
|
|
2444
|
+
);
|
|
2445
|
+
if (timedOut || !value) {
|
|
2446
|
+
reader.cancel();
|
|
2447
|
+
if (stats) {
|
|
2448
|
+
// The budget ran out: the whole elapsed read, split at the head.
|
|
2449
|
+
const elapsed = performance.now() - headStartedAt;
|
|
2450
|
+
if (stats.headMs === undefined) stats.headMs = elapsed;
|
|
2451
|
+
else stats.preludeMs = elapsed - stats.headMs;
|
|
2452
|
+
}
|
|
2453
|
+
return { status: "timeout" };
|
|
2454
|
+
}
|
|
2455
|
+
if (value.status !== "ok") {
|
|
2456
|
+
reader.cancel();
|
|
2457
|
+
return value;
|
|
2085
2458
|
}
|
|
2459
|
+
const preludeReadAt = stats ? performance.now() : 0;
|
|
2460
|
+
const invalidated = await marker!;
|
|
2461
|
+
if (stats) {
|
|
2462
|
+
stats.markerWaitMs = Math.max(0, markerSettledAt - preludeReadAt);
|
|
2463
|
+
}
|
|
2464
|
+
if (invalidated) {
|
|
2465
|
+
reader.cancel();
|
|
2466
|
+
return { status: "invalidated" };
|
|
2467
|
+
}
|
|
2468
|
+
return value;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
/**
|
|
2472
|
+
* The ShellDocumentRead for an opened frame. Its snapshot promise starts
|
|
2473
|
+
* reading the rest of the body now (off the commit path, not after it),
|
|
2474
|
+
* bounded by `budget` ({@link snapshotReadBudget}), and is registered with
|
|
2475
|
+
* waitUntil so the KV tier's L1 promotion and a corrupt entry's eviction
|
|
2476
|
+
* still run when nothing awaits it (a read that is not served). A snapshot
|
|
2477
|
+
* whose length differs from the head's `sl`, or that fails to parse, evicts
|
|
2478
|
+
* the entry from both tiers and reports cache-corrupt. Every failure
|
|
2479
|
+
* resolves undefined and names itself on `snapshotFailure` (`unavailable`
|
|
2480
|
+
* for a timeout or read error, `corrupt` otherwise): a document HIT then
|
|
2481
|
+
* degrades (rsc-rendering.ts serveShellHit), and getShell misses. Only
|
|
2482
|
+
* a read whose snapshot parsed goes into the per-isolate memo (when still
|
|
2483
|
+
* fresh), and `promote` receives its snapshot bytes (the KV tier's L1
|
|
2484
|
+
* promotion). `headers` are the L1 response's, kept for the KV-less
|
|
2485
|
+
* memo-hit marker check.
|
|
2486
|
+
*/
|
|
2487
|
+
private shellDocumentRead(
|
|
2488
|
+
key: string,
|
|
2489
|
+
opened: Extract<OpenedShellFrame, { status: "ok" }>,
|
|
2490
|
+
budget: SnapshotReadBudget,
|
|
2491
|
+
stats: ShellReadStats | undefined,
|
|
2492
|
+
headers?: Headers,
|
|
2493
|
+
promote?: (snapshotBytes: Uint8Array) => void,
|
|
2494
|
+
): ShellDocumentRead {
|
|
2495
|
+
const { head, prelude, reader } = opened;
|
|
2496
|
+
this.recordShellHit(key, head.t, stats);
|
|
2497
|
+
type SnapshotOutcome = {
|
|
2498
|
+
records?: ShellSnapshotRecord[];
|
|
2499
|
+
failure?: ShellSnapshotFailure;
|
|
2500
|
+
};
|
|
2501
|
+
const outcome = (async (): Promise<SnapshotOutcome> => {
|
|
2502
|
+
const readStartedAt = stats ? performance.now() : 0;
|
|
2503
|
+
const { value: rest, timedOut } = await this.readWithTimeout(
|
|
2504
|
+
() => reader.readRest(),
|
|
2505
|
+
budget.ms,
|
|
2506
|
+
"shell snapshot read",
|
|
2507
|
+
budget.consequence,
|
|
2508
|
+
);
|
|
2509
|
+
if (timedOut || !rest) {
|
|
2510
|
+
reader.cancel();
|
|
2511
|
+
return { failure: "unavailable" };
|
|
2512
|
+
}
|
|
2513
|
+
const parseStartedAt = stats ? performance.now() : 0;
|
|
2514
|
+
let records: ShellSnapshotRecord[] | undefined;
|
|
2515
|
+
try {
|
|
2516
|
+
if (rest.length !== head.sl) {
|
|
2517
|
+
throw new Error(
|
|
2518
|
+
`shell snapshot is ${rest.length} bytes, the head says ${head.sl}`,
|
|
2519
|
+
);
|
|
2520
|
+
}
|
|
2521
|
+
records = parseShellSnapshot(rest);
|
|
2522
|
+
} catch (error) {
|
|
2523
|
+
reportCacheError(
|
|
2524
|
+
error,
|
|
2525
|
+
"cache-corrupt",
|
|
2526
|
+
"[CFCacheStore] getShell: corrupt shell snapshot, evicting",
|
|
2527
|
+
);
|
|
2528
|
+
this.evictShell(key);
|
|
2529
|
+
return { failure: "corrupt" };
|
|
2530
|
+
}
|
|
2531
|
+
if (stats) {
|
|
2532
|
+
stats.snapshot = {
|
|
2533
|
+
readMs: parseStartedAt - readStartedAt,
|
|
2534
|
+
parseMs: performance.now() - parseStartedAt,
|
|
2535
|
+
bytes: rest.length,
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
if (isMemoizableShell(head)) {
|
|
2539
|
+
// A view into a larger body chunk would pin that chunk in the memo.
|
|
2540
|
+
const ownPrelude =
|
|
2541
|
+
prelude.byteLength === prelude.buffer.byteLength
|
|
2542
|
+
? prelude
|
|
2543
|
+
: prelude.slice();
|
|
2544
|
+
cfShellMemo.set(
|
|
2545
|
+
this.shellMemoKey(key),
|
|
2546
|
+
{ head, prelude: ownPrelude, snapshot: records, headers },
|
|
2547
|
+
ownPrelude.length + rest.length,
|
|
2548
|
+
this.memo.shellMs,
|
|
2549
|
+
this.memo.shellMaxBytes,
|
|
2550
|
+
);
|
|
2551
|
+
}
|
|
2552
|
+
promote?.(rest);
|
|
2553
|
+
return { records };
|
|
2554
|
+
})().catch((error: unknown): SnapshotOutcome => {
|
|
2555
|
+
reportCacheError(error, "cache-read", "[CFCacheStore] getShell snapshot");
|
|
2556
|
+
return { failure: "unavailable" };
|
|
2557
|
+
});
|
|
2558
|
+
if (this.waitUntil) this.waitUntil(() => outcome.then(() => {}));
|
|
2559
|
+
return {
|
|
2560
|
+
entry: shellHeadToEntry(head),
|
|
2561
|
+
prelude,
|
|
2562
|
+
shouldRevalidate: head.s > 0 && Date.now() > head.s,
|
|
2563
|
+
snapshot: outcome.then(({ records }) => records),
|
|
2564
|
+
snapshotFailure: outcome.then(({ failure }) => failure),
|
|
2565
|
+
...(stats && { stats }),
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
/** Delete a shell from both tiers (non-blocking). */
|
|
2570
|
+
private evictShell(key: string): void {
|
|
2571
|
+
const evict = async (): Promise<void> => {
|
|
2572
|
+
const cache = await this.getCache();
|
|
2573
|
+
await reportingAsync(
|
|
2574
|
+
() => cache.delete(this.keyToRequest(`${SHELL_KEY_PREFIX}${key}`)),
|
|
2575
|
+
"cache-delete",
|
|
2576
|
+
"[CFCacheStore] getShell: evict corrupt L1",
|
|
2577
|
+
);
|
|
2578
|
+
if (this.kv) {
|
|
2579
|
+
await this.evictKvKey(
|
|
2580
|
+
await this.toKVKey(`${SHELL_KEY_PREFIX}${key}`),
|
|
2581
|
+
"getShell",
|
|
2582
|
+
);
|
|
2583
|
+
}
|
|
2584
|
+
};
|
|
2585
|
+
if (this.waitUntil) this.waitUntil(evict);
|
|
2586
|
+
else void evict();
|
|
2086
2587
|
}
|
|
2087
2588
|
|
|
2088
2589
|
/**
|
|
2089
|
-
* Store a PPR shell
|
|
2090
|
-
*
|
|
2091
|
-
*
|
|
2092
|
-
*
|
|
2093
|
-
*
|
|
2094
|
-
*
|
|
2590
|
+
* Store a PPR shell in Cache API and, when KV is configured, KV — the same
|
|
2591
|
+
* cf-shell-frame bytes in both. The shared write is registered with
|
|
2592
|
+
* waitUntil and awaited so invalidation rejection can be acknowledged to
|
|
2593
|
+
* the capture scheduler. A KV-less store is L1-only by design (edge-only
|
|
2594
|
+
* ppr — see the section comment).
|
|
2595
|
+
*
|
|
2596
|
+
* KV rejects an expirationTtl below KV_MIN_EXPIRATION_TTL, so a shorter
|
|
2597
|
+
* retention (a `ppr` ttl + swr under 60, or a shell capped to a short or
|
|
2598
|
+
* aging route cache() entry) is written with that floor: the frame head's deadlines (`s`, `e`) are the
|
|
2599
|
+
* entry's, and every read checks `e` (openShellFrame), so the extra KV
|
|
2600
|
+
* lifetime is never served, and the shell is readable from every colo, not
|
|
2601
|
+
* only from the capturing colo's L1.
|
|
2095
2602
|
*/
|
|
2096
2603
|
async putShell(
|
|
2097
2604
|
key: string,
|
|
@@ -2111,6 +2618,9 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2111
2618
|
if (!this.kv && !this.tagPurge && Array.isArray(tags) && tags.length > 0) {
|
|
2112
2619
|
this.warnShellTagsNoEvictionOnce();
|
|
2113
2620
|
}
|
|
2621
|
+
// This isolate serves its own new capture from the next read on.
|
|
2622
|
+
cfShellMemo.delete(this.shellMemoKey(key));
|
|
2623
|
+
cfTagHints.remember(this.shellMemoKey(key), tags);
|
|
2114
2624
|
try {
|
|
2115
2625
|
const ttl = resolveTtl(ttlSeconds, this.defaults, DEFAULT_FUNCTION_TTL);
|
|
2116
2626
|
const swrWindow = resolveSwrWindow(swrSeconds, this.defaults);
|
|
@@ -2130,8 +2640,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2130
2640
|
const taggedAt =
|
|
2131
2641
|
Array.isArray(tags) && tags.length > 0 ? entry.createdAt : undefined;
|
|
2132
2642
|
|
|
2133
|
-
const writeKv = !!this.kv && retentionTtl
|
|
2134
|
-
const kvKey = writeKv
|
|
2643
|
+
const writeKv = !!this.kv && retentionTtl > 0;
|
|
2644
|
+
const kvKey = writeKv
|
|
2645
|
+
? await this.toKVKey(`${SHELL_KEY_PREFIX}${key}`)
|
|
2646
|
+
: null;
|
|
2135
2647
|
|
|
2136
2648
|
const write = (async (): Promise<"stored" | "invalidated" | void> => {
|
|
2137
2649
|
if (
|
|
@@ -2143,12 +2655,14 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2143
2655
|
return "invalidated";
|
|
2144
2656
|
}
|
|
2145
2657
|
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2658
|
+
// A navigationOnly entry stores no document half: no prelude bytes
|
|
2659
|
+
// and no postponed field (an omitted key, not an explicit undefined).
|
|
2660
|
+
const prelude =
|
|
2661
|
+
entry.prelude !== undefined
|
|
2662
|
+
? base64ToBytes(entry.prelude)
|
|
2663
|
+
: new Uint8Array(0);
|
|
2664
|
+
const snapshotBytes = encodeShellSnapshot(entry.snapshot);
|
|
2665
|
+
const head: ShellFrameHead = {
|
|
2152
2666
|
rv: entry.reactVersion,
|
|
2153
2667
|
bv: entry.buildVersion,
|
|
2154
2668
|
c: entry.createdAt,
|
|
@@ -2157,24 +2671,23 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2157
2671
|
t: tags,
|
|
2158
2672
|
ta: taggedAt,
|
|
2159
2673
|
i: entry.initialTheme,
|
|
2160
|
-
sn: entry.snapshot,
|
|
2161
2674
|
dk: entry.docKey,
|
|
2162
|
-
|
|
2163
|
-
tw: entry.transitionWhen,
|
|
2675
|
+
pr: entry.prunedRecords,
|
|
2164
2676
|
no: entry.navigationOnly,
|
|
2677
|
+
...(entry.postponed !== undefined ? { po: entry.postponed } : {}),
|
|
2678
|
+
pl: prelude.length,
|
|
2679
|
+
sl: snapshotBytes?.length ?? 0,
|
|
2165
2680
|
};
|
|
2166
|
-
const
|
|
2681
|
+
const frame = encodeShellFrame(head, prelude, snapshotBytes);
|
|
2167
2682
|
const writes: Promise<boolean>[] = [
|
|
2168
2683
|
(async () => {
|
|
2169
2684
|
try {
|
|
2170
2685
|
const cache = await this.getCache();
|
|
2171
2686
|
await cache.put(
|
|
2172
|
-
this.keyToRequest(
|
|
2173
|
-
this.
|
|
2687
|
+
this.keyToRequest(`${SHELL_KEY_PREFIX}${key}`),
|
|
2688
|
+
this.shellFrameResponse(frame, head),
|
|
2174
2689
|
);
|
|
2175
|
-
this.debugShell(key, "l1-stored", {
|
|
2176
|
-
expiresAt: envelope.e,
|
|
2177
|
-
});
|
|
2690
|
+
this.debugShell(key, "l1-stored", { expiresAt: head.e });
|
|
2178
2691
|
return true;
|
|
2179
2692
|
} catch (error) {
|
|
2180
2693
|
reportCacheError(
|
|
@@ -2190,12 +2703,15 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2190
2703
|
writes.push(
|
|
2191
2704
|
(async () => {
|
|
2192
2705
|
try {
|
|
2193
|
-
await this.kv!.put(kvKey,
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2706
|
+
await this.kv!.put(kvKey, shellFrameToText(frame), {
|
|
2707
|
+
// Whole seconds, at least KV's floor: the frame head keeps
|
|
2708
|
+
// the exact deadlines the reads check.
|
|
2709
|
+
expirationTtl: Math.max(
|
|
2710
|
+
KV_MIN_EXPIRATION_TTL,
|
|
2711
|
+
Math.ceil(retentionTtl),
|
|
2712
|
+
),
|
|
2198
2713
|
});
|
|
2714
|
+
this.debugShell(key, "kv-stored", { expiresAt: head.e });
|
|
2199
2715
|
return true;
|
|
2200
2716
|
} catch (error) {
|
|
2201
2717
|
reportCacheError(
|
|
@@ -2219,107 +2735,134 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2219
2735
|
}
|
|
2220
2736
|
}
|
|
2221
2737
|
|
|
2222
|
-
/**
|
|
2223
|
-
private
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
postponed: envelope.po,
|
|
2227
|
-
reactVersion: envelope.rv,
|
|
2228
|
-
buildVersion: envelope.bv,
|
|
2229
|
-
initialTheme: envelope.i,
|
|
2230
|
-
snapshot: envelope.sn,
|
|
2231
|
-
docKey: envelope.dk,
|
|
2232
|
-
handlerLiveHoles: envelope.lh,
|
|
2233
|
-
transitionWhen: envelope.tw,
|
|
2234
|
-
navigationOnly: envelope.no,
|
|
2235
|
-
createdAt: envelope.c,
|
|
2236
|
-
};
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
/** Build the Cache API representation of the coupled shell envelope. */
|
|
2240
|
-
private shellEnvelopeResponse(
|
|
2241
|
-
body: string,
|
|
2242
|
-
envelope: CFShellEnvelope,
|
|
2738
|
+
/** The Cache API representation of a shell frame. */
|
|
2739
|
+
private shellFrameResponse(
|
|
2740
|
+
frame: Uint8Array<ArrayBuffer>,
|
|
2741
|
+
head: ShellFrameHead,
|
|
2243
2742
|
): Response {
|
|
2244
|
-
const remainingTtl = Math.max(
|
|
2245
|
-
|
|
2246
|
-
Math.floor((envelope.e - Date.now()) / 1000),
|
|
2247
|
-
);
|
|
2248
|
-
return new Response(body, {
|
|
2743
|
+
const remainingTtl = Math.max(1, Math.floor((head.e - Date.now()) / 1000));
|
|
2744
|
+
return new Response(frame, {
|
|
2249
2745
|
headers: {
|
|
2250
|
-
"Content-Type": "application/
|
|
2746
|
+
"Content-Type": "application/octet-stream",
|
|
2251
2747
|
"Cache-Control": `public, max-age=${remainingTtl}`,
|
|
2252
|
-
[CACHE_STALE_AT_HEADER]: String(
|
|
2253
|
-
[CACHE_EXPIRES_AT_HEADER]: String(
|
|
2748
|
+
[CACHE_STALE_AT_HEADER]: String(head.s),
|
|
2749
|
+
[CACHE_EXPIRES_AT_HEADER]: String(head.e),
|
|
2254
2750
|
[CACHE_STATUS_HEADER]: "HIT",
|
|
2255
|
-
...this.tagHeaderEntries(
|
|
2751
|
+
...this.tagHeaderEntries(head.t, head.ta),
|
|
2256
2752
|
},
|
|
2257
2753
|
});
|
|
2258
2754
|
}
|
|
2259
2755
|
|
|
2260
|
-
/**
|
|
2261
|
-
|
|
2756
|
+
/**
|
|
2757
|
+
* KV shell fallback: the same prelude-first read, then L1 promotion. After
|
|
2758
|
+
* an L1 attempt, `stats` is that read's and gains the L1 attempt's time
|
|
2759
|
+
* (`l1MissMs`) before its per-tier fields restart for KV.
|
|
2760
|
+
*/
|
|
2761
|
+
private async kvReadShellDocument(
|
|
2262
2762
|
key: string,
|
|
2263
|
-
|
|
2763
|
+
snapshotBudget: SnapshotReadBudget,
|
|
2764
|
+
stats?: ShellReadStats,
|
|
2765
|
+
l1StartedAt = 0,
|
|
2766
|
+
l1MissReason?: string,
|
|
2767
|
+
): Promise<ShellDocumentRead | null> {
|
|
2264
2768
|
if (!this.kv) return null;
|
|
2769
|
+
if (stats) {
|
|
2770
|
+
stats.l1MissMs = performance.now() - l1StartedAt;
|
|
2771
|
+
stats.l1MissReason = l1MissReason;
|
|
2772
|
+
stats.tier = "kv";
|
|
2773
|
+
stats.matchMs = stats.headMs = stats.preludeMs = undefined;
|
|
2774
|
+
stats.markerMs = stats.markerWaitMs = undefined;
|
|
2775
|
+
stats.headBytes = stats.preludeBytes = stats.tags = undefined;
|
|
2776
|
+
} else {
|
|
2777
|
+
stats = this.shellReadStats("kv");
|
|
2778
|
+
}
|
|
2265
2779
|
try {
|
|
2266
|
-
const readStartedAt =
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
const
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2780
|
+
const readStartedAt = stats ? performance.now() : 0;
|
|
2781
|
+
// One kvReadTimeoutMs budget covers opening the value and reading its
|
|
2782
|
+
// head and prelude; the remainder after the open bounds the frame read.
|
|
2783
|
+
const deadline = Date.now() + this.kvReadTimeoutMs;
|
|
2784
|
+
const kvKey = await this.toKVKey(`${SHELL_KEY_PREFIX}${key}`);
|
|
2785
|
+
// A transient read REJECTION propagates to the catch below (reported
|
|
2786
|
+
// cache-read, the entry left intact); only a malformed frame on a body
|
|
2787
|
+
// that WAS read is corruption that evicts.
|
|
2788
|
+
const { value: raw, timedOut } = await this.readWithTimeout<unknown>(
|
|
2789
|
+
() => this.kv!.get(kvKey, { type: "stream" }),
|
|
2790
|
+
this.kvReadTimeoutMs,
|
|
2791
|
+
"KV read",
|
|
2792
|
+
);
|
|
2793
|
+
if (stats) stats.matchMs = performance.now() - readStartedAt;
|
|
2794
|
+
if (timedOut || raw == null) {
|
|
2278
2795
|
this.debugShell(key, "kv-miss", {
|
|
2279
2796
|
reason: timedOut ? "timeout" : "unavailable",
|
|
2280
|
-
|
|
2797
|
+
stats,
|
|
2281
2798
|
});
|
|
2282
2799
|
return null;
|
|
2283
2800
|
}
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2801
|
+
// A binding that ignores `type` (a test shim) hands back the stored
|
|
2802
|
+
// string itself.
|
|
2803
|
+
const body =
|
|
2804
|
+
raw instanceof ReadableStream
|
|
2805
|
+
? (raw as ReadableStream<Uint8Array>)
|
|
2806
|
+
: new Response(raw as BodyInit).body!;
|
|
2807
|
+
const opened = await this.openShellFrame(
|
|
2808
|
+
body,
|
|
2809
|
+
// readWithTimeout treats <= 0 as "no budget": keep a spent budget
|
|
2810
|
+
// spent (1 ms) rather than unbounded.
|
|
2811
|
+
this.kvReadTimeoutMs > 0 ? Math.max(1, deadline - Date.now()) : 0,
|
|
2812
|
+
"KV read",
|
|
2813
|
+
(head) => this.isGloballyInvalidated(head.t, head.ta, true),
|
|
2814
|
+
stats,
|
|
2815
|
+
);
|
|
2816
|
+
if (opened.status === "corrupt") {
|
|
2817
|
+
reportCacheError(
|
|
2818
|
+
opened.error,
|
|
2819
|
+
"cache-corrupt",
|
|
2820
|
+
"[CFCacheStore] getShell: malformed shell frame in KV, evicting",
|
|
2821
|
+
);
|
|
2822
|
+
this.scheduleKvEvict(kvKey, "getShell");
|
|
2287
2823
|
this.debugShell(key, "kv-miss", {
|
|
2288
|
-
reason: "
|
|
2289
|
-
|
|
2290
|
-
expiresAt: envelope.e,
|
|
2824
|
+
reason: "malformed",
|
|
2825
|
+
stats,
|
|
2291
2826
|
});
|
|
2292
2827
|
return null;
|
|
2293
2828
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
if (invalidated) {
|
|
2829
|
+
if (opened.status === "timeout" || opened.status === "expired") {
|
|
2830
|
+
this.debugShell(key, "kv-miss", {
|
|
2831
|
+
reason: opened.status,
|
|
2832
|
+
stats,
|
|
2833
|
+
expiresAt: opened.head?.e,
|
|
2834
|
+
});
|
|
2835
|
+
return null;
|
|
2836
|
+
}
|
|
2837
|
+
if (opened.status === "invalidated") {
|
|
2303
2838
|
this.debugShell(key, "marker-invalidated", {
|
|
2304
2839
|
tier: "kv",
|
|
2305
|
-
|
|
2306
|
-
markerMs,
|
|
2840
|
+
stats,
|
|
2307
2841
|
});
|
|
2308
2842
|
return null;
|
|
2309
2843
|
}
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2844
|
+
const read = this.shellDocumentRead(
|
|
2845
|
+
key,
|
|
2846
|
+
opened,
|
|
2847
|
+
snapshotBudget,
|
|
2848
|
+
stats,
|
|
2849
|
+
undefined,
|
|
2850
|
+
(snapshotBytes) =>
|
|
2851
|
+
this.promoteShellToL1(
|
|
2852
|
+
key,
|
|
2853
|
+
opened.head,
|
|
2854
|
+
opened.prelude,
|
|
2855
|
+
snapshotBytes,
|
|
2856
|
+
),
|
|
2857
|
+
);
|
|
2858
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
2859
|
+
this.debugShell(key, "kv-hit", {
|
|
2860
|
+
freshness: read.shouldRevalidate ? "stale" : "fresh",
|
|
2861
|
+
stats,
|
|
2862
|
+
expiresAt: opened.head.e,
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
return read;
|
|
2323
2866
|
} catch (error) {
|
|
2324
2867
|
reportCacheError(error, "cache-read", "[CFCacheStore] kvGetShell");
|
|
2325
2868
|
this.debugShell(key, "kv-miss", { reason: "error" });
|
|
@@ -2328,24 +2871,28 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2328
2871
|
}
|
|
2329
2872
|
|
|
2330
2873
|
/** Promote a valid KV shell into the per-colo Cache API tier. */
|
|
2331
|
-
private promoteShellToL1(
|
|
2874
|
+
private promoteShellToL1(
|
|
2875
|
+
key: string,
|
|
2876
|
+
head: ShellFrameHead,
|
|
2877
|
+
prelude: Uint8Array,
|
|
2878
|
+
snapshotBytes: Uint8Array,
|
|
2879
|
+
): void {
|
|
2332
2880
|
if (!this.waitUntil) return;
|
|
2333
2881
|
this.waitUntil(() =>
|
|
2334
2882
|
reportingAsync(
|
|
2335
2883
|
async () => {
|
|
2336
|
-
if (Date.now() >
|
|
2884
|
+
if (Date.now() > head.e) return;
|
|
2337
2885
|
const cache = await this.getCache();
|
|
2338
|
-
const body = JSON.stringify(envelope);
|
|
2339
2886
|
await cache.put(
|
|
2340
|
-
this.keyToRequest(
|
|
2341
|
-
this.
|
|
2887
|
+
this.keyToRequest(`${SHELL_KEY_PREFIX}${key}`),
|
|
2888
|
+
this.shellFrameResponse(
|
|
2889
|
+
encodeShellFrame(head, prelude, snapshotBytes),
|
|
2890
|
+
head,
|
|
2891
|
+
),
|
|
2342
2892
|
);
|
|
2343
2893
|
this.debugShell(key, "kv-promoted", {
|
|
2344
|
-
remainingTtl: Math.max(
|
|
2345
|
-
|
|
2346
|
-
Math.floor((envelope.e - Date.now()) / 1000),
|
|
2347
|
-
),
|
|
2348
|
-
expiresAt: envelope.e,
|
|
2894
|
+
remainingTtl: Math.max(1, Math.floor((head.e - Date.now()) / 1000)),
|
|
2895
|
+
expiresAt: head.e,
|
|
2349
2896
|
});
|
|
2350
2897
|
},
|
|
2351
2898
|
"cache-write",
|
|
@@ -2692,6 +3239,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2692
3239
|
private async isGloballyInvalidated(
|
|
2693
3240
|
tags: string[] | undefined,
|
|
2694
3241
|
taggedAt: number | undefined,
|
|
3242
|
+
isolateMemo = false,
|
|
2695
3243
|
): Promise<boolean> {
|
|
2696
3244
|
// Array.isArray (not just truthiness): a non-array tags value - direct store
|
|
2697
3245
|
// misuse like setItem(k, v, { tags: "products" }), or a skewed KV envelope -
|
|
@@ -2700,12 +3248,21 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2700
3248
|
if (!this.kv || !Array.isArray(tags) || tags.length === 0 || !taggedAt)
|
|
2701
3249
|
return false;
|
|
2702
3250
|
const ctx = _getRequestContext();
|
|
3251
|
+
// This request's own invalidations first (#973): no marker read needed.
|
|
3252
|
+
if (maskedForRequest(ctx, this, tags, taggedAt)) return true;
|
|
2703
3253
|
const memo = ctx ? getTagMarkerMemo(ctx, this) : undefined;
|
|
2704
3254
|
const inflight = ctx ? getTagMarkerInflight(ctx, this) : undefined;
|
|
2705
3255
|
try {
|
|
2706
3256
|
const markers = await Promise.all(
|
|
2707
|
-
tags.map((tag) =>
|
|
3257
|
+
tags.map((tag) =>
|
|
3258
|
+
isolateMemo
|
|
3259
|
+
? this.readShellTagMarker(tag)
|
|
3260
|
+
: this.readTagMarker(tag, memo, inflight),
|
|
3261
|
+
),
|
|
2708
3262
|
);
|
|
3263
|
+
// A read in flight when this request invalidated one of the tags (a
|
|
3264
|
+
// sibling segment's updateTag()) resolved to the marker before it.
|
|
3265
|
+
if (maskedForRequest(ctx, this, tags, taggedAt)) return true;
|
|
2709
3266
|
for (const marker of markers) {
|
|
2710
3267
|
if (marker != null && marker >= taggedAt) return true;
|
|
2711
3268
|
}
|
|
@@ -2725,15 +3282,16 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2725
3282
|
* configured), invalidateTags() evicts L1 entries via a Cloudflare
|
|
2726
3283
|
* purge-by-tag call, so a hit that SURVIVED is trusted without a per-read
|
|
2727
3284
|
* marker lookup — that skipped lookup is the entire point of purge mode.
|
|
2728
|
-
* Only the
|
|
2729
|
-
* request that ran updateTag() still
|
|
2730
|
-
* propagation window (read-your-own-writes).
|
|
3285
|
+
* Only the request's mask and its memo are consulted (synchronous, no KV
|
|
3286
|
+
* read) so a request that ran updateTag() still misses its own entries
|
|
3287
|
+
* during the purge propagation window (read-your-own-writes).
|
|
2731
3288
|
*
|
|
2732
3289
|
* The trust is conditional on the entry actually CARRYING this store's
|
|
2733
3290
|
* entry Cache-Tag tokens (`headers`): an entry a purge cannot reach — one
|
|
2734
3291
|
* written before the tokens existed, or whose tag set overflowed the
|
|
2735
3292
|
* Cache-Tag header limit — keeps the full marker check, or purge mode
|
|
2736
|
-
* would serve it stale until TTL with no eviction path.
|
|
3293
|
+
* would serve it stale until TTL with no eviction path. KV-less there are
|
|
3294
|
+
* no markers, so it gets the per-request mask check like any hit (#973).
|
|
2737
3295
|
*
|
|
2738
3296
|
* Without tagPurge this is the full marker cascade. KV-tier reads and
|
|
2739
3297
|
* shells always use isGloballyInvalidated directly: purge cannot reach KV,
|
|
@@ -2747,11 +3305,13 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2747
3305
|
): Promise<boolean> {
|
|
2748
3306
|
if (!this.tagPurge) return this.isGloballyInvalidated(tags, taggedAt);
|
|
2749
3307
|
if (!Array.isArray(tags) || tags.length === 0 || !taggedAt) return false;
|
|
2750
|
-
if (!this.hasEntryCacheTags(headers)) {
|
|
3308
|
+
if (this.kv && !this.hasEntryCacheTags(headers)) {
|
|
2751
3309
|
return this.isGloballyInvalidated(tags, taggedAt);
|
|
2752
3310
|
}
|
|
2753
3311
|
const ctx = _getRequestContext();
|
|
2754
3312
|
if (!ctx) return false;
|
|
3313
|
+
if (maskedForRequest(ctx, this, tags, taggedAt)) return true;
|
|
3314
|
+
// Markers this request read from KV for other entries.
|
|
2755
3315
|
const memo = getTagMarkerMemo(ctx, this);
|
|
2756
3316
|
for (const tag of tags) {
|
|
2757
3317
|
const marker = memo.get(tag);
|
|
@@ -2768,9 +3328,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2768
3328
|
/**
|
|
2769
3329
|
* Read a tag's latest invalidation timestamp (or null if never invalidated)
|
|
2770
3330
|
* through the cascade: per-request memo -> per-colo L1 cache (only when
|
|
2771
|
-
* tagCacheTtl > 0) -> KV (the global truth).
|
|
2772
|
-
*
|
|
2773
|
-
* and every KV/L1 result is written back into the memo. A Cache API miss
|
|
3331
|
+
* tagCacheTtl > 0) -> KV (the global truth). Every KV/L1 result is written
|
|
3332
|
+
* back into the memo, so the request reads one value per tag. A Cache API miss
|
|
2774
3333
|
* always falls through to KV; absence is represented by a cached sentinel,
|
|
2775
3334
|
* never by a miss.
|
|
2776
3335
|
*
|
|
@@ -2803,6 +3362,52 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2803
3362
|
return this.fetchTagMarker(tag, memo);
|
|
2804
3363
|
}
|
|
2805
3364
|
|
|
3365
|
+
/**
|
|
3366
|
+
* A PPR shell read's marker for `tag`: this request's own store read if it
|
|
3367
|
+
* has one, else through the per-isolate marker memo (isolate-tag-memo.ts,
|
|
3368
|
+
* stale-while-revalidate
|
|
3369
|
+
* under `markerFreshMs` / `markerMaxStaleMs`; a request carrying the
|
|
3370
|
+
* fresh-reads cookie reads L1/KV). The value is kept in the request's
|
|
3371
|
+
* shell-read record, never in the per-request memo the data families read,
|
|
3372
|
+
* HIT or MISS. A store read still lands in the per-request memo, as any
|
|
3373
|
+
* marker read does; it is not stale. A timed-out read fails open for this
|
|
3374
|
+
* request and is not memoized.
|
|
3375
|
+
*/
|
|
3376
|
+
private readShellTagMarker(tag: string): Promise<number | null> {
|
|
3377
|
+
const ctx = _getRequestContext();
|
|
3378
|
+
const memo = ctx ? getTagMarkerMemo(ctx, this) : undefined;
|
|
3379
|
+
if (memo?.has(tag)) return Promise.resolve(memo.get(tag) ?? null);
|
|
3380
|
+
const shell = ctx ? getShellMarkerReads(ctx, this) : undefined;
|
|
3381
|
+
const pending = shell?.reads.get(tag);
|
|
3382
|
+
if (pending) return pending;
|
|
3383
|
+
const read = cfMarkerMemo.readThrough(
|
|
3384
|
+
this.markerMemoKey(tag),
|
|
3385
|
+
async (background) => {
|
|
3386
|
+
const outcome = { timedOut: false, masked: false };
|
|
3387
|
+
const value = await this.fetchTagMarker(
|
|
3388
|
+
tag,
|
|
3389
|
+
background ? undefined : memo,
|
|
3390
|
+
outcome,
|
|
3391
|
+
);
|
|
3392
|
+
return { value, memoize: !outcome.timedOut && !outcome.masked };
|
|
3393
|
+
},
|
|
3394
|
+
{
|
|
3395
|
+
freshMs: this.memo.markerFreshMs,
|
|
3396
|
+
maxStaleMs: this.memo.markerMaxStaleMs,
|
|
3397
|
+
bypass: freshReadsRequired(ctx),
|
|
3398
|
+
keepAlive: this.waitUntil
|
|
3399
|
+
? (refresh) => this.waitUntil!(() => refresh)
|
|
3400
|
+
: undefined,
|
|
3401
|
+
onOutcome:
|
|
3402
|
+
shell && (INTERNAL_RANGO_DEBUG || ctx?._metricsStore)
|
|
3403
|
+
? (outcome) => shell.outcomes.set(tag, outcome)
|
|
3404
|
+
: undefined,
|
|
3405
|
+
},
|
|
3406
|
+
);
|
|
3407
|
+
shell?.reads.set(tag, read);
|
|
3408
|
+
return read;
|
|
3409
|
+
}
|
|
3410
|
+
|
|
2806
3411
|
/**
|
|
2807
3412
|
* Uncached body of readTagMarker: L1 (per-colo Cache API, opt-in via
|
|
2808
3413
|
* tagCacheTtl) -> KV. Writes the resolved value back into the memo.
|
|
@@ -2811,19 +3416,18 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2811
3416
|
private async fetchTagMarker(
|
|
2812
3417
|
tag: string,
|
|
2813
3418
|
memo: Map<string, number | null> | undefined,
|
|
3419
|
+
read?: { timedOut: boolean; masked?: boolean },
|
|
3420
|
+
/** false: populate no L1 marker (isInvalidatedSincePastMemo). */
|
|
3421
|
+
publish = true,
|
|
2814
3422
|
): Promise<number | null> {
|
|
2815
|
-
// Write the resolved marker into the memo WITHOUT clobbering a value
|
|
2816
|
-
//
|
|
2817
|
-
//
|
|
2818
|
-
//
|
|
2819
|
-
|
|
2820
|
-
// read-your-own-writes for the rest of the request. If the tag was memoized
|
|
2821
|
-
// mid-read, that value wins and is returned. Without a memo, the read result
|
|
2822
|
-
// stands as-is.
|
|
2823
|
-
const memoize = (read: number | null): number | null => {
|
|
3423
|
+
// Write the resolved marker into the memo WITHOUT clobbering a value
|
|
3424
|
+
// another read of the tag memoized during our await: that value wins and
|
|
3425
|
+
// is returned, so the request keeps one value per tag. Without a memo,
|
|
3426
|
+
// the read result stands.
|
|
3427
|
+
const memoize = (value: number | null): number | null => {
|
|
2824
3428
|
if (memo && memo.has(tag)) return memo.get(tag) ?? null;
|
|
2825
|
-
memo?.set(tag,
|
|
2826
|
-
return
|
|
3429
|
+
memo?.set(tag, value);
|
|
3430
|
+
return value;
|
|
2827
3431
|
};
|
|
2828
3432
|
|
|
2829
3433
|
// L1 (per-colo) marker cache - opt-in via tagCacheTtl. Bounded by the same
|
|
@@ -2871,6 +3475,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2871
3475
|
"tag marker KV read",
|
|
2872
3476
|
);
|
|
2873
3477
|
if (timedOut) {
|
|
3478
|
+
if (read) read.timedOut = true;
|
|
2874
3479
|
// Memoize the fail-open result so the rest of this request is consistent
|
|
2875
3480
|
// (and does not re-pay the timeout per segment sharing the tag).
|
|
2876
3481
|
return memoize(null);
|
|
@@ -2878,10 +3483,17 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2878
3483
|
const value = raw != null ? Number(raw) : null;
|
|
2879
3484
|
const resolved = memoize(value);
|
|
2880
3485
|
|
|
2881
|
-
//
|
|
2882
|
-
//
|
|
2883
|
-
//
|
|
2884
|
-
|
|
3486
|
+
// A value read while this request invalidates the tag may predate the
|
|
3487
|
+
// invalidation's KV put: an L1 put of it could land after the put's own
|
|
3488
|
+
// write-through and hide the invalidation from the colo. The request
|
|
3489
|
+
// itself answers from its mask (#973). A shell read also skips the isolate
|
|
3490
|
+
// marker memo through `read.masked`; that memo keeps the higher value, so
|
|
3491
|
+
// the skip is conservative there.
|
|
3492
|
+
const masked = maskedForRequest(_getRequestContext(), this, [tag], 0);
|
|
3493
|
+
if (read) read.masked = masked;
|
|
3494
|
+
|
|
3495
|
+
// Populate L1 for subsequent reads in this colo (non-blocking).
|
|
3496
|
+
if (this.tagCacheTtl > 0 && !masked && publish) {
|
|
2885
3497
|
const put = () => this.putTagMarkerL1(tag, resolved);
|
|
2886
3498
|
if (this.waitUntil) this.waitUntil(put);
|
|
2887
3499
|
else void put();
|
|
@@ -3057,49 +3669,163 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
3057
3669
|
}
|
|
3058
3670
|
|
|
3059
3671
|
/**
|
|
3060
|
-
*
|
|
3061
|
-
*
|
|
3062
|
-
*
|
|
3063
|
-
*
|
|
3064
|
-
*
|
|
3065
|
-
*
|
|
3066
|
-
*
|
|
3067
|
-
*
|
|
3068
|
-
* consumer's purge-by-tag call with the entry Cache-Tags — the eviction the
|
|
3069
|
-
* per-read marker skip on L1 hits relies on. Finally fires onRevalidateTag
|
|
3070
|
-
* with the namespaced lookup Cache-Tags so a consumer purge evicts the
|
|
3071
|
-
* cached lookups in other colos promptly (otherwise they converge within
|
|
3072
|
-
* tagCacheTtl).
|
|
3073
|
-
*
|
|
3074
|
-
* Durable-write integrity: the in-memory write-through (memo + L1) for a tag
|
|
3075
|
-
* runs ONLY after that tag's KV marker write is confirmed. If any KV write
|
|
3076
|
-
* fails (transient error; over-limit keys are normalized by toKVKey rather
|
|
3077
|
-
* than rejected), this rejects with the
|
|
3078
|
-
* failed tags so an awaiting updateTag() surfaces the failure instead of
|
|
3079
|
-
* silently reporting success while other requests/colos serve stale data. The
|
|
3080
|
-
* eager purge still fires for the whole batch first (it is additive).
|
|
3081
|
-
*/
|
|
3082
|
-
/**
|
|
3083
|
-
* Shell tag-generation gate (SegmentCacheStore.isTagsInvalidatedSince): the
|
|
3084
|
-
* SAME KV markers used by runtime envelopes also evict immutable build shells
|
|
3085
|
-
* and captures whose write races updateTag(). Thin public wrapper over the
|
|
3086
|
-
* shell generation check (marker >= since, fail open); KV-less it degrades
|
|
3087
|
-
* to the per-request memo in purge mode and to false otherwise — see
|
|
3672
|
+
* SegmentCacheStore.isTagsInvalidatedSince: the SAME KV markers used by
|
|
3673
|
+
* runtime envelopes also evict immutable build shells, and gate the writes
|
|
3674
|
+
* of cache executions that started before an invalidation (#977,
|
|
3675
|
+
* tag-invalidation.ts predatesInvalidation). Marker >= since. A marker read
|
|
3676
|
+
* that fails or times out answers false (fail open), or true for the write
|
|
3677
|
+
* gate (`failClosed`). With KV each marker is read past the per-request
|
|
3678
|
+
* memo (isInvalidatedSincePastMemo); KV-less it degrades to the per-request
|
|
3679
|
+
* mask in purge mode and to false otherwise — see
|
|
3088
3680
|
* isShellGenerationInvalidated.
|
|
3089
3681
|
*/
|
|
3090
3682
|
async isTagsInvalidatedSince(
|
|
3091
3683
|
tags: string[],
|
|
3092
3684
|
sinceMs: number,
|
|
3685
|
+
options?: { failClosed?: boolean },
|
|
3093
3686
|
): Promise<boolean> {
|
|
3094
|
-
return this.isShellGenerationInvalidated(tags, sinceMs);
|
|
3687
|
+
if (!this.kv) return this.isShellGenerationInvalidated(tags, sinceMs);
|
|
3688
|
+
return this.isInvalidatedSincePastMemo(
|
|
3689
|
+
tags,
|
|
3690
|
+
sinceMs,
|
|
3691
|
+
options?.failClosed === true,
|
|
3692
|
+
);
|
|
3095
3693
|
}
|
|
3096
3694
|
|
|
3695
|
+
/**
|
|
3696
|
+
* isTagsInvalidatedSince with KV, never answered from a marker value this
|
|
3697
|
+
* request read before the asking gate. A write gate asks after an
|
|
3698
|
+
* execution that can run for seconds, and the per-request memo can hold
|
|
3699
|
+
* the marker the request read before it (a stale hit reads its entry's
|
|
3700
|
+
* markers, then its refresh runs): answered from it, another isolate's
|
|
3701
|
+
* invalidation in between let the refresh write.
|
|
3702
|
+
*
|
|
3703
|
+
* - This request's own invalidations answer from its mask, checked again
|
|
3704
|
+
* once the reads are back: a read in flight when the request invalidated
|
|
3705
|
+
* one of the tags resolved to the marker before it.
|
|
3706
|
+
* - Each tag is read through gateMarkerRead (request-tag-mask.ts): gates
|
|
3707
|
+
* asking while a read is in flight share it; a settled read is not
|
|
3708
|
+
* reused.
|
|
3709
|
+
* - The read returns its own value from L1 (tagCacheTtl) or KV, not one an
|
|
3710
|
+
* older read of the tag memoized while it was in flight, and fills the
|
|
3711
|
+
* memo only when it holds nothing, so a build shell's check still serves
|
|
3712
|
+
* the request's later reads. A timed-out read fills nothing.
|
|
3713
|
+
* - It publishes to L1 unless the request masked the tag
|
|
3714
|
+
* (fetchTagMarker), and never without a request context (a detached
|
|
3715
|
+
* waitUntil task), where a mask cannot be told apart.
|
|
3716
|
+
* - A read that fails or times out answers `failClosed`: the write gate
|
|
3717
|
+
* counts it as an invalidation, other callers as none.
|
|
3718
|
+
* @internal
|
|
3719
|
+
*/
|
|
3720
|
+
private async isInvalidatedSincePastMemo(
|
|
3721
|
+
tags: string[],
|
|
3722
|
+
since: number,
|
|
3723
|
+
failClosed: boolean,
|
|
3724
|
+
): Promise<boolean> {
|
|
3725
|
+
if (!Array.isArray(tags) || tags.length === 0 || !since) return false;
|
|
3726
|
+
const ctx = _getRequestContext();
|
|
3727
|
+
if (maskedForRequest(ctx, this, tags, since)) return true;
|
|
3728
|
+
const memo = ctx ? getTagMarkerMemo(ctx, this) : undefined;
|
|
3729
|
+
try {
|
|
3730
|
+
const reads = await Promise.all(
|
|
3731
|
+
tags.map((tag) =>
|
|
3732
|
+
gateMarkerRead(ctx, this, tag, since, async () => {
|
|
3733
|
+
const outcome = { timedOut: false };
|
|
3734
|
+
const marker = await this.fetchTagMarker(
|
|
3735
|
+
tag,
|
|
3736
|
+
undefined,
|
|
3737
|
+
outcome,
|
|
3738
|
+
ctx !== undefined,
|
|
3739
|
+
);
|
|
3740
|
+
if (outcome.timedOut) return { marker, answered: false };
|
|
3741
|
+
if (memo && !memo.has(tag)) memo.set(tag, marker);
|
|
3742
|
+
return { marker, answered: true };
|
|
3743
|
+
}),
|
|
3744
|
+
),
|
|
3745
|
+
);
|
|
3746
|
+
if (maskedForRequest(ctx, this, tags, since)) return true;
|
|
3747
|
+
return reads.some(
|
|
3748
|
+
({ marker, answered }) =>
|
|
3749
|
+
(!answered && failClosed) || (marker != null && marker >= since),
|
|
3750
|
+
);
|
|
3751
|
+
} catch (error) {
|
|
3752
|
+
reportCacheError(
|
|
3753
|
+
error,
|
|
3754
|
+
"cache-read",
|
|
3755
|
+
"[CFCacheStore] tag invalidation check",
|
|
3756
|
+
);
|
|
3757
|
+
return failClosed;
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
/**
|
|
3762
|
+
* Invalidate every entry tagged with any of `tags`. Receives the whole batch
|
|
3763
|
+
* from one updateTag()/revalidateTag() call so the eager-purge hook fires
|
|
3764
|
+
* ONCE (one CDN purge request, not one per tag). Before its first await it
|
|
3765
|
+
* masks the tags for the rest of the request (maskTagsForRequest), which is
|
|
3766
|
+
* all revalidateTag() waits for. For each tag it then records the KV marker
|
|
3767
|
+
* (the durable cross-colo truth that reads compare taggedAt against) and
|
|
3768
|
+
* writes the fresh marker straight into this colo's L1 (write-through, NOT
|
|
3769
|
+
* delete - a delete would let the next read re-read a not-yet-converged KV
|
|
3770
|
+
* value and re-arm the stale window). In purge mode (tagPurge) it then
|
|
3771
|
+
* AWAITS the consumer's purge-by-tag call with the entry Cache-Tags — the
|
|
3772
|
+
* eviction the per-read marker skip on L1 hits relies on. Finally fires
|
|
3773
|
+
* onRevalidateTag with the namespaced lookup Cache-Tags so a consumer purge
|
|
3774
|
+
* evicts the cached lookups in other colos promptly (otherwise they converge
|
|
3775
|
+
* within tagCacheTtl).
|
|
3776
|
+
*
|
|
3777
|
+
* Durable-write integrity: the write-through other requests read (the
|
|
3778
|
+
* isolate marker memo and L1) for a tag runs ONLY after that tag's KV marker
|
|
3779
|
+
* write is confirmed. If any KV write fails (transient error; over-limit
|
|
3780
|
+
* keys are normalized by toKVKey rather than rejected), this rejects with
|
|
3781
|
+
* the failed tags so an awaiting updateTag() surfaces the failure instead of
|
|
3782
|
+
* silently reporting success while other requests/colos serve stale data.
|
|
3783
|
+
* The purge and onRevalidateTag still fire for the whole batch once the
|
|
3784
|
+
* puts settle, whether or not one failed (they are additive); the rejection
|
|
3785
|
+
* follows them.
|
|
3786
|
+
*/
|
|
3097
3787
|
async invalidateTags(tags: string[]): Promise<void> {
|
|
3098
3788
|
if (tags.length === 0) return;
|
|
3099
3789
|
const invalidatedAt = Date.now();
|
|
3790
|
+
this.maskTagsForRequest(tags, invalidatedAt);
|
|
3791
|
+
// The invalidating isolate drops its memoized shells for these tags at
|
|
3792
|
+
// once; with KV every isolate's per-read marker check rejects the rest.
|
|
3793
|
+
// Reads already in flight must not memoize them again while the markers
|
|
3794
|
+
// and the purge are written (KV-less purge mode has no marker to reject
|
|
3795
|
+
// that copy): the record holds them out until one window after this
|
|
3796
|
+
// settles, at least the default window so a store with the memo off still
|
|
3797
|
+
// covers another store's reads in this isolate.
|
|
3798
|
+
cfShellMemo.deleteWhere((shell) => shellHasAnyTag(shell.head.t, tags));
|
|
3799
|
+
recentShellInvalidations.begin(tags, invalidatedAt);
|
|
3800
|
+
try {
|
|
3801
|
+
await this.writeTagInvalidation(tags, invalidatedAt);
|
|
3802
|
+
} finally {
|
|
3803
|
+
recentShellInvalidations.settle(
|
|
3804
|
+
tags,
|
|
3805
|
+
Math.max(this.memo.shellMs, DEFAULT_SHELL_MEMO_MS),
|
|
3806
|
+
);
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
/**
|
|
3811
|
+
* The request-local half of invalidateTags(), run before its first await
|
|
3812
|
+
* (#973): the request masks the tags (request-tag-mask.ts), so every later
|
|
3813
|
+
* read in the request (data families, purge-mode L1 hits, shell reads)
|
|
3814
|
+
* treats them as invalidated at `invalidatedAt` without waiting for the KV
|
|
3815
|
+
* put or the purge that revalidateTag() leaves to waitUntil. Skipped where
|
|
3816
|
+
* no read consults the mask (neither KV nor tagPurge).
|
|
3817
|
+
*/
|
|
3818
|
+
private maskTagsForRequest(tags: string[], invalidatedAt: number): void {
|
|
3819
|
+
if (!this.kv && !this.tagPurge) return;
|
|
3100
3820
|
const ctx = _getRequestContext();
|
|
3101
|
-
|
|
3821
|
+
if (ctx) maskRequestTags(ctx, this, tags, invalidatedAt);
|
|
3822
|
+
}
|
|
3102
3823
|
|
|
3824
|
+
/** invalidateTags' marker writes, tag purge, and hooks. */
|
|
3825
|
+
private async writeTagInvalidation(
|
|
3826
|
+
tags: string[],
|
|
3827
|
+
invalidatedAt: number,
|
|
3828
|
+
): Promise<void> {
|
|
3103
3829
|
if (!this.kv && !this.onRevalidateTag && !this.tagPurge) {
|
|
3104
3830
|
console.warn(
|
|
3105
3831
|
`[CFCacheStore] invalidateTags had no effect: configure a KV namespace ` +
|
|
@@ -3110,6 +3836,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
3110
3836
|
|
|
3111
3837
|
const failedTags = new Set<string>();
|
|
3112
3838
|
const errors: unknown[] = [];
|
|
3839
|
+
const lookupMarkerCacheActive = Boolean(this.kv) && this.tagCacheTtl > 0;
|
|
3113
3840
|
if (this.kv) {
|
|
3114
3841
|
// Markers written with no expiry (tagInvalidationTtl unset) never expire,
|
|
3115
3842
|
// so high-cardinality tags accumulate KV keys unboundedly with no reaper.
|
|
@@ -3145,25 +3872,16 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
3145
3872
|
}
|
|
3146
3873
|
}),
|
|
3147
3874
|
);
|
|
3148
|
-
}
|
|
3149
3875
|
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
// path ever consults — EXCEPT the memo in purge mode: isL1Invalidated()
|
|
3155
|
-
// consults it (and only it) on every L1 hit, so a KV-less purge-mode store
|
|
3156
|
-
// still writes the memo for same-request read-your-own-writes. The
|
|
3157
|
-
// onRevalidateTag purge below still fires regardless (it is additive and
|
|
3158
|
-
// external to the marker cascade). The memo write is synchronous
|
|
3159
|
-
// (read-your-own-writes); the L1 Cache API writes are independent, so fan
|
|
3160
|
-
// them out in parallel rather than awaiting each.
|
|
3161
|
-
const lookupMarkerCacheActive = Boolean(this.kv) && this.tagCacheTtl > 0;
|
|
3162
|
-
if (this.kv || this.tagPurge) {
|
|
3876
|
+
// Write-through (isolate marker memo + L1) only for tags with a
|
|
3877
|
+
// confirmed durable marker; this request's own mask was set up front
|
|
3878
|
+
// (maskTagsForRequest). The L1 Cache API writes are independent,
|
|
3879
|
+
// so fan them out in parallel rather than awaiting each.
|
|
3163
3880
|
const l1Writes: Promise<void>[] = [];
|
|
3164
3881
|
for (const tag of tags) {
|
|
3165
3882
|
if (failedTags.has(tag)) continue;
|
|
3166
|
-
|
|
3883
|
+
// Same isolate: later requests here see the invalidation at once.
|
|
3884
|
+
cfMarkerMemo.store(this.markerMemoKey(tag), invalidatedAt);
|
|
3167
3885
|
if (lookupMarkerCacheActive) {
|
|
3168
3886
|
l1Writes.push(
|
|
3169
3887
|
this.putTagMarkerL1(tag, invalidatedAt, { critical: true }),
|