@rangojs/router 0.0.0-experimental.d20dd405 → 0.0.0-experimental.d5d3070a
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/AGENTS.md +8 -0
- package/README.md +57 -14
- package/dist/bin/rango.js +312 -93
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +1632 -705
- package/package.json +64 -12
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +61 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +224 -32
- package/skills/caching/SKILL.md +279 -17
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +217 -31
- package/skills/host-router/SKILL.md +83 -23
- package/skills/intercept/SKILL.md +5 -6
- package/skills/layout/SKILL.md +11 -9
- package/skills/links/SKILL.md +22 -10
- package/skills/loader/SKILL.md +172 -23
- package/skills/middleware/SKILL.md +16 -10
- package/skills/migrate-nextjs/SKILL.md +38 -16
- package/skills/migrate-react-router/SKILL.md +4 -2
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +9 -7
- package/skills/prerender/SKILL.md +45 -43
- package/skills/rango/SKILL.md +251 -26
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +115 -48
- package/skills/route/SKILL.md +22 -5
- package/skills/router-setup/SKILL.md +29 -6
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +78 -42
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/typesafety/SKILL.md +313 -28
- package/skills/use-cache/SKILL.md +46 -14
- package/skills/view-transitions/SKILL.md +85 -3
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +151 -149
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +67 -126
- package/src/browser/navigation-client.ts +110 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +32 -82
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +108 -144
- package/src/browser/prefetch/cache.ts +107 -72
- package/src/browser/prefetch/fetch.ts +186 -34
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +30 -7
- package/src/browser/react/NavigationProvider.tsx +173 -160
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +17 -2
- package/src/browser/react/index.ts +0 -51
- package/src/browser/react/location-state-shared.ts +178 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +10 -5
- package/src/browser/react/use-params.ts +3 -6
- package/src/browser/react/use-reverse.ts +19 -12
- package/src/browser/react/use-router.ts +20 -5
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/response-adapter.ts +80 -5
- package/src/browser/rsc-router.tsx +73 -34
- package/src/browser/scroll-restoration.ts +38 -28
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +214 -55
- package/src/browser/types.ts +51 -21
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +117 -15
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +117 -23
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +55 -28
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +199 -37
- package/src/cache/cache-scope.ts +188 -82
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +1990 -377
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +72 -20
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +10 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +31 -31
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +42 -22
- package/src/client.tsx +33 -62
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +18 -6
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +62 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +37 -8
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +136 -24
- package/src/index.rsc.ts +87 -11
- package/src/index.ts +85 -10
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +34 -0
- package/src/reverse.ts +16 -13
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +276 -283
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +40 -17
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +73 -25
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +6 -1
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +39 -20
- package/src/router/lazy-includes.ts +9 -46
- package/src/router/loader-resolution.ts +152 -72
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +57 -58
- package/src/router/match-middleware/background-revalidation.ts +6 -7
- package/src/router/match-middleware/cache-lookup.ts +159 -285
- package/src/router/match-middleware/cache-store.ts +31 -34
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +31 -80
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +216 -163
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +47 -89
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +104 -65
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +110 -74
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +3 -28
- package/src/router/router-interfaces.ts +83 -35
- package/src/router/router-options.ts +136 -5
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +85 -84
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +62 -37
- package/src/router/segment-resolution/revalidation.ts +227 -306
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +163 -64
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +109 -53
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +262 -219
- package/src/rsc/helpers.ts +100 -6
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +59 -9
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +85 -188
- package/src/rsc/rsc-rendering.ts +91 -77
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +256 -117
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +17 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-loader-promise.ts +31 -4
- package/src/segment-system.tsx +151 -136
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +150 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +21 -42
- package/src/server/request-context.ts +225 -133
- package/src/ssr/index.tsx +19 -18
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +701 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +343 -0
- package/src/testing/render-route.tsx +581 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +97 -22
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +8 -22
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +18 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +19 -5
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +95 -82
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +48 -67
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +44 -0
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +2 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +126 -8
- package/src/vite/plugins/cjs-to-esm.ts +11 -19
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +12 -11
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +4 -75
- package/src/vite/plugins/expose-id-utils.ts +88 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
- package/src/vite/plugins/expose-internal-ids.ts +57 -68
- package/src/vite/plugins/performance-tracks.ts +9 -16
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +26 -49
- package/src/vite/plugins/version-injector.ts +2 -32
- package/src/vite/plugins/version-plugin.ts +37 -40
- package/src/vite/plugins/virtual-entries.ts +13 -18
- package/src/vite/rango.ts +60 -104
- package/src/vite/router-discovery.ts +236 -83
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +54 -40
- package/src/vite/utils/shared-utils.ts +90 -41
- package/dist/__internal.d.ts +0 -83
- package/dist/__internal.d.ts.map +0 -1
- package/dist/__internal.js +0 -19
- package/dist/__internal.js.map +0 -1
- package/dist/__mocks__/version.d.ts +0 -7
- package/dist/__mocks__/version.d.ts.map +0 -1
- package/dist/__mocks__/version.js +0 -7
- package/dist/__mocks__/version.js.map +0 -1
- package/dist/__tests__/client-href.test.d.ts +0 -2
- package/dist/__tests__/client-href.test.d.ts.map +0 -1
- package/dist/__tests__/client-href.test.js +0 -74
- package/dist/__tests__/client-href.test.js.map +0 -1
- package/dist/__tests__/component-utils.test.d.ts +0 -2
- package/dist/__tests__/component-utils.test.d.ts.map +0 -1
- package/dist/__tests__/component-utils.test.js +0 -51
- package/dist/__tests__/component-utils.test.js.map +0 -1
- package/dist/__tests__/event-controller.test.d.ts +0 -2
- package/dist/__tests__/event-controller.test.d.ts.map +0 -1
- package/dist/__tests__/event-controller.test.js +0 -538
- package/dist/__tests__/event-controller.test.js.map +0 -1
- package/dist/__tests__/helpers/route-tree.d.ts +0 -118
- package/dist/__tests__/helpers/route-tree.d.ts.map +0 -1
- package/dist/__tests__/helpers/route-tree.js +0 -374
- package/dist/__tests__/helpers/route-tree.js.map +0 -1
- package/dist/__tests__/match-result.test.d.ts +0 -2
- package/dist/__tests__/match-result.test.d.ts.map +0 -1
- package/dist/__tests__/match-result.test.js +0 -154
- package/dist/__tests__/match-result.test.js.map +0 -1
- package/dist/__tests__/navigation-store.test.d.ts +0 -2
- package/dist/__tests__/navigation-store.test.d.ts.map +0 -1
- package/dist/__tests__/navigation-store.test.js +0 -440
- package/dist/__tests__/navigation-store.test.js.map +0 -1
- package/dist/__tests__/partial-update.test.d.ts +0 -2
- package/dist/__tests__/partial-update.test.d.ts.map +0 -1
- package/dist/__tests__/partial-update.test.js +0 -1009
- package/dist/__tests__/partial-update.test.js.map +0 -1
- package/dist/__tests__/reverse-types.test.d.ts +0 -8
- package/dist/__tests__/reverse-types.test.d.ts.map +0 -1
- package/dist/__tests__/reverse-types.test.js +0 -656
- package/dist/__tests__/reverse-types.test.js.map +0 -1
- package/dist/__tests__/route-definition.test.d.ts +0 -2
- package/dist/__tests__/route-definition.test.d.ts.map +0 -1
- package/dist/__tests__/route-definition.test.js +0 -55
- package/dist/__tests__/route-definition.test.js.map +0 -1
- package/dist/__tests__/router-helpers.test.d.ts +0 -2
- package/dist/__tests__/router-helpers.test.d.ts.map +0 -1
- package/dist/__tests__/router-helpers.test.js +0 -377
- package/dist/__tests__/router-helpers.test.js.map +0 -1
- package/dist/__tests__/router-integration-2.test.d.ts +0 -2
- package/dist/__tests__/router-integration-2.test.d.ts.map +0 -1
- package/dist/__tests__/router-integration-2.test.js +0 -426
- package/dist/__tests__/router-integration-2.test.js.map +0 -1
- package/dist/__tests__/router-integration.test.d.ts +0 -2
- package/dist/__tests__/router-integration.test.d.ts.map +0 -1
- package/dist/__tests__/router-integration.test.js +0 -1051
- package/dist/__tests__/router-integration.test.js.map +0 -1
- package/dist/__tests__/search-params.test.d.ts +0 -5
- package/dist/__tests__/search-params.test.d.ts.map +0 -1
- package/dist/__tests__/search-params.test.js +0 -306
- package/dist/__tests__/search-params.test.js.map +0 -1
- package/dist/__tests__/segment-system.test.d.ts +0 -2
- package/dist/__tests__/segment-system.test.d.ts.map +0 -1
- package/dist/__tests__/segment-system.test.js +0 -627
- package/dist/__tests__/segment-system.test.js.map +0 -1
- package/dist/__tests__/static-handler-types.test.d.ts +0 -8
- package/dist/__tests__/static-handler-types.test.d.ts.map +0 -1
- package/dist/__tests__/static-handler-types.test.js +0 -63
- package/dist/__tests__/static-handler-types.test.js.map +0 -1
- package/dist/__tests__/urls.test.d.ts +0 -2
- package/dist/__tests__/urls.test.d.ts.map +0 -1
- package/dist/__tests__/urls.test.js +0 -421
- package/dist/__tests__/urls.test.js.map +0 -1
- package/dist/__tests__/use-mount.test.d.ts +0 -2
- package/dist/__tests__/use-mount.test.d.ts.map +0 -1
- package/dist/__tests__/use-mount.test.js +0 -35
- package/dist/__tests__/use-mount.test.js.map +0 -1
- package/dist/bin/rango.d.ts +0 -2
- package/dist/bin/rango.d.ts.map +0 -1
- package/dist/bin/rango.js.map +0 -1
- package/dist/browser/event-controller.d.ts +0 -191
- package/dist/browser/event-controller.d.ts.map +0 -1
- package/dist/browser/event-controller.js +0 -559
- package/dist/browser/event-controller.js.map +0 -1
- package/dist/browser/index.d.ts +0 -2
- package/dist/browser/index.d.ts.map +0 -1
- package/dist/browser/index.js +0 -14
- package/dist/browser/index.js.map +0 -1
- package/dist/browser/link-interceptor.d.ts +0 -38
- package/dist/browser/link-interceptor.d.ts.map +0 -1
- package/dist/browser/link-interceptor.js +0 -99
- package/dist/browser/link-interceptor.js.map +0 -1
- package/dist/browser/logging.d.ts +0 -10
- package/dist/browser/logging.d.ts.map +0 -1
- package/dist/browser/logging.js +0 -29
- package/dist/browser/logging.js.map +0 -1
- package/dist/browser/lru-cache.d.ts +0 -17
- package/dist/browser/lru-cache.d.ts.map +0 -1
- package/dist/browser/lru-cache.js +0 -50
- package/dist/browser/lru-cache.js.map +0 -1
- package/dist/browser/merge-segment-loaders.d.ts +0 -39
- package/dist/browser/merge-segment-loaders.d.ts.map +0 -1
- package/dist/browser/merge-segment-loaders.js +0 -102
- package/dist/browser/merge-segment-loaders.js.map +0 -1
- package/dist/browser/navigation-bridge.d.ts +0 -102
- package/dist/browser/navigation-bridge.d.ts.map +0 -1
- package/dist/browser/navigation-bridge.js +0 -708
- package/dist/browser/navigation-bridge.js.map +0 -1
- package/dist/browser/navigation-client.d.ts +0 -25
- package/dist/browser/navigation-client.d.ts.map +0 -1
- package/dist/browser/navigation-client.js +0 -157
- package/dist/browser/navigation-client.js.map +0 -1
- package/dist/browser/navigation-store.d.ts +0 -101
- package/dist/browser/navigation-store.d.ts.map +0 -1
- package/dist/browser/navigation-store.js +0 -625
- package/dist/browser/navigation-store.js.map +0 -1
- package/dist/browser/partial-update.d.ts +0 -75
- package/dist/browser/partial-update.d.ts.map +0 -1
- package/dist/browser/partial-update.js +0 -426
- package/dist/browser/partial-update.js.map +0 -1
- package/dist/browser/react/Link.d.ts +0 -86
- package/dist/browser/react/Link.d.ts.map +0 -1
- package/dist/browser/react/Link.js +0 -128
- package/dist/browser/react/Link.js.map +0 -1
- package/dist/browser/react/NavigationProvider.d.ts +0 -63
- package/dist/browser/react/NavigationProvider.d.ts.map +0 -1
- package/dist/browser/react/NavigationProvider.js +0 -216
- package/dist/browser/react/NavigationProvider.js.map +0 -1
- package/dist/browser/react/ScrollRestoration.d.ts +0 -75
- package/dist/browser/react/ScrollRestoration.d.ts.map +0 -1
- package/dist/browser/react/ScrollRestoration.js +0 -57
- package/dist/browser/react/ScrollRestoration.js.map +0 -1
- package/dist/browser/react/context.d.ts +0 -46
- package/dist/browser/react/context.d.ts.map +0 -1
- package/dist/browser/react/context.js +0 -10
- package/dist/browser/react/context.js.map +0 -1
- package/dist/browser/react/index.d.ts +0 -11
- package/dist/browser/react/index.d.ts.map +0 -1
- package/dist/browser/react/index.js +0 -22
- package/dist/browser/react/index.js.map +0 -1
- package/dist/browser/react/location-state-shared.d.ts +0 -63
- package/dist/browser/react/location-state-shared.d.ts.map +0 -1
- package/dist/browser/react/location-state-shared.js +0 -81
- package/dist/browser/react/location-state-shared.js.map +0 -1
- package/dist/browser/react/location-state.d.ts +0 -23
- package/dist/browser/react/location-state.d.ts.map +0 -1
- package/dist/browser/react/location-state.js +0 -29
- package/dist/browser/react/location-state.js.map +0 -1
- package/dist/browser/react/mount-context.d.ts +0 -24
- package/dist/browser/react/mount-context.d.ts.map +0 -1
- package/dist/browser/react/mount-context.js +0 -24
- package/dist/browser/react/mount-context.js.map +0 -1
- package/dist/browser/react/use-action.d.ts +0 -64
- package/dist/browser/react/use-action.d.ts.map +0 -1
- package/dist/browser/react/use-action.js +0 -134
- package/dist/browser/react/use-action.js.map +0 -1
- package/dist/browser/react/use-client-cache.d.ts +0 -41
- package/dist/browser/react/use-client-cache.d.ts.map +0 -1
- package/dist/browser/react/use-client-cache.js +0 -39
- package/dist/browser/react/use-client-cache.js.map +0 -1
- package/dist/browser/react/use-handle.d.ts +0 -31
- package/dist/browser/react/use-handle.d.ts.map +0 -1
- package/dist/browser/react/use-handle.js +0 -144
- package/dist/browser/react/use-handle.js.map +0 -1
- package/dist/browser/react/use-href.d.ts +0 -33
- package/dist/browser/react/use-href.d.ts.map +0 -1
- package/dist/browser/react/use-href.js +0 -39
- package/dist/browser/react/use-href.js.map +0 -1
- package/dist/browser/react/use-link-status.d.ts +0 -37
- package/dist/browser/react/use-link-status.d.ts.map +0 -1
- package/dist/browser/react/use-link-status.js +0 -99
- package/dist/browser/react/use-link-status.js.map +0 -1
- package/dist/browser/react/use-mount.d.ts +0 -25
- package/dist/browser/react/use-mount.d.ts.map +0 -1
- package/dist/browser/react/use-mount.js +0 -30
- package/dist/browser/react/use-mount.js.map +0 -1
- package/dist/browser/react/use-navigation.d.ts +0 -27
- package/dist/browser/react/use-navigation.d.ts.map +0 -1
- package/dist/browser/react/use-navigation.js +0 -87
- package/dist/browser/react/use-navigation.js.map +0 -1
- package/dist/browser/react/use-segments.d.ts +0 -38
- package/dist/browser/react/use-segments.d.ts.map +0 -1
- package/dist/browser/react/use-segments.js +0 -130
- package/dist/browser/react/use-segments.js.map +0 -1
- package/dist/browser/request-controller.d.ts +0 -26
- package/dist/browser/request-controller.d.ts.map +0 -1
- package/dist/browser/request-controller.js +0 -147
- package/dist/browser/request-controller.js.map +0 -1
- package/dist/browser/rsc-router.d.ts +0 -129
- package/dist/browser/rsc-router.d.ts.map +0 -1
- package/dist/browser/rsc-router.js +0 -195
- package/dist/browser/rsc-router.js.map +0 -1
- package/dist/browser/scroll-restoration.d.ts +0 -93
- package/dist/browser/scroll-restoration.d.ts.map +0 -1
- package/dist/browser/scroll-restoration.js +0 -321
- package/dist/browser/scroll-restoration.js.map +0 -1
- package/dist/browser/segment-structure-assert.d.ts +0 -17
- package/dist/browser/segment-structure-assert.d.ts.map +0 -1
- package/dist/browser/segment-structure-assert.js +0 -59
- package/dist/browser/segment-structure-assert.js.map +0 -1
- package/dist/browser/server-action-bridge.d.ts +0 -26
- package/dist/browser/server-action-bridge.d.ts.map +0 -1
- package/dist/browser/server-action-bridge.js +0 -668
- package/dist/browser/server-action-bridge.js.map +0 -1
- package/dist/browser/shallow.d.ts +0 -12
- package/dist/browser/shallow.d.ts.map +0 -1
- package/dist/browser/shallow.js +0 -34
- package/dist/browser/shallow.js.map +0 -1
- package/dist/browser/types.d.ts +0 -369
- package/dist/browser/types.d.ts.map +0 -1
- package/dist/browser/types.js +0 -2
- package/dist/browser/types.js.map +0 -1
- package/dist/build/__tests__/generate-cli.test.d.ts +0 -2
- package/dist/build/__tests__/generate-cli.test.d.ts.map +0 -1
- package/dist/build/__tests__/generate-cli.test.js +0 -237
- package/dist/build/__tests__/generate-cli.test.js.map +0 -1
- package/dist/build/__tests__/generate-manifest.test.d.ts +0 -2
- package/dist/build/__tests__/generate-manifest.test.d.ts.map +0 -1
- package/dist/build/__tests__/generate-manifest.test.js +0 -119
- package/dist/build/__tests__/generate-manifest.test.js.map +0 -1
- package/dist/build/__tests__/generate-route-types.test.d.ts +0 -2
- package/dist/build/__tests__/generate-route-types.test.d.ts.map +0 -1
- package/dist/build/__tests__/generate-route-types.test.js +0 -620
- package/dist/build/__tests__/generate-route-types.test.js.map +0 -1
- package/dist/build/__tests__/per-router-manifest.test.d.ts +0 -2
- package/dist/build/__tests__/per-router-manifest.test.d.ts.map +0 -1
- package/dist/build/__tests__/per-router-manifest.test.js +0 -308
- package/dist/build/__tests__/per-router-manifest.test.js.map +0 -1
- package/dist/build/generate-manifest.d.ts +0 -81
- package/dist/build/generate-manifest.d.ts.map +0 -1
- package/dist/build/generate-manifest.js +0 -276
- package/dist/build/generate-manifest.js.map +0 -1
- package/dist/build/generate-route-types.d.ts +0 -115
- package/dist/build/generate-route-types.d.ts.map +0 -1
- package/dist/build/generate-route-types.js +0 -740
- package/dist/build/generate-route-types.js.map +0 -1
- package/dist/build/index.d.ts +0 -21
- package/dist/build/index.d.ts.map +0 -1
- package/dist/build/index.js +0 -21
- package/dist/build/index.js.map +0 -1
- package/dist/build/route-trie.d.ts +0 -71
- package/dist/build/route-trie.d.ts.map +0 -1
- package/dist/build/route-trie.js +0 -175
- package/dist/build/route-trie.js.map +0 -1
- package/dist/cache/__tests__/cache-scope.test.d.ts +0 -2
- package/dist/cache/__tests__/cache-scope.test.d.ts.map +0 -1
- package/dist/cache/__tests__/cache-scope.test.js +0 -208
- package/dist/cache/__tests__/cache-scope.test.js.map +0 -1
- package/dist/cache/__tests__/document-cache.test.d.ts +0 -2
- package/dist/cache/__tests__/document-cache.test.d.ts.map +0 -1
- package/dist/cache/__tests__/document-cache.test.js +0 -345
- package/dist/cache/__tests__/document-cache.test.js.map +0 -1
- package/dist/cache/__tests__/memory-segment-store.test.d.ts +0 -2
- package/dist/cache/__tests__/memory-segment-store.test.d.ts.map +0 -1
- package/dist/cache/__tests__/memory-segment-store.test.js +0 -425
- package/dist/cache/__tests__/memory-segment-store.test.js.map +0 -1
- package/dist/cache/__tests__/memory-store.test.d.ts +0 -2
- package/dist/cache/__tests__/memory-store.test.d.ts.map +0 -1
- package/dist/cache/__tests__/memory-store.test.js +0 -367
- package/dist/cache/__tests__/memory-store.test.js.map +0 -1
- package/dist/cache/cache-scope.d.ts +0 -102
- package/dist/cache/cache-scope.d.ts.map +0 -1
- package/dist/cache/cache-scope.js +0 -440
- package/dist/cache/cache-scope.js.map +0 -1
- package/dist/cache/cf/__tests__/cf-cache-store.test.d.ts +0 -2
- package/dist/cache/cf/__tests__/cf-cache-store.test.d.ts.map +0 -1
- package/dist/cache/cf/__tests__/cf-cache-store.test.js +0 -330
- package/dist/cache/cf/__tests__/cf-cache-store.test.js.map +0 -1
- package/dist/cache/cf/cf-cache-store.d.ts +0 -165
- package/dist/cache/cf/cf-cache-store.d.ts.map +0 -1
- package/dist/cache/cf/cf-cache-store.js +0 -242
- package/dist/cache/cf/cf-cache-store.js.map +0 -1
- package/dist/cache/cf/index.d.ts +0 -14
- package/dist/cache/cf/index.d.ts.map +0 -1
- package/dist/cache/cf/index.js +0 -17
- package/dist/cache/cf/index.js.map +0 -1
- package/dist/cache/document-cache.d.ts +0 -64
- package/dist/cache/document-cache.d.ts.map +0 -1
- package/dist/cache/document-cache.js +0 -228
- package/dist/cache/document-cache.js.map +0 -1
- package/dist/cache/index.d.ts +0 -19
- package/dist/cache/index.d.ts.map +0 -1
- package/dist/cache/index.js +0 -21
- package/dist/cache/index.js.map +0 -1
- package/dist/cache/memory-segment-store.d.ts +0 -110
- package/dist/cache/memory-segment-store.d.ts.map +0 -1
- package/dist/cache/memory-segment-store.js +0 -117
- package/dist/cache/memory-segment-store.js.map +0 -1
- package/dist/cache/memory-store.d.ts +0 -41
- package/dist/cache/memory-store.d.ts.map +0 -1
- package/dist/cache/memory-store.js +0 -191
- package/dist/cache/memory-store.js.map +0 -1
- package/dist/cache/types.d.ts +0 -317
- package/dist/cache/types.d.ts.map +0 -1
- package/dist/cache/types.js +0 -12
- package/dist/cache/types.js.map +0 -1
- package/dist/client.d.ts +0 -248
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -367
- package/dist/client.js.map +0 -1
- package/dist/client.rsc.d.ts +0 -26
- package/dist/client.rsc.d.ts.map +0 -1
- package/dist/client.rsc.js +0 -46
- package/dist/client.rsc.js.map +0 -1
- package/dist/component-utils.d.ts +0 -36
- package/dist/component-utils.d.ts.map +0 -1
- package/dist/component-utils.js +0 -61
- package/dist/component-utils.js.map +0 -1
- package/dist/components/DefaultDocument.d.ts +0 -13
- package/dist/components/DefaultDocument.d.ts.map +0 -1
- package/dist/components/DefaultDocument.js +0 -15
- package/dist/components/DefaultDocument.js.map +0 -1
- package/dist/debug.d.ts +0 -58
- package/dist/debug.d.ts.map +0 -1
- package/dist/debug.js +0 -157
- package/dist/debug.js.map +0 -1
- package/dist/default-error-boundary.d.ts +0 -11
- package/dist/default-error-boundary.d.ts.map +0 -1
- package/dist/default-error-boundary.js +0 -45
- package/dist/default-error-boundary.js.map +0 -1
- package/dist/deps/browser.d.ts +0 -2
- package/dist/deps/browser.d.ts.map +0 -1
- package/dist/deps/browser.js +0 -3
- package/dist/deps/browser.js.map +0 -1
- package/dist/deps/html-stream-client.d.ts +0 -2
- package/dist/deps/html-stream-client.d.ts.map +0 -1
- package/dist/deps/html-stream-client.js +0 -3
- package/dist/deps/html-stream-client.js.map +0 -1
- package/dist/deps/html-stream-server.d.ts +0 -2
- package/dist/deps/html-stream-server.d.ts.map +0 -1
- package/dist/deps/html-stream-server.js +0 -3
- package/dist/deps/html-stream-server.js.map +0 -1
- package/dist/deps/rsc.d.ts +0 -2
- package/dist/deps/rsc.d.ts.map +0 -1
- package/dist/deps/rsc.js +0 -4
- package/dist/deps/rsc.js.map +0 -1
- package/dist/deps/ssr.d.ts +0 -2
- package/dist/deps/ssr.d.ts.map +0 -1
- package/dist/deps/ssr.js +0 -3
- package/dist/deps/ssr.js.map +0 -1
- package/dist/errors.d.ts +0 -174
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -241
- package/dist/errors.js.map +0 -1
- package/dist/handle.d.ts +0 -78
- package/dist/handle.d.ts.map +0 -1
- package/dist/handle.js +0 -82
- package/dist/handle.js.map +0 -1
- package/dist/handles/MetaTags.d.ts +0 -14
- package/dist/handles/MetaTags.d.ts.map +0 -1
- package/dist/handles/MetaTags.js +0 -136
- package/dist/handles/MetaTags.js.map +0 -1
- package/dist/handles/index.d.ts +0 -6
- package/dist/handles/index.d.ts.map +0 -1
- package/dist/handles/index.js +0 -6
- package/dist/handles/index.js.map +0 -1
- package/dist/handles/meta.d.ts +0 -39
- package/dist/handles/meta.d.ts.map +0 -1
- package/dist/handles/meta.js +0 -202
- package/dist/handles/meta.js.map +0 -1
- package/dist/host/__tests__/errors.test.d.ts +0 -2
- package/dist/host/__tests__/errors.test.d.ts.map +0 -1
- package/dist/host/__tests__/errors.test.js +0 -76
- package/dist/host/__tests__/errors.test.js.map +0 -1
- package/dist/host/__tests__/pattern-comprehensive.test.d.ts +0 -2
- package/dist/host/__tests__/pattern-comprehensive.test.d.ts.map +0 -1
- package/dist/host/__tests__/pattern-comprehensive.test.js +0 -732
- package/dist/host/__tests__/pattern-comprehensive.test.js.map +0 -1
- package/dist/host/__tests__/pattern-matcher.test.d.ts +0 -2
- package/dist/host/__tests__/pattern-matcher.test.d.ts.map +0 -1
- package/dist/host/__tests__/pattern-matcher.test.js +0 -251
- package/dist/host/__tests__/pattern-matcher.test.js.map +0 -1
- package/dist/host/__tests__/router.test.d.ts +0 -2
- package/dist/host/__tests__/router.test.d.ts.map +0 -1
- package/dist/host/__tests__/router.test.js +0 -241
- package/dist/host/__tests__/router.test.js.map +0 -1
- package/dist/host/__tests__/testing.test.d.ts +0 -2
- package/dist/host/__tests__/testing.test.d.ts.map +0 -1
- package/dist/host/__tests__/testing.test.js +0 -64
- package/dist/host/__tests__/testing.test.js.map +0 -1
- package/dist/host/__tests__/utils.test.d.ts +0 -2
- package/dist/host/__tests__/utils.test.d.ts.map +0 -1
- package/dist/host/__tests__/utils.test.js +0 -29
- package/dist/host/__tests__/utils.test.js.map +0 -1
- package/dist/host/cookie-handler.d.ts +0 -34
- package/dist/host/cookie-handler.d.ts.map +0 -1
- package/dist/host/cookie-handler.js +0 -124
- package/dist/host/cookie-handler.js.map +0 -1
- package/dist/host/errors.d.ts +0 -56
- package/dist/host/errors.d.ts.map +0 -1
- package/dist/host/errors.js +0 -79
- package/dist/host/errors.js.map +0 -1
- package/dist/host/index.d.ts +0 -29
- package/dist/host/index.d.ts.map +0 -1
- package/dist/host/index.js +0 -32
- package/dist/host/index.js.map +0 -1
- package/dist/host/pattern-matcher.d.ts +0 -36
- package/dist/host/pattern-matcher.d.ts.map +0 -1
- package/dist/host/pattern-matcher.js +0 -172
- package/dist/host/pattern-matcher.js.map +0 -1
- package/dist/host/router.d.ts +0 -26
- package/dist/host/router.d.ts.map +0 -1
- package/dist/host/router.js +0 -218
- package/dist/host/router.js.map +0 -1
- package/dist/host/testing.d.ts +0 -36
- package/dist/host/testing.d.ts.map +0 -1
- package/dist/host/testing.js +0 -55
- package/dist/host/testing.js.map +0 -1
- package/dist/host/types.d.ts +0 -115
- package/dist/host/types.d.ts.map +0 -1
- package/dist/host/types.js +0 -7
- package/dist/host/types.js.map +0 -1
- package/dist/host/utils.d.ts +0 -21
- package/dist/host/utils.d.ts.map +0 -1
- package/dist/host/utils.js +0 -23
- package/dist/host/utils.js.map +0 -1
- package/dist/href-client.d.ts +0 -131
- package/dist/href-client.d.ts.map +0 -1
- package/dist/href-client.js +0 -64
- package/dist/href-client.js.map +0 -1
- package/dist/href-context.d.ts +0 -29
- package/dist/href-context.d.ts.map +0 -1
- package/dist/href-context.js +0 -21
- package/dist/href-context.js.map +0 -1
- package/dist/index.d.ts +0 -73
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -91
- package/dist/index.js.map +0 -1
- package/dist/index.rsc.d.ts +0 -32
- package/dist/index.rsc.d.ts.map +0 -1
- package/dist/index.rsc.js +0 -40
- package/dist/index.rsc.js.map +0 -1
- package/dist/internal-debug.d.ts +0 -2
- package/dist/internal-debug.d.ts.map +0 -1
- package/dist/internal-debug.js +0 -5
- package/dist/internal-debug.js.map +0 -1
- package/dist/loader.d.ts +0 -14
- package/dist/loader.d.ts.map +0 -1
- package/dist/loader.js +0 -20
- package/dist/loader.js.map +0 -1
- package/dist/loader.rsc.d.ts +0 -19
- package/dist/loader.rsc.d.ts.map +0 -1
- package/dist/loader.rsc.js +0 -99
- package/dist/loader.rsc.js.map +0 -1
- package/dist/network-error-thrower.d.ts +0 -17
- package/dist/network-error-thrower.d.ts.map +0 -1
- package/dist/network-error-thrower.js +0 -14
- package/dist/network-error-thrower.js.map +0 -1
- package/dist/outlet-context.d.ts +0 -13
- package/dist/outlet-context.d.ts.map +0 -1
- package/dist/outlet-context.js +0 -3
- package/dist/outlet-context.js.map +0 -1
- package/dist/prerender/__tests__/param-hash.test.d.ts +0 -2
- package/dist/prerender/__tests__/param-hash.test.d.ts.map +0 -1
- package/dist/prerender/__tests__/param-hash.test.js +0 -148
- package/dist/prerender/__tests__/param-hash.test.js.map +0 -1
- package/dist/prerender/param-hash.d.ts +0 -16
- package/dist/prerender/param-hash.d.ts.map +0 -1
- package/dist/prerender/param-hash.js +0 -36
- package/dist/prerender/param-hash.js.map +0 -1
- package/dist/prerender/store.d.ts +0 -38
- package/dist/prerender/store.d.ts.map +0 -1
- package/dist/prerender/store.js +0 -61
- package/dist/prerender/store.js.map +0 -1
- package/dist/prerender.d.ts +0 -66
- package/dist/prerender.d.ts.map +0 -1
- package/dist/prerender.js +0 -57
- package/dist/prerender.js.map +0 -1
- package/dist/reverse.d.ts +0 -196
- package/dist/reverse.d.ts.map +0 -1
- package/dist/reverse.js +0 -78
- package/dist/reverse.js.map +0 -1
- package/dist/root-error-boundary.d.ts +0 -33
- package/dist/root-error-boundary.d.ts.map +0 -1
- package/dist/root-error-boundary.js +0 -165
- package/dist/root-error-boundary.js.map +0 -1
- package/dist/route-content-wrapper.d.ts +0 -46
- package/dist/route-content-wrapper.d.ts.map +0 -1
- package/dist/route-content-wrapper.js +0 -77
- package/dist/route-content-wrapper.js.map +0 -1
- package/dist/route-definition.d.ts +0 -421
- package/dist/route-definition.d.ts.map +0 -1
- package/dist/route-definition.js +0 -868
- package/dist/route-definition.js.map +0 -1
- package/dist/route-map-builder.d.ts +0 -155
- package/dist/route-map-builder.d.ts.map +0 -1
- package/dist/route-map-builder.js +0 -237
- package/dist/route-map-builder.js.map +0 -1
- package/dist/route-types.d.ts +0 -165
- package/dist/route-types.d.ts.map +0 -1
- package/dist/route-types.js +0 -7
- package/dist/route-types.js.map +0 -1
- package/dist/router/__tests__/handler-context.test.d.ts +0 -2
- package/dist/router/__tests__/handler-context.test.d.ts.map +0 -1
- package/dist/router/__tests__/handler-context.test.js +0 -65
- package/dist/router/__tests__/handler-context.test.js.map +0 -1
- package/dist/router/__tests__/loader-cycle-detection.test.d.ts +0 -2
- package/dist/router/__tests__/loader-cycle-detection.test.d.ts.map +0 -1
- package/dist/router/__tests__/loader-cycle-detection.test.js +0 -221
- package/dist/router/__tests__/loader-cycle-detection.test.js.map +0 -1
- package/dist/router/__tests__/match-context.test.d.ts +0 -2
- package/dist/router/__tests__/match-context.test.d.ts.map +0 -1
- package/dist/router/__tests__/match-context.test.js +0 -92
- package/dist/router/__tests__/match-context.test.js.map +0 -1
- package/dist/router/__tests__/match-pipelines.test.d.ts +0 -2
- package/dist/router/__tests__/match-pipelines.test.d.ts.map +0 -1
- package/dist/router/__tests__/match-pipelines.test.js +0 -417
- package/dist/router/__tests__/match-pipelines.test.js.map +0 -1
- package/dist/router/__tests__/match-result.test.d.ts +0 -2
- package/dist/router/__tests__/match-result.test.d.ts.map +0 -1
- package/dist/router/__tests__/match-result.test.js +0 -457
- package/dist/router/__tests__/match-result.test.js.map +0 -1
- package/dist/router/__tests__/on-error.test.d.ts +0 -2
- package/dist/router/__tests__/on-error.test.d.ts.map +0 -1
- package/dist/router/__tests__/on-error.test.js +0 -678
- package/dist/router/__tests__/on-error.test.js.map +0 -1
- package/dist/router/__tests__/pattern-matching.test.d.ts +0 -2
- package/dist/router/__tests__/pattern-matching.test.d.ts.map +0 -1
- package/dist/router/__tests__/pattern-matching.test.js +0 -629
- package/dist/router/__tests__/pattern-matching.test.js.map +0 -1
- package/dist/router/__tests__/segment-resolution-parallel-loading.test.d.ts +0 -2
- package/dist/router/__tests__/segment-resolution-parallel-loading.test.d.ts.map +0 -1
- package/dist/router/__tests__/segment-resolution-parallel-loading.test.js +0 -155
- package/dist/router/__tests__/segment-resolution-parallel-loading.test.js.map +0 -1
- package/dist/router/error-handling.d.ts +0 -77
- package/dist/router/error-handling.d.ts.map +0 -1
- package/dist/router/error-handling.js +0 -202
- package/dist/router/error-handling.js.map +0 -1
- package/dist/router/handler-context.d.ts +0 -20
- package/dist/router/handler-context.d.ts.map +0 -1
- package/dist/router/handler-context.js +0 -198
- package/dist/router/handler-context.js.map +0 -1
- package/dist/router/intercept-resolution.d.ts +0 -66
- package/dist/router/intercept-resolution.d.ts.map +0 -1
- package/dist/router/intercept-resolution.js +0 -246
- package/dist/router/intercept-resolution.js.map +0 -1
- package/dist/router/loader-resolution.d.ts +0 -64
- package/dist/router/loader-resolution.d.ts.map +0 -1
- package/dist/router/loader-resolution.js +0 -284
- package/dist/router/loader-resolution.js.map +0 -1
- package/dist/router/logging.d.ts +0 -15
- package/dist/router/logging.d.ts.map +0 -1
- package/dist/router/logging.js +0 -99
- package/dist/router/logging.js.map +0 -1
- package/dist/router/manifest.d.ts +0 -22
- package/dist/router/manifest.d.ts.map +0 -1
- package/dist/router/manifest.js +0 -181
- package/dist/router/manifest.js.map +0 -1
- package/dist/router/match-api.d.ts +0 -35
- package/dist/router/match-api.d.ts.map +0 -1
- package/dist/router/match-api.js +0 -406
- package/dist/router/match-api.js.map +0 -1
- package/dist/router/match-context.d.ts +0 -206
- package/dist/router/match-context.d.ts.map +0 -1
- package/dist/router/match-context.js +0 -17
- package/dist/router/match-context.js.map +0 -1
- package/dist/router/match-middleware/background-revalidation.d.ts +0 -127
- package/dist/router/match-middleware/background-revalidation.d.ts.map +0 -1
- package/dist/router/match-middleware/background-revalidation.js +0 -75
- package/dist/router/match-middleware/background-revalidation.js.map +0 -1
- package/dist/router/match-middleware/cache-lookup.d.ts +0 -112
- package/dist/router/match-middleware/cache-lookup.d.ts.map +0 -1
- package/dist/router/match-middleware/cache-lookup.js +0 -257
- package/dist/router/match-middleware/cache-lookup.js.map +0 -1
- package/dist/router/match-middleware/cache-store.d.ts +0 -113
- package/dist/router/match-middleware/cache-store.d.ts.map +0 -1
- package/dist/router/match-middleware/cache-store.js +0 -108
- package/dist/router/match-middleware/cache-store.js.map +0 -1
- package/dist/router/match-middleware/index.d.ts +0 -81
- package/dist/router/match-middleware/index.d.ts.map +0 -1
- package/dist/router/match-middleware/index.js +0 -80
- package/dist/router/match-middleware/index.js.map +0 -1
- package/dist/router/match-middleware/intercept-resolution.d.ts +0 -117
- package/dist/router/match-middleware/intercept-resolution.d.ts.map +0 -1
- package/dist/router/match-middleware/intercept-resolution.js +0 -134
- package/dist/router/match-middleware/intercept-resolution.js.map +0 -1
- package/dist/router/match-middleware/segment-resolution.d.ts +0 -99
- package/dist/router/match-middleware/segment-resolution.d.ts.map +0 -1
- package/dist/router/match-middleware/segment-resolution.js +0 -53
- package/dist/router/match-middleware/segment-resolution.js.map +0 -1
- package/dist/router/match-pipelines.d.ts +0 -147
- package/dist/router/match-pipelines.d.ts.map +0 -1
- package/dist/router/match-pipelines.js +0 -82
- package/dist/router/match-pipelines.js.map +0 -1
- package/dist/router/match-result.d.ts +0 -126
- package/dist/router/match-result.d.ts.map +0 -1
- package/dist/router/match-result.js +0 -93
- package/dist/router/match-result.js.map +0 -1
- package/dist/router/metrics.d.ts +0 -20
- package/dist/router/metrics.d.ts.map +0 -1
- package/dist/router/metrics.js +0 -47
- package/dist/router/metrics.js.map +0 -1
- package/dist/router/middleware.d.ts +0 -249
- package/dist/router/middleware.d.ts.map +0 -1
- package/dist/router/middleware.js +0 -434
- package/dist/router/middleware.js.map +0 -1
- package/dist/router/middleware.test.d.ts +0 -2
- package/dist/router/middleware.test.d.ts.map +0 -1
- package/dist/router/middleware.test.js +0 -816
- package/dist/router/middleware.test.js.map +0 -1
- package/dist/router/pattern-matching.d.ts +0 -149
- package/dist/router/pattern-matching.d.ts.map +0 -1
- package/dist/router/pattern-matching.js +0 -349
- package/dist/router/pattern-matching.js.map +0 -1
- package/dist/router/revalidation.d.ts +0 -44
- package/dist/router/revalidation.d.ts.map +0 -1
- package/dist/router/revalidation.js +0 -147
- package/dist/router/revalidation.js.map +0 -1
- package/dist/router/router-context.d.ts +0 -135
- package/dist/router/router-context.d.ts.map +0 -1
- package/dist/router/router-context.js +0 -36
- package/dist/router/router-context.js.map +0 -1
- package/dist/router/segment-resolution.d.ts +0 -127
- package/dist/router/segment-resolution.d.ts.map +0 -1
- package/dist/router/segment-resolution.js +0 -919
- package/dist/router/segment-resolution.js.map +0 -1
- package/dist/router/trie-matching.d.ts +0 -40
- package/dist/router/trie-matching.d.ts.map +0 -1
- package/dist/router/trie-matching.js +0 -127
- package/dist/router/trie-matching.js.map +0 -1
- package/dist/router/types.d.ts +0 -136
- package/dist/router/types.d.ts.map +0 -1
- package/dist/router/types.js +0 -7
- package/dist/router/types.js.map +0 -1
- package/dist/router.d.ts +0 -753
- package/dist/router.d.ts.map +0 -1
- package/dist/router.gen.d.ts +0 -6
- package/dist/router.gen.d.ts.map +0 -1
- package/dist/router.gen.js +0 -6
- package/dist/router.gen.js.map +0 -1
- package/dist/router.js +0 -1304
- package/dist/router.js.map +0 -1
- package/dist/rsc/__tests__/helpers.test.d.ts +0 -2
- package/dist/rsc/__tests__/helpers.test.d.ts.map +0 -1
- package/dist/rsc/__tests__/helpers.test.js +0 -140
- package/dist/rsc/__tests__/helpers.test.js.map +0 -1
- package/dist/rsc/handler.d.ts +0 -45
- package/dist/rsc/handler.d.ts.map +0 -1
- package/dist/rsc/handler.js +0 -1172
- package/dist/rsc/handler.js.map +0 -1
- package/dist/rsc/helpers.d.ts +0 -16
- package/dist/rsc/helpers.d.ts.map +0 -1
- package/dist/rsc/helpers.js +0 -55
- package/dist/rsc/helpers.js.map +0 -1
- package/dist/rsc/index.d.ts +0 -22
- package/dist/rsc/index.d.ts.map +0 -1
- package/dist/rsc/index.js +0 -23
- package/dist/rsc/index.js.map +0 -1
- package/dist/rsc/nonce.d.ts +0 -9
- package/dist/rsc/nonce.d.ts.map +0 -1
- package/dist/rsc/nonce.js +0 -18
- package/dist/rsc/nonce.js.map +0 -1
- package/dist/rsc/types.d.ts +0 -206
- package/dist/rsc/types.d.ts.map +0 -1
- package/dist/rsc/types.js +0 -8
- package/dist/rsc/types.js.map +0 -1
- package/dist/search-params.d.ts +0 -103
- package/dist/search-params.d.ts.map +0 -1
- package/dist/search-params.js +0 -74
- package/dist/search-params.js.map +0 -1
- package/dist/segment-system.d.ts +0 -75
- package/dist/segment-system.d.ts.map +0 -1
- package/dist/segment-system.js +0 -336
- package/dist/segment-system.js.map +0 -1
- package/dist/server/context.d.ts +0 -245
- package/dist/server/context.d.ts.map +0 -1
- package/dist/server/context.js +0 -197
- package/dist/server/context.js.map +0 -1
- package/dist/server/fetchable-loader-store.d.ts +0 -18
- package/dist/server/fetchable-loader-store.d.ts.map +0 -1
- package/dist/server/fetchable-loader-store.js +0 -18
- package/dist/server/fetchable-loader-store.js.map +0 -1
- package/dist/server/handle-store.d.ts +0 -85
- package/dist/server/handle-store.d.ts.map +0 -1
- package/dist/server/handle-store.js +0 -142
- package/dist/server/handle-store.js.map +0 -1
- package/dist/server/loader-registry.d.ts +0 -55
- package/dist/server/loader-registry.d.ts.map +0 -1
- package/dist/server/loader-registry.js +0 -132
- package/dist/server/loader-registry.js.map +0 -1
- package/dist/server/request-context.d.ts +0 -226
- package/dist/server/request-context.d.ts.map +0 -1
- package/dist/server/request-context.js +0 -290
- package/dist/server/request-context.js.map +0 -1
- package/dist/server/root-layout.d.ts +0 -4
- package/dist/server/root-layout.d.ts.map +0 -1
- package/dist/server/root-layout.js +0 -5
- package/dist/server/root-layout.js.map +0 -1
- package/dist/server.d.ts +0 -15
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -20
- package/dist/server.js.map +0 -1
- package/dist/ssr/__tests__/ssr-handler.test.d.ts +0 -2
- package/dist/ssr/__tests__/ssr-handler.test.d.ts.map +0 -1
- package/dist/ssr/__tests__/ssr-handler.test.js +0 -132
- package/dist/ssr/__tests__/ssr-handler.test.js.map +0 -1
- package/dist/ssr/index.d.ts +0 -98
- package/dist/ssr/index.d.ts.map +0 -1
- package/dist/ssr/index.js +0 -158
- package/dist/ssr/index.js.map +0 -1
- package/dist/static-handler.d.ts +0 -50
- package/dist/static-handler.d.ts.map +0 -1
- package/dist/static-handler.gen.d.ts +0 -5
- package/dist/static-handler.gen.d.ts.map +0 -1
- package/dist/static-handler.gen.js +0 -5
- package/dist/static-handler.gen.js.map +0 -1
- package/dist/static-handler.js +0 -29
- package/dist/static-handler.js.map +0 -1
- package/dist/theme/ThemeProvider.d.ts +0 -20
- package/dist/theme/ThemeProvider.d.ts.map +0 -1
- package/dist/theme/ThemeProvider.js +0 -240
- package/dist/theme/ThemeProvider.js.map +0 -1
- package/dist/theme/ThemeScript.d.ts +0 -48
- package/dist/theme/ThemeScript.d.ts.map +0 -1
- package/dist/theme/ThemeScript.js +0 -13
- package/dist/theme/ThemeScript.js.map +0 -1
- package/dist/theme/__tests__/theme.test.d.ts +0 -2
- package/dist/theme/__tests__/theme.test.d.ts.map +0 -1
- package/dist/theme/__tests__/theme.test.js +0 -103
- package/dist/theme/__tests__/theme.test.js.map +0 -1
- package/dist/theme/constants.d.ts +0 -29
- package/dist/theme/constants.d.ts.map +0 -1
- package/dist/theme/constants.js +0 -48
- package/dist/theme/constants.js.map +0 -1
- package/dist/theme/index.d.ts +0 -31
- package/dist/theme/index.d.ts.map +0 -1
- package/dist/theme/index.js +0 -36
- package/dist/theme/index.js.map +0 -1
- package/dist/theme/theme-context.d.ts +0 -40
- package/dist/theme/theme-context.d.ts.map +0 -1
- package/dist/theme/theme-context.js +0 -60
- package/dist/theme/theme-context.js.map +0 -1
- package/dist/theme/theme-script.d.ts +0 -27
- package/dist/theme/theme-script.d.ts.map +0 -1
- package/dist/theme/theme-script.js +0 -147
- package/dist/theme/theme-script.js.map +0 -1
- package/dist/theme/types.d.ts +0 -163
- package/dist/theme/types.d.ts.map +0 -1
- package/dist/theme/types.js +0 -11
- package/dist/theme/types.js.map +0 -1
- package/dist/theme/use-theme.d.ts +0 -12
- package/dist/theme/use-theme.d.ts.map +0 -1
- package/dist/theme/use-theme.js +0 -40
- package/dist/theme/use-theme.js.map +0 -1
- package/dist/types.d.ts +0 -1479
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -10
- package/dist/types.js.map +0 -1
- package/dist/urls.d.ts +0 -441
- package/dist/urls.d.ts.map +0 -1
- package/dist/urls.gen.d.ts +0 -8
- package/dist/urls.gen.d.ts.map +0 -1
- package/dist/urls.gen.js +0 -8
- package/dist/urls.gen.js.map +0 -1
- package/dist/urls.js +0 -443
- package/dist/urls.js.map +0 -1
- package/dist/use-loader.d.ts +0 -127
- package/dist/use-loader.d.ts.map +0 -1
- package/dist/use-loader.js +0 -237
- package/dist/use-loader.js.map +0 -1
- package/dist/vite/__tests__/ast-handler-extract.test.d.ts +0 -2
- package/dist/vite/__tests__/ast-handler-extract.test.d.ts.map +0 -1
- package/dist/vite/__tests__/ast-handler-extract.test.js +0 -294
- package/dist/vite/__tests__/ast-handler-extract.test.js.map +0 -1
- package/dist/vite/__tests__/expose-id-utils.test.d.ts +0 -2
- package/dist/vite/__tests__/expose-id-utils.test.d.ts.map +0 -1
- package/dist/vite/__tests__/expose-id-utils.test.js +0 -224
- package/dist/vite/__tests__/expose-id-utils.test.js.map +0 -1
- package/dist/vite/__tests__/expose-internal-ids.test.d.ts +0 -2
- package/dist/vite/__tests__/expose-internal-ids.test.d.ts.map +0 -1
- package/dist/vite/__tests__/expose-internal-ids.test.js +0 -647
- package/dist/vite/__tests__/expose-internal-ids.test.js.map +0 -1
- package/dist/vite/__tests__/expose-router-id.test.d.ts +0 -2
- package/dist/vite/__tests__/expose-router-id.test.d.ts.map +0 -1
- package/dist/vite/__tests__/expose-router-id.test.js +0 -39
- package/dist/vite/__tests__/expose-router-id.test.js.map +0 -1
- package/dist/vite/ast-handler-extract.d.ts +0 -49
- package/dist/vite/ast-handler-extract.d.ts.map +0 -1
- package/dist/vite/ast-handler-extract.js +0 -249
- package/dist/vite/ast-handler-extract.js.map +0 -1
- package/dist/vite/expose-action-id.d.ts +0 -19
- package/dist/vite/expose-action-id.d.ts.map +0 -1
- package/dist/vite/expose-action-id.js +0 -250
- package/dist/vite/expose-action-id.js.map +0 -1
- package/dist/vite/expose-id-utils.d.ts +0 -69
- package/dist/vite/expose-id-utils.d.ts.map +0 -1
- package/dist/vite/expose-id-utils.js +0 -289
- package/dist/vite/expose-id-utils.js.map +0 -1
- package/dist/vite/expose-internal-ids.d.ts +0 -22
- package/dist/vite/expose-internal-ids.d.ts.map +0 -1
- package/dist/vite/expose-internal-ids.js +0 -886
- package/dist/vite/expose-internal-ids.js.map +0 -1
- package/dist/vite/index.d.ts +0 -149
- package/dist/vite/index.d.ts.map +0 -1
- package/dist/vite/index.js.bak +0 -5448
- package/dist/vite/index.js.map +0 -1
- package/dist/vite/index.named-routes.gen.ts +0 -103
- package/dist/vite/package-resolution.d.ts +0 -43
- package/dist/vite/package-resolution.d.ts.map +0 -1
- package/dist/vite/package-resolution.js +0 -112
- package/dist/vite/package-resolution.js.map +0 -1
- package/dist/vite/virtual-entries.d.ts +0 -25
- package/dist/vite/virtual-entries.d.ts.map +0 -1
- package/dist/vite/virtual-entries.js +0 -110
- package/dist/vite/virtual-entries.js.map +0 -1
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -40,52 +40,122 @@ import {
|
|
|
40
40
|
type RequestContext,
|
|
41
41
|
} from "../../server/request-context.js";
|
|
42
42
|
import { VERSION } from "@rangojs/router:version";
|
|
43
|
+
import {
|
|
44
|
+
isPerClientSignalHeader,
|
|
45
|
+
stripPerClientSignals,
|
|
46
|
+
} from "../../browser/cookie-name.js";
|
|
43
47
|
import {
|
|
44
48
|
resolveTtl,
|
|
45
49
|
resolveSwrWindow,
|
|
46
50
|
DEFAULT_FUNCTION_TTL,
|
|
47
51
|
} from "../cache-policy.js";
|
|
52
|
+
import { reportCacheError, reportingAsync } from "../cache-error.js";
|
|
53
|
+
import type { CacheErrorCategory } from "../cache-error.js";
|
|
54
|
+
import { bufferToBase64, base64ToBuffer } from "./cf-base64.js";
|
|
55
|
+
import {
|
|
56
|
+
KV_MAX_KEY_BYTES,
|
|
57
|
+
KV_MIN_EXPIRATION_TTL,
|
|
58
|
+
kvKeyByteLength,
|
|
59
|
+
remainingCacheControl,
|
|
60
|
+
} from "./cf-kv-utils.js";
|
|
61
|
+
import {
|
|
62
|
+
TAG_MARKER_CACHE_PREFIX,
|
|
63
|
+
TAG_MARKER_ABSENT,
|
|
64
|
+
getTagMarkerMemo,
|
|
65
|
+
getTagMarkerInflight,
|
|
66
|
+
} from "./cf-tag-marker-memo.js";
|
|
48
67
|
|
|
49
68
|
// ============================================================================
|
|
50
69
|
// Constants
|
|
51
70
|
// ============================================================================
|
|
71
|
+
//
|
|
72
|
+
// Header names, KV prefixes, and timeout/interval defaults live in
|
|
73
|
+
// cf-cache-constants.ts so collaborator modules can share them without a
|
|
74
|
+
// circular import back to this class. They are re-exported below so existing
|
|
75
|
+
// import paths (`../cf-cache-store`, `./cf-cache-store.js`) still resolve.
|
|
76
|
+
import {
|
|
77
|
+
CACHE_STALE_AT_HEADER,
|
|
78
|
+
CACHE_STATUS_HEADER,
|
|
79
|
+
CACHE_TAGS_HEADER,
|
|
80
|
+
CACHE_TAGGED_AT_HEADER,
|
|
81
|
+
TAG_MARKER_PREFIX,
|
|
82
|
+
CACHE_REVALIDATING_AT_HEADER,
|
|
83
|
+
CACHE_EXPIRES_AT_HEADER,
|
|
84
|
+
CACHE_ORIG_CC_HEADER,
|
|
85
|
+
MAX_REVALIDATION_INTERVAL,
|
|
86
|
+
EDGE_LOOKUP_TIMEOUT_MS,
|
|
87
|
+
EDGE_READ_TIMEOUT_MS,
|
|
88
|
+
KV_READ_TIMEOUT_MS,
|
|
89
|
+
} from "./cf-cache-constants.js";
|
|
90
|
+
|
|
91
|
+
// Re-export the public constants so consumers/tests importing them from
|
|
92
|
+
// cf-cache-store keep working after the move.
|
|
93
|
+
export {
|
|
94
|
+
CACHE_STALE_AT_HEADER,
|
|
95
|
+
CACHE_STATUS_HEADER,
|
|
96
|
+
CACHE_TAGS_HEADER,
|
|
97
|
+
CACHE_TAGGED_AT_HEADER,
|
|
98
|
+
TAG_MARKER_PREFIX,
|
|
99
|
+
CACHE_REVALIDATING_AT_HEADER,
|
|
100
|
+
MAX_REVALIDATION_INTERVAL,
|
|
101
|
+
EDGE_LOOKUP_TIMEOUT_MS,
|
|
102
|
+
EDGE_READ_TIMEOUT_MS,
|
|
103
|
+
KV_READ_TIMEOUT_MS,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// The tag-marker prefix/sentinel and per-request memo helpers (with their
|
|
107
|
+
// module-singleton WeakMaps) live in cf-tag-marker-memo.ts; imported above.
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Per-request memo of the derived cache-key base URL.
|
|
111
|
+
*
|
|
112
|
+
* deriveBaseUrl() is a pure function of the live request URL, but keyToRequest
|
|
113
|
+
* calls it on EVERY cache operation (each segment/item get/set/delete, each
|
|
114
|
+
* KV->L1 promote, each tag-marker read), so a page composed of many cached
|
|
115
|
+
* entries re-parses the same request.url and re-runs the host validation tens
|
|
116
|
+
* of times. Keying by the request-context object collapses that to one derive
|
|
117
|
+
* per request. Keyed by ctx alone (not by store) because the derived value
|
|
118
|
+
* depends only on the request URL, not on which store asked.
|
|
119
|
+
*/
|
|
120
|
+
const derivedBaseUrlMemo = new WeakMap<object, string>();
|
|
52
121
|
|
|
53
|
-
|
|
54
|
-
|
|
122
|
+
// Pure KV helpers (key byte-length limits, expirationTtl floor, stale-path
|
|
123
|
+
// Cache-Control recompute) live in cf-kv-utils.ts; imported above.
|
|
55
124
|
|
|
56
|
-
/**
|
|
57
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Stores (by namespace) already warned about tag machinery configured without a
|
|
127
|
+
* KV namespace, so the warning fires once per process rather than per request
|
|
128
|
+
* (CFCacheStore is constructed per request).
|
|
129
|
+
*/
|
|
130
|
+
const warnedNoKvReadInvalidation = new Set<string>();
|
|
58
131
|
|
|
59
132
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
133
|
+
* Stores (by namespace) already warned about a tagInvalidationTtl below KV's
|
|
134
|
+
* expirationTtl floor, so the floor warning fires once per process rather than
|
|
135
|
+
* once per request (CFCacheStore is constructed per request).
|
|
63
136
|
*/
|
|
64
|
-
|
|
137
|
+
const warnedTagInvalidationTtlFloor = new Set<string>();
|
|
65
138
|
|
|
66
139
|
// ============================================================================
|
|
67
140
|
// Types
|
|
68
141
|
// ============================================================================
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
import type {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
export
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
): Promise<void>;
|
|
87
|
-
delete(key: string): Promise<void>;
|
|
88
|
-
}
|
|
142
|
+
//
|
|
143
|
+
// The shared public types (KVNamespace, CFCacheReadDebugEvent, CFCacheDebug,
|
|
144
|
+
// CFCacheStoreOptions) live in cf-cache-types.ts; imported and re-exported below
|
|
145
|
+
// so existing import paths still resolve. The private KV envelope interfaces
|
|
146
|
+
// stay here with the methods that read/write them.
|
|
147
|
+
import type {
|
|
148
|
+
KVNamespace,
|
|
149
|
+
CFCacheReadDebugEvent,
|
|
150
|
+
CFCacheDebug,
|
|
151
|
+
CFCacheStoreOptions,
|
|
152
|
+
} from "./cf-cache-types.js";
|
|
153
|
+
export type {
|
|
154
|
+
KVNamespace,
|
|
155
|
+
CFCacheReadDebugEvent,
|
|
156
|
+
CFCacheDebug,
|
|
157
|
+
CFCacheStoreOptions,
|
|
158
|
+
};
|
|
89
159
|
|
|
90
160
|
/**
|
|
91
161
|
* KV envelope for segment cache entries.
|
|
@@ -107,12 +177,16 @@ interface KVSegmentEnvelope {
|
|
|
107
177
|
interface KVItemEnvelope {
|
|
108
178
|
/** RSC-serialized return value */
|
|
109
179
|
v: string;
|
|
110
|
-
/**
|
|
111
|
-
h?:
|
|
180
|
+
/** RSC-encoded handle data (see handle-snapshot.ts encodeHandles) */
|
|
181
|
+
h?: string;
|
|
112
182
|
/** When entry becomes stale (ms epoch) */
|
|
113
183
|
s: number;
|
|
114
184
|
/** When entry hard-expires (ms epoch) */
|
|
115
185
|
e: number;
|
|
186
|
+
/** Cache tags (for distributed tag invalidation) */
|
|
187
|
+
t?: string[];
|
|
188
|
+
/** Timestamp when tags were attached (ms epoch) */
|
|
189
|
+
ta?: number;
|
|
116
190
|
}
|
|
117
191
|
|
|
118
192
|
/**
|
|
@@ -126,108 +200,18 @@ interface KVResponseEnvelope {
|
|
|
126
200
|
st: number;
|
|
127
201
|
/** HTTP status text */
|
|
128
202
|
stx: string;
|
|
129
|
-
/** Serialized headers as key-value pairs */
|
|
203
|
+
/** Serialized headers as key-value pairs (client-facing; no internal headers) */
|
|
130
204
|
hd: [string, string][];
|
|
131
205
|
/** When entry becomes stale (ms epoch) */
|
|
132
206
|
s: number;
|
|
133
207
|
/** When entry hard-expires (ms epoch) */
|
|
134
208
|
e: number;
|
|
209
|
+
/** Cache tags (for distributed tag invalidation) */
|
|
210
|
+
t?: string[];
|
|
211
|
+
/** Timestamp when tags were attached (ms epoch) */
|
|
212
|
+
ta?: number;
|
|
135
213
|
}
|
|
136
214
|
|
|
137
|
-
export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
138
|
-
/**
|
|
139
|
-
* Cache namespace. If not provided, uses caches.default (recommended).
|
|
140
|
-
* Only set this if you need isolated cache storage.
|
|
141
|
-
*/
|
|
142
|
-
namespace?: string;
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Base URL for cache keys.
|
|
146
|
-
*
|
|
147
|
-
* If not provided, derives from request hostname via requestContext:
|
|
148
|
-
* - Production domains → uses `https://{hostname}/`
|
|
149
|
-
* - Dev/preview (localhost, workers.dev, pages.dev) → uses internal fallback URL
|
|
150
|
-
*/
|
|
151
|
-
baseUrl?: string;
|
|
152
|
-
|
|
153
|
-
/** Default cache options */
|
|
154
|
-
defaults?: CacheDefaults;
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Cloudflare ExecutionContext for non-blocking cache writes.
|
|
158
|
-
* Pass the `ctx` from your worker's fetch handler.
|
|
159
|
-
*
|
|
160
|
-
* @example
|
|
161
|
-
* ```typescript
|
|
162
|
-
* new CFCacheStore({ ctx: env.ctx })
|
|
163
|
-
* ```
|
|
164
|
-
*/
|
|
165
|
-
ctx: ExecutionContext;
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Optional KV namespace for L2 cache persistence.
|
|
169
|
-
*
|
|
170
|
-
* When provided, KV acts as a global fallback behind the per-colo Cache API.
|
|
171
|
-
* On L1 miss, KV is checked and hits are promoted back to L1.
|
|
172
|
-
* On writes, data is persisted to both L1 and KV.
|
|
173
|
-
*
|
|
174
|
-
* @example
|
|
175
|
-
* ```typescript
|
|
176
|
-
* new CFCacheStore({ ctx: env.ctx, kv: env.CACHE_KV })
|
|
177
|
-
* ```
|
|
178
|
-
*/
|
|
179
|
-
kv?: KVNamespace;
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Cache version string override. When this changes, all cached entries are
|
|
183
|
-
* effectively invalidated (new keys won't match old entries).
|
|
184
|
-
*
|
|
185
|
-
* Defaults to the auto-generated VERSION from `rsc-router:version` virtual module.
|
|
186
|
-
* Only set this if you need a custom versioning strategy.
|
|
187
|
-
*/
|
|
188
|
-
version?: string;
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Custom key generator applied to all cache operations.
|
|
192
|
-
* Receives the full RequestContext (including env) and the default-generated key.
|
|
193
|
-
* Return value becomes the final cache key (unless route overrides with `key` option).
|
|
194
|
-
*
|
|
195
|
-
* @example Using headers for user segmentation
|
|
196
|
-
* ```typescript
|
|
197
|
-
* keyGenerator: (ctx, defaultKey) => {
|
|
198
|
-
* const segment = ctx.request.headers.get('x-user-segment') || 'default';
|
|
199
|
-
* return `${segment}:${defaultKey}`;
|
|
200
|
-
* }
|
|
201
|
-
* ```
|
|
202
|
-
*
|
|
203
|
-
* @example Using env bindings for multi-region
|
|
204
|
-
* ```typescript
|
|
205
|
-
* keyGenerator: (ctx, defaultKey) => {
|
|
206
|
-
* const region = ctx.env.REGION || 'us';
|
|
207
|
-
* return `${region}:${defaultKey}`;
|
|
208
|
-
* }
|
|
209
|
-
* ```
|
|
210
|
-
*
|
|
211
|
-
* @example Using cookies for locale-aware caching
|
|
212
|
-
* ```typescript
|
|
213
|
-
* keyGenerator: (ctx, defaultKey) => {
|
|
214
|
-
* const locale = cookies().get('locale')?.value || 'en';
|
|
215
|
-
* return `${locale}:${defaultKey}`;
|
|
216
|
-
* }
|
|
217
|
-
* ```
|
|
218
|
-
*/
|
|
219
|
-
keyGenerator?: (
|
|
220
|
-
ctx: RequestContext<TEnv>,
|
|
221
|
-
defaultKey: string,
|
|
222
|
-
) => string | Promise<string>;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Cache status values for the x-edge-cache-status header.
|
|
227
|
-
* @internal
|
|
228
|
-
*/
|
|
229
|
-
export type CacheStatus = "HIT" | "REVALIDATING";
|
|
230
|
-
|
|
231
215
|
// ============================================================================
|
|
232
216
|
// CFCacheStore Implementation
|
|
233
217
|
// ============================================================================
|
|
@@ -240,10 +224,17 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
240
224
|
) => string | Promise<string>;
|
|
241
225
|
|
|
242
226
|
private readonly namespace?: string;
|
|
243
|
-
private readonly
|
|
227
|
+
private readonly explicitBaseUrl?: string;
|
|
244
228
|
private readonly waitUntil?: (fn: () => Promise<void>) => void;
|
|
245
229
|
private readonly version?: string;
|
|
230
|
+
private readonly edgeLookupTimeoutMs: number;
|
|
231
|
+
private readonly edgeReadTimeoutMs: number;
|
|
232
|
+
private readonly kvReadTimeoutMs: number;
|
|
233
|
+
private readonly debug?: (event: CFCacheReadDebugEvent) => void;
|
|
246
234
|
private readonly kv?: KVNamespace;
|
|
235
|
+
private readonly onRevalidateTag?: (tags: string[]) => Promise<void>;
|
|
236
|
+
private readonly tagInvalidationTtl?: number;
|
|
237
|
+
private readonly tagCacheTtl: number;
|
|
247
238
|
|
|
248
239
|
constructor(options: CFCacheStoreOptions<TEnv>) {
|
|
249
240
|
if (!options.ctx) {
|
|
@@ -255,52 +246,192 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
255
246
|
}
|
|
256
247
|
|
|
257
248
|
this.namespace = options.namespace;
|
|
258
|
-
|
|
249
|
+
// Base URL is resolved lazily per cache operation (see resolveBaseUrl).
|
|
250
|
+
// The store is constructed before the per-request context ALS is entered
|
|
251
|
+
// (the cache factory runs ahead of runWithRequestContext in the handler),
|
|
252
|
+
// so deriving the host here would always miss the request and fall back to
|
|
253
|
+
// the internal host. Only the explicit override can be captured eagerly.
|
|
254
|
+
this.explicitBaseUrl = options.baseUrl;
|
|
259
255
|
this.defaults = options.defaults;
|
|
260
256
|
this.version = options.version ?? VERSION;
|
|
257
|
+
// Coalesce only finite numbers to the override; a non-finite value (NaN from
|
|
258
|
+
// `Number(env.UNSET)`, or Infinity) would otherwise sail past `?? DEFAULT`
|
|
259
|
+
// (which only replaces null/undefined) into setTimeout, where NaN/Infinity
|
|
260
|
+
// are spec-coerced to ~1ms and silently turn the budget into a near-100%
|
|
261
|
+
// false-miss on that tier. A genuine finite 0 or negative still passes
|
|
262
|
+
// through and disables the budget per the documented `<= 0` contract.
|
|
263
|
+
const finiteBudget = (
|
|
264
|
+
value: number | undefined,
|
|
265
|
+
fallback: number,
|
|
266
|
+
): number =>
|
|
267
|
+
typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
268
|
+
this.edgeLookupTimeoutMs = finiteBudget(
|
|
269
|
+
options.edgeLookupTimeoutMs,
|
|
270
|
+
EDGE_LOOKUP_TIMEOUT_MS,
|
|
271
|
+
);
|
|
272
|
+
this.edgeReadTimeoutMs = finiteBudget(
|
|
273
|
+
options.edgeReadTimeoutMs,
|
|
274
|
+
EDGE_READ_TIMEOUT_MS,
|
|
275
|
+
);
|
|
276
|
+
this.kvReadTimeoutMs = finiteBudget(
|
|
277
|
+
options.kvReadTimeoutMs,
|
|
278
|
+
KV_READ_TIMEOUT_MS,
|
|
279
|
+
);
|
|
280
|
+
this.debug =
|
|
281
|
+
options.debug === true
|
|
282
|
+
? (event) =>
|
|
283
|
+
console.log(`[CFCacheStore:debug] ${JSON.stringify(event)}`)
|
|
284
|
+
: typeof options.debug === "function"
|
|
285
|
+
? options.debug
|
|
286
|
+
: undefined;
|
|
261
287
|
this.keyGenerator = options.keyGenerator;
|
|
262
288
|
this.waitUntil = (fn) => options.ctx.waitUntil(fn());
|
|
263
289
|
this.kv = options.kv;
|
|
290
|
+
this.onRevalidateTag = options.onRevalidateTag;
|
|
291
|
+
// tagInvalidationTtl feeds KV's expirationTtl, which CF rejects below
|
|
292
|
+
// KV_MIN_EXPIRATION_TTL (60s) -- a too-small finite value would make EVERY
|
|
293
|
+
// marker write throw and break ALL invalidation. Floor it (and warn once);
|
|
294
|
+
// a non-finite/non-positive value falls back to the no-expiry default
|
|
295
|
+
// (markers persist) rather than silently sailing a NaN into expirationTtl.
|
|
296
|
+
this.tagInvalidationTtl = this.sanitizeTagInvalidationTtl(
|
|
297
|
+
options.tagInvalidationTtl,
|
|
298
|
+
);
|
|
299
|
+
// tagCacheTtl gates the L1 marker cache via `> 0`. A non-finite value (NaN
|
|
300
|
+
// from `Number(env.UNSET)`) is not null/undefined, so `?? 0` would let it
|
|
301
|
+
// through and silently disable the cache while reading as "configured".
|
|
302
|
+
// finiteBudget coerces non-finite/null/undefined to 0; the `> 0` guard then
|
|
303
|
+
// collapses a finite non-positive value to the documented 0 = disabled.
|
|
304
|
+
const tagCacheTtl = finiteBudget(options.tagCacheTtl, 0);
|
|
305
|
+
this.tagCacheTtl = tagCacheTtl > 0 ? tagCacheTtl : 0;
|
|
306
|
+
|
|
307
|
+
// Read-side tag invalidation requires KV: isGloballyInvalidated() compares an
|
|
308
|
+
// entry's taggedAt against the per-tag KV marker and short-circuits to "not
|
|
309
|
+
// invalidated" when no KV namespace is configured. A consumer who wires the
|
|
310
|
+
// tag machinery (tagCacheTtl for L1 markers, or onRevalidateTag for CDN purge)
|
|
311
|
+
// but omits kv gets only the purge fired - marker writes are skipped without
|
|
312
|
+
// kv - yet every tagged read still serves stale data with no other signal.
|
|
313
|
+
// Surface that misconfiguration.
|
|
314
|
+
if (!this.kv && (this.tagCacheTtl > 0 || this.onRevalidateTag)) {
|
|
315
|
+
const id = this.namespace ?? "default";
|
|
316
|
+
if (!warnedNoKvReadInvalidation.has(id)) {
|
|
317
|
+
warnedNoKvReadInvalidation.add(id);
|
|
318
|
+
console.warn(
|
|
319
|
+
`[CFCacheStore] tagCacheTtl/onRevalidateTag is configured without a KV ` +
|
|
320
|
+
`namespace, so tag invalidation has NO read-side effect: tagged reads ` +
|
|
321
|
+
`are never treated as invalidated and serve stale data. Configure ` +
|
|
322
|
+
`{ kv } for distributed tag invalidation.`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Validate a consumer-supplied tagInvalidationTtl against CF KV's expirationTtl
|
|
330
|
+
* floor. A finite value below KV_MIN_EXPIRATION_TTL is raised to it (with a
|
|
331
|
+
* one-time warning) so invalidation keeps working instead of every marker
|
|
332
|
+
* write throwing; a non-finite or non-positive value returns undefined (the
|
|
333
|
+
* no-expiry default). The warning still notes the sizing rule: the TTL must
|
|
334
|
+
* exceed the largest entry TTL+SWR or invalidated entries can resurrect.
|
|
335
|
+
* @internal
|
|
336
|
+
*/
|
|
337
|
+
private sanitizeTagInvalidationTtl(
|
|
338
|
+
value: number | undefined,
|
|
339
|
+
): number | undefined {
|
|
340
|
+
if (value == null) return undefined;
|
|
341
|
+
if (!Number.isFinite(value) || value <= 0) return undefined;
|
|
342
|
+
if (value < KV_MIN_EXPIRATION_TTL) {
|
|
343
|
+
const id = this.namespace ?? "default";
|
|
344
|
+
if (!warnedTagInvalidationTtlFloor.has(id)) {
|
|
345
|
+
warnedTagInvalidationTtlFloor.add(id);
|
|
346
|
+
console.warn(
|
|
347
|
+
`[CFCacheStore] tagInvalidationTtl ${value} is below Cloudflare KV's ` +
|
|
348
|
+
`${KV_MIN_EXPIRATION_TTL}s expirationTtl floor; raising to ` +
|
|
349
|
+
`${KV_MIN_EXPIRATION_TTL}. It must still exceed your largest entry ` +
|
|
350
|
+
`TTL+SWR or invalidated entries can resurrect when the marker expires.`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
return KV_MIN_EXPIRATION_TTL;
|
|
354
|
+
}
|
|
355
|
+
return value;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Emit a debug event if `debug` is enabled. Swallows sink errors so a faulty
|
|
360
|
+
* debug callback can never break a cache read.
|
|
361
|
+
* @internal
|
|
362
|
+
*/
|
|
363
|
+
private emitDebug(event: CFCacheReadDebugEvent): void {
|
|
364
|
+
if (!this.debug) return;
|
|
365
|
+
try {
|
|
366
|
+
this.debug(event);
|
|
367
|
+
} catch {
|
|
368
|
+
// A broken debug sink must not affect the request.
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Resolve the cache-key base URL for the current cache operation.
|
|
374
|
+
* Prefers an explicit `baseUrl` option; otherwise derives it from the live
|
|
375
|
+
* request. Called per operation (from keyToRequest), which runs inside the
|
|
376
|
+
* request-context ALS, so deriveBaseUrl sees the request and can use the
|
|
377
|
+
* production host instead of the internal fallback.
|
|
378
|
+
* @internal
|
|
379
|
+
*/
|
|
380
|
+
private resolveBaseUrl(): string {
|
|
381
|
+
return this.explicitBaseUrl ?? this.deriveBaseUrl();
|
|
264
382
|
}
|
|
265
383
|
|
|
266
384
|
/**
|
|
267
385
|
* Derive base URL from request hostname via requestContext.
|
|
268
386
|
* Uses internal fallback for dev/preview environments and untrusted hostnames.
|
|
387
|
+
* Must run inside the request context (invoked lazily via resolveBaseUrl).
|
|
269
388
|
* @internal
|
|
270
389
|
*/
|
|
271
390
|
private deriveBaseUrl(): string {
|
|
272
|
-
const fallback = "https://rsc-
|
|
391
|
+
const fallback = "https://rsc-dummy-host-1.com/";
|
|
273
392
|
|
|
274
393
|
const ctx = _getRequestContext();
|
|
275
394
|
if (!ctx?.request) {
|
|
276
395
|
return fallback;
|
|
277
396
|
}
|
|
278
397
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
hostname === "localhost" ||
|
|
286
|
-
hostname === "127.0.0.1" ||
|
|
287
|
-
hostname.endsWith(".workers.dev") ||
|
|
288
|
-
hostname.endsWith(".pages.dev")
|
|
289
|
-
) {
|
|
290
|
-
return fallback;
|
|
291
|
-
}
|
|
398
|
+
// The result is deterministic per request, but keyToRequest calls this on
|
|
399
|
+
// every cache operation; memoize per request context (see derivedBaseUrlMemo).
|
|
400
|
+
const memoized = derivedBaseUrlMemo.get(ctx);
|
|
401
|
+
if (memoized !== undefined) {
|
|
402
|
+
return memoized;
|
|
403
|
+
}
|
|
292
404
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
405
|
+
const derived = ((): string => {
|
|
406
|
+
try {
|
|
407
|
+
const url = new URL(ctx.request.url);
|
|
408
|
+
const hostname = url.hostname;
|
|
409
|
+
|
|
410
|
+
// Use fallback for dev/preview environments
|
|
411
|
+
if (
|
|
412
|
+
hostname === "localhost" ||
|
|
413
|
+
hostname === "127.0.0.1" ||
|
|
414
|
+
hostname.endsWith(".workers.dev") ||
|
|
415
|
+
hostname.endsWith(".pages.dev")
|
|
416
|
+
) {
|
|
417
|
+
return fallback;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Validate hostname: must be a valid domain (alphanumeric, hyphens, dots)
|
|
421
|
+
// to prevent host header injection into cache keys
|
|
422
|
+
if (!/^[a-zA-Z0-9.-]+$/.test(hostname) || hostname.length > 253) {
|
|
423
|
+
return fallback;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Use actual hostname for production
|
|
427
|
+
return `https://${hostname}/`;
|
|
428
|
+
} catch {
|
|
296
429
|
return fallback;
|
|
297
430
|
}
|
|
431
|
+
})();
|
|
298
432
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
} catch {
|
|
302
|
-
return fallback;
|
|
303
|
-
}
|
|
433
|
+
derivedBaseUrlMemo.set(ctx, derived);
|
|
434
|
+
return derived;
|
|
304
435
|
}
|
|
305
436
|
|
|
306
437
|
/**
|
|
@@ -314,10 +445,297 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
314
445
|
return caches.default;
|
|
315
446
|
}
|
|
316
447
|
|
|
448
|
+
/**
|
|
449
|
+
* Race an async cache read against a latency budget. Shared by all three read
|
|
450
|
+
* tiers (L1 match, L1 body, L2/KV) so the timeout policy lives in one place:
|
|
451
|
+
* on timeout it returns `{ value: undefined, timedOut: true }` and logs
|
|
452
|
+
* `${label} exceeded ${budgetMs}ms; treating as miss`; the abandoned read is
|
|
453
|
+
* left to settle in the background (late rejection swallowed) rather than
|
|
454
|
+
* aborted, since the underlying CF primitives expose no cancellation. A budget
|
|
455
|
+
* <= 0 disables the bound and awaits the read directly. `read` is a thunk so
|
|
456
|
+
* the disabled path and the raced path start the read identically.
|
|
457
|
+
* @internal
|
|
458
|
+
*/
|
|
459
|
+
private async readWithTimeout<T>(
|
|
460
|
+
read: () => Promise<T>,
|
|
461
|
+
budgetMs: number,
|
|
462
|
+
label: string,
|
|
463
|
+
): Promise<{ value: T | undefined; timedOut: boolean }> {
|
|
464
|
+
if (budgetMs <= 0) return { value: await read(), timedOut: false };
|
|
465
|
+
|
|
466
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
467
|
+
const timeout = new Promise<{ timedOut: true }>((resolve) => {
|
|
468
|
+
timer = setTimeout(() => resolve({ timedOut: true }), budgetMs);
|
|
469
|
+
});
|
|
470
|
+
try {
|
|
471
|
+
const readPromise = read();
|
|
472
|
+
// The losing branch keeps running; ensure a late rejection can't surface
|
|
473
|
+
// as an unhandled rejection once we've stopped awaiting it.
|
|
474
|
+
readPromise.catch(() => {});
|
|
475
|
+
const result = await Promise.race([
|
|
476
|
+
readPromise.then((value) => ({ timedOut: false as const, value })),
|
|
477
|
+
timeout,
|
|
478
|
+
]);
|
|
479
|
+
if (result.timedOut) {
|
|
480
|
+
console.warn(
|
|
481
|
+
`[CFCacheStore] ${label} exceeded ${budgetMs}ms; treating as miss`,
|
|
482
|
+
);
|
|
483
|
+
return { value: undefined, timedOut: true };
|
|
484
|
+
}
|
|
485
|
+
return { value: result.value, timedOut: false };
|
|
486
|
+
} finally {
|
|
487
|
+
if (timer) clearTimeout(timer);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Read from the L1 edge cache under the edgeLookupTimeoutMs budget. A `match`
|
|
493
|
+
* slower than the budget is abandoned and reported as a miss
|
|
494
|
+
* (`{ response: undefined, timedOut: true }`) so a degraded colo cannot stall
|
|
495
|
+
* the request; callers fall through to their normal miss path (L2/KV or
|
|
496
|
+
* render). The `timedOut` flag lets callers distinguish an abandoned slow
|
|
497
|
+
* match from a genuine miss for debug reporting; `error` is set when the
|
|
498
|
+
* `match` itself rejected (a transient L1 infra error) so the caller can
|
|
499
|
+
* report it as cache-read while still degrading to L2/KV -- distinct from a
|
|
500
|
+
* genuine miss (no entry), which sets neither flag.
|
|
501
|
+
* @internal
|
|
502
|
+
*/
|
|
503
|
+
private async matchWithTimeout(
|
|
504
|
+
cache: Cache,
|
|
505
|
+
request: Request,
|
|
506
|
+
): Promise<{
|
|
507
|
+
response: Response | undefined;
|
|
508
|
+
timedOut: boolean;
|
|
509
|
+
error?: unknown;
|
|
510
|
+
}> {
|
|
511
|
+
let matchError: unknown;
|
|
512
|
+
const { value, timedOut } = await this.readWithTimeout(
|
|
513
|
+
// A fast match rejection is caught at the thunk and reported as a miss
|
|
514
|
+
// (response undefined), so the caller falls through to L2/KV rather than
|
|
515
|
+
// escaping to the outer catch -- symmetric with the body-read thunk. The
|
|
516
|
+
// error is captured (not swallowed) so the caller can surface it via
|
|
517
|
+
// onError as a cache-read degradation.
|
|
518
|
+
() =>
|
|
519
|
+
cache.match(request).catch((e) => {
|
|
520
|
+
matchError = e;
|
|
521
|
+
return undefined;
|
|
522
|
+
}),
|
|
523
|
+
this.edgeLookupTimeoutMs,
|
|
524
|
+
"edge cache lookup",
|
|
525
|
+
);
|
|
526
|
+
return { response: value, timedOut, error: matchError };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Read and JSON-parse a matched L1 Response's body under the edgeReadTimeoutMs
|
|
531
|
+
* budget. CF resolves `match()` with a lazily-streamed body, so the latency
|
|
532
|
+
* tail surfaces here -- after matchWithTimeout has already passed -- not in the
|
|
533
|
+
* match itself. On timeout `undefined` is returned so the caller falls through
|
|
534
|
+
* to L2/KV or render.
|
|
535
|
+
* @internal
|
|
536
|
+
*/
|
|
537
|
+
private async readJsonWithTimeout<T>(
|
|
538
|
+
response: Response,
|
|
539
|
+
): Promise<{ value: T | undefined; errored: boolean; error?: unknown }> {
|
|
540
|
+
// A FAST json() rejection (a corrupt body, or a foreign 200 non-JSON
|
|
541
|
+
// response that collided on this key) is caught at the thunk and turned into
|
|
542
|
+
// a miss, so the caller falls through to L2/KV exactly like a body-timeout
|
|
543
|
+
// -- instead of escaping to get()/getItem()'s outer catch, which returns
|
|
544
|
+
// null WITHOUT ever consulting KV. The catch lives here, not in
|
|
545
|
+
// readWithTimeout, so the L2/KV tier keeps propagating a genuine kv.get
|
|
546
|
+
// rejection to its own error sink. The `errored` flag lets the caller emit a
|
|
547
|
+
// distinct "body-error" debug outcome rather than masquerading as a timeout.
|
|
548
|
+
// On a TIMEOUT the json() promise is still pending, so the catch has not
|
|
549
|
+
// fired: errored stays false and the outcome is correctly a body-timeout. A
|
|
550
|
+
// late rejection after the timeout only mutates the closure flag, which the
|
|
551
|
+
// already-returned object no longer reads.
|
|
552
|
+
let errored = false;
|
|
553
|
+
let error: unknown;
|
|
554
|
+
const { value } = await this.readWithTimeout<T | undefined>(
|
|
555
|
+
() =>
|
|
556
|
+
(response.json() as Promise<T>).catch((e) => {
|
|
557
|
+
errored = true;
|
|
558
|
+
error = e;
|
|
559
|
+
return undefined;
|
|
560
|
+
}),
|
|
561
|
+
this.edgeReadTimeoutMs,
|
|
562
|
+
"edge cache body read",
|
|
563
|
+
);
|
|
564
|
+
return { value, errored, error };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Self-heal a corrupt L1 entry, then return the fall-through result. Reports
|
|
569
|
+
* the corruption as cache-corrupt (so an onError consumer sees it distinctly
|
|
570
|
+
* from a transient outage), runs the caller's L2/KV fall-through, and evicts
|
|
571
|
+
* the faulty per-colo entry ONLY when that fall-through found no good copy.
|
|
572
|
+
*
|
|
573
|
+
* The conditional evict is the load-bearing detail: when KV DOES serve a copy,
|
|
574
|
+
* kvGet* has already scheduled a same-key promote (`cache.put`); an eager
|
|
575
|
+
* `cache.delete` here would race that put with no CF Cache API ordering
|
|
576
|
+
* guarantee and could clobber the freshly-restored entry. So in that case we
|
|
577
|
+
* lean on #558's heal-by-overwrite (the non-suppressed fall-through promotes /
|
|
578
|
+
* a fresh render re-`set`s over the bad entry) and skip the delete. Only when
|
|
579
|
+
* this request's fall-through found no copy (=== null) is the eager evict
|
|
580
|
+
* scheduled -- useful then, since nothing else will overwrite the poison entry.
|
|
581
|
+
* A null fall-through can also be a KV-read TIMEOUT rather than a genuine miss:
|
|
582
|
+
* a concurrent request that read KV successfully may be promoting the same key,
|
|
583
|
+
* and this evict could race it. That is benign -- the worst case is one wasted
|
|
584
|
+
* colo-local promote, never a wrong served value, and the next read self-heals
|
|
585
|
+
* -- so we accept it rather than suppressing the evict on a timeout (which
|
|
586
|
+
* would strand the poison entry when KV really is empty). The evict is
|
|
587
|
+
* non-blocking (waitUntil) so it never adds latency to the degraded read.
|
|
588
|
+
* @internal
|
|
589
|
+
*/
|
|
590
|
+
private async healCorruptL1<T>(
|
|
591
|
+
cache: Cache,
|
|
592
|
+
request: Request,
|
|
593
|
+
error: unknown,
|
|
594
|
+
label: string,
|
|
595
|
+
fallThrough: () => Promise<T | null>,
|
|
596
|
+
): Promise<T | null> {
|
|
597
|
+
reportCacheError(
|
|
598
|
+
error ?? new Error("corrupt/partial L1 body"),
|
|
599
|
+
"cache-corrupt",
|
|
600
|
+
`[CFCacheStore] ${label}: corrupt L1 body`,
|
|
601
|
+
);
|
|
602
|
+
const result = await fallThrough();
|
|
603
|
+
if (result === null) {
|
|
604
|
+
const evict = (): Promise<void> =>
|
|
605
|
+
reportingAsync(
|
|
606
|
+
() => cache.delete(request),
|
|
607
|
+
"cache-delete",
|
|
608
|
+
`[CFCacheStore] ${label}: evict corrupt L1`,
|
|
609
|
+
);
|
|
610
|
+
if (this.waitUntil) this.waitUntil(evict);
|
|
611
|
+
else void evict();
|
|
612
|
+
}
|
|
613
|
+
return result;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Re-put a stale L1 entry marked REVALIDATING, so concurrent requests serve it
|
|
618
|
+
* without each triggering a revalidation. Shared by get()/getItem().
|
|
619
|
+
*
|
|
620
|
+
* The write is NON-BLOCKING (waitUntil) and best-effort by design:
|
|
621
|
+
* - It runs in waitUntil, so it never adds the put latency to the served stale
|
|
622
|
+
* read and a put failure can never turn that good read into a miss. The put
|
|
623
|
+
* is still initiated synchronously (this.waitUntil invokes its callback
|
|
624
|
+
* immediately), so concurrent readers see the marker land at the same time an
|
|
625
|
+
* awaited write would -- awaiting only blocks the current request.
|
|
626
|
+
* - The background revalidation's fresh set() is gated behind a full re-render,
|
|
627
|
+
* so it lands well after this put; a stale-clobbers-fresh race would require
|
|
628
|
+
* this single put to be slower than that entire render+set, and self-heals
|
|
629
|
+
* within MAX_REVALIDATION_INTERVAL.
|
|
630
|
+
*
|
|
631
|
+
* Cache-Control is recomputed to the REMAINING ttl from the stored hard-expiry
|
|
632
|
+
* deadline (see remainingCacheControl), not copied from the original
|
|
633
|
+
* full-window header -- copying it would restart CF retention on every re-arm
|
|
634
|
+
* and pin a perpetually-failing entry past hard-expiry. A legacy/tampered entry
|
|
635
|
+
* without a valid deadline floors to max-age=1 and self-heals via KV.
|
|
636
|
+
* @internal
|
|
637
|
+
*/
|
|
638
|
+
private markRevalidating(
|
|
639
|
+
cache: Cache,
|
|
640
|
+
request: Request,
|
|
641
|
+
sourceHeaders: Headers,
|
|
642
|
+
status: number,
|
|
643
|
+
body: string,
|
|
644
|
+
): void {
|
|
645
|
+
const reputNow = Date.now();
|
|
646
|
+
const headers = new Headers(sourceHeaders);
|
|
647
|
+
headers.set(CACHE_STATUS_HEADER, "REVALIDATING");
|
|
648
|
+
headers.set(CACHE_REVALIDATING_AT_HEADER, String(reputNow));
|
|
649
|
+
headers.set("Cache-Control", remainingCacheControl(headers, reputNow));
|
|
650
|
+
const markerResponse = new Response(body, { status, headers });
|
|
651
|
+
const write = async (): Promise<void> => {
|
|
652
|
+
try {
|
|
653
|
+
await cache.put(request, markerResponse);
|
|
654
|
+
} catch {
|
|
655
|
+
// Best-effort: a failed marker write must not affect the served read;
|
|
656
|
+
// the entry simply re-arms on the next stale read.
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
if (this.waitUntil) this.waitUntil(write);
|
|
660
|
+
else void write();
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Document-tier counterpart of markRevalidating for getResponse's herd guard.
|
|
665
|
+
* The segment/item tiers JSON-parse the body, so they re-put with a string
|
|
666
|
+
* body; document bodies are streamed verbatim, so we re-put with a CLONED
|
|
667
|
+
* response body (`response.clone()`) supplied by the caller -- the original
|
|
668
|
+
* body still streams to the client while the marker carries the clone. Same
|
|
669
|
+
* REVALIDATING status header, same revalidating-at stamp, same
|
|
670
|
+
* remainingCacheControl re-put math as markRevalidating, so the document tier
|
|
671
|
+
* suppresses concurrent revalidation for the identical MAX_REVALIDATION_INTERVAL
|
|
672
|
+
* window the segment tier does. Best-effort and non-blocking: a failed marker
|
|
673
|
+
* write must not affect the served stale read.
|
|
674
|
+
* @internal
|
|
675
|
+
*/
|
|
676
|
+
private markResponseRevalidating(
|
|
677
|
+
cache: Cache,
|
|
678
|
+
request: Request,
|
|
679
|
+
clonedResponse: Response,
|
|
680
|
+
): void {
|
|
681
|
+
const reputNow = Date.now();
|
|
682
|
+
const headers = new Headers(clonedResponse.headers);
|
|
683
|
+
headers.set(CACHE_STATUS_HEADER, "REVALIDATING");
|
|
684
|
+
headers.set(CACHE_REVALIDATING_AT_HEADER, String(reputNow));
|
|
685
|
+
headers.set("Cache-Control", remainingCacheControl(headers, reputNow));
|
|
686
|
+
const markerResponse = new Response(clonedResponse.body, {
|
|
687
|
+
status: clonedResponse.status,
|
|
688
|
+
statusText: clonedResponse.statusText,
|
|
689
|
+
headers,
|
|
690
|
+
});
|
|
691
|
+
const write = async (): Promise<void> => {
|
|
692
|
+
try {
|
|
693
|
+
await cache.put(request, markerResponse);
|
|
694
|
+
} catch {
|
|
695
|
+
// Best-effort: see markRevalidating.
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
if (this.waitUntil) this.waitUntil(write);
|
|
699
|
+
else void write();
|
|
700
|
+
}
|
|
701
|
+
|
|
317
702
|
// ============================================================================
|
|
318
703
|
// Segment Cache Methods
|
|
319
704
|
// ============================================================================
|
|
320
705
|
|
|
706
|
+
/**
|
|
707
|
+
* Guard the segment tier against a `keyGenerator` that returns a key colliding
|
|
708
|
+
* with a reserved tag-marker namespace: `__tag__/` (the KV marker key) or
|
|
709
|
+
* `__tagmarker__/` (the L1 Cache API marker request). The item/doc tiers are
|
|
710
|
+
* internally prefixed (`fn:`/`doc:`) so only the bare segment key can collide;
|
|
711
|
+
* a collision would let a segment write clobber - or a segment read/delete
|
|
712
|
+
* evict - a live tag marker, silently breaking invalidation. Report loudly
|
|
713
|
+
* (so a misconfigured keyGenerator surfaces immediately) and treat the segment
|
|
714
|
+
* operation as a miss/no-op rather than corrupting the marker namespace.
|
|
715
|
+
* @internal
|
|
716
|
+
*/
|
|
717
|
+
private isReservedSegmentKey(
|
|
718
|
+
key: string,
|
|
719
|
+
category: CacheErrorCategory,
|
|
720
|
+
): boolean {
|
|
721
|
+
const reserved = key.startsWith(TAG_MARKER_PREFIX)
|
|
722
|
+
? TAG_MARKER_PREFIX
|
|
723
|
+
: key.startsWith(TAG_MARKER_CACHE_PREFIX)
|
|
724
|
+
? TAG_MARKER_CACHE_PREFIX
|
|
725
|
+
: null;
|
|
726
|
+
if (!reserved) return false;
|
|
727
|
+
reportCacheError(
|
|
728
|
+
new Error(
|
|
729
|
+
`segment key "${key}" collides with the reserved "${reserved}" ` +
|
|
730
|
+
`tag-marker namespace; the operation is ignored. Fix the store ` +
|
|
731
|
+
`keyGenerator so it does not produce keys with this prefix.`,
|
|
732
|
+
),
|
|
733
|
+
category,
|
|
734
|
+
"[CFCacheStore] reserved key",
|
|
735
|
+
);
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
|
|
321
739
|
/**
|
|
322
740
|
* Get cached entry data by key.
|
|
323
741
|
*
|
|
@@ -330,48 +748,219 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
330
748
|
* KV hits are promoted to L1 in the background.
|
|
331
749
|
*/
|
|
332
750
|
async get(key: string): Promise<CacheGetResult | null> {
|
|
751
|
+
if (this.isReservedSegmentKey(key, "cache-read")) return null;
|
|
333
752
|
try {
|
|
334
753
|
const cache = await this.getCache();
|
|
335
754
|
const request = this.keyToRequest(key);
|
|
336
|
-
const
|
|
755
|
+
const matchStart = Date.now();
|
|
756
|
+
const {
|
|
757
|
+
response,
|
|
758
|
+
timedOut,
|
|
759
|
+
error: matchError,
|
|
760
|
+
} = await this.matchWithTimeout(cache, request);
|
|
761
|
+
const matchMs = Date.now() - matchStart;
|
|
337
762
|
|
|
338
763
|
if (!response) {
|
|
764
|
+
// A transient L1 match error (matchError set) is reported as cache-read
|
|
765
|
+
// but, like a genuine miss or an abandoned slow match (timedOut), still
|
|
766
|
+
// degrades to L2/KV rather than failing the read.
|
|
767
|
+
if (matchError)
|
|
768
|
+
reportCacheError(
|
|
769
|
+
matchError,
|
|
770
|
+
"cache-read",
|
|
771
|
+
"[CFCacheStore] get L1 match",
|
|
772
|
+
);
|
|
773
|
+
if (this.debug)
|
|
774
|
+
this.emitDebug({
|
|
775
|
+
op: "get",
|
|
776
|
+
key,
|
|
777
|
+
// A match REJECTION (matchError) is distinct from a genuine absence:
|
|
778
|
+
// surface it as match-error so debug agrees with the cache-read
|
|
779
|
+
// already routed to onError, instead of masquerading as l1-miss.
|
|
780
|
+
outcome: matchError
|
|
781
|
+
? "match-error"
|
|
782
|
+
: timedOut
|
|
783
|
+
? "match-timeout"
|
|
784
|
+
: "l1-miss",
|
|
785
|
+
matchMs,
|
|
786
|
+
});
|
|
339
787
|
return this.kvGetSegment(key);
|
|
340
788
|
}
|
|
341
789
|
|
|
790
|
+
// A non-200 entry (a cached error response, or a foreign response that
|
|
791
|
+
// landed on this key) is not valid segment data; treat it as a miss
|
|
792
|
+
// rather than JSON-parsing garbage and serving it as a hit.
|
|
793
|
+
if (response.status !== 200) {
|
|
794
|
+
if (this.debug)
|
|
795
|
+
this.emitDebug({
|
|
796
|
+
op: "get",
|
|
797
|
+
key,
|
|
798
|
+
outcome: "non-200",
|
|
799
|
+
status: response.status,
|
|
800
|
+
matchMs,
|
|
801
|
+
});
|
|
802
|
+
// Degraded fall-through: suppress revalidation so a broken L1 entry hit
|
|
803
|
+
// concurrently serves KV-stale, not a herd. See kvGetSegment.
|
|
804
|
+
return this.kvGetSegment(key, { suppressRevalidate: true });
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Tag invalidation: an entry whose tags were invalidated after it was
|
|
808
|
+
// cached is treated as a miss, so the next render re-populates it. We
|
|
809
|
+
// return null (re-render locally) rather than falling through to KV. In
|
|
810
|
+
// the common case the L1 entry and its KV twin were written together with
|
|
811
|
+
// the same taggedAt, so kvGetSegment's own tag check would miss too and a
|
|
812
|
+
// fall-through is pure cost. The tiers CAN diverge -- another colo may have
|
|
813
|
+
// already re-rendered and written a fresher KV envelope -- in which case a
|
|
814
|
+
// fall-through could serve that copy instead of re-rendering here.
|
|
815
|
+
// Capturing that cross-colo optimization is a deferred follow-up, not a
|
|
816
|
+
// correctness gap: this colo's next read after its own re-render self-heals.
|
|
817
|
+
const tagInfo = this.readTagInfo(response.headers);
|
|
818
|
+
// Measure the marker-resolution tail (memo -> L1 marker cache -> KV) only
|
|
819
|
+
// when debug is on, so the hot path pays nothing. It is the serial read
|
|
820
|
+
// that sits between matchMs and bodyReadMs for a tagged entry.
|
|
821
|
+
const markerStart = this.debug ? Date.now() : 0;
|
|
822
|
+
const invalidated = await this.isGloballyInvalidated(
|
|
823
|
+
tagInfo.tags,
|
|
824
|
+
tagInfo.taggedAt,
|
|
825
|
+
);
|
|
826
|
+
const markerMs = this.debug ? Date.now() - markerStart : undefined;
|
|
827
|
+
if (invalidated) {
|
|
828
|
+
if (this.debug)
|
|
829
|
+
this.emitDebug({
|
|
830
|
+
op: "get",
|
|
831
|
+
key,
|
|
832
|
+
outcome: "tag-invalidated",
|
|
833
|
+
status: response.status,
|
|
834
|
+
matchMs,
|
|
835
|
+
markerMs,
|
|
836
|
+
});
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
|
|
342
840
|
// Read status headers
|
|
343
841
|
const status = response.headers.get(CACHE_STATUS_HEADER);
|
|
344
|
-
const age = Number(response.headers.get("age") ?? "0");
|
|
345
842
|
const staleAt = Number(
|
|
346
843
|
response.headers.get(CACHE_STALE_AT_HEADER) ?? "0",
|
|
347
844
|
);
|
|
845
|
+
const revalidatingAt = Number(
|
|
846
|
+
response.headers.get(CACHE_REVALIDATING_AT_HEADER) ?? "0",
|
|
847
|
+
);
|
|
348
848
|
|
|
349
|
-
const
|
|
849
|
+
const now = Date.now();
|
|
850
|
+
const isStale = staleAt > 0 && now > staleAt;
|
|
851
|
+
// Recency comes from our explicit revalidating-at stamp, not CF's `Age`
|
|
852
|
+
// header (see CACHE_REVALIDATING_AT_HEADER). An absent/zero stamp counts
|
|
853
|
+
// as "not recent" so a dropped revalidation re-arms instead of pinning.
|
|
350
854
|
const isRevalidating =
|
|
351
|
-
status === "REVALIDATING" &&
|
|
855
|
+
status === "REVALIDATING" &&
|
|
856
|
+
revalidatingAt > 0 &&
|
|
857
|
+
now - revalidatingAt < MAX_REVALIDATION_INTERVAL * 1000;
|
|
858
|
+
|
|
859
|
+
// Single emitter for the post-header L1 outcomes. Undefined (so the event
|
|
860
|
+
// object is never allocated) when debug is off; the informational-only
|
|
861
|
+
// `age` header is read lazily inside for the same reason.
|
|
862
|
+
const debugRead = this.debug
|
|
863
|
+
? (
|
|
864
|
+
outcome: CFCacheReadDebugEvent["outcome"],
|
|
865
|
+
bodyReadMs: number,
|
|
866
|
+
shouldRevalidate?: boolean,
|
|
867
|
+
) =>
|
|
868
|
+
this.emitDebug({
|
|
869
|
+
op: "get",
|
|
870
|
+
key,
|
|
871
|
+
outcome,
|
|
872
|
+
status: response.status,
|
|
873
|
+
cacheStatus: status,
|
|
874
|
+
staleAt,
|
|
875
|
+
revalidatingAt,
|
|
876
|
+
ageHeader: response.headers.get("age"),
|
|
877
|
+
isStale,
|
|
878
|
+
isRevalidating,
|
|
879
|
+
shouldRevalidate,
|
|
880
|
+
matchMs,
|
|
881
|
+
markerMs,
|
|
882
|
+
bodyReadMs,
|
|
883
|
+
})
|
|
884
|
+
: undefined;
|
|
352
885
|
|
|
353
886
|
// Case 1: Fresh or already being revalidated - just return data
|
|
354
887
|
if (!isStale || isRevalidating) {
|
|
355
|
-
const
|
|
888
|
+
const bodyStart = Date.now();
|
|
889
|
+
const {
|
|
890
|
+
value: data,
|
|
891
|
+
errored,
|
|
892
|
+
error,
|
|
893
|
+
} = await this.readJsonWithTimeout<CachedEntryData>(response);
|
|
894
|
+
const bodyReadMs = Date.now() - bodyStart;
|
|
895
|
+
if (data === undefined) {
|
|
896
|
+
debugRead?.(errored ? "body-error" : "body-timeout", bodyReadMs);
|
|
897
|
+
// A body-ERROR (corrupt/foreign body) self-heals via healCorruptL1:
|
|
898
|
+
// report cache-corrupt, fall through to L2/KV (which overwrites the
|
|
899
|
+
// bad entry), and evict only if KV had no good copy to promote. A
|
|
900
|
+
// body-TIMEOUT is a degraded read of a likely-valid entry: leave it
|
|
901
|
+
// intact and suppress revalidation so a stalling colo cannot herd.
|
|
902
|
+
if (errored)
|
|
903
|
+
return this.healCorruptL1(cache, request, error, "get", () =>
|
|
904
|
+
this.kvGetSegment(key, { suppressRevalidate: false }),
|
|
905
|
+
);
|
|
906
|
+
return this.kvGetSegment(key, { suppressRevalidate: true });
|
|
907
|
+
}
|
|
908
|
+
debugRead?.(
|
|
909
|
+
isRevalidating ? "l1-revalidating-guarded" : "l1-fresh",
|
|
910
|
+
bodyReadMs,
|
|
911
|
+
false,
|
|
912
|
+
);
|
|
356
913
|
return { data, shouldRevalidate: false };
|
|
357
914
|
}
|
|
358
915
|
|
|
359
|
-
// Case 2: Stale and needs revalidation
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
916
|
+
// Case 2: Stale and needs revalidation.
|
|
917
|
+
// Read the body under the edge-read budget BEFORE writing the REVALIDATING
|
|
918
|
+
// marker. CF can resolve match() fast but stall the body stream; the prior
|
|
919
|
+
// approach teed the stream and awaited cache.put(b1) first, which blocked
|
|
920
|
+
// on that same stalled stream so the read budget could never fire on a
|
|
921
|
+
// stale hit. Reading first bounds the stall and lets us skip marking an
|
|
922
|
+
// entry we could not even read.
|
|
923
|
+
const bodyStart = Date.now();
|
|
924
|
+
const {
|
|
925
|
+
value: data,
|
|
926
|
+
errored,
|
|
927
|
+
error,
|
|
928
|
+
} = await this.readJsonWithTimeout<CachedEntryData>(response);
|
|
929
|
+
const bodyReadMs = Date.now() - bodyStart;
|
|
930
|
+
if (data === undefined) {
|
|
931
|
+
debugRead?.(errored ? "body-error" : "body-timeout", bodyReadMs);
|
|
932
|
+
// Heal + conditionally evict a body-error, suppress a body-timeout; see
|
|
933
|
+
// Case 1.
|
|
934
|
+
if (errored)
|
|
935
|
+
return this.healCorruptL1(
|
|
936
|
+
cache,
|
|
937
|
+
request,
|
|
938
|
+
error,
|
|
939
|
+
"get(revalidating)",
|
|
940
|
+
() => this.kvGetSegment(key, { suppressRevalidate: false }),
|
|
941
|
+
);
|
|
942
|
+
return this.kvGetSegment(key, { suppressRevalidate: true });
|
|
943
|
+
}
|
|
364
944
|
|
|
365
|
-
//
|
|
366
|
-
|
|
945
|
+
// Mark REVALIDATING so concurrent requests don't all revalidate, then
|
|
946
|
+
// return the stale data. The marker write is non-blocking and best-effort
|
|
947
|
+
// (see markRevalidating) -- it must not add latency to, or fail, the served
|
|
948
|
+
// stale read.
|
|
949
|
+
this.markRevalidating(
|
|
950
|
+
cache,
|
|
367
951
|
request,
|
|
368
|
-
|
|
952
|
+
response.headers,
|
|
953
|
+
response.status,
|
|
954
|
+
JSON.stringify(data),
|
|
369
955
|
);
|
|
370
956
|
|
|
371
|
-
|
|
957
|
+
debugRead?.("l1-stale-revalidate", bodyReadMs, true);
|
|
372
958
|
return { data, shouldRevalidate: true };
|
|
373
959
|
} catch (error) {
|
|
374
|
-
|
|
960
|
+
// reportCacheError logs and routes to onError (cache-read); the debug
|
|
961
|
+
// emit is the separate wrangler-tail signal. Keep both observability paths.
|
|
962
|
+
reportCacheError(error, "cache-read", "[CFCacheStore] get");
|
|
963
|
+
if (this.debug) this.emitDebug({ op: "get", key, outcome: "error" });
|
|
375
964
|
return null;
|
|
376
965
|
}
|
|
377
966
|
}
|
|
@@ -387,6 +976,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
387
976
|
ttl: number,
|
|
388
977
|
swr?: number,
|
|
389
978
|
): Promise<void> {
|
|
979
|
+
if (this.isReservedSegmentKey(key, "cache-write")) return;
|
|
390
980
|
try {
|
|
391
981
|
const cache = await this.getCache();
|
|
392
982
|
const request = this.keyToRequest(key);
|
|
@@ -396,32 +986,57 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
396
986
|
const totalTtl = ttl + swrWindow;
|
|
397
987
|
const staleAt = Date.now() + ttl * 1000;
|
|
398
988
|
|
|
399
|
-
|
|
989
|
+
// Stamp the tag timestamp at write time and carry it (with the tags)
|
|
990
|
+
// into both the L1 body and the KV envelope so reads can run the
|
|
991
|
+
// invalidation check.
|
|
992
|
+
const taggedAt =
|
|
993
|
+
Array.isArray(data.tags) && data.tags.length > 0
|
|
994
|
+
? Date.now()
|
|
995
|
+
: undefined;
|
|
996
|
+
const dataToStore: CachedEntryData = taggedAt
|
|
997
|
+
? { ...data, taggedAt }
|
|
998
|
+
: data;
|
|
999
|
+
|
|
1000
|
+
const body = JSON.stringify(dataToStore);
|
|
400
1001
|
const response = new Response(body, {
|
|
401
1002
|
headers: {
|
|
402
1003
|
"Content-Type": "application/json",
|
|
403
1004
|
"Cache-Control": `public, max-age=${totalTtl}`,
|
|
404
1005
|
[CACHE_STALE_AT_HEADER]: String(staleAt),
|
|
1006
|
+
// Absolute hard-expiry deadline so a stale-path re-put can recompute a
|
|
1007
|
+
// shrinking max-age instead of restarting retention (see
|
|
1008
|
+
// remainingCacheControl / CACHE_EXPIRES_AT_HEADER).
|
|
1009
|
+
[CACHE_EXPIRES_AT_HEADER]: String(staleAt + swrWindow * 1000),
|
|
405
1010
|
[CACHE_STATUS_HEADER]: "HIT",
|
|
1011
|
+
...this.tagHeaderEntries(dataToStore.tags, taggedAt),
|
|
406
1012
|
},
|
|
407
1013
|
});
|
|
408
1014
|
|
|
409
1015
|
const putPromise = cache.put(request, response);
|
|
410
1016
|
|
|
411
1017
|
if (this.waitUntil) {
|
|
412
|
-
// Non-blocking write
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
1018
|
+
// Non-blocking write. These store-level background tasks intentionally
|
|
1019
|
+
// omit the reportingAsync ctx argument: the store is a request-agnostic
|
|
1020
|
+
// singleton and this.waitUntil is the execution context's, not a single
|
|
1021
|
+
// request's, so a failure is reported console-loud only (it cannot be
|
|
1022
|
+
// attributed to one request's onError). The request-scoped tag verbs
|
|
1023
|
+
// (revalidateTag / stale-revalidation) DO thread their captured ctx.
|
|
1024
|
+
this.waitUntil(() =>
|
|
1025
|
+
reportingAsync(
|
|
1026
|
+
() => putPromise,
|
|
1027
|
+
"cache-write",
|
|
1028
|
+
"[CFCacheStore] L1 write",
|
|
1029
|
+
),
|
|
1030
|
+
);
|
|
416
1031
|
} else {
|
|
417
1032
|
// Blocking fallback
|
|
418
1033
|
await putPromise;
|
|
419
1034
|
}
|
|
420
1035
|
|
|
421
1036
|
// L2: persist to KV
|
|
422
|
-
this.kvSetSegment(key,
|
|
1037
|
+
this.kvSetSegment(key, dataToStore, staleAt, totalTtl, swrWindow);
|
|
423
1038
|
} catch (error) {
|
|
424
|
-
|
|
1039
|
+
reportCacheError(error, "cache-write", "[CFCacheStore] set");
|
|
425
1040
|
}
|
|
426
1041
|
}
|
|
427
1042
|
|
|
@@ -429,6 +1044,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
429
1044
|
* Delete a cached entry from L1 and L2.
|
|
430
1045
|
*/
|
|
431
1046
|
async delete(key: string): Promise<boolean> {
|
|
1047
|
+
if (this.isReservedSegmentKey(key, "cache-delete")) return false;
|
|
432
1048
|
try {
|
|
433
1049
|
const cache = await this.getCache();
|
|
434
1050
|
const result = await cache.delete(this.keyToRequest(key));
|
|
@@ -436,18 +1052,18 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
436
1052
|
// L2: delete from KV
|
|
437
1053
|
if (this.kv && this.waitUntil) {
|
|
438
1054
|
const kvKey = this.toKVKey(key);
|
|
439
|
-
this.waitUntil(
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
1055
|
+
this.waitUntil(() =>
|
|
1056
|
+
reportingAsync(
|
|
1057
|
+
() => this.kv!.delete(kvKey),
|
|
1058
|
+
"cache-delete",
|
|
1059
|
+
"[CFCacheStore] delete L2",
|
|
1060
|
+
),
|
|
1061
|
+
);
|
|
446
1062
|
}
|
|
447
1063
|
|
|
448
1064
|
return result;
|
|
449
1065
|
} catch (error) {
|
|
450
|
-
|
|
1066
|
+
reportCacheError(error, "cache-delete", "[CFCacheStore] delete");
|
|
451
1067
|
return false;
|
|
452
1068
|
}
|
|
453
1069
|
}
|
|
@@ -467,26 +1083,121 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
467
1083
|
try {
|
|
468
1084
|
const cache = await this.getCache();
|
|
469
1085
|
const request = this.keyToRequest(`doc:${key}`);
|
|
470
|
-
|
|
1086
|
+
// The document path is outside the debug surface (op is only get/getItem),
|
|
1087
|
+
// so the match-timeout flag is not surfaced as an event here -- though
|
|
1088
|
+
// matchWithTimeout still warns on a slow match. A miss or timeout falls
|
|
1089
|
+
// through to the KV document path and then render.
|
|
1090
|
+
const { response, error: matchError } = await this.matchWithTimeout(
|
|
1091
|
+
cache,
|
|
1092
|
+
request,
|
|
1093
|
+
);
|
|
471
1094
|
|
|
472
1095
|
if (!response || response.status !== 200) {
|
|
1096
|
+
// A transient L1 match rejection (matchError set; only ever set when
|
|
1097
|
+
// response is undefined) is surfaced as cache-read before degrading to
|
|
1098
|
+
// L2/KV -- matching get()/getItem(). A genuine miss or a non-200 hit
|
|
1099
|
+
// carries no matchError and reports nothing.
|
|
1100
|
+
if (matchError)
|
|
1101
|
+
reportCacheError(
|
|
1102
|
+
matchError,
|
|
1103
|
+
"cache-read",
|
|
1104
|
+
"[CFCacheStore] getResponse L1 match",
|
|
1105
|
+
);
|
|
473
1106
|
return this.kvGetResponse(key);
|
|
474
1107
|
}
|
|
475
1108
|
|
|
1109
|
+
// Tag invalidation check (treat invalidated entry as a miss).
|
|
1110
|
+
const tagInfo = this.readTagInfo(response.headers);
|
|
1111
|
+
if (await this.isGloballyInvalidated(tagInfo.tags, tagInfo.taggedAt)) {
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
476
1115
|
// Check staleness
|
|
477
1116
|
const staleAt = Number(response.headers.get(CACHE_STALE_AT_HEADER) || 0);
|
|
478
|
-
const
|
|
1117
|
+
const now = Date.now();
|
|
1118
|
+
const isStale = staleAt > 0 && now > staleAt;
|
|
1119
|
+
|
|
1120
|
+
// Thundering-herd guard, mirroring the segment (get) and item (getItem)
|
|
1121
|
+
// tiers. Without it, every concurrent stale reader returned
|
|
1122
|
+
// shouldRevalidate=true and document-cache.ts scheduled a fresh render for
|
|
1123
|
+
// each one. Recency comes from our own revalidating-at stamp, not CF's Age
|
|
1124
|
+
// header (see CACHE_REVALIDATING_AT_HEADER); an absent/zero stamp counts as
|
|
1125
|
+
// "not recent" so a dropped revalidation re-arms instead of pinning.
|
|
1126
|
+
const status = response.headers.get(CACHE_STATUS_HEADER);
|
|
1127
|
+
const revalidatingAt = Number(
|
|
1128
|
+
response.headers.get(CACHE_REVALIDATING_AT_HEADER) ?? "0",
|
|
1129
|
+
);
|
|
1130
|
+
const isRevalidating =
|
|
1131
|
+
status === "REVALIDATING" &&
|
|
1132
|
+
revalidatingAt > 0 &&
|
|
1133
|
+
now - revalidatingAt < MAX_REVALIDATION_INTERVAL * 1000;
|
|
1134
|
+
|
|
1135
|
+
// L1 document bodies are streamed through verbatim - unlike the segment/
|
|
1136
|
+
// item tiers (which JSON-parse and so structurally detect corruption) and
|
|
1137
|
+
// the KV doc tier (validated in kvGetResponse, KV being the real partial-
|
|
1138
|
+
// read vector). Integrity here relies on the Cache API: cache.put stores a
|
|
1139
|
+
// response atomically or fails, so a truncated body is not served back. We
|
|
1140
|
+
// deliberately do NOT buffer+hash the body to re-verify it: that would
|
|
1141
|
+
// defeat streaming the document and add a full read to every cache hit.
|
|
1142
|
+
|
|
1143
|
+
if (isStale && !isRevalidating) {
|
|
1144
|
+
// First stale reader within the window: mark REVALIDATING (non-blocking,
|
|
1145
|
+
// best-effort) so concurrent readers below see the guard and suppress,
|
|
1146
|
+
// then return shouldRevalidate=true so this caller revalidates. Clone the
|
|
1147
|
+
// matched response for the marker since its original body must still
|
|
1148
|
+
// stream to the client.
|
|
1149
|
+
this.markResponseRevalidating(cache, request, response.clone());
|
|
1150
|
+
return {
|
|
1151
|
+
response: this.toClientResponse(response),
|
|
1152
|
+
shouldRevalidate: true,
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
479
1155
|
|
|
1156
|
+
// Fresh, or stale-but-already-REVALIDATING: serve without scheduling a
|
|
1157
|
+
// (re-)revalidation. A recent marker already has a render in flight.
|
|
480
1158
|
return {
|
|
481
|
-
response,
|
|
482
|
-
shouldRevalidate:
|
|
1159
|
+
response: this.toClientResponse(response),
|
|
1160
|
+
shouldRevalidate: false,
|
|
483
1161
|
};
|
|
484
1162
|
} catch (error) {
|
|
485
|
-
|
|
1163
|
+
reportCacheError(error, "cache-read", "[CFCacheStore] getResponse");
|
|
486
1164
|
return null;
|
|
487
1165
|
}
|
|
488
1166
|
}
|
|
489
1167
|
|
|
1168
|
+
/**
|
|
1169
|
+
* Strip internal edge headers and restore the author's Cache-Control before a
|
|
1170
|
+
* cached document Response is served to a client. L1 entries carry the
|
|
1171
|
+
* internal staleness/status headers and a rewritten Cache-Control; none of
|
|
1172
|
+
* those should reach the browser or an upstream CDN.
|
|
1173
|
+
*/
|
|
1174
|
+
private toClientResponse(response: Response): Response {
|
|
1175
|
+
const headers = new Headers(response.headers);
|
|
1176
|
+
const originalCacheControl = headers.get(CACHE_ORIG_CC_HEADER);
|
|
1177
|
+
if (originalCacheControl !== null) {
|
|
1178
|
+
headers.set("Cache-Control", originalCacheControl);
|
|
1179
|
+
} else {
|
|
1180
|
+
headers.delete("Cache-Control");
|
|
1181
|
+
}
|
|
1182
|
+
headers.delete(CACHE_ORIG_CC_HEADER);
|
|
1183
|
+
headers.delete(CACHE_STALE_AT_HEADER);
|
|
1184
|
+
headers.delete(CACHE_STATUS_HEADER);
|
|
1185
|
+
headers.delete(CACHE_TAGS_HEADER);
|
|
1186
|
+
headers.delete(CACHE_TAGGED_AT_HEADER);
|
|
1187
|
+
// Internal stale-path bookkeeping (hard-expiry deadline + REVALIDATING
|
|
1188
|
+
// stamp). Carried on doc L1 entries for the herd guard; never serve them.
|
|
1189
|
+
headers.delete(CACHE_EXPIRES_AT_HEADER);
|
|
1190
|
+
headers.delete(CACHE_REVALIDATING_AT_HEADER);
|
|
1191
|
+
// Finding #3 (read side): strip per-client signals a pre-fix or
|
|
1192
|
+
// pinned-version L1 entry may carry. See the read-side note in the design doc.
|
|
1193
|
+
stripPerClientSignals(headers);
|
|
1194
|
+
return new Response(response.body, {
|
|
1195
|
+
status: response.status,
|
|
1196
|
+
statusText: response.statusText,
|
|
1197
|
+
headers,
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
|
|
490
1201
|
/**
|
|
491
1202
|
* Store a Response with TTL and optional SWR window (for document-level caching).
|
|
492
1203
|
* When KV is configured, also persists to L2.
|
|
@@ -496,6 +1207,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
496
1207
|
response: Response,
|
|
497
1208
|
ttl: number,
|
|
498
1209
|
swr?: number,
|
|
1210
|
+
tags?: string[],
|
|
499
1211
|
): Promise<void> {
|
|
500
1212
|
try {
|
|
501
1213
|
const cache = await this.getCache();
|
|
@@ -505,6 +1217,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
505
1217
|
const swrWindow = resolveSwrWindow(swr, this.defaults);
|
|
506
1218
|
const totalTtl = ttl + swrWindow;
|
|
507
1219
|
const staleAt = Date.now() + ttl * 1000;
|
|
1220
|
+
const taggedAt =
|
|
1221
|
+
Array.isArray(tags) && tags.length > 0 ? Date.now() : undefined;
|
|
508
1222
|
|
|
509
1223
|
// Clone body for potential KV write before consuming it for L1
|
|
510
1224
|
const [l1Body, kvBody] = this.kv
|
|
@@ -513,10 +1227,27 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
513
1227
|
: [null, null]
|
|
514
1228
|
: [response.body, null];
|
|
515
1229
|
|
|
516
|
-
// Clone and add cache headers
|
|
1230
|
+
// Clone and add cache headers. The author's Cache-Control is stashed and
|
|
1231
|
+
// replaced with a long max-age so the CF Cache API holds the entry across
|
|
1232
|
+
// the SWR window; getResponse restores the original before serving.
|
|
517
1233
|
const headers = new Headers(response.headers);
|
|
1234
|
+
// Finding #3: never persist a per-client signal in the shared L1 entry
|
|
1235
|
+
// (the platform's Set-Cookie rejection is unverified and ignores the
|
|
1236
|
+
// directive anyway). See stripPerClientSignals.
|
|
1237
|
+
stripPerClientSignals(headers);
|
|
1238
|
+
const originalCacheControl = response.headers.get("Cache-Control");
|
|
1239
|
+
if (originalCacheControl !== null) {
|
|
1240
|
+
headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
|
|
1241
|
+
}
|
|
518
1242
|
headers.set("Cache-Control", `public, max-age=${totalTtl}`);
|
|
519
1243
|
headers.set(CACHE_STALE_AT_HEADER, String(staleAt));
|
|
1244
|
+
// Absolute hard-expiry deadline so a stale-path REVALIDATING re-put can
|
|
1245
|
+
// recompute a shrinking max-age (remainingCacheControl) instead of
|
|
1246
|
+
// restarting retention. Mirrors set()/setItem(). Stripped by
|
|
1247
|
+
// toClientResponse before serving.
|
|
1248
|
+
headers.set(CACHE_EXPIRES_AT_HEADER, String(staleAt + swrWindow * 1000));
|
|
1249
|
+
// Internal tag headers (stripped by toClientResponse before serving).
|
|
1250
|
+
this.setTagHeaders(headers, tags, taggedAt);
|
|
520
1251
|
|
|
521
1252
|
const toCache = new Response(l1Body, {
|
|
522
1253
|
status: response.status,
|
|
@@ -528,9 +1259,13 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
528
1259
|
|
|
529
1260
|
if (this.waitUntil) {
|
|
530
1261
|
// Non-blocking write
|
|
531
|
-
this.waitUntil(
|
|
532
|
-
|
|
533
|
-
|
|
1262
|
+
this.waitUntil(() =>
|
|
1263
|
+
reportingAsync(
|
|
1264
|
+
() => putPromise,
|
|
1265
|
+
"cache-write",
|
|
1266
|
+
"[CFCacheStore] L1 write",
|
|
1267
|
+
),
|
|
1268
|
+
);
|
|
534
1269
|
} else {
|
|
535
1270
|
// Blocking fallback
|
|
536
1271
|
await putPromise;
|
|
@@ -538,35 +1273,43 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
538
1273
|
|
|
539
1274
|
// L2: persist to KV (KV requires expirationTtl >= 60s)
|
|
540
1275
|
if (this.kv && this.waitUntil && totalTtl >= 60) {
|
|
541
|
-
const kvKey = this.
|
|
1276
|
+
const kvKey = this.toDocKVKey(key);
|
|
1277
|
+
// Finding #3: never persist a per-client signal in the KV envelope.
|
|
542
1278
|
const headersArray: [string, string][] = [];
|
|
543
|
-
response.headers.forEach((v, k) =>
|
|
1279
|
+
response.headers.forEach((v, k) => {
|
|
1280
|
+
if (isPerClientSignalHeader(k)) return;
|
|
1281
|
+
headersArray.push([k, v]);
|
|
1282
|
+
});
|
|
544
1283
|
// Read body as ArrayBuffer and encode to base64 to preserve binary payloads
|
|
545
1284
|
const bodyBuf = kvBody
|
|
546
1285
|
? await new Response(kvBody).arrayBuffer()
|
|
547
1286
|
: new ArrayBuffer(0);
|
|
548
1287
|
const bodyBase64 = bufferToBase64(bodyBuf);
|
|
549
1288
|
|
|
550
|
-
this.waitUntil(
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
1289
|
+
this.waitUntil(() =>
|
|
1290
|
+
reportingAsync(
|
|
1291
|
+
() => {
|
|
1292
|
+
const envelope: KVResponseEnvelope = {
|
|
1293
|
+
b: bodyBase64,
|
|
1294
|
+
st: response.status,
|
|
1295
|
+
stx: response.statusText,
|
|
1296
|
+
hd: headersArray,
|
|
1297
|
+
s: staleAt,
|
|
1298
|
+
e: staleAt + swrWindow * 1000,
|
|
1299
|
+
t: tags,
|
|
1300
|
+
ta: taggedAt,
|
|
1301
|
+
};
|
|
1302
|
+
return this.kv!.put(kvKey, JSON.stringify(envelope), {
|
|
1303
|
+
expirationTtl: totalTtl,
|
|
1304
|
+
});
|
|
1305
|
+
},
|
|
1306
|
+
"cache-write",
|
|
1307
|
+
"[CFCacheStore] kvPutResponse",
|
|
1308
|
+
),
|
|
1309
|
+
);
|
|
567
1310
|
}
|
|
568
1311
|
} catch (error) {
|
|
569
|
-
|
|
1312
|
+
reportCacheError(error, "cache-write", "[CFCacheStore] putResponse");
|
|
570
1313
|
}
|
|
571
1314
|
}
|
|
572
1315
|
|
|
@@ -583,48 +1326,173 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
583
1326
|
try {
|
|
584
1327
|
const cache = await this.getCache();
|
|
585
1328
|
const request = this.keyToRequest(`fn:${key}`);
|
|
586
|
-
const
|
|
1329
|
+
const matchStart = Date.now();
|
|
1330
|
+
const {
|
|
1331
|
+
response,
|
|
1332
|
+
timedOut,
|
|
1333
|
+
error: matchError,
|
|
1334
|
+
} = await this.matchWithTimeout(cache, request);
|
|
1335
|
+
const matchMs = Date.now() - matchStart;
|
|
1336
|
+
|
|
1337
|
+
if (!response) {
|
|
1338
|
+
// Transient match error reported cache-read; still degrades to L2/KV.
|
|
1339
|
+
if (matchError)
|
|
1340
|
+
reportCacheError(
|
|
1341
|
+
matchError,
|
|
1342
|
+
"cache-read",
|
|
1343
|
+
"[CFCacheStore] getItem L1 match",
|
|
1344
|
+
);
|
|
1345
|
+
if (this.debug)
|
|
1346
|
+
this.emitDebug({
|
|
1347
|
+
op: "getItem",
|
|
1348
|
+
key,
|
|
1349
|
+
// match-error (rejection) vs l1-miss (absence); see get().
|
|
1350
|
+
outcome: matchError
|
|
1351
|
+
? "match-error"
|
|
1352
|
+
: timedOut
|
|
1353
|
+
? "match-timeout"
|
|
1354
|
+
: "l1-miss",
|
|
1355
|
+
matchMs,
|
|
1356
|
+
});
|
|
1357
|
+
return this.kvGetItem(key);
|
|
1358
|
+
}
|
|
587
1359
|
|
|
588
|
-
|
|
1360
|
+
// Non-200 entry is not a valid cached function result; treat as a miss.
|
|
1361
|
+
if (response.status !== 200) {
|
|
1362
|
+
if (this.debug)
|
|
1363
|
+
this.emitDebug({
|
|
1364
|
+
op: "getItem",
|
|
1365
|
+
key,
|
|
1366
|
+
outcome: "non-200",
|
|
1367
|
+
status: response.status,
|
|
1368
|
+
matchMs,
|
|
1369
|
+
});
|
|
1370
|
+
// Degraded fall-through: suppress revalidation so a broken L1 entry hit
|
|
1371
|
+
// concurrently serves KV-stale instead of spawning a herd (see get()).
|
|
1372
|
+
return this.kvGetItem(key, { suppressRevalidate: true });
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// Tag invalidation check (treat invalidated entry as a miss). Measure the
|
|
1376
|
+
// marker-resolution tail only under debug (see get()).
|
|
1377
|
+
const tagInfo = this.readTagInfo(response.headers);
|
|
1378
|
+
const markerStart = this.debug ? Date.now() : 0;
|
|
1379
|
+
const invalidated = await this.isGloballyInvalidated(
|
|
1380
|
+
tagInfo.tags,
|
|
1381
|
+
tagInfo.taggedAt,
|
|
1382
|
+
);
|
|
1383
|
+
const markerMs = this.debug ? Date.now() - markerStart : undefined;
|
|
1384
|
+
if (invalidated) {
|
|
1385
|
+
if (this.debug)
|
|
1386
|
+
this.emitDebug({
|
|
1387
|
+
op: "getItem",
|
|
1388
|
+
key,
|
|
1389
|
+
outcome: "tag-invalidated",
|
|
1390
|
+
status: response.status,
|
|
1391
|
+
matchMs,
|
|
1392
|
+
markerMs,
|
|
1393
|
+
});
|
|
1394
|
+
return null;
|
|
1395
|
+
}
|
|
589
1396
|
|
|
590
1397
|
const staleAt = Number(
|
|
591
1398
|
response.headers.get(CACHE_STALE_AT_HEADER) ?? "0",
|
|
592
1399
|
);
|
|
593
1400
|
const status = response.headers.get(CACHE_STATUS_HEADER);
|
|
594
|
-
const
|
|
1401
|
+
const revalidatingAt = Number(
|
|
1402
|
+
response.headers.get(CACHE_REVALIDATING_AT_HEADER) ?? "0",
|
|
1403
|
+
);
|
|
595
1404
|
|
|
596
|
-
const
|
|
1405
|
+
const now = Date.now();
|
|
1406
|
+
const isStale = staleAt > 0 && now > staleAt;
|
|
1407
|
+
// Recency from our explicit stamp, not CF's `Age` header (see get()).
|
|
597
1408
|
const isRevalidating =
|
|
598
|
-
status === "REVALIDATING" &&
|
|
599
|
-
|
|
600
|
-
|
|
1409
|
+
status === "REVALIDATING" &&
|
|
1410
|
+
revalidatingAt > 0 &&
|
|
1411
|
+
now - revalidatingAt < MAX_REVALIDATION_INTERVAL * 1000;
|
|
1412
|
+
|
|
1413
|
+
// Single emitter for the post-header L1 outcomes (see get()). Undefined
|
|
1414
|
+
// when debug is off, so the event object is never allocated on the hot
|
|
1415
|
+
// path; the informational-only `age` header is read lazily inside.
|
|
1416
|
+
const debugRead = this.debug
|
|
1417
|
+
? (
|
|
1418
|
+
outcome: CFCacheReadDebugEvent["outcome"],
|
|
1419
|
+
bodyReadMs: number,
|
|
1420
|
+
shouldRevalidate?: boolean,
|
|
1421
|
+
) =>
|
|
1422
|
+
this.emitDebug({
|
|
1423
|
+
op: "getItem",
|
|
1424
|
+
key,
|
|
1425
|
+
outcome,
|
|
1426
|
+
status: response.status,
|
|
1427
|
+
cacheStatus: status,
|
|
1428
|
+
staleAt,
|
|
1429
|
+
revalidatingAt,
|
|
1430
|
+
ageHeader: response.headers.get("age"),
|
|
1431
|
+
isStale,
|
|
1432
|
+
isRevalidating,
|
|
1433
|
+
shouldRevalidate,
|
|
1434
|
+
matchMs,
|
|
1435
|
+
markerMs,
|
|
1436
|
+
bodyReadMs,
|
|
1437
|
+
})
|
|
1438
|
+
: undefined;
|
|
1439
|
+
|
|
1440
|
+
const bodyStart = Date.now();
|
|
1441
|
+
const {
|
|
1442
|
+
value: data,
|
|
1443
|
+
errored,
|
|
1444
|
+
error,
|
|
1445
|
+
} = await this.readJsonWithTimeout<{
|
|
601
1446
|
value: string;
|
|
602
|
-
handles?:
|
|
603
|
-
};
|
|
1447
|
+
handles?: string;
|
|
1448
|
+
}>(response);
|
|
1449
|
+
const bodyReadMs = Date.now() - bodyStart;
|
|
1450
|
+
if (data === undefined) {
|
|
1451
|
+
debugRead?.(errored ? "body-error" : "body-timeout", bodyReadMs);
|
|
1452
|
+
// Heal + conditionally evict a body-error, suppress a body-timeout; see
|
|
1453
|
+
// get().
|
|
1454
|
+
if (errored)
|
|
1455
|
+
return this.healCorruptL1(cache, request, error, "getItem", () =>
|
|
1456
|
+
this.kvGetItem(key, { suppressRevalidate: false }),
|
|
1457
|
+
);
|
|
1458
|
+
return this.kvGetItem(key, { suppressRevalidate: true });
|
|
1459
|
+
}
|
|
604
1460
|
|
|
605
1461
|
if (!isStale || isRevalidating) {
|
|
1462
|
+
debugRead?.(
|
|
1463
|
+
isRevalidating ? "l1-revalidating-guarded" : "l1-fresh",
|
|
1464
|
+
bodyReadMs,
|
|
1465
|
+
false,
|
|
1466
|
+
);
|
|
606
1467
|
return {
|
|
607
1468
|
value: data.value,
|
|
608
1469
|
handles: data.handles,
|
|
609
1470
|
shouldRevalidate: false,
|
|
1471
|
+
tags: tagInfo.tags,
|
|
610
1472
|
};
|
|
611
1473
|
}
|
|
612
1474
|
|
|
613
|
-
// Stale and needs revalidation
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
1475
|
+
// Stale and needs revalidation -- mark REVALIDATING (non-blocking,
|
|
1476
|
+
// best-effort, remaining-ttl) and return the stale value. See get() /
|
|
1477
|
+
// markRevalidating for the full rationale.
|
|
1478
|
+
this.markRevalidating(
|
|
1479
|
+
cache,
|
|
617
1480
|
request,
|
|
618
|
-
|
|
1481
|
+
response.headers,
|
|
1482
|
+
200,
|
|
1483
|
+
JSON.stringify(data),
|
|
619
1484
|
);
|
|
620
1485
|
|
|
1486
|
+
debugRead?.("l1-stale-revalidate", bodyReadMs, true);
|
|
621
1487
|
return {
|
|
622
1488
|
value: data.value,
|
|
623
1489
|
handles: data.handles,
|
|
624
1490
|
shouldRevalidate: true,
|
|
1491
|
+
tags: tagInfo.tags,
|
|
625
1492
|
};
|
|
626
1493
|
} catch (error) {
|
|
627
|
-
|
|
1494
|
+
reportCacheError(error, "cache-read", "[CFCacheStore] getItem");
|
|
1495
|
+
if (this.debug) this.emitDebug({ op: "getItem", key, outcome: "error" });
|
|
628
1496
|
return null;
|
|
629
1497
|
}
|
|
630
1498
|
}
|
|
@@ -647,22 +1515,33 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
647
1515
|
const totalTtl = ttl + swrWindow;
|
|
648
1516
|
const staleAt = Date.now() + ttl * 1000;
|
|
649
1517
|
|
|
1518
|
+
const tags = options?.tags;
|
|
1519
|
+
const taggedAt =
|
|
1520
|
+
Array.isArray(tags) && tags.length > 0 ? Date.now() : undefined;
|
|
1521
|
+
|
|
650
1522
|
const body = JSON.stringify({ value, handles: options?.handles });
|
|
651
1523
|
const response = new Response(body, {
|
|
652
1524
|
headers: {
|
|
653
1525
|
"Content-Type": "application/json",
|
|
654
1526
|
"Cache-Control": `public, max-age=${totalTtl}`,
|
|
655
1527
|
[CACHE_STALE_AT_HEADER]: String(staleAt),
|
|
1528
|
+
// Absolute hard-expiry deadline; see set() / remainingCacheControl.
|
|
1529
|
+
[CACHE_EXPIRES_AT_HEADER]: String(staleAt + swrWindow * 1000),
|
|
656
1530
|
[CACHE_STATUS_HEADER]: "HIT",
|
|
1531
|
+
...this.tagHeaderEntries(tags, taggedAt),
|
|
657
1532
|
},
|
|
658
1533
|
});
|
|
659
1534
|
|
|
660
1535
|
const putPromise = cache.put(request, response);
|
|
661
1536
|
|
|
662
1537
|
if (this.waitUntil) {
|
|
663
|
-
this.waitUntil(
|
|
664
|
-
|
|
665
|
-
|
|
1538
|
+
this.waitUntil(() =>
|
|
1539
|
+
reportingAsync(
|
|
1540
|
+
() => putPromise,
|
|
1541
|
+
"cache-write",
|
|
1542
|
+
"[CFCacheStore] L1 write",
|
|
1543
|
+
),
|
|
1544
|
+
);
|
|
666
1545
|
} else {
|
|
667
1546
|
await putPromise;
|
|
668
1547
|
}
|
|
@@ -670,24 +1549,28 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
670
1549
|
// L2: persist to KV (KV requires expirationTtl >= 60s)
|
|
671
1550
|
if (this.kv && this.waitUntil && totalTtl >= 60) {
|
|
672
1551
|
const kvKey = this.toKVKey(`fn:${key}`);
|
|
673
|
-
this.waitUntil(
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
1552
|
+
this.waitUntil(() =>
|
|
1553
|
+
reportingAsync(
|
|
1554
|
+
() => {
|
|
1555
|
+
const envelope: KVItemEnvelope = {
|
|
1556
|
+
v: value,
|
|
1557
|
+
h: options?.handles,
|
|
1558
|
+
s: staleAt,
|
|
1559
|
+
e: staleAt + swrWindow * 1000,
|
|
1560
|
+
t: tags,
|
|
1561
|
+
ta: taggedAt,
|
|
1562
|
+
};
|
|
1563
|
+
return this.kv!.put(kvKey, JSON.stringify(envelope), {
|
|
1564
|
+
expirationTtl: totalTtl,
|
|
1565
|
+
});
|
|
1566
|
+
},
|
|
1567
|
+
"cache-write",
|
|
1568
|
+
"[CFCacheStore] kvSetItem",
|
|
1569
|
+
),
|
|
1570
|
+
);
|
|
688
1571
|
}
|
|
689
1572
|
} catch (error) {
|
|
690
|
-
|
|
1573
|
+
reportCacheError(error, "cache-write", "[CFCacheStore] setItem");
|
|
691
1574
|
}
|
|
692
1575
|
}
|
|
693
1576
|
|
|
@@ -704,7 +1587,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
704
1587
|
const encodedKey = encodeURIComponent(key);
|
|
705
1588
|
// Include version in URL path to invalidate cache when version changes
|
|
706
1589
|
const versionPath = this.version ? `v/${this.version}/` : "";
|
|
707
|
-
return new Request(`${this.
|
|
1590
|
+
return new Request(`${this.resolveBaseUrl()}${versionPath}${encodedKey}`, {
|
|
708
1591
|
method: "GET",
|
|
709
1592
|
});
|
|
710
1593
|
}
|
|
@@ -719,6 +1602,570 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
719
1602
|
return `${versionPath}${key}`;
|
|
720
1603
|
}
|
|
721
1604
|
|
|
1605
|
+
/**
|
|
1606
|
+
* Host token for the current request, used to namespace the document KV key.
|
|
1607
|
+
* Derived from the same resolveBaseUrl() that namespaces the L1 (Cache API)
|
|
1608
|
+
* tier, so a doc entry's KV twin lands under the identical host bucket.
|
|
1609
|
+
* Falls back to "_" if the base URL cannot be parsed (it always carries a
|
|
1610
|
+
* trailing-slash origin, so parsing succeeds in practice).
|
|
1611
|
+
* @internal
|
|
1612
|
+
*/
|
|
1613
|
+
private docKVHost(): string {
|
|
1614
|
+
try {
|
|
1615
|
+
return new URL(this.resolveBaseUrl()).host || "_";
|
|
1616
|
+
} catch {
|
|
1617
|
+
return "_";
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
/**
|
|
1622
|
+
* Convert a document key to its host-namespaced KV key. The L1 tier already
|
|
1623
|
+
* namespaces document entries by host via keyToRequest/resolveBaseUrl, but the
|
|
1624
|
+
* KV fallback keyed only on `doc:{key}`, so two hosts serving the same path
|
|
1625
|
+
* could collide on the KV tier (one host serving another's cached document).
|
|
1626
|
+
* Prefixing the host closes that cross-host collision. Deterministic per
|
|
1627
|
+
* (host, key). Segment/fn/tag-marker KV keys keep toKVKey unchanged: tag
|
|
1628
|
+
* markers are intentionally global (invalidation must cross hosts), and the
|
|
1629
|
+
* document tier is the one with a request-host context here.
|
|
1630
|
+
* @internal
|
|
1631
|
+
*/
|
|
1632
|
+
private toDocKVKey(key: string): string {
|
|
1633
|
+
return this.toKVKey(`h/${this.docKVHost()}/doc:${key}`);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
/**
|
|
1637
|
+
* Best-effort delete of a single KV key, reporting (not swallowing) a delete
|
|
1638
|
+
* failure as cache-delete. Used by the corrupt-entry self-heal paths.
|
|
1639
|
+
* @internal
|
|
1640
|
+
*/
|
|
1641
|
+
private async evictKvKey(kvKey: string, label: string): Promise<void> {
|
|
1642
|
+
try {
|
|
1643
|
+
await this.kv!.delete(kvKey);
|
|
1644
|
+
} catch (error) {
|
|
1645
|
+
reportCacheError(
|
|
1646
|
+
error,
|
|
1647
|
+
"cache-delete",
|
|
1648
|
+
`[CFCacheStore] ${label}: evict failed`,
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
/**
|
|
1654
|
+
* Schedule a corrupt-entry KV eviction as a NON-BLOCKING background task
|
|
1655
|
+
* (waitUntil) instead of awaiting it on the request path. The corrupt read has
|
|
1656
|
+
* already resolved to a miss; awaiting an unbounded kv.delete here would re-add
|
|
1657
|
+
* exactly the multi-second stall the read budgets exist to prevent when the KV
|
|
1658
|
+
* namespace is degraded. evictKvKey never rejects (it reports its own failure),
|
|
1659
|
+
* so the fire-and-forget fallback is safe when no waitUntil is available.
|
|
1660
|
+
* @internal
|
|
1661
|
+
*/
|
|
1662
|
+
private scheduleKvEvict(kvKey: string, label: string): void {
|
|
1663
|
+
const evict = (): Promise<void> => this.evictKvKey(kvKey, label);
|
|
1664
|
+
if (this.waitUntil) this.waitUntil(evict);
|
|
1665
|
+
else void evict();
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
/**
|
|
1669
|
+
* KV-get a JSON envelope, EVICTING the key only when it is genuinely corrupt.
|
|
1670
|
+
*
|
|
1671
|
+
* Reads as { type: "text" }, NOT { type: "json" }, on purpose: the "json" form
|
|
1672
|
+
* fuses the network read and the JSON parse, so a transient KV outage (5xx/429/
|
|
1673
|
+
* network blip) is indistinguishable from a malformed body and would delete a
|
|
1674
|
+
* still-good cross-colo entry - a self-inflicted miss storm. Reading text lets a
|
|
1675
|
+
* transient read error propagate to the caller's outer catch (reported
|
|
1676
|
+
* cache-read, the entry left intact); only a JSON.parse failure on a body that
|
|
1677
|
+
* WAS successfully read - or an envelope that parses but fails `validate`
|
|
1678
|
+
* (fields missing from a truncated write) - is true corruption that evicts +
|
|
1679
|
+
* reports cache-corrupt. A MISSING key (kv.get -> null) is a normal miss.
|
|
1680
|
+
* @internal
|
|
1681
|
+
*/
|
|
1682
|
+
private async kvGetOrEvict<T>(
|
|
1683
|
+
kvKey: string,
|
|
1684
|
+
validate: (envelope: T) => boolean,
|
|
1685
|
+
label: string,
|
|
1686
|
+
): Promise<{ value: T | null; timedOut: boolean }> {
|
|
1687
|
+
// Bound the read with the KV latency budget (inherited from #558) so a
|
|
1688
|
+
// degraded namespace cannot pin the request. readWithTimeout reports
|
|
1689
|
+
// timedOut on budget expiry; a transient read REJECTION (5xx/429/network)
|
|
1690
|
+
// instead propagates out to the caller's outer catch (reported cache-read,
|
|
1691
|
+
// the entry left intact) -- deliberately NOT caught as corruption.
|
|
1692
|
+
const { value: raw, timedOut } = await this.readWithTimeout<unknown>(
|
|
1693
|
+
() => this.kv!.get(kvKey, { type: "text" }),
|
|
1694
|
+
this.kvReadTimeoutMs,
|
|
1695
|
+
"KV read",
|
|
1696
|
+
);
|
|
1697
|
+
if (timedOut) return { value: null, timedOut: true };
|
|
1698
|
+
if (raw == null) return { value: null, timedOut: false }; // missing = miss
|
|
1699
|
+
|
|
1700
|
+
// Real CF KV with { type: "text" } returns a string: parse + structurally
|
|
1701
|
+
// validate it; a parse/validate failure on a successfully-read body is the
|
|
1702
|
+
// only true corruption (evict + cache-corrupt). A KV binding that already
|
|
1703
|
+
// returns a parsed object (some shims/tests) is used as-is.
|
|
1704
|
+
let envelope: T;
|
|
1705
|
+
if (typeof raw === "string") {
|
|
1706
|
+
try {
|
|
1707
|
+
envelope = JSON.parse(raw) as T;
|
|
1708
|
+
} catch (error) {
|
|
1709
|
+
reportCacheError(
|
|
1710
|
+
error,
|
|
1711
|
+
"cache-corrupt",
|
|
1712
|
+
`[CFCacheStore] ${label}: corrupt JSON in KV, evicting`,
|
|
1713
|
+
);
|
|
1714
|
+
this.scheduleKvEvict(kvKey, label);
|
|
1715
|
+
return { value: null, timedOut: false };
|
|
1716
|
+
}
|
|
1717
|
+
} else {
|
|
1718
|
+
envelope = raw as T;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// A body that parses to null or a primitive ('null', '42', 'true', '"x"')
|
|
1722
|
+
// is not a valid envelope. Guard it BEFORE validate(): the property-reading
|
|
1723
|
+
// validators throw on a null/primitive rather than returning false, which
|
|
1724
|
+
// would escape to the caller's outer catch as a transient cache-read and
|
|
1725
|
+
// leave the bad key un-evicted (re-failing every read until its KV TTL). The
|
|
1726
|
+
// typeof check short-circuits validate() so it only ever runs on an object.
|
|
1727
|
+
if (
|
|
1728
|
+
envelope == null ||
|
|
1729
|
+
typeof envelope !== "object" ||
|
|
1730
|
+
!validate(envelope)
|
|
1731
|
+
) {
|
|
1732
|
+
reportCacheError(
|
|
1733
|
+
new Error("malformed/partial KV envelope"),
|
|
1734
|
+
"cache-corrupt",
|
|
1735
|
+
`[CFCacheStore] ${label}: malformed envelope, evicting`,
|
|
1736
|
+
);
|
|
1737
|
+
this.scheduleKvEvict(kvKey, label);
|
|
1738
|
+
return { value: null, timedOut: false };
|
|
1739
|
+
}
|
|
1740
|
+
return { value: envelope, timedOut: false };
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// ============================================================================
|
|
1744
|
+
// Tag Invalidation (single-store: markers live in this.kv)
|
|
1745
|
+
// ============================================================================
|
|
1746
|
+
|
|
1747
|
+
/** KV key for a tag's invalidation marker. */
|
|
1748
|
+
private tagMarkerKey(tag: string): string {
|
|
1749
|
+
return this.toKVKey(`${TAG_MARKER_PREFIX}${tag}`);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
/**
|
|
1753
|
+
* Header entries carrying an entry's tags (JSON-encoded, comma-safe) and the
|
|
1754
|
+
* timestamp they were attached. Returns an empty object when there are no
|
|
1755
|
+
* tags so untagged entries stay header-free and skip the invalidation check.
|
|
1756
|
+
*/
|
|
1757
|
+
private tagHeaderEntries(
|
|
1758
|
+
tags: string[] | undefined,
|
|
1759
|
+
taggedAt: number | undefined,
|
|
1760
|
+
): Record<string, string> {
|
|
1761
|
+
if (!Array.isArray(tags) || tags.length === 0 || !taggedAt) return {};
|
|
1762
|
+
return {
|
|
1763
|
+
// encodeURIComponent so the value is pure ASCII: HTTP header values are
|
|
1764
|
+
// ByteStrings, but JSON.stringify leaves codepoints > U+00FF (emoji/CJK)
|
|
1765
|
+
// verbatim, which makes new Response({ headers }) throw and the outer
|
|
1766
|
+
// try/catch silently drop the whole entry from cache. Decoded in
|
|
1767
|
+
// readTagInfo. The L1 marker Cache-Tag path encodes for the same reason.
|
|
1768
|
+
[CACHE_TAGS_HEADER]: encodeURIComponent(JSON.stringify(tags)),
|
|
1769
|
+
[CACHE_TAGGED_AT_HEADER]: String(taggedAt),
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
/**
|
|
1774
|
+
* Merge the internal tag headers onto an existing Headers instance. The
|
|
1775
|
+
* from-scratch paths spread tagHeaderEntries() into an object-literal init;
|
|
1776
|
+
* the document put/promote paths build a Headers first, so they .set() each
|
|
1777
|
+
* entry instead.
|
|
1778
|
+
*/
|
|
1779
|
+
private setTagHeaders(
|
|
1780
|
+
headers: Headers,
|
|
1781
|
+
tags: string[] | undefined,
|
|
1782
|
+
taggedAt: number | undefined,
|
|
1783
|
+
): void {
|
|
1784
|
+
for (const [name, value] of Object.entries(
|
|
1785
|
+
this.tagHeaderEntries(tags, taggedAt),
|
|
1786
|
+
)) {
|
|
1787
|
+
headers.set(name, value);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
/** Read an entry's tags/taggedAt back from its headers. */
|
|
1792
|
+
private readTagInfo(headers: Headers): {
|
|
1793
|
+
tags?: string[];
|
|
1794
|
+
taggedAt?: number;
|
|
1795
|
+
} {
|
|
1796
|
+
const rawTags = headers.get(CACHE_TAGS_HEADER);
|
|
1797
|
+
const rawTaggedAt = headers.get(CACHE_TAGGED_AT_HEADER);
|
|
1798
|
+
if (!rawTags || !rawTaggedAt) return {};
|
|
1799
|
+
try {
|
|
1800
|
+
const taggedAt = Number(rawTaggedAt);
|
|
1801
|
+
// A corrupt/non-numeric tagged-at header yields NaN. isGloballyInvalidated
|
|
1802
|
+
// short-circuits on a falsy taggedAt (NaN is falsy), so returning
|
|
1803
|
+
// { taggedAt: NaN } would make the entry permanently NON-invalidatable -
|
|
1804
|
+
// a revalidateTag could never evict it. Treat a non-finite stamp the same
|
|
1805
|
+
// as the missing-header case (untagged): drop both tags and taggedAt so the
|
|
1806
|
+
// entry is re-rendered/re-tagged rather than silently un-invalidatable.
|
|
1807
|
+
if (!Number.isFinite(taggedAt)) return {};
|
|
1808
|
+
return {
|
|
1809
|
+
tags: JSON.parse(decodeURIComponent(rawTags)) as string[],
|
|
1810
|
+
taggedAt,
|
|
1811
|
+
};
|
|
1812
|
+
} catch {
|
|
1813
|
+
return {};
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/**
|
|
1818
|
+
* Whether an entry tagged at `taggedAt` with `tags` has been invalidated since.
|
|
1819
|
+
* Reads the per-tag invalidation markers from KV and returns true if any tag's
|
|
1820
|
+
* latest invalidation is at or after taggedAt (>= so a same-millisecond
|
|
1821
|
+
* invalidate wins, favouring freshness over staleness). Fails open: KV errors
|
|
1822
|
+
* never turn a hit into a wrongful miss-storm beyond this single read.
|
|
1823
|
+
*/
|
|
1824
|
+
private async isGloballyInvalidated(
|
|
1825
|
+
tags: string[] | undefined,
|
|
1826
|
+
taggedAt: number | undefined,
|
|
1827
|
+
): Promise<boolean> {
|
|
1828
|
+
// Array.isArray (not just truthiness): a non-array tags value - direct store
|
|
1829
|
+
// misuse like setItem(k, v, { tags: "products" }), or a skewed KV envelope -
|
|
1830
|
+
// must fail safe to "not invalidated" rather than throwing `.map` on every
|
|
1831
|
+
// read (which the outer catch would mis-report as a transient cache-read).
|
|
1832
|
+
if (!this.kv || !Array.isArray(tags) || tags.length === 0 || !taggedAt)
|
|
1833
|
+
return false;
|
|
1834
|
+
const ctx = _getRequestContext();
|
|
1835
|
+
const memo = ctx ? getTagMarkerMemo(ctx, this) : undefined;
|
|
1836
|
+
const inflight = ctx ? getTagMarkerInflight(ctx, this) : undefined;
|
|
1837
|
+
try {
|
|
1838
|
+
const markers = await Promise.all(
|
|
1839
|
+
tags.map((tag) => this.readTagMarker(tag, memo, inflight)),
|
|
1840
|
+
);
|
|
1841
|
+
for (const marker of markers) {
|
|
1842
|
+
if (marker != null && marker >= taggedAt) return true;
|
|
1843
|
+
}
|
|
1844
|
+
return false;
|
|
1845
|
+
} catch (error) {
|
|
1846
|
+
reportCacheError(
|
|
1847
|
+
error,
|
|
1848
|
+
"cache-read",
|
|
1849
|
+
"[CFCacheStore] tag invalidation check",
|
|
1850
|
+
);
|
|
1851
|
+
return false;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
/** Synthetic Cache API request for a tag's L1-cached invalidation marker. */
|
|
1856
|
+
private tagMarkerRequest(tag: string): Request {
|
|
1857
|
+
return this.keyToRequest(`${TAG_MARKER_CACHE_PREFIX}${tag}`);
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
/**
|
|
1861
|
+
* Read a tag's latest invalidation timestamp (or null if never invalidated)
|
|
1862
|
+
* through the cascade: per-request memo -> per-colo L1 cache (only when
|
|
1863
|
+
* tagCacheTtl > 0) -> KV (the global truth). The memo is always consulted
|
|
1864
|
+
* first so it stays authoritative within a request (read-your-own-writes),
|
|
1865
|
+
* and every KV/L1 result is written back into the memo. A Cache API miss
|
|
1866
|
+
* always falls through to KV; absence is represented by a cached sentinel,
|
|
1867
|
+
* never by a miss.
|
|
1868
|
+
*
|
|
1869
|
+
* Concurrent reads of the same tag within a request share one in-flight read
|
|
1870
|
+
* (the resolved-value memo only collapses sequential reads; parallel segment
|
|
1871
|
+
* loading would otherwise issue one KV read per concurrent reader).
|
|
1872
|
+
* @internal
|
|
1873
|
+
*/
|
|
1874
|
+
private async readTagMarker(
|
|
1875
|
+
tag: string,
|
|
1876
|
+
memo: Map<string, number | null> | undefined,
|
|
1877
|
+
inflight: Map<string, Promise<number | null>> | undefined,
|
|
1878
|
+
): Promise<number | null> {
|
|
1879
|
+
if (memo && memo.has(tag)) return memo.get(tag) ?? null;
|
|
1880
|
+
|
|
1881
|
+
// Collapse concurrent (not-yet-resolved) reads of this tag onto one promise.
|
|
1882
|
+
if (inflight) {
|
|
1883
|
+
const pending = inflight.get(tag);
|
|
1884
|
+
if (pending) return pending;
|
|
1885
|
+
const read = this.fetchTagMarker(tag, memo);
|
|
1886
|
+
inflight.set(tag, read);
|
|
1887
|
+
try {
|
|
1888
|
+
return await read;
|
|
1889
|
+
} finally {
|
|
1890
|
+
// Resolved values now live in the memo; drop the in-flight entry.
|
|
1891
|
+
inflight.delete(tag);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
return this.fetchTagMarker(tag, memo);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
/**
|
|
1899
|
+
* Uncached body of readTagMarker: L1 (per-colo Cache API, opt-in via
|
|
1900
|
+
* tagCacheTtl) -> KV. Writes the resolved value back into the memo.
|
|
1901
|
+
* @internal
|
|
1902
|
+
*/
|
|
1903
|
+
private async fetchTagMarker(
|
|
1904
|
+
tag: string,
|
|
1905
|
+
memo: Map<string, number | null> | undefined,
|
|
1906
|
+
): Promise<number | null> {
|
|
1907
|
+
// Write the resolved marker into the memo WITHOUT clobbering a value a
|
|
1908
|
+
// concurrent invalidateTags() wrote during our await. The router resolves
|
|
1909
|
+
// sibling slots in parallel, so a slot's updateTag() can land the
|
|
1910
|
+
// authoritative invalidatedAt into the memo while this read is still in
|
|
1911
|
+
// flight; overwriting it with our (pre-invalidation) read result would break
|
|
1912
|
+
// read-your-own-writes for the rest of the request. If the tag was memoized
|
|
1913
|
+
// mid-read, that value wins and is returned. Without a memo, the read result
|
|
1914
|
+
// stands as-is.
|
|
1915
|
+
const memoize = (read: number | null): number | null => {
|
|
1916
|
+
if (memo && memo.has(tag)) return memo.get(tag) ?? null;
|
|
1917
|
+
memo?.set(tag, read);
|
|
1918
|
+
return read;
|
|
1919
|
+
};
|
|
1920
|
+
|
|
1921
|
+
// L1 (per-colo) marker cache - opt-in via tagCacheTtl. Bounded by the same
|
|
1922
|
+
// edge budgets as data reads (inherited from #558) so a degraded colo cannot
|
|
1923
|
+
// stall a tagged read; a miss, timeout, or error all fall through to KV.
|
|
1924
|
+
if (this.tagCacheTtl > 0) {
|
|
1925
|
+
try {
|
|
1926
|
+
const cache = await this.getCache();
|
|
1927
|
+
const { response: hit, error: matchError } =
|
|
1928
|
+
await this.matchWithTimeout(cache, this.tagMarkerRequest(tag));
|
|
1929
|
+
// A transient match REJECTION is captured (not thrown) by
|
|
1930
|
+
// matchWithTimeout; surface it as cache-read like the data read paths
|
|
1931
|
+
// before falling through to KV, rather than silently dropping it.
|
|
1932
|
+
if (matchError)
|
|
1933
|
+
reportCacheError(
|
|
1934
|
+
matchError,
|
|
1935
|
+
"cache-read",
|
|
1936
|
+
"[CFCacheStore] tag marker L1 match",
|
|
1937
|
+
);
|
|
1938
|
+
if (hit) {
|
|
1939
|
+
const { value: body } = await this.readWithTimeout(
|
|
1940
|
+
() => hit.text(),
|
|
1941
|
+
this.edgeReadTimeoutMs,
|
|
1942
|
+
"tag marker L1 body read",
|
|
1943
|
+
);
|
|
1944
|
+
if (body !== undefined) {
|
|
1945
|
+
const value = body === TAG_MARKER_ABSENT ? null : Number(body);
|
|
1946
|
+
return memoize(value);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
} catch {
|
|
1950
|
+
// Fall through to KV on any L1 read error.
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
// KV (global truth), bounded by the KV budget. On TIMEOUT fail OPEN: treat
|
|
1955
|
+
// the marker as absent (-> entry not invalidated -> served) so a degraded
|
|
1956
|
+
// namespace cannot pin every tagged read behind a slow global lookup. A
|
|
1957
|
+
// transient REJECTION instead propagates to isGloballyInvalidated's catch
|
|
1958
|
+
// (reported cache-read), which also fails open. Either way one slow tag
|
|
1959
|
+
// never amplifies into a per-segment stall.
|
|
1960
|
+
const { value: raw, timedOut } = await this.readWithTimeout<string | null>(
|
|
1961
|
+
() => this.kv!.get(this.tagMarkerKey(tag), { type: "text" }),
|
|
1962
|
+
this.kvReadTimeoutMs,
|
|
1963
|
+
"tag marker KV read",
|
|
1964
|
+
);
|
|
1965
|
+
if (timedOut) {
|
|
1966
|
+
// Memoize the fail-open result so the rest of this request is consistent
|
|
1967
|
+
// (and does not re-pay the timeout per segment sharing the tag).
|
|
1968
|
+
return memoize(null);
|
|
1969
|
+
}
|
|
1970
|
+
const value = raw != null ? Number(raw) : null;
|
|
1971
|
+
const resolved = memoize(value);
|
|
1972
|
+
|
|
1973
|
+
// Populate L1 for subsequent reads in this colo (non-blocking). Use the
|
|
1974
|
+
// resolved (memo-aware) value so a marker invalidated mid-read is not
|
|
1975
|
+
// re-cached stale into this colo's L1.
|
|
1976
|
+
if (this.tagCacheTtl > 0) {
|
|
1977
|
+
const put = () => this.putTagMarkerL1(tag, resolved);
|
|
1978
|
+
if (this.waitUntil) this.waitUntil(put);
|
|
1979
|
+
else void put();
|
|
1980
|
+
}
|
|
1981
|
+
return resolved;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
/**
|
|
1985
|
+
* Cloudflare Cache-Tags written on a tag's L1 marker entry, namespaced per
|
|
1986
|
+
* store so purges never collide with other Cache-Tags in the zone. Three
|
|
1987
|
+
* tiers, broad to specific:
|
|
1988
|
+
* rg:{ns} - everything this store cached (deploy/nuclear reset)
|
|
1989
|
+
* rg:{ns}:lk - all tag-lookup markers
|
|
1990
|
+
* rg:{ns}:lk:{tag} - this tag's lookup (the normal updateTag purge target)
|
|
1991
|
+
* The tag value is encodeURIComponent'd so commas/spaces can't corrupt the
|
|
1992
|
+
* comma-delimited Cache-Tag header.
|
|
1993
|
+
* @internal
|
|
1994
|
+
*/
|
|
1995
|
+
private lookupCacheTags(tag: string): string[] {
|
|
1996
|
+
const ns = this.namespace ?? "default";
|
|
1997
|
+
return [`rg:${ns}`, `rg:${ns}:lk`, this.lookupPurgeTag(tag)];
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
/** The specific Cache-Tag a consumer purges to evict tag `tag`'s lookup. */
|
|
2001
|
+
private lookupPurgeTag(tag: string): string {
|
|
2002
|
+
const ns = this.namespace ?? "default";
|
|
2003
|
+
return `rg:${ns}:lk:${encodeURIComponent(tag)}`;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
/**
|
|
2007
|
+
* Write a tag marker value into the per-colo L1 Cache API with tagCacheTtl.
|
|
2008
|
+
* `null` is stored as the TAG_MARKER_ABSENT sentinel so "no marker yet" is
|
|
2009
|
+
* cacheable (most tags are never invalidated - that is where the read savings
|
|
2010
|
+
* come from). The entry also carries a namespaced Cache-Tag so an external
|
|
2011
|
+
* purge-by-tag (via onRevalidateTag) can evict it across colos promptly,
|
|
2012
|
+
* rather than waiting out tagCacheTtl. Best-effort.
|
|
2013
|
+
* @internal
|
|
2014
|
+
*/
|
|
2015
|
+
private async putTagMarkerL1(
|
|
2016
|
+
tag: string,
|
|
2017
|
+
value: number | null,
|
|
2018
|
+
opts?: { critical?: boolean },
|
|
2019
|
+
): Promise<void> {
|
|
2020
|
+
if (this.tagCacheTtl <= 0) return;
|
|
2021
|
+
try {
|
|
2022
|
+
const cache = await this.getCache();
|
|
2023
|
+
const body = value != null ? String(value) : TAG_MARKER_ABSENT;
|
|
2024
|
+
await cache.put(
|
|
2025
|
+
this.tagMarkerRequest(tag),
|
|
2026
|
+
new Response(body, {
|
|
2027
|
+
headers: {
|
|
2028
|
+
"Cache-Control": `public, max-age=${this.tagCacheTtl}`,
|
|
2029
|
+
"Cache-Tag": this.lookupCacheTags(tag).join(","),
|
|
2030
|
+
},
|
|
2031
|
+
}),
|
|
2032
|
+
);
|
|
2033
|
+
} catch (error) {
|
|
2034
|
+
// The read-path populate is best-effort: a failed populate just means the
|
|
2035
|
+
// next read consults KV. The invalidation WRITE-THROUGH (critical) is not
|
|
2036
|
+
// - silently swallowing it would leave this colo's stale marker (often the
|
|
2037
|
+
// ABSENT sentinel) authoritative for tagCacheTtl while updateTag reports
|
|
2038
|
+
// success. Surface it, and best-effort delete the L1 marker so the next
|
|
2039
|
+
// read re-reads KV, which already holds the fresh marker (written before
|
|
2040
|
+
// this write-through in invalidateTags).
|
|
2041
|
+
if (opts?.critical) {
|
|
2042
|
+
reportCacheError(
|
|
2043
|
+
error,
|
|
2044
|
+
"cache-invalidate",
|
|
2045
|
+
"[CFCacheStore] tag marker L1 write-through",
|
|
2046
|
+
);
|
|
2047
|
+
await reportingAsync(
|
|
2048
|
+
async () => {
|
|
2049
|
+
const cache = await this.getCache();
|
|
2050
|
+
await cache.delete(this.tagMarkerRequest(tag));
|
|
2051
|
+
},
|
|
2052
|
+
"cache-delete",
|
|
2053
|
+
"[CFCacheStore] tag marker L1 evict after failed write-through",
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
/**
|
|
2060
|
+
* Invalidate every entry tagged with any of `tags`. Receives the whole batch
|
|
2061
|
+
* from one updateTag()/revalidateTag() call so the eager-purge hook fires
|
|
2062
|
+
* ONCE (one CDN purge request, not one per tag). For each tag: records the KV
|
|
2063
|
+
* marker (the durable cross-colo truth that reads compare taggedAt against),
|
|
2064
|
+
* writes the fresh marker straight into this colo's L1 (write-through, NOT
|
|
2065
|
+
* delete - a delete would let the next read re-read a not-yet-converged KV
|
|
2066
|
+
* value and re-arm the stale window), and memoizes it for same-request
|
|
2067
|
+
* read-your-own-writes. Finally fires onRevalidateTag with the namespaced
|
|
2068
|
+
* lookup Cache-Tags so a consumer purge evicts the cached lookups in other
|
|
2069
|
+
* colos promptly (otherwise they converge within tagCacheTtl).
|
|
2070
|
+
*
|
|
2071
|
+
* Durable-write integrity: the in-memory write-through (memo + L1) for a tag
|
|
2072
|
+
* runs ONLY after that tag's KV marker write is confirmed. If any KV write
|
|
2073
|
+
* fails (transient error, or an over-512-byte key), this rejects with the
|
|
2074
|
+
* failed tags so an awaiting updateTag() surfaces the failure instead of
|
|
2075
|
+
* silently reporting success while other requests/colos serve stale data. The
|
|
2076
|
+
* eager purge still fires for the whole batch first (it is additive).
|
|
2077
|
+
*/
|
|
2078
|
+
async invalidateTags(tags: string[]): Promise<void> {
|
|
2079
|
+
if (tags.length === 0) return;
|
|
2080
|
+
const invalidatedAt = Date.now();
|
|
2081
|
+
const ctx = _getRequestContext();
|
|
2082
|
+
const memo = ctx ? getTagMarkerMemo(ctx, this) : undefined;
|
|
2083
|
+
|
|
2084
|
+
if (!this.kv && !this.onRevalidateTag) {
|
|
2085
|
+
console.warn(
|
|
2086
|
+
`[CFCacheStore] invalidateTags had no effect: configure a KV namespace ` +
|
|
2087
|
+
`for distributed invalidation, or an onRevalidateTag hook.`,
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
const failedTags = new Set<string>();
|
|
2092
|
+
const errors: unknown[] = [];
|
|
2093
|
+
if (this.kv) {
|
|
2094
|
+
await Promise.all(
|
|
2095
|
+
tags.map(async (tag) => {
|
|
2096
|
+
const markerKey = this.tagMarkerKey(tag);
|
|
2097
|
+
const markerKeyBytes = kvKeyByteLength(markerKey);
|
|
2098
|
+
if (markerKeyBytes > KV_MAX_KEY_BYTES) {
|
|
2099
|
+
failedTags.add(tag);
|
|
2100
|
+
errors.push(
|
|
2101
|
+
new Error(
|
|
2102
|
+
`tag "${tag}" produces a ${markerKeyBytes}-byte KV ` +
|
|
2103
|
+
`marker key, over the ${KV_MAX_KEY_BYTES}-byte limit`,
|
|
2104
|
+
),
|
|
2105
|
+
);
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
try {
|
|
2109
|
+
await this.kv!.put(markerKey, String(invalidatedAt), {
|
|
2110
|
+
...(this.tagInvalidationTtl
|
|
2111
|
+
? { expirationTtl: this.tagInvalidationTtl }
|
|
2112
|
+
: {}),
|
|
2113
|
+
});
|
|
2114
|
+
} catch (error) {
|
|
2115
|
+
failedTags.add(tag);
|
|
2116
|
+
errors.push(error);
|
|
2117
|
+
}
|
|
2118
|
+
}),
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
// Write-through memo + L1 only for tags with a confirmed durable marker, and
|
|
2123
|
+
// only when KV is configured. Markers are read exclusively through
|
|
2124
|
+
// isGloballyInvalidated(), which short-circuits to "not invalidated" when
|
|
2125
|
+
// !this.kv; writing memo/L1 markers without KV would be dead state no read
|
|
2126
|
+
// path ever consults. The onRevalidateTag purge below still fires regardless
|
|
2127
|
+
// (it is additive and external to the marker cascade). The memo write is
|
|
2128
|
+
// synchronous (read-your-own-writes); the L1 Cache API writes are
|
|
2129
|
+
// independent, so fan them out in parallel rather than awaiting each.
|
|
2130
|
+
if (this.kv) {
|
|
2131
|
+
const l1Writes: Promise<void>[] = [];
|
|
2132
|
+
for (const tag of tags) {
|
|
2133
|
+
if (failedTags.has(tag)) continue;
|
|
2134
|
+
memo?.set(tag, invalidatedAt);
|
|
2135
|
+
if (this.tagCacheTtl > 0) {
|
|
2136
|
+
l1Writes.push(
|
|
2137
|
+
this.putTagMarkerL1(tag, invalidatedAt, { critical: true }),
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
if (l1Writes.length > 0) await Promise.all(l1Writes);
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// One batched eager purge of the lookup markers for the whole call. Fired
|
|
2145
|
+
// regardless of KV write outcome (it is additive and uses pure string ops).
|
|
2146
|
+
if (this.onRevalidateTag) {
|
|
2147
|
+
try {
|
|
2148
|
+
await this.onRevalidateTag(tags.map((tag) => this.lookupPurgeTag(tag)));
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
reportCacheError(
|
|
2151
|
+
error,
|
|
2152
|
+
"cache-invalidate",
|
|
2153
|
+
"[CFCacheStore] onRevalidateTag hook",
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
if (failedTags.size > 0) {
|
|
2159
|
+
const err = new Error(
|
|
2160
|
+
`[CFCacheStore] ${failedTags.size}/${tags.length} tag marker write(s) ` +
|
|
2161
|
+
`failed: ${[...failedTags].join(", ")}. Those tags may still serve ` +
|
|
2162
|
+
`stale data across requests/colos; retry the invalidation.`,
|
|
2163
|
+
);
|
|
2164
|
+
(err as Error & { cause?: unknown }).cause = errors[0];
|
|
2165
|
+
throw err;
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
|
|
722
2169
|
// ============================================================================
|
|
723
2170
|
// KV L2 Helpers
|
|
724
2171
|
// ============================================================================
|
|
@@ -729,28 +2176,78 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
729
2176
|
* Promotes hits to L1 via waitUntil.
|
|
730
2177
|
* @internal
|
|
731
2178
|
*/
|
|
732
|
-
private async kvGetSegment(
|
|
2179
|
+
private async kvGetSegment(
|
|
2180
|
+
key: string,
|
|
2181
|
+
opts?: { suppressRevalidate?: boolean },
|
|
2182
|
+
): Promise<CacheGetResult | null> {
|
|
733
2183
|
if (!this.kv) return null;
|
|
734
2184
|
|
|
735
2185
|
try {
|
|
736
2186
|
const kvKey = this.toKVKey(key);
|
|
737
|
-
const
|
|
738
|
-
|
|
2187
|
+
const { value: envelope, timedOut } =
|
|
2188
|
+
await this.kvGetOrEvict<KVSegmentEnvelope>(
|
|
2189
|
+
kvKey,
|
|
2190
|
+
(e) =>
|
|
2191
|
+
typeof e.e === "number" && typeof e.s === "number" && e.d != null,
|
|
2192
|
+
"kvGetSegment",
|
|
2193
|
+
);
|
|
2194
|
+
if (timedOut) {
|
|
2195
|
+
// Abandoned slow KV read: no envelope, so no promote-to-L1. Distinct
|
|
2196
|
+
// from a genuine kv-miss so the degradation is visible on wrangler tail.
|
|
2197
|
+
if (this.debug)
|
|
2198
|
+
this.emitDebug({ op: "get", key, outcome: "kv-timeout" });
|
|
2199
|
+
return null;
|
|
2200
|
+
}
|
|
2201
|
+
if (!envelope) {
|
|
2202
|
+
// Missing key, or a corrupt entry already evicted + reported by
|
|
2203
|
+
// kvGetOrEvict. Either way a miss.
|
|
2204
|
+
if (this.debug) this.emitDebug({ op: "get", key, outcome: "kv-miss" });
|
|
2205
|
+
return null;
|
|
2206
|
+
}
|
|
739
2207
|
|
|
740
|
-
const envelope = raw as KVSegmentEnvelope;
|
|
741
2208
|
const now = Date.now();
|
|
742
2209
|
|
|
743
2210
|
// Hard-expired — treat as miss
|
|
744
|
-
if (now > envelope.e)
|
|
2211
|
+
if (now > envelope.e) {
|
|
2212
|
+
if (this.debug) this.emitDebug({ op: "get", key, outcome: "kv-miss" });
|
|
2213
|
+
return null;
|
|
2214
|
+
}
|
|
745
2215
|
|
|
746
|
-
|
|
2216
|
+
// Tag invalidation check (also covers the KV tier, not just L1).
|
|
2217
|
+
if (
|
|
2218
|
+
await this.isGloballyInvalidated(envelope.d.tags, envelope.d.taggedAt)
|
|
2219
|
+
) {
|
|
2220
|
+
if (this.debug)
|
|
2221
|
+
this.emitDebug({ op: "get", key, outcome: "tag-invalidated" });
|
|
2222
|
+
return null;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
// When this is a degraded L1 fall-through (body-timeout / non-200), the
|
|
2226
|
+
// caller asks us to suppress revalidation: KV has no REVALIDATING herd
|
|
2227
|
+
// guard, so N concurrent degraded reads would otherwise each spawn a
|
|
2228
|
+
// render exactly when the colo is already struggling. We still serve the
|
|
2229
|
+
// stale data and still promote to L1; only the revalidation is withheld.
|
|
2230
|
+
const stale = now > envelope.s;
|
|
2231
|
+
const shouldRevalidate = stale && !opts?.suppressRevalidate;
|
|
747
2232
|
|
|
748
2233
|
// Promote to L1 in background
|
|
749
2234
|
this.promoteSegmentToL1(key, envelope);
|
|
750
2235
|
|
|
2236
|
+
if (this.debug)
|
|
2237
|
+
this.emitDebug({
|
|
2238
|
+
op: "get",
|
|
2239
|
+
key,
|
|
2240
|
+
outcome: !stale
|
|
2241
|
+
? "kv-fresh"
|
|
2242
|
+
: opts?.suppressRevalidate
|
|
2243
|
+
? "kv-stale-suppressed"
|
|
2244
|
+
: "kv-stale",
|
|
2245
|
+
shouldRevalidate,
|
|
2246
|
+
});
|
|
751
2247
|
return { data: envelope.d, shouldRevalidate };
|
|
752
2248
|
} catch (error) {
|
|
753
|
-
|
|
2249
|
+
reportCacheError(error, "cache-read", "[CFCacheStore] kvGetSegment");
|
|
2250
|
+
if (this.debug) this.emitDebug({ op: "get", key, outcome: "error" });
|
|
754
2251
|
return null;
|
|
755
2252
|
}
|
|
756
2253
|
}
|
|
@@ -764,28 +2261,30 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
764
2261
|
data: CachedEntryData,
|
|
765
2262
|
staleAt: number,
|
|
766
2263
|
totalTtl: number,
|
|
2264
|
+
swrWindow: number,
|
|
767
2265
|
): void {
|
|
768
2266
|
// KV requires expirationTtl >= 60s. Skip write for short-lived entries.
|
|
769
2267
|
if (!this.kv || !this.waitUntil || totalTtl < 60) return;
|
|
770
2268
|
|
|
771
2269
|
const kvKey = this.toKVKey(key);
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
2270
|
+
const expiresAt = staleAt + swrWindow * 1000;
|
|
2271
|
+
|
|
2272
|
+
this.waitUntil(() =>
|
|
2273
|
+
reportingAsync(
|
|
2274
|
+
() => {
|
|
2275
|
+
const envelope: KVSegmentEnvelope = {
|
|
2276
|
+
d: data,
|
|
2277
|
+
s: staleAt,
|
|
2278
|
+
e: expiresAt,
|
|
2279
|
+
};
|
|
2280
|
+
return this.kv!.put(kvKey, JSON.stringify(envelope), {
|
|
2281
|
+
expirationTtl: totalTtl,
|
|
2282
|
+
});
|
|
2283
|
+
},
|
|
2284
|
+
"cache-write",
|
|
2285
|
+
"[CFCacheStore] kvSetSegment",
|
|
2286
|
+
),
|
|
2287
|
+
);
|
|
789
2288
|
}
|
|
790
2289
|
|
|
791
2290
|
/**
|
|
@@ -795,58 +2294,115 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
795
2294
|
private promoteSegmentToL1(key: string, envelope: KVSegmentEnvelope): void {
|
|
796
2295
|
if (!this.waitUntil) return;
|
|
797
2296
|
|
|
798
|
-
this.waitUntil(
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
2297
|
+
this.waitUntil(() =>
|
|
2298
|
+
reportingAsync(
|
|
2299
|
+
async () => {
|
|
2300
|
+
const now = Date.now();
|
|
2301
|
+
const remainingTtl = Math.max(
|
|
2302
|
+
1,
|
|
2303
|
+
Math.floor((envelope.e - now) / 1000),
|
|
2304
|
+
);
|
|
2305
|
+
const cache = await this.getCache();
|
|
2306
|
+
const request = this.keyToRequest(key);
|
|
2307
|
+
|
|
2308
|
+
const response = new Response(JSON.stringify(envelope.d), {
|
|
2309
|
+
headers: {
|
|
2310
|
+
"Content-Type": "application/json",
|
|
2311
|
+
"Cache-Control": `public, max-age=${remainingTtl}`,
|
|
2312
|
+
[CACHE_STALE_AT_HEADER]: String(envelope.s),
|
|
2313
|
+
// Carry the hard-expiry deadline so a promoted entry that later
|
|
2314
|
+
// goes stale re-puts with the correct remaining ttl (see set()).
|
|
2315
|
+
[CACHE_EXPIRES_AT_HEADER]: String(envelope.e),
|
|
2316
|
+
[CACHE_STATUS_HEADER]: "HIT",
|
|
2317
|
+
// Preserve tags across KV->L1 promotion so the promoted entry
|
|
2318
|
+
// stays tag-invalidatable.
|
|
2319
|
+
...this.tagHeaderEntries(envelope.d.tags, envelope.d.taggedAt),
|
|
2320
|
+
},
|
|
2321
|
+
});
|
|
2322
|
+
|
|
2323
|
+
await cache.put(request, response);
|
|
2324
|
+
},
|
|
2325
|
+
"cache-write",
|
|
2326
|
+
"[CFCacheStore] promoteSegmentToL1",
|
|
2327
|
+
),
|
|
2328
|
+
);
|
|
819
2329
|
}
|
|
820
2330
|
|
|
821
2331
|
/**
|
|
822
2332
|
* KV fallback for function cache reads.
|
|
823
2333
|
* @internal
|
|
824
2334
|
*/
|
|
825
|
-
private async kvGetItem(
|
|
2335
|
+
private async kvGetItem(
|
|
2336
|
+
key: string,
|
|
2337
|
+
opts?: { suppressRevalidate?: boolean },
|
|
2338
|
+
): Promise<CacheItemResult | null> {
|
|
826
2339
|
if (!this.kv) return null;
|
|
827
2340
|
|
|
828
2341
|
try {
|
|
829
2342
|
const kvKey = this.toKVKey(`fn:${key}`);
|
|
830
|
-
const
|
|
831
|
-
|
|
2343
|
+
const { value: envelope, timedOut } =
|
|
2344
|
+
await this.kvGetOrEvict<KVItemEnvelope>(
|
|
2345
|
+
kvKey,
|
|
2346
|
+
(e) =>
|
|
2347
|
+
typeof e.v === "string" &&
|
|
2348
|
+
typeof e.e === "number" &&
|
|
2349
|
+
typeof e.s === "number",
|
|
2350
|
+
"kvGetItem",
|
|
2351
|
+
);
|
|
2352
|
+
if (timedOut) {
|
|
2353
|
+
if (this.debug)
|
|
2354
|
+
this.emitDebug({ op: "getItem", key, outcome: "kv-timeout" });
|
|
2355
|
+
return null;
|
|
2356
|
+
}
|
|
2357
|
+
if (!envelope) {
|
|
2358
|
+
if (this.debug)
|
|
2359
|
+
this.emitDebug({ op: "getItem", key, outcome: "kv-miss" });
|
|
2360
|
+
return null;
|
|
2361
|
+
}
|
|
832
2362
|
|
|
833
|
-
const envelope = raw as KVItemEnvelope;
|
|
834
2363
|
const now = Date.now();
|
|
835
2364
|
|
|
836
|
-
if (now > envelope.e)
|
|
2365
|
+
if (now > envelope.e) {
|
|
2366
|
+
if (this.debug)
|
|
2367
|
+
this.emitDebug({ op: "getItem", key, outcome: "kv-miss" });
|
|
2368
|
+
return null;
|
|
2369
|
+
}
|
|
837
2370
|
|
|
838
|
-
|
|
2371
|
+
// Tag invalidation check (also covers the KV tier, not just L1).
|
|
2372
|
+
if (await this.isGloballyInvalidated(envelope.t, envelope.ta)) {
|
|
2373
|
+
if (this.debug)
|
|
2374
|
+
this.emitDebug({ op: "getItem", key, outcome: "tag-invalidated" });
|
|
2375
|
+
return null;
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
// Degraded fall-through suppresses revalidation (no KV herd guard); see
|
|
2379
|
+
// kvGetSegment. Still serves stale and still promotes.
|
|
2380
|
+
const stale = now > envelope.s;
|
|
2381
|
+
const shouldRevalidate = stale && !opts?.suppressRevalidate;
|
|
839
2382
|
|
|
840
2383
|
// Promote to L1
|
|
841
2384
|
this.promoteItemToL1(key, envelope);
|
|
842
2385
|
|
|
2386
|
+
if (this.debug)
|
|
2387
|
+
this.emitDebug({
|
|
2388
|
+
op: "getItem",
|
|
2389
|
+
key,
|
|
2390
|
+
outcome: !stale
|
|
2391
|
+
? "kv-fresh"
|
|
2392
|
+
: opts?.suppressRevalidate
|
|
2393
|
+
? "kv-stale-suppressed"
|
|
2394
|
+
: "kv-stale",
|
|
2395
|
+
shouldRevalidate,
|
|
2396
|
+
});
|
|
843
2397
|
return {
|
|
844
2398
|
value: envelope.v,
|
|
845
2399
|
handles: envelope.h,
|
|
846
2400
|
shouldRevalidate,
|
|
2401
|
+
tags: envelope.t,
|
|
847
2402
|
};
|
|
848
2403
|
} catch (error) {
|
|
849
|
-
|
|
2404
|
+
reportCacheError(error, "cache-read", "[CFCacheStore] kvGetItem");
|
|
2405
|
+
if (this.debug) this.emitDebug({ op: "getItem", key, outcome: "error" });
|
|
850
2406
|
return null;
|
|
851
2407
|
}
|
|
852
2408
|
}
|
|
@@ -858,28 +2414,41 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
858
2414
|
private promoteItemToL1(key: string, envelope: KVItemEnvelope): void {
|
|
859
2415
|
if (!this.waitUntil) return;
|
|
860
2416
|
|
|
861
|
-
this.waitUntil(
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
2417
|
+
this.waitUntil(() =>
|
|
2418
|
+
reportingAsync(
|
|
2419
|
+
async () => {
|
|
2420
|
+
const now = Date.now();
|
|
2421
|
+
const remainingTtl = Math.max(
|
|
2422
|
+
1,
|
|
2423
|
+
Math.floor((envelope.e - now) / 1000),
|
|
2424
|
+
);
|
|
2425
|
+
const cache = await this.getCache();
|
|
2426
|
+
const request = this.keyToRequest(`fn:${key}`);
|
|
2427
|
+
|
|
2428
|
+
const body = JSON.stringify({
|
|
2429
|
+
value: envelope.v,
|
|
2430
|
+
handles: envelope.h,
|
|
2431
|
+
});
|
|
2432
|
+
const response = new Response(body, {
|
|
2433
|
+
headers: {
|
|
2434
|
+
"Content-Type": "application/json",
|
|
2435
|
+
"Cache-Control": `public, max-age=${remainingTtl}`,
|
|
2436
|
+
[CACHE_STALE_AT_HEADER]: String(envelope.s),
|
|
2437
|
+
// Carry the hard-expiry deadline; see promoteSegmentToL1 / set().
|
|
2438
|
+
[CACHE_EXPIRES_AT_HEADER]: String(envelope.e),
|
|
2439
|
+
[CACHE_STATUS_HEADER]: "HIT",
|
|
2440
|
+
// Preserve tags across KV->L1 promotion (the item tier previously
|
|
2441
|
+
// dropped them, permanently disabling tag invalidation here).
|
|
2442
|
+
...this.tagHeaderEntries(envelope.t, envelope.ta),
|
|
2443
|
+
},
|
|
2444
|
+
});
|
|
2445
|
+
|
|
2446
|
+
await cache.put(request, response);
|
|
2447
|
+
},
|
|
2448
|
+
"cache-write",
|
|
2449
|
+
"[CFCacheStore] promoteItemToL1",
|
|
2450
|
+
),
|
|
2451
|
+
);
|
|
883
2452
|
}
|
|
884
2453
|
|
|
885
2454
|
/**
|
|
@@ -892,32 +2461,82 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
892
2461
|
if (!this.kv) return null;
|
|
893
2462
|
|
|
894
2463
|
try {
|
|
895
|
-
const kvKey = this.
|
|
896
|
-
|
|
897
|
-
|
|
2464
|
+
const kvKey = this.toDocKVKey(key);
|
|
2465
|
+
// The document path is debug-silent (op is only get/getItem): a KV-read
|
|
2466
|
+
// timeout here is bounded for resilience parity (kvGetOrEvict applies the
|
|
2467
|
+
// budget) but emits no kv-timeout event, so its absence from the debug
|
|
2468
|
+
// stream is expected. A null envelope is a miss -- missing key, a budget
|
|
2469
|
+
// timeout, or a corrupt entry already evicted + reported by kvGetOrEvict.
|
|
2470
|
+
const { value: envelope } = await this.kvGetOrEvict<KVResponseEnvelope>(
|
|
2471
|
+
kvKey,
|
|
2472
|
+
(e) =>
|
|
2473
|
+
typeof e.b === "string" &&
|
|
2474
|
+
typeof e.st === "number" &&
|
|
2475
|
+
typeof e.e === "number" &&
|
|
2476
|
+
typeof e.s === "number" &&
|
|
2477
|
+
// stx is optional but, if present, must be a string (feeds Response).
|
|
2478
|
+
(e.stx === undefined || typeof e.stx === "string") &&
|
|
2479
|
+
// hd must be an array of [name, value] string tuples; a malformed
|
|
2480
|
+
// shape would otherwise throw in `new Headers(hd)`. Validate it here
|
|
2481
|
+
// so a faulty envelope is a fail-open MISS, never a thrown read.
|
|
2482
|
+
Array.isArray(e.hd) &&
|
|
2483
|
+
e.hd.every(
|
|
2484
|
+
(entry) =>
|
|
2485
|
+
Array.isArray(entry) &&
|
|
2486
|
+
entry.length === 2 &&
|
|
2487
|
+
typeof entry[0] === "string" &&
|
|
2488
|
+
typeof entry[1] === "string",
|
|
2489
|
+
),
|
|
2490
|
+
"kvGetResponse",
|
|
2491
|
+
);
|
|
2492
|
+
if (!envelope) return null;
|
|
898
2493
|
|
|
899
|
-
const envelope = raw as KVResponseEnvelope;
|
|
900
2494
|
const now = Date.now();
|
|
901
2495
|
|
|
902
2496
|
if (now > envelope.e) return null;
|
|
903
2497
|
|
|
2498
|
+
// Tag invalidation check (also covers the KV tier, not just L1).
|
|
2499
|
+
if (await this.isGloballyInvalidated(envelope.t, envelope.ta)) {
|
|
2500
|
+
return null;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
904
2503
|
const shouldRevalidate = now > envelope.s;
|
|
905
2504
|
|
|
906
|
-
// Reconstruct Response
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
2505
|
+
// Reconstruct Response: decode base64 -> binary, rebuild headers/status.
|
|
2506
|
+
// Corrupt/partial base64 throws in atob; malformed `hd` or an out-of-range
|
|
2507
|
+
// `st` throws in new Headers/new Response. Any of these is a faulty entry,
|
|
2508
|
+
// so evict it and miss rather than re-failing every read until TTL.
|
|
2509
|
+
let response: Response;
|
|
2510
|
+
try {
|
|
2511
|
+
// Finding #3 (read side): strip per-client signals a stale envelope may
|
|
2512
|
+
// carry. Inside the try so a malformed `hd` evicts (not throws through);
|
|
2513
|
+
// mutates `hd` in place so promoteResponseToL1 re-seeds from it too.
|
|
2514
|
+
envelope.hd = envelope.hd.filter(
|
|
2515
|
+
([name]) => !isPerClientSignalHeader(name),
|
|
2516
|
+
);
|
|
2517
|
+
const bodyBuffer = base64ToBuffer(envelope.b);
|
|
2518
|
+
const headers = new Headers(envelope.hd);
|
|
2519
|
+
response = new Response(bodyBuffer, {
|
|
2520
|
+
status: envelope.st,
|
|
2521
|
+
statusText: envelope.stx,
|
|
2522
|
+
headers,
|
|
2523
|
+
});
|
|
2524
|
+
} catch (error) {
|
|
2525
|
+
reportCacheError(
|
|
2526
|
+
error,
|
|
2527
|
+
"cache-corrupt",
|
|
2528
|
+
"[CFCacheStore] kvGetResponse: corrupt response envelope, evicting",
|
|
2529
|
+
);
|
|
2530
|
+
this.scheduleKvEvict(kvKey, "kvGetResponse");
|
|
2531
|
+
return null;
|
|
2532
|
+
}
|
|
914
2533
|
|
|
915
2534
|
// Promote to L1
|
|
916
2535
|
this.promoteResponseToL1(key, envelope);
|
|
917
2536
|
|
|
918
2537
|
return { response, shouldRevalidate };
|
|
919
2538
|
} catch (error) {
|
|
920
|
-
|
|
2539
|
+
reportCacheError(error, "cache-read", "[CFCacheStore] kvGetResponse");
|
|
921
2540
|
return null;
|
|
922
2541
|
}
|
|
923
2542
|
}
|
|
@@ -929,52 +2548,46 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
929
2548
|
private promoteResponseToL1(key: string, envelope: KVResponseEnvelope): void {
|
|
930
2549
|
if (!this.waitUntil) return;
|
|
931
2550
|
|
|
932
|
-
this.waitUntil(
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
function base64ToBuffer(base64: string): ArrayBuffer {
|
|
974
|
-
const binary = atob(base64);
|
|
975
|
-
const bytes = new Uint8Array(binary.length);
|
|
976
|
-
for (let i = 0; i < binary.length; i++) {
|
|
977
|
-
bytes[i] = binary.charCodeAt(i);
|
|
2551
|
+
this.waitUntil(() =>
|
|
2552
|
+
reportingAsync(
|
|
2553
|
+
async () => {
|
|
2554
|
+
const now = Date.now();
|
|
2555
|
+
const remainingTtl = Math.max(
|
|
2556
|
+
1,
|
|
2557
|
+
Math.floor((envelope.e - now) / 1000),
|
|
2558
|
+
);
|
|
2559
|
+
const cache = await this.getCache();
|
|
2560
|
+
const request = this.keyToRequest(`doc:${key}`);
|
|
2561
|
+
|
|
2562
|
+
const headers = new Headers(envelope.hd);
|
|
2563
|
+
const originalCacheControl = headers.get("Cache-Control");
|
|
2564
|
+
if (originalCacheControl !== null) {
|
|
2565
|
+
headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
|
|
2566
|
+
}
|
|
2567
|
+
headers.set("Cache-Control", `public, max-age=${remainingTtl}`);
|
|
2568
|
+
headers.set(CACHE_STALE_AT_HEADER, String(envelope.s));
|
|
2569
|
+
// Carry the hard-expiry deadline so the document herd guard's
|
|
2570
|
+
// markResponseRevalidating re-put can compute the remaining window
|
|
2571
|
+
// (matches promoteSegmentToL1/promoteItemToL1); without it a stale
|
|
2572
|
+
// re-put would floor to max-age=1 and churn the KV-promoted twin.
|
|
2573
|
+
headers.set(CACHE_EXPIRES_AT_HEADER, String(envelope.e));
|
|
2574
|
+
// Re-attach the internal tag headers (envelope.hd is client-facing
|
|
2575
|
+
// and intentionally excludes them) so the promoted entry stays
|
|
2576
|
+
// invalidatable.
|
|
2577
|
+
this.setTagHeaders(headers, envelope.t, envelope.ta);
|
|
2578
|
+
|
|
2579
|
+
const bodyBuffer = base64ToBuffer(envelope.b);
|
|
2580
|
+
const response = new Response(bodyBuffer, {
|
|
2581
|
+
status: envelope.st,
|
|
2582
|
+
statusText: envelope.stx,
|
|
2583
|
+
headers,
|
|
2584
|
+
});
|
|
2585
|
+
|
|
2586
|
+
await cache.put(request, response);
|
|
2587
|
+
},
|
|
2588
|
+
"cache-write",
|
|
2589
|
+
"[CFCacheStore] promoteResponseToL1",
|
|
2590
|
+
),
|
|
2591
|
+
);
|
|
978
2592
|
}
|
|
979
|
-
return bytes.buffer;
|
|
980
2593
|
}
|