@rangojs/router 0.0.0-experimental.e16b7c00 → 0.0.0-experimental.e9c0b2f2
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 +64 -6
- package/dist/__internal.d.ts +83 -0
- package/dist/__internal.d.ts.map +1 -0
- package/dist/__internal.js +19 -0
- package/dist/__internal.js.map +1 -0
- package/dist/__mocks__/version.d.ts +7 -0
- package/dist/__mocks__/version.d.ts.map +1 -0
- package/dist/__mocks__/version.js +7 -0
- package/dist/__mocks__/version.js.map +1 -0
- package/dist/__tests__/client-href.test.d.ts +2 -0
- package/dist/__tests__/client-href.test.d.ts.map +1 -0
- package/dist/__tests__/client-href.test.js +74 -0
- package/dist/__tests__/client-href.test.js.map +1 -0
- package/dist/__tests__/component-utils.test.d.ts +2 -0
- package/dist/__tests__/component-utils.test.d.ts.map +1 -0
- package/dist/__tests__/component-utils.test.js +51 -0
- package/dist/__tests__/component-utils.test.js.map +1 -0
- package/dist/__tests__/event-controller.test.d.ts +2 -0
- package/dist/__tests__/event-controller.test.d.ts.map +1 -0
- package/dist/__tests__/event-controller.test.js +538 -0
- package/dist/__tests__/event-controller.test.js.map +1 -0
- package/dist/__tests__/helpers/route-tree.d.ts +118 -0
- package/dist/__tests__/helpers/route-tree.d.ts.map +1 -0
- package/dist/__tests__/helpers/route-tree.js +374 -0
- package/dist/__tests__/helpers/route-tree.js.map +1 -0
- package/dist/__tests__/match-result.test.d.ts +2 -0
- package/dist/__tests__/match-result.test.d.ts.map +1 -0
- package/dist/__tests__/match-result.test.js +154 -0
- package/dist/__tests__/match-result.test.js.map +1 -0
- package/dist/__tests__/navigation-store.test.d.ts +2 -0
- package/dist/__tests__/navigation-store.test.d.ts.map +1 -0
- package/dist/__tests__/navigation-store.test.js +440 -0
- package/dist/__tests__/navigation-store.test.js.map +1 -0
- package/dist/__tests__/partial-update.test.d.ts +2 -0
- package/dist/__tests__/partial-update.test.d.ts.map +1 -0
- package/dist/__tests__/partial-update.test.js +1009 -0
- package/dist/__tests__/partial-update.test.js.map +1 -0
- package/dist/__tests__/reverse-types.test.d.ts +8 -0
- package/dist/__tests__/reverse-types.test.d.ts.map +1 -0
- package/dist/__tests__/reverse-types.test.js +656 -0
- package/dist/__tests__/reverse-types.test.js.map +1 -0
- package/dist/__tests__/route-definition.test.d.ts +2 -0
- package/dist/__tests__/route-definition.test.d.ts.map +1 -0
- package/dist/__tests__/route-definition.test.js +55 -0
- package/dist/__tests__/route-definition.test.js.map +1 -0
- package/dist/__tests__/router-helpers.test.d.ts +2 -0
- package/dist/__tests__/router-helpers.test.d.ts.map +1 -0
- package/dist/__tests__/router-helpers.test.js +377 -0
- package/dist/__tests__/router-helpers.test.js.map +1 -0
- package/dist/__tests__/router-integration-2.test.d.ts +2 -0
- package/dist/__tests__/router-integration-2.test.d.ts.map +1 -0
- package/dist/__tests__/router-integration-2.test.js +426 -0
- package/dist/__tests__/router-integration-2.test.js.map +1 -0
- package/dist/__tests__/router-integration.test.d.ts +2 -0
- package/dist/__tests__/router-integration.test.d.ts.map +1 -0
- package/dist/__tests__/router-integration.test.js +1051 -0
- package/dist/__tests__/router-integration.test.js.map +1 -0
- package/dist/__tests__/search-params.test.d.ts +5 -0
- package/dist/__tests__/search-params.test.d.ts.map +1 -0
- package/dist/__tests__/search-params.test.js +306 -0
- package/dist/__tests__/search-params.test.js.map +1 -0
- package/dist/__tests__/segment-system.test.d.ts +2 -0
- package/dist/__tests__/segment-system.test.d.ts.map +1 -0
- package/dist/__tests__/segment-system.test.js +627 -0
- package/dist/__tests__/segment-system.test.js.map +1 -0
- package/dist/__tests__/static-handler-types.test.d.ts +8 -0
- package/dist/__tests__/static-handler-types.test.d.ts.map +1 -0
- package/dist/__tests__/static-handler-types.test.js +63 -0
- package/dist/__tests__/static-handler-types.test.js.map +1 -0
- package/dist/__tests__/urls.test.d.ts +2 -0
- package/dist/__tests__/urls.test.d.ts.map +1 -0
- package/dist/__tests__/urls.test.js +421 -0
- package/dist/__tests__/urls.test.js.map +1 -0
- package/dist/__tests__/use-mount.test.d.ts +2 -0
- package/dist/__tests__/use-mount.test.d.ts.map +1 -0
- package/dist/__tests__/use-mount.test.js +35 -0
- package/dist/__tests__/use-mount.test.js.map +1 -0
- package/dist/bin/rango.d.ts +2 -0
- package/dist/bin/rango.d.ts.map +1 -0
- package/dist/bin/rango.js +195 -52
- package/dist/bin/rango.js.map +1 -0
- package/dist/browser/event-controller.d.ts +191 -0
- package/dist/browser/event-controller.d.ts.map +1 -0
- package/dist/browser/event-controller.js +559 -0
- package/dist/browser/event-controller.js.map +1 -0
- package/dist/browser/index.d.ts +2 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +14 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/link-interceptor.d.ts +38 -0
- package/dist/browser/link-interceptor.d.ts.map +1 -0
- package/dist/browser/link-interceptor.js +99 -0
- package/dist/browser/link-interceptor.js.map +1 -0
- package/dist/browser/logging.d.ts +10 -0
- package/dist/browser/logging.d.ts.map +1 -0
- package/dist/browser/logging.js +29 -0
- package/dist/browser/logging.js.map +1 -0
- package/dist/browser/lru-cache.d.ts +17 -0
- package/dist/browser/lru-cache.d.ts.map +1 -0
- package/dist/browser/lru-cache.js +50 -0
- package/dist/browser/lru-cache.js.map +1 -0
- package/dist/browser/merge-segment-loaders.d.ts +39 -0
- package/dist/browser/merge-segment-loaders.d.ts.map +1 -0
- package/dist/browser/merge-segment-loaders.js +102 -0
- package/dist/browser/merge-segment-loaders.js.map +1 -0
- package/dist/browser/navigation-bridge.d.ts +102 -0
- package/dist/browser/navigation-bridge.d.ts.map +1 -0
- package/dist/browser/navigation-bridge.js +708 -0
- package/dist/browser/navigation-bridge.js.map +1 -0
- package/dist/browser/navigation-client.d.ts +25 -0
- package/dist/browser/navigation-client.d.ts.map +1 -0
- package/dist/browser/navigation-client.js +157 -0
- package/dist/browser/navigation-client.js.map +1 -0
- package/dist/browser/navigation-store.d.ts +101 -0
- package/dist/browser/navigation-store.d.ts.map +1 -0
- package/dist/browser/navigation-store.js +625 -0
- package/dist/browser/navigation-store.js.map +1 -0
- package/dist/browser/partial-update.d.ts +75 -0
- package/dist/browser/partial-update.d.ts.map +1 -0
- package/dist/browser/partial-update.js +426 -0
- package/dist/browser/partial-update.js.map +1 -0
- package/dist/browser/react/Link.d.ts +86 -0
- package/dist/browser/react/Link.d.ts.map +1 -0
- package/dist/browser/react/Link.js +128 -0
- package/dist/browser/react/Link.js.map +1 -0
- package/dist/browser/react/NavigationProvider.d.ts +63 -0
- package/dist/browser/react/NavigationProvider.d.ts.map +1 -0
- package/dist/browser/react/NavigationProvider.js +216 -0
- package/dist/browser/react/NavigationProvider.js.map +1 -0
- package/dist/browser/react/ScrollRestoration.d.ts +75 -0
- package/dist/browser/react/ScrollRestoration.d.ts.map +1 -0
- package/dist/browser/react/ScrollRestoration.js +57 -0
- package/dist/browser/react/ScrollRestoration.js.map +1 -0
- package/dist/browser/react/context.d.ts +46 -0
- package/dist/browser/react/context.d.ts.map +1 -0
- package/dist/browser/react/context.js +10 -0
- package/dist/browser/react/context.js.map +1 -0
- package/dist/browser/react/index.d.ts +11 -0
- package/dist/browser/react/index.d.ts.map +1 -0
- package/dist/browser/react/index.js +22 -0
- package/dist/browser/react/index.js.map +1 -0
- package/dist/browser/react/location-state-shared.d.ts +63 -0
- package/dist/browser/react/location-state-shared.d.ts.map +1 -0
- package/dist/browser/react/location-state-shared.js +81 -0
- package/dist/browser/react/location-state-shared.js.map +1 -0
- package/dist/browser/react/location-state.d.ts +23 -0
- package/dist/browser/react/location-state.d.ts.map +1 -0
- package/dist/browser/react/location-state.js +29 -0
- package/dist/browser/react/location-state.js.map +1 -0
- package/dist/browser/react/mount-context.d.ts +24 -0
- package/dist/browser/react/mount-context.d.ts.map +1 -0
- package/dist/browser/react/mount-context.js +24 -0
- package/dist/browser/react/mount-context.js.map +1 -0
- package/dist/browser/react/use-action.d.ts +64 -0
- package/dist/browser/react/use-action.d.ts.map +1 -0
- package/dist/browser/react/use-action.js +134 -0
- package/dist/browser/react/use-action.js.map +1 -0
- package/dist/browser/react/use-client-cache.d.ts +41 -0
- package/dist/browser/react/use-client-cache.d.ts.map +1 -0
- package/{src/browser/react/use-client-cache.ts → dist/browser/react/use-client-cache.js} +9 -28
- package/dist/browser/react/use-client-cache.js.map +1 -0
- package/dist/browser/react/use-handle.d.ts +31 -0
- package/dist/browser/react/use-handle.d.ts.map +1 -0
- package/dist/browser/react/use-handle.js +144 -0
- package/dist/browser/react/use-handle.js.map +1 -0
- package/dist/browser/react/use-href.d.ts +33 -0
- package/dist/browser/react/use-href.d.ts.map +1 -0
- package/dist/browser/react/use-href.js +39 -0
- package/dist/browser/react/use-href.js.map +1 -0
- package/dist/browser/react/use-link-status.d.ts +37 -0
- package/dist/browser/react/use-link-status.d.ts.map +1 -0
- package/dist/browser/react/use-link-status.js +99 -0
- package/dist/browser/react/use-link-status.js.map +1 -0
- package/dist/browser/react/use-mount.d.ts +25 -0
- package/dist/browser/react/use-mount.d.ts.map +1 -0
- package/dist/browser/react/use-mount.js +30 -0
- package/dist/browser/react/use-mount.js.map +1 -0
- package/dist/browser/react/use-navigation.d.ts +27 -0
- package/dist/browser/react/use-navigation.d.ts.map +1 -0
- package/dist/browser/react/use-navigation.js +87 -0
- package/dist/browser/react/use-navigation.js.map +1 -0
- package/dist/browser/react/use-segments.d.ts +38 -0
- package/dist/browser/react/use-segments.d.ts.map +1 -0
- package/dist/browser/react/use-segments.js +130 -0
- package/dist/browser/react/use-segments.js.map +1 -0
- package/dist/browser/request-controller.d.ts +26 -0
- package/dist/browser/request-controller.d.ts.map +1 -0
- package/dist/browser/request-controller.js +147 -0
- package/dist/browser/request-controller.js.map +1 -0
- package/dist/browser/rsc-router.d.ts +129 -0
- package/dist/browser/rsc-router.d.ts.map +1 -0
- package/dist/browser/rsc-router.js +195 -0
- package/dist/browser/rsc-router.js.map +1 -0
- package/dist/browser/scroll-restoration.d.ts +93 -0
- package/dist/browser/scroll-restoration.d.ts.map +1 -0
- package/dist/browser/scroll-restoration.js +321 -0
- package/dist/browser/scroll-restoration.js.map +1 -0
- package/dist/browser/segment-structure-assert.d.ts +17 -0
- package/dist/browser/segment-structure-assert.d.ts.map +1 -0
- package/dist/browser/segment-structure-assert.js +59 -0
- package/dist/browser/segment-structure-assert.js.map +1 -0
- package/dist/browser/server-action-bridge.d.ts +26 -0
- package/dist/browser/server-action-bridge.d.ts.map +1 -0
- package/dist/browser/server-action-bridge.js +668 -0
- package/dist/browser/server-action-bridge.js.map +1 -0
- package/dist/browser/shallow.d.ts +12 -0
- package/dist/browser/shallow.d.ts.map +1 -0
- package/dist/browser/shallow.js +34 -0
- package/dist/browser/shallow.js.map +1 -0
- package/dist/browser/types.d.ts +369 -0
- package/dist/browser/types.d.ts.map +1 -0
- package/dist/browser/types.js +2 -0
- package/dist/browser/types.js.map +1 -0
- package/dist/build/__tests__/generate-cli.test.d.ts +2 -0
- package/dist/build/__tests__/generate-cli.test.d.ts.map +1 -0
- package/dist/build/__tests__/generate-cli.test.js +237 -0
- package/dist/build/__tests__/generate-cli.test.js.map +1 -0
- package/dist/build/__tests__/generate-manifest.test.d.ts +2 -0
- package/dist/build/__tests__/generate-manifest.test.d.ts.map +1 -0
- package/dist/build/__tests__/generate-manifest.test.js +119 -0
- package/dist/build/__tests__/generate-manifest.test.js.map +1 -0
- package/dist/build/__tests__/generate-route-types.test.d.ts +2 -0
- package/dist/build/__tests__/generate-route-types.test.d.ts.map +1 -0
- package/dist/build/__tests__/generate-route-types.test.js +620 -0
- package/dist/build/__tests__/generate-route-types.test.js.map +1 -0
- package/dist/build/__tests__/per-router-manifest.test.d.ts +2 -0
- package/dist/build/__tests__/per-router-manifest.test.d.ts.map +1 -0
- package/dist/build/__tests__/per-router-manifest.test.js +308 -0
- package/dist/build/__tests__/per-router-manifest.test.js.map +1 -0
- package/dist/build/generate-manifest.d.ts +81 -0
- package/dist/build/generate-manifest.d.ts.map +1 -0
- package/dist/build/generate-manifest.js +276 -0
- package/dist/build/generate-manifest.js.map +1 -0
- package/dist/build/generate-route-types.d.ts +115 -0
- package/dist/build/generate-route-types.d.ts.map +1 -0
- package/dist/build/generate-route-types.js +740 -0
- package/dist/build/generate-route-types.js.map +1 -0
- package/dist/build/index.d.ts +21 -0
- package/dist/build/index.d.ts.map +1 -0
- package/dist/build/index.js +21 -0
- package/dist/build/index.js.map +1 -0
- package/dist/build/route-trie.d.ts +71 -0
- package/dist/build/route-trie.d.ts.map +1 -0
- package/dist/build/route-trie.js +175 -0
- package/dist/build/route-trie.js.map +1 -0
- package/dist/cache/__tests__/cache-scope.test.d.ts +2 -0
- package/dist/cache/__tests__/cache-scope.test.d.ts.map +1 -0
- package/dist/cache/__tests__/cache-scope.test.js +208 -0
- package/dist/cache/__tests__/cache-scope.test.js.map +1 -0
- package/dist/cache/__tests__/document-cache.test.d.ts +2 -0
- package/dist/cache/__tests__/document-cache.test.d.ts.map +1 -0
- package/dist/cache/__tests__/document-cache.test.js +345 -0
- package/dist/cache/__tests__/document-cache.test.js.map +1 -0
- package/dist/cache/__tests__/memory-segment-store.test.d.ts +2 -0
- package/dist/cache/__tests__/memory-segment-store.test.d.ts.map +1 -0
- package/dist/cache/__tests__/memory-segment-store.test.js +425 -0
- package/dist/cache/__tests__/memory-segment-store.test.js.map +1 -0
- package/dist/cache/__tests__/memory-store.test.d.ts +2 -0
- package/dist/cache/__tests__/memory-store.test.d.ts.map +1 -0
- package/dist/cache/__tests__/memory-store.test.js +367 -0
- package/dist/cache/__tests__/memory-store.test.js.map +1 -0
- package/dist/cache/cache-scope.d.ts +102 -0
- package/dist/cache/cache-scope.d.ts.map +1 -0
- package/dist/cache/cache-scope.js +440 -0
- package/dist/cache/cache-scope.js.map +1 -0
- package/dist/cache/cf/__tests__/cf-cache-store.test.d.ts +2 -0
- package/dist/cache/cf/__tests__/cf-cache-store.test.d.ts.map +1 -0
- package/dist/cache/cf/__tests__/cf-cache-store.test.js +330 -0
- package/dist/cache/cf/__tests__/cf-cache-store.test.js.map +1 -0
- package/dist/cache/cf/cf-cache-store.d.ts +165 -0
- package/dist/cache/cf/cf-cache-store.d.ts.map +1 -0
- package/dist/cache/cf/cf-cache-store.js +242 -0
- package/dist/cache/cf/cf-cache-store.js.map +1 -0
- package/dist/cache/cf/index.d.ts +14 -0
- package/dist/cache/cf/index.d.ts.map +1 -0
- package/dist/cache/cf/index.js +17 -0
- package/dist/cache/cf/index.js.map +1 -0
- package/dist/cache/document-cache.d.ts +64 -0
- package/dist/cache/document-cache.d.ts.map +1 -0
- package/dist/cache/document-cache.js +228 -0
- package/dist/cache/document-cache.js.map +1 -0
- package/dist/cache/index.d.ts +19 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +21 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/memory-segment-store.d.ts +110 -0
- package/dist/cache/memory-segment-store.d.ts.map +1 -0
- package/dist/cache/memory-segment-store.js +117 -0
- package/dist/cache/memory-segment-store.js.map +1 -0
- package/dist/cache/memory-store.d.ts +41 -0
- package/dist/cache/memory-store.d.ts.map +1 -0
- package/dist/cache/memory-store.js +191 -0
- package/dist/cache/memory-store.js.map +1 -0
- package/dist/cache/types.d.ts +317 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +12 -0
- package/dist/cache/types.js.map +1 -0
- package/dist/client.d.ts +248 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +367 -0
- package/dist/client.js.map +1 -0
- package/dist/client.rsc.d.ts +26 -0
- package/dist/client.rsc.d.ts.map +1 -0
- package/dist/client.rsc.js +46 -0
- package/dist/client.rsc.js.map +1 -0
- package/dist/component-utils.d.ts +36 -0
- package/dist/component-utils.d.ts.map +1 -0
- package/dist/component-utils.js +61 -0
- package/dist/component-utils.js.map +1 -0
- package/dist/components/DefaultDocument.d.ts +13 -0
- package/dist/components/DefaultDocument.d.ts.map +1 -0
- package/dist/components/DefaultDocument.js +15 -0
- package/dist/components/DefaultDocument.js.map +1 -0
- package/dist/debug.d.ts +58 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +157 -0
- package/dist/debug.js.map +1 -0
- package/dist/default-error-boundary.d.ts +11 -0
- package/dist/default-error-boundary.d.ts.map +1 -0
- package/dist/default-error-boundary.js +45 -0
- package/dist/default-error-boundary.js.map +1 -0
- package/dist/deps/browser.d.ts +2 -0
- package/dist/deps/browser.d.ts.map +1 -0
- package/dist/deps/browser.js +3 -0
- package/dist/deps/browser.js.map +1 -0
- package/dist/deps/html-stream-client.d.ts +2 -0
- package/dist/deps/html-stream-client.d.ts.map +1 -0
- package/dist/deps/html-stream-client.js +3 -0
- package/dist/deps/html-stream-client.js.map +1 -0
- package/dist/deps/html-stream-server.d.ts +2 -0
- package/dist/deps/html-stream-server.d.ts.map +1 -0
- package/dist/deps/html-stream-server.js +3 -0
- package/dist/deps/html-stream-server.js.map +1 -0
- package/dist/deps/rsc.d.ts +2 -0
- package/dist/deps/rsc.d.ts.map +1 -0
- package/dist/deps/rsc.js +4 -0
- package/dist/deps/rsc.js.map +1 -0
- package/dist/deps/ssr.d.ts +2 -0
- package/dist/deps/ssr.d.ts.map +1 -0
- package/dist/deps/ssr.js +3 -0
- package/dist/deps/ssr.js.map +1 -0
- package/dist/errors.d.ts +174 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +241 -0
- package/dist/errors.js.map +1 -0
- package/dist/handle.d.ts +78 -0
- package/dist/handle.d.ts.map +1 -0
- package/dist/handle.js +82 -0
- package/dist/handle.js.map +1 -0
- package/dist/handles/MetaTags.d.ts +14 -0
- package/dist/handles/MetaTags.d.ts.map +1 -0
- package/dist/handles/MetaTags.js +136 -0
- package/dist/handles/MetaTags.js.map +1 -0
- package/{src/handles/index.ts → dist/handles/index.d.ts} +1 -2
- package/dist/handles/index.d.ts.map +1 -0
- package/dist/handles/index.js +6 -0
- package/dist/handles/index.js.map +1 -0
- package/dist/handles/meta.d.ts +39 -0
- package/dist/handles/meta.d.ts.map +1 -0
- package/dist/handles/meta.js +202 -0
- package/dist/handles/meta.js.map +1 -0
- package/dist/host/__tests__/errors.test.d.ts +2 -0
- package/dist/host/__tests__/errors.test.d.ts.map +1 -0
- package/dist/host/__tests__/errors.test.js +76 -0
- package/dist/host/__tests__/errors.test.js.map +1 -0
- package/dist/host/__tests__/pattern-comprehensive.test.d.ts +2 -0
- package/dist/host/__tests__/pattern-comprehensive.test.d.ts.map +1 -0
- package/dist/host/__tests__/pattern-comprehensive.test.js +732 -0
- package/dist/host/__tests__/pattern-comprehensive.test.js.map +1 -0
- package/dist/host/__tests__/pattern-matcher.test.d.ts +2 -0
- package/dist/host/__tests__/pattern-matcher.test.d.ts.map +1 -0
- package/dist/host/__tests__/pattern-matcher.test.js +251 -0
- package/dist/host/__tests__/pattern-matcher.test.js.map +1 -0
- package/dist/host/__tests__/router.test.d.ts +2 -0
- package/dist/host/__tests__/router.test.d.ts.map +1 -0
- package/dist/host/__tests__/router.test.js +241 -0
- package/dist/host/__tests__/router.test.js.map +1 -0
- package/dist/host/__tests__/testing.test.d.ts +2 -0
- package/dist/host/__tests__/testing.test.d.ts.map +1 -0
- package/dist/host/__tests__/testing.test.js +64 -0
- package/dist/host/__tests__/testing.test.js.map +1 -0
- package/dist/host/__tests__/utils.test.d.ts +2 -0
- package/dist/host/__tests__/utils.test.d.ts.map +1 -0
- package/dist/host/__tests__/utils.test.js +29 -0
- package/dist/host/__tests__/utils.test.js.map +1 -0
- package/dist/host/cookie-handler.d.ts +34 -0
- package/dist/host/cookie-handler.d.ts.map +1 -0
- package/dist/host/cookie-handler.js +124 -0
- package/dist/host/cookie-handler.js.map +1 -0
- package/dist/host/errors.d.ts +56 -0
- package/dist/host/errors.d.ts.map +1 -0
- package/dist/host/errors.js +79 -0
- package/dist/host/errors.js.map +1 -0
- package/dist/host/index.d.ts +29 -0
- package/dist/host/index.d.ts.map +1 -0
- package/dist/host/index.js +32 -0
- package/dist/host/index.js.map +1 -0
- package/dist/host/pattern-matcher.d.ts +36 -0
- package/dist/host/pattern-matcher.d.ts.map +1 -0
- package/dist/host/pattern-matcher.js +172 -0
- package/dist/host/pattern-matcher.js.map +1 -0
- package/dist/host/router.d.ts +26 -0
- package/dist/host/router.d.ts.map +1 -0
- package/dist/host/router.js +218 -0
- package/dist/host/router.js.map +1 -0
- package/dist/host/testing.d.ts +36 -0
- package/dist/host/testing.d.ts.map +1 -0
- package/dist/host/testing.js +55 -0
- package/dist/host/testing.js.map +1 -0
- package/dist/host/types.d.ts +115 -0
- package/dist/host/types.d.ts.map +1 -0
- package/dist/host/types.js +7 -0
- package/dist/host/types.js.map +1 -0
- package/dist/host/utils.d.ts +21 -0
- package/dist/host/utils.d.ts.map +1 -0
- package/dist/host/utils.js +23 -0
- package/dist/host/utils.js.map +1 -0
- package/dist/href-client.d.ts +131 -0
- package/dist/href-client.d.ts.map +1 -0
- package/dist/href-client.js +64 -0
- package/dist/href-client.js.map +1 -0
- package/dist/href-context.d.ts +29 -0
- package/dist/href-context.d.ts.map +1 -0
- package/dist/href-context.js +21 -0
- package/dist/href-context.js.map +1 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/index.rsc.d.ts +32 -0
- package/dist/index.rsc.d.ts.map +1 -0
- package/dist/index.rsc.js +40 -0
- package/dist/index.rsc.js.map +1 -0
- package/dist/internal-debug.d.ts +2 -0
- package/dist/internal-debug.d.ts.map +1 -0
- package/dist/internal-debug.js +5 -0
- package/dist/internal-debug.js.map +1 -0
- package/dist/loader.d.ts +14 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +20 -0
- package/dist/loader.js.map +1 -0
- package/dist/loader.rsc.d.ts +19 -0
- package/dist/loader.rsc.d.ts.map +1 -0
- package/dist/loader.rsc.js +99 -0
- package/dist/loader.rsc.js.map +1 -0
- package/dist/network-error-thrower.d.ts +17 -0
- package/dist/network-error-thrower.d.ts.map +1 -0
- package/dist/network-error-thrower.js +14 -0
- package/dist/network-error-thrower.js.map +1 -0
- package/dist/outlet-context.d.ts +13 -0
- package/dist/outlet-context.d.ts.map +1 -0
- package/dist/outlet-context.js +3 -0
- package/dist/outlet-context.js.map +1 -0
- package/dist/prerender/__tests__/param-hash.test.d.ts +2 -0
- package/dist/prerender/__tests__/param-hash.test.d.ts.map +1 -0
- package/dist/prerender/__tests__/param-hash.test.js +148 -0
- package/dist/prerender/__tests__/param-hash.test.js.map +1 -0
- package/dist/prerender/param-hash.d.ts +16 -0
- package/dist/prerender/param-hash.d.ts.map +1 -0
- package/dist/prerender/param-hash.js +36 -0
- package/dist/prerender/param-hash.js.map +1 -0
- package/dist/prerender/store.d.ts +38 -0
- package/dist/prerender/store.d.ts.map +1 -0
- package/dist/prerender/store.js +61 -0
- package/dist/prerender/store.js.map +1 -0
- package/dist/prerender.d.ts +66 -0
- package/dist/prerender.d.ts.map +1 -0
- package/dist/prerender.js +57 -0
- package/dist/prerender.js.map +1 -0
- package/dist/reverse.d.ts +196 -0
- package/dist/reverse.d.ts.map +1 -0
- package/dist/reverse.js +78 -0
- package/dist/reverse.js.map +1 -0
- package/dist/root-error-boundary.d.ts +33 -0
- package/dist/root-error-boundary.d.ts.map +1 -0
- package/dist/root-error-boundary.js +165 -0
- package/dist/root-error-boundary.js.map +1 -0
- package/dist/route-content-wrapper.d.ts +46 -0
- package/dist/route-content-wrapper.d.ts.map +1 -0
- package/dist/route-content-wrapper.js +77 -0
- package/dist/route-content-wrapper.js.map +1 -0
- package/dist/route-definition.d.ts +421 -0
- package/dist/route-definition.d.ts.map +1 -0
- package/dist/route-definition.js +868 -0
- package/dist/route-definition.js.map +1 -0
- package/dist/route-map-builder.d.ts +155 -0
- package/dist/route-map-builder.d.ts.map +1 -0
- package/dist/route-map-builder.js +237 -0
- package/dist/route-map-builder.js.map +1 -0
- package/dist/route-types.d.ts +165 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +7 -0
- package/dist/route-types.js.map +1 -0
- package/dist/router/__tests__/handler-context.test.d.ts +2 -0
- package/dist/router/__tests__/handler-context.test.d.ts.map +1 -0
- package/dist/router/__tests__/handler-context.test.js +65 -0
- package/dist/router/__tests__/handler-context.test.js.map +1 -0
- package/dist/router/__tests__/loader-cycle-detection.test.d.ts +2 -0
- package/dist/router/__tests__/loader-cycle-detection.test.d.ts.map +1 -0
- package/dist/router/__tests__/loader-cycle-detection.test.js +221 -0
- package/dist/router/__tests__/loader-cycle-detection.test.js.map +1 -0
- package/dist/router/__tests__/match-context.test.d.ts +2 -0
- package/dist/router/__tests__/match-context.test.d.ts.map +1 -0
- package/dist/router/__tests__/match-context.test.js +92 -0
- package/dist/router/__tests__/match-context.test.js.map +1 -0
- package/dist/router/__tests__/match-pipelines.test.d.ts +2 -0
- package/dist/router/__tests__/match-pipelines.test.d.ts.map +1 -0
- package/dist/router/__tests__/match-pipelines.test.js +417 -0
- package/dist/router/__tests__/match-pipelines.test.js.map +1 -0
- package/dist/router/__tests__/match-result.test.d.ts +2 -0
- package/dist/router/__tests__/match-result.test.d.ts.map +1 -0
- package/dist/router/__tests__/match-result.test.js +457 -0
- package/dist/router/__tests__/match-result.test.js.map +1 -0
- package/dist/router/__tests__/on-error.test.d.ts +2 -0
- package/dist/router/__tests__/on-error.test.d.ts.map +1 -0
- package/dist/router/__tests__/on-error.test.js +678 -0
- package/dist/router/__tests__/on-error.test.js.map +1 -0
- package/dist/router/__tests__/pattern-matching.test.d.ts +2 -0
- package/dist/router/__tests__/pattern-matching.test.d.ts.map +1 -0
- package/dist/router/__tests__/pattern-matching.test.js +629 -0
- package/dist/router/__tests__/pattern-matching.test.js.map +1 -0
- package/dist/router/__tests__/segment-resolution-parallel-loading.test.d.ts +2 -0
- package/dist/router/__tests__/segment-resolution-parallel-loading.test.d.ts.map +1 -0
- package/dist/router/__tests__/segment-resolution-parallel-loading.test.js +155 -0
- package/dist/router/__tests__/segment-resolution-parallel-loading.test.js.map +1 -0
- package/dist/router/error-handling.d.ts +77 -0
- package/dist/router/error-handling.d.ts.map +1 -0
- package/dist/router/error-handling.js +202 -0
- package/dist/router/error-handling.js.map +1 -0
- package/dist/router/handler-context.d.ts +20 -0
- package/dist/router/handler-context.d.ts.map +1 -0
- package/dist/router/handler-context.js +198 -0
- package/dist/router/handler-context.js.map +1 -0
- package/dist/router/intercept-resolution.d.ts +66 -0
- package/dist/router/intercept-resolution.d.ts.map +1 -0
- package/dist/router/intercept-resolution.js +246 -0
- package/dist/router/intercept-resolution.js.map +1 -0
- package/dist/router/loader-resolution.d.ts +64 -0
- package/dist/router/loader-resolution.d.ts.map +1 -0
- package/dist/router/loader-resolution.js +284 -0
- package/dist/router/loader-resolution.js.map +1 -0
- package/dist/router/logging.d.ts +15 -0
- package/dist/router/logging.d.ts.map +1 -0
- package/dist/router/logging.js +99 -0
- package/dist/router/logging.js.map +1 -0
- package/dist/router/manifest.d.ts +22 -0
- package/dist/router/manifest.d.ts.map +1 -0
- package/dist/router/manifest.js +181 -0
- package/dist/router/manifest.js.map +1 -0
- package/dist/router/match-api.d.ts +35 -0
- package/dist/router/match-api.d.ts.map +1 -0
- package/dist/router/match-api.js +406 -0
- package/dist/router/match-api.js.map +1 -0
- package/dist/router/match-context.d.ts +206 -0
- package/dist/router/match-context.d.ts.map +1 -0
- package/dist/router/match-context.js +17 -0
- package/dist/router/match-context.js.map +1 -0
- package/dist/router/match-middleware/background-revalidation.d.ts +127 -0
- package/dist/router/match-middleware/background-revalidation.d.ts.map +1 -0
- package/dist/router/match-middleware/background-revalidation.js +75 -0
- package/dist/router/match-middleware/background-revalidation.js.map +1 -0
- package/dist/router/match-middleware/cache-lookup.d.ts +112 -0
- package/dist/router/match-middleware/cache-lookup.d.ts.map +1 -0
- package/dist/router/match-middleware/cache-lookup.js +257 -0
- package/dist/router/match-middleware/cache-lookup.js.map +1 -0
- package/dist/router/match-middleware/cache-store.d.ts +113 -0
- package/dist/router/match-middleware/cache-store.d.ts.map +1 -0
- package/dist/router/match-middleware/cache-store.js +108 -0
- package/dist/router/match-middleware/cache-store.js.map +1 -0
- package/dist/router/match-middleware/index.d.ts +81 -0
- package/dist/router/match-middleware/index.d.ts.map +1 -0
- package/dist/router/match-middleware/index.js +80 -0
- package/dist/router/match-middleware/index.js.map +1 -0
- package/dist/router/match-middleware/intercept-resolution.d.ts +117 -0
- package/dist/router/match-middleware/intercept-resolution.d.ts.map +1 -0
- package/dist/router/match-middleware/intercept-resolution.js +134 -0
- package/dist/router/match-middleware/intercept-resolution.js.map +1 -0
- package/dist/router/match-middleware/segment-resolution.d.ts +99 -0
- package/dist/router/match-middleware/segment-resolution.d.ts.map +1 -0
- package/dist/router/match-middleware/segment-resolution.js +53 -0
- package/dist/router/match-middleware/segment-resolution.js.map +1 -0
- package/dist/router/match-pipelines.d.ts +147 -0
- package/dist/router/match-pipelines.d.ts.map +1 -0
- package/dist/router/match-pipelines.js +82 -0
- package/dist/router/match-pipelines.js.map +1 -0
- package/dist/router/match-result.d.ts +126 -0
- package/dist/router/match-result.d.ts.map +1 -0
- package/dist/router/match-result.js +93 -0
- package/dist/router/match-result.js.map +1 -0
- package/dist/router/metrics.d.ts +20 -0
- package/dist/router/metrics.d.ts.map +1 -0
- package/dist/router/metrics.js +47 -0
- package/dist/router/metrics.js.map +1 -0
- package/dist/router/middleware.d.ts +249 -0
- package/dist/router/middleware.d.ts.map +1 -0
- package/dist/router/middleware.js +434 -0
- package/dist/router/middleware.js.map +1 -0
- package/dist/router/middleware.test.d.ts +2 -0
- package/dist/router/middleware.test.d.ts.map +1 -0
- package/dist/router/middleware.test.js +816 -0
- package/dist/router/middleware.test.js.map +1 -0
- package/dist/router/pattern-matching.d.ts +149 -0
- package/dist/router/pattern-matching.d.ts.map +1 -0
- package/dist/router/pattern-matching.js +349 -0
- package/dist/router/pattern-matching.js.map +1 -0
- package/dist/router/revalidation.d.ts +44 -0
- package/dist/router/revalidation.d.ts.map +1 -0
- package/dist/router/revalidation.js +147 -0
- package/dist/router/revalidation.js.map +1 -0
- package/dist/router/router-context.d.ts +135 -0
- package/dist/router/router-context.d.ts.map +1 -0
- package/dist/router/router-context.js +36 -0
- package/dist/router/router-context.js.map +1 -0
- package/dist/router/segment-resolution.d.ts +127 -0
- package/dist/router/segment-resolution.d.ts.map +1 -0
- package/dist/router/segment-resolution.js +919 -0
- package/dist/router/segment-resolution.js.map +1 -0
- package/dist/router/trie-matching.d.ts +40 -0
- package/dist/router/trie-matching.d.ts.map +1 -0
- package/dist/router/trie-matching.js +127 -0
- package/dist/router/trie-matching.js.map +1 -0
- package/dist/router/types.d.ts +136 -0
- package/dist/router/types.d.ts.map +1 -0
- package/dist/router/types.js +7 -0
- package/dist/router/types.js.map +1 -0
- package/dist/router.d.ts +753 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.gen.d.ts +6 -0
- package/dist/router.gen.d.ts.map +1 -0
- package/dist/router.gen.js +6 -0
- package/dist/router.gen.js.map +1 -0
- package/dist/router.js +1304 -0
- package/dist/router.js.map +1 -0
- package/dist/rsc/__tests__/helpers.test.d.ts +2 -0
- package/dist/rsc/__tests__/helpers.test.d.ts.map +1 -0
- package/dist/rsc/__tests__/helpers.test.js +140 -0
- package/dist/rsc/__tests__/helpers.test.js.map +1 -0
- package/dist/rsc/handler.d.ts +45 -0
- package/dist/rsc/handler.d.ts.map +1 -0
- package/dist/rsc/handler.js +1172 -0
- package/dist/rsc/handler.js.map +1 -0
- package/dist/rsc/helpers.d.ts +16 -0
- package/dist/rsc/helpers.d.ts.map +1 -0
- package/dist/rsc/helpers.js +55 -0
- package/dist/rsc/helpers.js.map +1 -0
- package/dist/rsc/index.d.ts +22 -0
- package/dist/rsc/index.d.ts.map +1 -0
- package/dist/rsc/index.js +23 -0
- package/dist/rsc/index.js.map +1 -0
- package/dist/rsc/nonce.d.ts +9 -0
- package/dist/rsc/nonce.d.ts.map +1 -0
- package/dist/rsc/nonce.js +18 -0
- package/dist/rsc/nonce.js.map +1 -0
- package/dist/rsc/types.d.ts +206 -0
- package/dist/rsc/types.d.ts.map +1 -0
- package/dist/rsc/types.js +8 -0
- package/dist/rsc/types.js.map +1 -0
- package/dist/search-params.d.ts +103 -0
- package/dist/search-params.d.ts.map +1 -0
- package/dist/search-params.js +74 -0
- package/dist/search-params.js.map +1 -0
- package/dist/segment-system.d.ts +75 -0
- package/dist/segment-system.d.ts.map +1 -0
- package/dist/segment-system.js +336 -0
- package/dist/segment-system.js.map +1 -0
- package/dist/server/context.d.ts +245 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +197 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/fetchable-loader-store.d.ts +18 -0
- package/dist/server/fetchable-loader-store.d.ts.map +1 -0
- package/dist/server/fetchable-loader-store.js +18 -0
- package/dist/server/fetchable-loader-store.js.map +1 -0
- package/dist/server/handle-store.d.ts +85 -0
- package/dist/server/handle-store.d.ts.map +1 -0
- package/dist/server/handle-store.js +142 -0
- package/dist/server/handle-store.js.map +1 -0
- package/dist/server/loader-registry.d.ts +55 -0
- package/dist/server/loader-registry.d.ts.map +1 -0
- package/dist/server/loader-registry.js +132 -0
- package/dist/server/loader-registry.js.map +1 -0
- package/dist/server/request-context.d.ts +226 -0
- package/dist/server/request-context.d.ts.map +1 -0
- package/dist/server/request-context.js +290 -0
- package/dist/server/request-context.js.map +1 -0
- package/dist/server/root-layout.d.ts +4 -0
- package/dist/server/root-layout.d.ts.map +1 -0
- package/dist/server/root-layout.js +5 -0
- package/dist/server/root-layout.js.map +1 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +20 -0
- package/dist/server.js.map +1 -0
- package/dist/ssr/__tests__/ssr-handler.test.d.ts +2 -0
- package/dist/ssr/__tests__/ssr-handler.test.d.ts.map +1 -0
- package/dist/ssr/__tests__/ssr-handler.test.js +132 -0
- package/dist/ssr/__tests__/ssr-handler.test.js.map +1 -0
- package/dist/ssr/index.d.ts +98 -0
- package/dist/ssr/index.d.ts.map +1 -0
- package/dist/ssr/index.js +158 -0
- package/dist/ssr/index.js.map +1 -0
- package/dist/static-handler.d.ts +50 -0
- package/dist/static-handler.d.ts.map +1 -0
- package/dist/static-handler.gen.d.ts +5 -0
- package/dist/static-handler.gen.d.ts.map +1 -0
- package/dist/static-handler.gen.js +5 -0
- package/dist/static-handler.gen.js.map +1 -0
- package/dist/static-handler.js +29 -0
- package/dist/static-handler.js.map +1 -0
- package/dist/testing/vitest.js +82 -0
- package/dist/theme/ThemeProvider.d.ts +20 -0
- package/dist/theme/ThemeProvider.d.ts.map +1 -0
- package/dist/theme/ThemeProvider.js +240 -0
- package/dist/theme/ThemeProvider.js.map +1 -0
- package/dist/theme/ThemeScript.d.ts +48 -0
- package/dist/theme/ThemeScript.d.ts.map +1 -0
- package/dist/theme/ThemeScript.js +13 -0
- package/dist/theme/ThemeScript.js.map +1 -0
- package/dist/theme/__tests__/theme.test.d.ts +2 -0
- package/dist/theme/__tests__/theme.test.d.ts.map +1 -0
- package/dist/theme/__tests__/theme.test.js +103 -0
- package/dist/theme/__tests__/theme.test.js.map +1 -0
- package/dist/theme/constants.d.ts +29 -0
- package/dist/theme/constants.d.ts.map +1 -0
- package/dist/theme/constants.js +48 -0
- package/dist/theme/constants.js.map +1 -0
- package/dist/theme/index.d.ts +31 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +36 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/theme-context.d.ts +40 -0
- package/dist/theme/theme-context.d.ts.map +1 -0
- package/dist/theme/theme-context.js +60 -0
- package/dist/theme/theme-context.js.map +1 -0
- package/dist/theme/theme-script.d.ts +27 -0
- package/dist/theme/theme-script.d.ts.map +1 -0
- package/dist/theme/theme-script.js +147 -0
- package/dist/theme/theme-script.js.map +1 -0
- package/dist/theme/types.d.ts +163 -0
- package/dist/theme/types.d.ts.map +1 -0
- package/dist/theme/types.js +11 -0
- package/dist/theme/types.js.map +1 -0
- package/dist/theme/use-theme.d.ts +12 -0
- package/dist/theme/use-theme.d.ts.map +1 -0
- package/dist/theme/use-theme.js +40 -0
- package/dist/theme/use-theme.js.map +1 -0
- package/dist/types.d.ts +1479 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/dist/urls.d.ts +441 -0
- package/dist/urls.d.ts.map +1 -0
- package/dist/urls.gen.d.ts +8 -0
- package/dist/urls.gen.d.ts.map +1 -0
- package/dist/urls.gen.js +8 -0
- package/dist/urls.gen.js.map +1 -0
- package/dist/urls.js +443 -0
- package/dist/urls.js.map +1 -0
- package/dist/use-loader.d.ts +127 -0
- package/dist/use-loader.d.ts.map +1 -0
- package/dist/use-loader.js +237 -0
- package/dist/use-loader.js.map +1 -0
- package/dist/vite/__tests__/ast-handler-extract.test.d.ts +2 -0
- package/dist/vite/__tests__/ast-handler-extract.test.d.ts.map +1 -0
- package/dist/vite/__tests__/ast-handler-extract.test.js +294 -0
- package/dist/vite/__tests__/ast-handler-extract.test.js.map +1 -0
- package/dist/vite/__tests__/expose-id-utils.test.d.ts +2 -0
- package/dist/vite/__tests__/expose-id-utils.test.d.ts.map +1 -0
- package/dist/vite/__tests__/expose-id-utils.test.js +224 -0
- package/dist/vite/__tests__/expose-id-utils.test.js.map +1 -0
- package/dist/vite/__tests__/expose-internal-ids.test.d.ts +2 -0
- package/dist/vite/__tests__/expose-internal-ids.test.d.ts.map +1 -0
- package/dist/vite/__tests__/expose-internal-ids.test.js +647 -0
- package/dist/vite/__tests__/expose-internal-ids.test.js.map +1 -0
- package/dist/vite/__tests__/expose-router-id.test.d.ts +2 -0
- package/dist/vite/__tests__/expose-router-id.test.d.ts.map +1 -0
- package/dist/vite/__tests__/expose-router-id.test.js +39 -0
- package/dist/vite/__tests__/expose-router-id.test.js.map +1 -0
- package/dist/vite/ast-handler-extract.d.ts +49 -0
- package/dist/vite/ast-handler-extract.d.ts.map +1 -0
- package/dist/vite/ast-handler-extract.js +249 -0
- package/dist/vite/ast-handler-extract.js.map +1 -0
- package/dist/vite/expose-action-id.d.ts +19 -0
- package/dist/vite/expose-action-id.d.ts.map +1 -0
- package/dist/vite/expose-action-id.js +250 -0
- package/dist/vite/expose-action-id.js.map +1 -0
- package/dist/vite/expose-id-utils.d.ts +69 -0
- package/dist/vite/expose-id-utils.d.ts.map +1 -0
- package/dist/vite/expose-id-utils.js +289 -0
- package/dist/vite/expose-id-utils.js.map +1 -0
- package/dist/vite/expose-internal-ids.d.ts +22 -0
- package/dist/vite/expose-internal-ids.d.ts.map +1 -0
- package/dist/vite/expose-internal-ids.js +886 -0
- package/dist/vite/expose-internal-ids.js.map +1 -0
- package/dist/vite/index.d.ts +149 -0
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +1165 -310
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/index.js.map +1 -0
- package/dist/vite/index.named-routes.gen.ts +103 -0
- package/dist/vite/package-resolution.d.ts +43 -0
- package/dist/vite/package-resolution.d.ts.map +1 -0
- package/dist/vite/package-resolution.js +112 -0
- package/dist/vite/package-resolution.js.map +1 -0
- package/dist/vite/virtual-entries.d.ts +25 -0
- package/dist/vite/virtual-entries.d.ts.map +1 -0
- package/dist/vite/virtual-entries.js +110 -0
- package/dist/vite/virtual-entries.js.map +1 -0
- package/package.json +71 -7
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +61 -1
- package/skills/bundle-analysis/SKILL.md +2 -2
- package/skills/cache-guide/SKILL.md +2 -2
- package/skills/caching/SKILL.md +92 -10
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +80 -3
- package/skills/intercept/SKILL.md +5 -3
- package/skills/layout/SKILL.md +11 -6
- package/skills/loader/SKILL.md +25 -19
- package/skills/middleware/SKILL.md +4 -2
- package/skills/migrate-nextjs/SKILL.md +3 -1
- package/skills/migrate-react-router/SKILL.md +4 -2
- package/skills/mime-routes/SKILL.md +2 -2
- package/skills/observability/SKILL.md +61 -4
- package/skills/parallel/SKILL.md +9 -4
- package/skills/prerender/SKILL.md +31 -10
- package/skills/rango/SKILL.md +31 -10
- package/skills/response-routes/SKILL.md +63 -45
- package/skills/route/SKILL.md +14 -2
- package/skills/router-setup/SKILL.md +27 -3
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +25 -1
- 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 +6 -5
- package/skills/use-cache/SKILL.md +10 -9
- package/skills/vercel/SKILL.md +128 -0
- package/src/__augment-tests__/augmented.check.ts +2 -3
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- 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 +115 -89
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +16 -52
- package/src/browser/navigation-client.ts +84 -69
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +25 -74
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/partial-update.ts +81 -127
- package/src/browser/prefetch/cache.ts +99 -72
- package/src/browser/prefetch/fetch.ts +176 -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 +32 -95
- 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 +14 -15
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- 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 +0 -2
- package/src/browser/react/use-router.ts +6 -4
- 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 +41 -4
- package/src/browser/rsc-router.tsx +43 -18
- package/src/browser/scroll-restoration.ts +8 -3
- package/src/browser/server-action-bridge.ts +194 -28
- package/src/browser/types.ts +40 -21
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/generate-route-types.ts +1 -1
- package/src/build/index.ts +8 -4
- 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 +12 -1
- package/src/build/route-types/include-resolution.ts +116 -22
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +15 -2
- package/src/build/route-types/router-processing.ts +77 -41
- package/src/build/route-types/source-scan.ts +105 -7
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +42 -62
- package/src/cache/cache-runtime.ts +126 -30
- package/src/cache/cache-scope.ts +118 -43
- package/src/cache/cache-tag.ts +7 -39
- 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 +1257 -537
- 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 -19
- package/src/cache/document-cache.ts +32 -32
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +20 -24
- package/src/cache/memory-segment-store.ts +3 -48
- 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 +29 -5
- package/src/cache/types.ts +14 -104
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +855 -0
- package/src/client.rsc.tsx +42 -22
- package/src/client.tsx +31 -58
- 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 +13 -1
- package/src/decode-loader-results.ts +18 -2
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +0 -3
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +28 -18
- package/src/handles/MetaTags.tsx +56 -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 +6 -0
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +7 -12
- package/src/index.rsc.ts +75 -8
- package/src/index.ts +60 -4
- package/src/internal-debug.ts +2 -4
- package/src/loader.rsc.ts +19 -9
- package/src/loader.ts +12 -4
- package/src/network-error-thrower.tsx +1 -6
- 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 +58 -3
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/response-utils.ts +25 -0
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +1 -44
- package/src/route-definition/dsl-helpers.ts +39 -21
- package/src/route-definition/helpers-types.ts +3 -3
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-map-builder.ts +0 -16
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +58 -23
- package/src/router/error-handling.ts +44 -17
- 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 +35 -2
- package/src/router/lazy-includes.ts +7 -44
- package/src/router/loader-resolution.ts +139 -73
- 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 +115 -194
- package/src/router/match-middleware/cache-store.ts +28 -32
- 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 +1 -52
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +175 -90
- 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 +101 -64
- package/src/router/request-classification.ts +25 -35
- package/src/router/revalidation.ts +75 -81
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +3 -28
- package/src/router/router-interfaces.ts +38 -7
- package/src/router/router-options.ts +111 -4
- package/src/router/segment-resolution/fresh.ts +66 -78
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +57 -23
- package/src/router/segment-resolution/revalidation.ts +212 -304
- 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.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 +1 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +87 -37
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +232 -153
- package/src/rsc/helpers.ts +97 -4
- package/src/rsc/index.ts +1 -4
- 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 +11 -15
- 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 +57 -147
- package/src/rsc/rsc-rendering.ts +64 -24
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +244 -81
- package/src/rsc/types.ts +15 -3
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +31 -26
- package/src/segment-loader-promise.ts +31 -4
- package/src/segment-system.tsx +89 -89
- package/src/server/context.ts +32 -0
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +52 -1
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +21 -42
- package/src/server/request-context.ts +189 -131
- 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 +3 -3
- package/src/types/error-types.ts +25 -89
- package/src/types/global-namespace.ts +15 -15
- package/src/types/handler-context.ts +44 -14
- package/src/types/index.ts +0 -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 +0 -13
- package/src/urls/include-helper.ts +0 -4
- package/src/urls/index.ts +1 -8
- package/src/urls/path-helper-types.ts +8 -2
- package/src/urls/path-helper.ts +0 -54
- package/src/urls/type-extraction.ts +33 -24
- package/src/urls/urls-function.ts +0 -13
- package/src/use-loader.tsx +0 -186
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/discover-routers.ts +33 -42
- package/src/vite/discovery/prerender-collection.ts +33 -46
- package/src/vite/discovery/state.ts +12 -1
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +143 -10
- package/src/vite/plugins/cjs-to-esm.ts +8 -12
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +85 -56
- package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
- package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
- package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
- package/src/vite/plugins/expose-internal-ids.ts +10 -1
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +21 -46
- package/src/vite/plugins/vercel-output.ts +295 -0
- package/src/vite/plugins/version-injector.ts +0 -20
- package/src/vite/plugins/version-plugin.ts +6 -66
- package/src/vite/plugins/virtual-entries.ts +70 -17
- package/src/vite/rango.ts +135 -134
- package/src/vite/router-discovery.ts +21 -8
- package/src/vite/utils/ast-handler-extract.ts +11 -20
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +4 -75
- 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 +8 -37
- package/src/browser/shallow.ts +0 -40
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -40,6 +40,10 @@ 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,
|
|
@@ -47,127 +51,76 @@ import {
|
|
|
47
51
|
} from "../cache-policy.js";
|
|
48
52
|
import { reportCacheError, reportingAsync } from "../cache-error.js";
|
|
49
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";
|
|
50
67
|
|
|
51
68
|
// ============================================================================
|
|
52
69
|
// Constants
|
|
53
70
|
// ============================================================================
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
* never "no marker" - absence is only ever represented by this cached sentinel.
|
|
92
|
-
*/
|
|
93
|
-
const TAG_MARKER_ABSENT = "none";
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Header stashing the route author's original Cache-Control on L1 document
|
|
97
|
-
* entries. putResponse/promoteResponseToL1 overwrite Cache-Control with a long
|
|
98
|
-
* `max-age` so the CF Cache API retains the entry across the whole SWR window;
|
|
99
|
-
* getResponse restores this original value before serving so the client and any
|
|
100
|
-
* upstream CDN see the author's intended directive, not the internal edge TTL.
|
|
101
|
-
*/
|
|
102
|
-
const CACHE_ORIG_CC_HEADER = "x-edge-cache-orig-cc";
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Maximum age in seconds for REVALIDATING status before allowing new revalidation.
|
|
106
|
-
* After this period, a stale entry in REVALIDATING status will trigger revalidation again.
|
|
107
|
-
* @internal
|
|
108
|
-
*/
|
|
109
|
-
export const MAX_REVALIDATION_INTERVAL = 30;
|
|
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.
|
|
110
108
|
|
|
111
109
|
/**
|
|
112
|
-
* Per-request memo of
|
|
113
|
-
* null when no marker exists). Keyed by the request context object so it is
|
|
114
|
-
* naturally request-scoped and garbage-collected with the request.
|
|
115
|
-
*
|
|
116
|
-
* Without it, isGloballyInvalidated() issues a KV read per tag on every tagged
|
|
117
|
-
* cache read, so a page composed of many segments/items sharing a tag pays that
|
|
118
|
-
* cost N times. The memo collapses it to one KV read per distinct tag per
|
|
119
|
-
* request. invalidateTags() writes through so a same-request updateTag() stays
|
|
120
|
-
* read-your-own-writes consistent (the action's own re-render sees its own
|
|
121
|
-
* invalidation from the memo, without a re-read).
|
|
110
|
+
* Per-request memo of the derived cache-key base URL.
|
|
122
111
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (!memo) {
|
|
131
|
-
memo = new Map();
|
|
132
|
-
tagMarkerMemo.set(ctx, memo);
|
|
133
|
-
}
|
|
134
|
-
return memo;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Per-request map of IN-FLIGHT marker reads (tag -> the pending read promise).
|
|
139
|
-
* The resolved-value memo above only collapses SEQUENTIAL reads of a tag; the
|
|
140
|
-
* router resolves sibling segments in PARALLEL, so without this several
|
|
141
|
-
* concurrently-resolving segments sharing a tag would each issue their own KV
|
|
142
|
-
* read before any of them populates the memo. Sharing the in-flight promise
|
|
143
|
-
* collapses those to a single KV read. Entries are dropped once resolved (the
|
|
144
|
-
* value is then in the memo), so this only spans the concurrent read window.
|
|
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.
|
|
145
119
|
*/
|
|
146
|
-
const
|
|
147
|
-
object,
|
|
148
|
-
Map<string, Promise<number | null>>
|
|
149
|
-
>();
|
|
150
|
-
|
|
151
|
-
function getTagMarkerInflight(
|
|
152
|
-
ctx: object,
|
|
153
|
-
): Map<string, Promise<number | null>> {
|
|
154
|
-
let inflight = tagMarkerInflight.get(ctx);
|
|
155
|
-
if (!inflight) {
|
|
156
|
-
inflight = new Map();
|
|
157
|
-
tagMarkerInflight.set(ctx, inflight);
|
|
158
|
-
}
|
|
159
|
-
return inflight;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/** KV key byte-length ceiling. Cloudflare KV rejects keys larger than this. */
|
|
163
|
-
const KV_MAX_KEY_BYTES = 512;
|
|
120
|
+
const derivedBaseUrlMemo = new WeakMap<object, string>();
|
|
164
121
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
/** UTF-8 byte length of a KV key (multibyte tags can exceed the char count). */
|
|
168
|
-
function kvKeyByteLength(key: string): number {
|
|
169
|
-
return kvKeyEncoder.encode(key).length;
|
|
170
|
-
}
|
|
122
|
+
// Pure KV helpers (key byte-length limits, expirationTtl floor, stale-path
|
|
123
|
+
// Cache-Control recompute) live in cf-kv-utils.ts; imported above.
|
|
171
124
|
|
|
172
125
|
/**
|
|
173
126
|
* Stores (by namespace) already warned about tag machinery configured without a
|
|
@@ -176,29 +129,33 @@ function kvKeyByteLength(key: string): number {
|
|
|
176
129
|
*/
|
|
177
130
|
const warnedNoKvReadInvalidation = new Set<string>();
|
|
178
131
|
|
|
132
|
+
/**
|
|
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).
|
|
136
|
+
*/
|
|
137
|
+
const warnedTagInvalidationTtlFloor = new Set<string>();
|
|
138
|
+
|
|
179
139
|
// ============================================================================
|
|
180
140
|
// Types
|
|
181
141
|
// ============================================================================
|
|
182
|
-
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
import type {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
export
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
): Promise<void>;
|
|
200
|
-
delete(key: string): Promise<void>;
|
|
201
|
-
}
|
|
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
|
+
};
|
|
202
159
|
|
|
203
160
|
/**
|
|
204
161
|
* KV envelope for segment cache entries.
|
|
@@ -220,8 +177,8 @@ interface KVSegmentEnvelope {
|
|
|
220
177
|
interface KVItemEnvelope {
|
|
221
178
|
/** RSC-serialized return value */
|
|
222
179
|
v: string;
|
|
223
|
-
/**
|
|
224
|
-
h?:
|
|
180
|
+
/** RSC-encoded handle data (see handle-snapshot.ts encodeHandles) */
|
|
181
|
+
h?: string;
|
|
225
182
|
/** When entry becomes stale (ms epoch) */
|
|
226
183
|
s: number;
|
|
227
184
|
/** When entry hard-expires (ms epoch) */
|
|
@@ -255,181 +212,6 @@ interface KVResponseEnvelope {
|
|
|
255
212
|
ta?: number;
|
|
256
213
|
}
|
|
257
214
|
|
|
258
|
-
export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
259
|
-
/**
|
|
260
|
-
* Cache namespace. If not provided, uses caches.default (recommended).
|
|
261
|
-
* Only set this if you need isolated cache storage.
|
|
262
|
-
*/
|
|
263
|
-
namespace?: string;
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Base URL for cache keys.
|
|
267
|
-
*
|
|
268
|
-
* If not provided, derives from request hostname via requestContext:
|
|
269
|
-
* - Production domains → uses `https://{hostname}/`
|
|
270
|
-
* - Dev/preview (localhost, workers.dev, pages.dev) → uses internal fallback URL
|
|
271
|
-
*/
|
|
272
|
-
baseUrl?: string;
|
|
273
|
-
|
|
274
|
-
/** Default cache options */
|
|
275
|
-
defaults?: CacheDefaults;
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Cloudflare ExecutionContext for non-blocking cache writes.
|
|
279
|
-
* Pass the `ctx` from your worker's fetch handler.
|
|
280
|
-
*
|
|
281
|
-
* @example
|
|
282
|
-
* ```typescript
|
|
283
|
-
* new CFCacheStore({ ctx: env.ctx })
|
|
284
|
-
* ```
|
|
285
|
-
*/
|
|
286
|
-
ctx: ExecutionContext;
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Optional KV namespace for L2 cache persistence.
|
|
290
|
-
*
|
|
291
|
-
* When provided, KV acts as a global fallback behind the per-colo Cache API.
|
|
292
|
-
* On L1 miss, KV is checked and hits are promoted back to L1.
|
|
293
|
-
* On writes, data is persisted to both L1 and KV.
|
|
294
|
-
*
|
|
295
|
-
* @example
|
|
296
|
-
* ```typescript
|
|
297
|
-
* new CFCacheStore({ ctx: env.ctx, kv: env.CACHE_KV })
|
|
298
|
-
* ```
|
|
299
|
-
*
|
|
300
|
-
* Tag-based invalidation (updateTag/revalidateTag) requires KV: the
|
|
301
|
-
* tag-invalidation markers are stored in this same namespace. There is no
|
|
302
|
-
* separate tag-invalidation store to configure.
|
|
303
|
-
*/
|
|
304
|
-
kv?: KVNamespace;
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Optional eager-purge hook, called ONCE per updateTag()/revalidateTag() with
|
|
308
|
-
* the namespaced Cloudflare Cache-Tags to purge (one batched call for the
|
|
309
|
-
* whole invalidation, not one per tag). These exactly match the `Cache-Tag`
|
|
310
|
-
* header this store writes on its tag-lookup marker entries
|
|
311
|
-
* (`rg:{namespace}:lk:{encodedTag}`), so forwarding them to Cloudflare's
|
|
312
|
-
* purge-by-tag API evicts the cached lookups in every colo - making
|
|
313
|
-
* cross-colo invalidation prompt instead of waiting out `tagCacheTtl`.
|
|
314
|
-
*
|
|
315
|
-
* Only meaningful with `tagCacheTtl > 0` (otherwise there are no cached
|
|
316
|
-
* lookups to purge). The values are pre-encoded, so commas in tag names are
|
|
317
|
-
* safe to pass straight to the purge API.
|
|
318
|
-
*
|
|
319
|
-
* @example
|
|
320
|
-
* ```ts
|
|
321
|
-
* onRevalidateTag: async (cacheTags) => {
|
|
322
|
-
* await fetch(`https://api.cloudflare.com/client/v4/zones/${ZONE}/purge_cache`, {
|
|
323
|
-
* method: "POST",
|
|
324
|
-
* headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
|
|
325
|
-
* body: JSON.stringify({ tags: cacheTags }),
|
|
326
|
-
* });
|
|
327
|
-
* }
|
|
328
|
-
* ```
|
|
329
|
-
*/
|
|
330
|
-
onRevalidateTag?: (cacheTags: string[]) => Promise<void>;
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Optional expiration (seconds) for tag-invalidation markers in KV. A marker
|
|
334
|
-
* must outlive every entry tagged before the invalidation, so this MUST
|
|
335
|
-
* exceed your largest entry TTL+SWR. Defaults to no expiration (markers
|
|
336
|
-
* persist; they are tiny - one timestamp per distinct invalidated tag).
|
|
337
|
-
*
|
|
338
|
-
* Note the opposite sizing from `tagCacheTtl` below: `tagInvalidationTtl` must
|
|
339
|
-
* be LARGE (outlive data); `tagCacheTtl` should be SMALL (a staleness ceiling).
|
|
340
|
-
*
|
|
341
|
-
* Cardinality matters: each DISTINCT invalidated tag writes one permanent KV
|
|
342
|
-
* marker (with the no-expiry default). Keep tags LOW-cardinality and never
|
|
343
|
-
* derive an invalidation tag from untrusted input (e.g.
|
|
344
|
-
* `revalidateTag(req.query.tag)`) - an attacker could otherwise grow your KV
|
|
345
|
-
* namespace without bound. Set a `tagInvalidationTtl` only if your tags are
|
|
346
|
-
* unavoidably high-cardinality AND it can still safely exceed your max entry
|
|
347
|
-
* TTL+SWR.
|
|
348
|
-
*/
|
|
349
|
-
tagInvalidationTtl?: number;
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Optional TTL (seconds) for caching tag-invalidation markers in the per-colo
|
|
353
|
-
* Cache API (L1), to avoid a KV marker read on every tagged cache read.
|
|
354
|
-
*
|
|
355
|
-
* Default `0` = disabled: the marker is read from KV on every tagged read
|
|
356
|
-
* (today's behavior), giving the strongest cross-colo invalidation latency
|
|
357
|
-
* (~KV consistency). A positive value caches each marker (including the
|
|
358
|
-
* "no marker yet" state) in L1 for that many seconds, so within the window a
|
|
359
|
-
* colo answers from L1 with no KV read.
|
|
360
|
-
*
|
|
361
|
-
* The colo that runs `updateTag`/`revalidateTag` writes the fresh marker
|
|
362
|
-
* straight into its own L1 (write-through), so it observes the invalidation
|
|
363
|
-
* immediately. By default OTHER colos only converge when their cached marker
|
|
364
|
-
* expires, so `tagCacheTtl` is the MAXIMUM extra cross-colo invalidation
|
|
365
|
-
* latency for them. Recommended 30-60 for high-read, low-mutation tags; leave
|
|
366
|
-
* at 0 when prompt global invalidation matters and you cannot wire a purge.
|
|
367
|
-
*
|
|
368
|
-
* To make other colos prompt WITHOUT a short TTL, wire `onRevalidateTag` to a
|
|
369
|
-
* Cloudflare purge-by-tag call: each marker entry carries a namespaced
|
|
370
|
-
* `Cache-Tag`, and `onRevalidateTag` is handed exactly those tags to purge, so
|
|
371
|
-
* the cached lookups are evicted everywhere on invalidation. With a purge
|
|
372
|
-
* wired, `tagCacheTtl` becomes purely a read-cost reducer + fallback window
|
|
373
|
-
* (safe to set large) rather than the invalidation-latency ceiling.
|
|
374
|
-
*/
|
|
375
|
-
tagCacheTtl?: number;
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Cache version string override. When this changes, all cached entries are
|
|
379
|
-
* effectively invalidated (new keys won't match old entries).
|
|
380
|
-
*
|
|
381
|
-
* Defaults to the auto-generated VERSION from the `@rangojs/router:version` virtual module.
|
|
382
|
-
* Only set this if you need a custom versioning strategy.
|
|
383
|
-
*/
|
|
384
|
-
version?: string;
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Custom key generator applied to all cache operations.
|
|
388
|
-
* Receives the full RequestContext (including env) and the default-generated key.
|
|
389
|
-
* Return value becomes the final cache key (unless route overrides with `key` option).
|
|
390
|
-
*
|
|
391
|
-
* Reserved prefixes: tag-invalidation markers live in the SAME KV namespace as
|
|
392
|
-
* data, keyed `__tag__/<tag>` (and `__tagmarker__/<tag>` for the L1 cache). A
|
|
393
|
-
* returned key must NOT begin with `__tag__/` or `__tagmarker__/`, or it can
|
|
394
|
-
* collide with a tag marker and corrupt invalidation. The documented
|
|
395
|
-
* prepend-style generators below are safe.
|
|
396
|
-
*
|
|
397
|
-
* @example Using headers for user segmentation
|
|
398
|
-
* ```typescript
|
|
399
|
-
* keyGenerator: (ctx, defaultKey) => {
|
|
400
|
-
* const segment = ctx.request.headers.get('x-user-segment') || 'default';
|
|
401
|
-
* return `${segment}:${defaultKey}`;
|
|
402
|
-
* }
|
|
403
|
-
* ```
|
|
404
|
-
*
|
|
405
|
-
* @example Using env bindings for multi-region
|
|
406
|
-
* ```typescript
|
|
407
|
-
* keyGenerator: (ctx, defaultKey) => {
|
|
408
|
-
* const region = ctx.env.REGION || 'us';
|
|
409
|
-
* return `${region}:${defaultKey}`;
|
|
410
|
-
* }
|
|
411
|
-
* ```
|
|
412
|
-
*
|
|
413
|
-
* @example Using cookies for locale-aware caching
|
|
414
|
-
* ```typescript
|
|
415
|
-
* keyGenerator: (ctx, defaultKey) => {
|
|
416
|
-
* const locale = cookies().get('locale')?.value || 'en';
|
|
417
|
-
* return `${locale}:${defaultKey}`;
|
|
418
|
-
* }
|
|
419
|
-
* ```
|
|
420
|
-
*/
|
|
421
|
-
keyGenerator?: (
|
|
422
|
-
ctx: RequestContext<TEnv>,
|
|
423
|
-
defaultKey: string,
|
|
424
|
-
) => string | Promise<string>;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* Cache status values for the x-edge-cache-status header.
|
|
429
|
-
* @internal
|
|
430
|
-
*/
|
|
431
|
-
export type CacheStatus = "HIT" | "REVALIDATING";
|
|
432
|
-
|
|
433
215
|
// ============================================================================
|
|
434
216
|
// CFCacheStore Implementation
|
|
435
217
|
// ============================================================================
|
|
@@ -442,9 +224,13 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
442
224
|
) => string | Promise<string>;
|
|
443
225
|
|
|
444
226
|
private readonly namespace?: string;
|
|
445
|
-
private readonly
|
|
227
|
+
private readonly explicitBaseUrl?: string;
|
|
446
228
|
private readonly waitUntil?: (fn: () => Promise<void>) => void;
|
|
447
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;
|
|
448
234
|
private readonly kv?: KVNamespace;
|
|
449
235
|
private readonly onRevalidateTag?: (tags: string[]) => Promise<void>;
|
|
450
236
|
private readonly tagInvalidationTtl?: number;
|
|
@@ -460,22 +246,71 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
460
246
|
}
|
|
461
247
|
|
|
462
248
|
this.namespace = options.namespace;
|
|
463
|
-
|
|
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;
|
|
464
255
|
this.defaults = options.defaults;
|
|
465
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;
|
|
466
287
|
this.keyGenerator = options.keyGenerator;
|
|
467
288
|
this.waitUntil = (fn) => options.ctx.waitUntil(fn());
|
|
468
289
|
this.kv = options.kv;
|
|
469
290
|
this.onRevalidateTag = options.onRevalidateTag;
|
|
470
|
-
|
|
471
|
-
|
|
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;
|
|
472
306
|
|
|
473
307
|
// Read-side tag invalidation requires KV: isGloballyInvalidated() compares an
|
|
474
308
|
// entry's taggedAt against the per-tag KV marker and short-circuits to "not
|
|
475
309
|
// invalidated" when no KV namespace is configured. A consumer who wires the
|
|
476
310
|
// tag machinery (tagCacheTtl for L1 markers, or onRevalidateTag for CDN purge)
|
|
477
|
-
// but omits kv gets
|
|
478
|
-
// still serves stale data with no other signal.
|
|
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.
|
|
479
314
|
if (!this.kv && (this.tagCacheTtl > 0 || this.onRevalidateTag)) {
|
|
480
315
|
const id = this.namespace ?? "default";
|
|
481
316
|
if (!warnedNoKvReadInvalidation.has(id)) {
|
|
@@ -490,44 +325,113 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
490
325
|
}
|
|
491
326
|
}
|
|
492
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();
|
|
382
|
+
}
|
|
383
|
+
|
|
493
384
|
/**
|
|
494
385
|
* Derive base URL from request hostname via requestContext.
|
|
495
386
|
* Uses internal fallback for dev/preview environments and untrusted hostnames.
|
|
387
|
+
* Must run inside the request context (invoked lazily via resolveBaseUrl).
|
|
496
388
|
* @internal
|
|
497
389
|
*/
|
|
498
390
|
private deriveBaseUrl(): string {
|
|
499
|
-
const fallback = "https://rsc-
|
|
391
|
+
const fallback = "https://rsc-dummy-host-1.com/";
|
|
500
392
|
|
|
501
393
|
const ctx = _getRequestContext();
|
|
502
394
|
if (!ctx?.request) {
|
|
503
395
|
return fallback;
|
|
504
396
|
}
|
|
505
397
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
+
}
|
|
509
404
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
hostname
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
+
}
|
|
519
419
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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 {
|
|
523
429
|
return fallback;
|
|
524
430
|
}
|
|
431
|
+
})();
|
|
525
432
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
} catch {
|
|
529
|
-
return fallback;
|
|
530
|
-
}
|
|
433
|
+
derivedBaseUrlMemo.set(ctx, derived);
|
|
434
|
+
return derived;
|
|
531
435
|
}
|
|
532
436
|
|
|
533
437
|
/**
|
|
@@ -541,6 +445,260 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
541
445
|
return caches.default;
|
|
542
446
|
}
|
|
543
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
|
+
|
|
544
702
|
// ============================================================================
|
|
545
703
|
// Segment Cache Methods
|
|
546
704
|
// ============================================================================
|
|
@@ -594,60 +752,215 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
594
752
|
try {
|
|
595
753
|
const cache = await this.getCache();
|
|
596
754
|
const request = this.keyToRequest(key);
|
|
597
|
-
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;
|
|
598
762
|
|
|
599
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
|
+
});
|
|
600
787
|
return this.kvGetSegment(key);
|
|
601
788
|
}
|
|
602
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
|
+
|
|
603
807
|
// Tag invalidation: an entry whose tags were invalidated after it was
|
|
604
|
-
// cached is treated as a miss, so the next render re-populates it.
|
|
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.
|
|
605
817
|
const tagInfo = this.readTagInfo(response.headers);
|
|
606
|
-
|
|
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
|
+
});
|
|
607
837
|
return null;
|
|
608
838
|
}
|
|
609
839
|
|
|
610
840
|
// Read status headers
|
|
611
841
|
const status = response.headers.get(CACHE_STATUS_HEADER);
|
|
612
|
-
const age = Number(response.headers.get("age") ?? "0");
|
|
613
842
|
const staleAt = Number(
|
|
614
843
|
response.headers.get(CACHE_STALE_AT_HEADER) ?? "0",
|
|
615
844
|
);
|
|
845
|
+
const revalidatingAt = Number(
|
|
846
|
+
response.headers.get(CACHE_REVALIDATING_AT_HEADER) ?? "0",
|
|
847
|
+
);
|
|
616
848
|
|
|
617
|
-
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.
|
|
618
854
|
const isRevalidating =
|
|
619
|
-
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;
|
|
620
885
|
|
|
621
886
|
// Case 1: Fresh or already being revalidated - just return data
|
|
622
887
|
if (!isStale || isRevalidating) {
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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,
|
|
627
912
|
);
|
|
628
|
-
return
|
|
913
|
+
return { data, shouldRevalidate: false };
|
|
629
914
|
}
|
|
630
915
|
|
|
631
|
-
// Case 2: Stale and needs revalidation
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
+
}
|
|
636
944
|
|
|
637
|
-
//
|
|
638
|
-
|
|
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,
|
|
639
951
|
request,
|
|
640
|
-
|
|
952
|
+
response.headers,
|
|
953
|
+
response.status,
|
|
954
|
+
JSON.stringify(data),
|
|
641
955
|
);
|
|
642
956
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
() => cache.delete(request),
|
|
646
|
-
"get(revalidating)",
|
|
647
|
-
);
|
|
648
|
-
return data === null ? null : { data, shouldRevalidate: true };
|
|
957
|
+
debugRead?.("l1-stale-revalidate", bodyReadMs, true);
|
|
958
|
+
return { data, shouldRevalidate: true };
|
|
649
959
|
} catch (error) {
|
|
960
|
+
// reportCacheError logs and routes to onError (cache-read); the debug
|
|
961
|
+
// emit is the separate wrangler-tail signal. Keep both observability paths.
|
|
650
962
|
reportCacheError(error, "cache-read", "[CFCacheStore] get");
|
|
963
|
+
if (this.debug) this.emitDebug({ op: "get", key, outcome: "error" });
|
|
651
964
|
return null;
|
|
652
965
|
}
|
|
653
966
|
}
|
|
@@ -677,7 +990,9 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
677
990
|
// into both the L1 body and the KV envelope so reads can run the
|
|
678
991
|
// invalidation check.
|
|
679
992
|
const taggedAt =
|
|
680
|
-
data.tags && data.tags.length > 0
|
|
993
|
+
Array.isArray(data.tags) && data.tags.length > 0
|
|
994
|
+
? Date.now()
|
|
995
|
+
: undefined;
|
|
681
996
|
const dataToStore: CachedEntryData = taggedAt
|
|
682
997
|
? { ...data, taggedAt }
|
|
683
998
|
: data;
|
|
@@ -688,6 +1003,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
688
1003
|
"Content-Type": "application/json",
|
|
689
1004
|
"Cache-Control": `public, max-age=${totalTtl}`,
|
|
690
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),
|
|
691
1010
|
[CACHE_STATUS_HEADER]: "HIT",
|
|
692
1011
|
...this.tagHeaderEntries(dataToStore.tags, taggedAt),
|
|
693
1012
|
},
|
|
@@ -764,9 +1083,26 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
764
1083
|
try {
|
|
765
1084
|
const cache = await this.getCache();
|
|
766
1085
|
const request = this.keyToRequest(`doc:${key}`);
|
|
767
|
-
|
|
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
|
+
);
|
|
768
1094
|
|
|
769
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
|
+
);
|
|
770
1106
|
return this.kvGetResponse(key);
|
|
771
1107
|
}
|
|
772
1108
|
|
|
@@ -778,7 +1114,23 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
778
1114
|
|
|
779
1115
|
// Check staleness
|
|
780
1116
|
const staleAt = Number(response.headers.get(CACHE_STALE_AT_HEADER) || 0);
|
|
781
|
-
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;
|
|
782
1134
|
|
|
783
1135
|
// L1 document bodies are streamed through verbatim - unlike the segment/
|
|
784
1136
|
// item tiers (which JSON-parse and so structurally detect corruption) and
|
|
@@ -787,9 +1139,25 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
787
1139
|
// response atomically or fails, so a truncated body is not served back. We
|
|
788
1140
|
// deliberately do NOT buffer+hash the body to re-verify it: that would
|
|
789
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
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Fresh, or stale-but-already-REVALIDATING: serve without scheduling a
|
|
1157
|
+
// (re-)revalidation. A recent marker already has a render in flight.
|
|
790
1158
|
return {
|
|
791
1159
|
response: this.toClientResponse(response),
|
|
792
|
-
shouldRevalidate:
|
|
1160
|
+
shouldRevalidate: false,
|
|
793
1161
|
};
|
|
794
1162
|
} catch (error) {
|
|
795
1163
|
reportCacheError(error, "cache-read", "[CFCacheStore] getResponse");
|
|
@@ -816,6 +1184,13 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
816
1184
|
headers.delete(CACHE_STATUS_HEADER);
|
|
817
1185
|
headers.delete(CACHE_TAGS_HEADER);
|
|
818
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);
|
|
819
1194
|
return new Response(response.body, {
|
|
820
1195
|
status: response.status,
|
|
821
1196
|
statusText: response.statusText,
|
|
@@ -842,7 +1217,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
842
1217
|
const swrWindow = resolveSwrWindow(swr, this.defaults);
|
|
843
1218
|
const totalTtl = ttl + swrWindow;
|
|
844
1219
|
const staleAt = Date.now() + ttl * 1000;
|
|
845
|
-
const taggedAt =
|
|
1220
|
+
const taggedAt =
|
|
1221
|
+
Array.isArray(tags) && tags.length > 0 ? Date.now() : undefined;
|
|
846
1222
|
|
|
847
1223
|
// Clone body for potential KV write before consuming it for L1
|
|
848
1224
|
const [l1Body, kvBody] = this.kv
|
|
@@ -855,17 +1231,23 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
855
1231
|
// replaced with a long max-age so the CF Cache API holds the entry across
|
|
856
1232
|
// the SWR window; getResponse restores the original before serving.
|
|
857
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);
|
|
858
1238
|
const originalCacheControl = response.headers.get("Cache-Control");
|
|
859
1239
|
if (originalCacheControl !== null) {
|
|
860
1240
|
headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
|
|
861
1241
|
}
|
|
862
1242
|
headers.set("Cache-Control", `public, max-age=${totalTtl}`);
|
|
863
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));
|
|
864
1249
|
// Internal tag headers (stripped by toClientResponse before serving).
|
|
865
|
-
|
|
866
|
-
for (const [name, value] of Object.entries(tagHeaders)) {
|
|
867
|
-
headers.set(name, value);
|
|
868
|
-
}
|
|
1250
|
+
this.setTagHeaders(headers, tags, taggedAt);
|
|
869
1251
|
|
|
870
1252
|
const toCache = new Response(l1Body, {
|
|
871
1253
|
status: response.status,
|
|
@@ -891,9 +1273,13 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
891
1273
|
|
|
892
1274
|
// L2: persist to KV (KV requires expirationTtl >= 60s)
|
|
893
1275
|
if (this.kv && this.waitUntil && totalTtl >= 60) {
|
|
894
|
-
const kvKey = this.
|
|
1276
|
+
const kvKey = this.toDocKVKey(key);
|
|
1277
|
+
// Finding #3: never persist a per-client signal in the KV envelope.
|
|
895
1278
|
const headersArray: [string, string][] = [];
|
|
896
|
-
response.headers.forEach((v, k) =>
|
|
1279
|
+
response.headers.forEach((v, k) => {
|
|
1280
|
+
if (isPerClientSignalHeader(k)) return;
|
|
1281
|
+
headersArray.push([k, v]);
|
|
1282
|
+
});
|
|
897
1283
|
// Read body as ArrayBuffer and encode to base64 to preserve binary payloads
|
|
898
1284
|
const bodyBuf = kvBody
|
|
899
1285
|
? await new Response(kvBody).arrayBuffer()
|
|
@@ -940,13 +1326,71 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
940
1326
|
try {
|
|
941
1327
|
const cache = await this.getCache();
|
|
942
1328
|
const request = this.keyToRequest(`fn:${key}`);
|
|
943
|
-
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;
|
|
944
1336
|
|
|
945
|
-
if (!response)
|
|
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
|
+
}
|
|
946
1359
|
|
|
947
|
-
//
|
|
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()).
|
|
948
1377
|
const tagInfo = this.readTagInfo(response.headers);
|
|
949
|
-
|
|
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
|
+
});
|
|
950
1394
|
return null;
|
|
951
1395
|
}
|
|
952
1396
|
|
|
@@ -954,27 +1398,72 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
954
1398
|
response.headers.get(CACHE_STALE_AT_HEADER) ?? "0",
|
|
955
1399
|
);
|
|
956
1400
|
const status = response.headers.get(CACHE_STATUS_HEADER);
|
|
957
|
-
const
|
|
1401
|
+
const revalidatingAt = Number(
|
|
1402
|
+
response.headers.get(CACHE_REVALIDATING_AT_HEADER) ?? "0",
|
|
1403
|
+
);
|
|
958
1404
|
|
|
959
|
-
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()).
|
|
960
1408
|
const isRevalidating =
|
|
961
|
-
status === "REVALIDATING" &&
|
|
962
|
-
|
|
963
|
-
|
|
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<{
|
|
964
1446
|
value: string;
|
|
965
|
-
handles?:
|
|
966
|
-
}>(
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
+
}
|
|
976
1460
|
|
|
977
1461
|
if (!isStale || isRevalidating) {
|
|
1462
|
+
debugRead?.(
|
|
1463
|
+
isRevalidating ? "l1-revalidating-guarded" : "l1-fresh",
|
|
1464
|
+
bodyReadMs,
|
|
1465
|
+
false,
|
|
1466
|
+
);
|
|
978
1467
|
return {
|
|
979
1468
|
value: data.value,
|
|
980
1469
|
handles: data.handles,
|
|
@@ -983,14 +1472,18 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
983
1472
|
};
|
|
984
1473
|
}
|
|
985
1474
|
|
|
986
|
-
// Stale and needs revalidation
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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,
|
|
990
1480
|
request,
|
|
991
|
-
|
|
1481
|
+
response.headers,
|
|
1482
|
+
200,
|
|
1483
|
+
JSON.stringify(data),
|
|
992
1484
|
);
|
|
993
1485
|
|
|
1486
|
+
debugRead?.("l1-stale-revalidate", bodyReadMs, true);
|
|
994
1487
|
return {
|
|
995
1488
|
value: data.value,
|
|
996
1489
|
handles: data.handles,
|
|
@@ -999,6 +1492,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
999
1492
|
};
|
|
1000
1493
|
} catch (error) {
|
|
1001
1494
|
reportCacheError(error, "cache-read", "[CFCacheStore] getItem");
|
|
1495
|
+
if (this.debug) this.emitDebug({ op: "getItem", key, outcome: "error" });
|
|
1002
1496
|
return null;
|
|
1003
1497
|
}
|
|
1004
1498
|
}
|
|
@@ -1022,7 +1516,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1022
1516
|
const staleAt = Date.now() + ttl * 1000;
|
|
1023
1517
|
|
|
1024
1518
|
const tags = options?.tags;
|
|
1025
|
-
const taggedAt =
|
|
1519
|
+
const taggedAt =
|
|
1520
|
+
Array.isArray(tags) && tags.length > 0 ? Date.now() : undefined;
|
|
1026
1521
|
|
|
1027
1522
|
const body = JSON.stringify({ value, handles: options?.handles });
|
|
1028
1523
|
const response = new Response(body, {
|
|
@@ -1030,6 +1525,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1030
1525
|
"Content-Type": "application/json",
|
|
1031
1526
|
"Cache-Control": `public, max-age=${totalTtl}`,
|
|
1032
1527
|
[CACHE_STALE_AT_HEADER]: String(staleAt),
|
|
1528
|
+
// Absolute hard-expiry deadline; see set() / remainingCacheControl.
|
|
1529
|
+
[CACHE_EXPIRES_AT_HEADER]: String(staleAt + swrWindow * 1000),
|
|
1033
1530
|
[CACHE_STATUS_HEADER]: "HIT",
|
|
1034
1531
|
...this.tagHeaderEntries(tags, taggedAt),
|
|
1035
1532
|
},
|
|
@@ -1090,7 +1587,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1090
1587
|
const encodedKey = encodeURIComponent(key);
|
|
1091
1588
|
// Include version in URL path to invalidate cache when version changes
|
|
1092
1589
|
const versionPath = this.version ? `v/${this.version}/` : "";
|
|
1093
|
-
return new Request(`${this.
|
|
1590
|
+
return new Request(`${this.resolveBaseUrl()}${versionPath}${encodedKey}`, {
|
|
1094
1591
|
method: "GET",
|
|
1095
1592
|
});
|
|
1096
1593
|
}
|
|
@@ -1106,41 +1603,36 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1106
1603
|
}
|
|
1107
1604
|
|
|
1108
1605
|
/**
|
|
1109
|
-
*
|
|
1110
|
-
* the
|
|
1111
|
-
*
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1114
|
-
* outer catch reports cache-read and does NOT delete a still-good entry).
|
|
1115
|
-
* Returns null on corruption; the caller treats null as a miss.
|
|
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).
|
|
1116
1611
|
* @internal
|
|
1117
1612
|
*/
|
|
1118
|
-
private
|
|
1119
|
-
parse: () => Promise<T> | T,
|
|
1120
|
-
evict: () => Promise<unknown>,
|
|
1121
|
-
label: string,
|
|
1122
|
-
): Promise<T | null> {
|
|
1613
|
+
private docKVHost(): string {
|
|
1123
1614
|
try {
|
|
1124
|
-
return
|
|
1125
|
-
} catch
|
|
1126
|
-
|
|
1127
|
-
error,
|
|
1128
|
-
"cache-corrupt",
|
|
1129
|
-
`[CFCacheStore] ${label}: corrupt/partial entry, evicting`,
|
|
1130
|
-
);
|
|
1131
|
-
try {
|
|
1132
|
-
await evict();
|
|
1133
|
-
} catch (evictError) {
|
|
1134
|
-
reportCacheError(
|
|
1135
|
-
evictError,
|
|
1136
|
-
"cache-delete",
|
|
1137
|
-
`[CFCacheStore] ${label}: evicting corrupt entry failed`,
|
|
1138
|
-
);
|
|
1139
|
-
}
|
|
1140
|
-
return null;
|
|
1615
|
+
return new URL(this.resolveBaseUrl()).host || "_";
|
|
1616
|
+
} catch {
|
|
1617
|
+
return "_";
|
|
1141
1618
|
}
|
|
1142
1619
|
}
|
|
1143
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
|
+
|
|
1144
1636
|
/**
|
|
1145
1637
|
* Best-effort delete of a single KV key, reporting (not swallowing) a delete
|
|
1146
1638
|
* failure as cache-delete. Used by the corrupt-entry self-heal paths.
|
|
@@ -1158,6 +1650,21 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1158
1650
|
}
|
|
1159
1651
|
}
|
|
1160
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
|
+
|
|
1161
1668
|
/**
|
|
1162
1669
|
* KV-get a JSON envelope, EVICTING the key only when it is genuinely corrupt.
|
|
1163
1670
|
*
|
|
@@ -1176,35 +1683,61 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1176
1683
|
kvKey: string,
|
|
1177
1684
|
validate: (envelope: T) => boolean,
|
|
1178
1685
|
label: string,
|
|
1179
|
-
): Promise<T | null> {
|
|
1180
|
-
//
|
|
1181
|
-
//
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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;
|
|
1196
1719
|
}
|
|
1197
1720
|
|
|
1198
|
-
|
|
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
|
+
) {
|
|
1199
1732
|
reportCacheError(
|
|
1200
1733
|
new Error("malformed/partial KV envelope"),
|
|
1201
1734
|
"cache-corrupt",
|
|
1202
1735
|
`[CFCacheStore] ${label}: malformed envelope, evicting`,
|
|
1203
1736
|
);
|
|
1204
|
-
|
|
1205
|
-
return null;
|
|
1737
|
+
this.scheduleKvEvict(kvKey, label);
|
|
1738
|
+
return { value: null, timedOut: false };
|
|
1206
1739
|
}
|
|
1207
|
-
return
|
|
1740
|
+
return { value: envelope, timedOut: false };
|
|
1208
1741
|
}
|
|
1209
1742
|
|
|
1210
1743
|
// ============================================================================
|
|
@@ -1225,7 +1758,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1225
1758
|
tags: string[] | undefined,
|
|
1226
1759
|
taggedAt: number | undefined,
|
|
1227
1760
|
): Record<string, string> {
|
|
1228
|
-
if (!tags || tags.length === 0 || !taggedAt) return {};
|
|
1761
|
+
if (!Array.isArray(tags) || tags.length === 0 || !taggedAt) return {};
|
|
1229
1762
|
return {
|
|
1230
1763
|
// encodeURIComponent so the value is pure ASCII: HTTP header values are
|
|
1231
1764
|
// ByteStrings, but JSON.stringify leaves codepoints > U+00FF (emoji/CJK)
|
|
@@ -1237,6 +1770,24 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1237
1770
|
};
|
|
1238
1771
|
}
|
|
1239
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
|
+
|
|
1240
1791
|
/** Read an entry's tags/taggedAt back from its headers. */
|
|
1241
1792
|
private readTagInfo(headers: Headers): {
|
|
1242
1793
|
tags?: string[];
|
|
@@ -1246,9 +1797,17 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1246
1797
|
const rawTaggedAt = headers.get(CACHE_TAGGED_AT_HEADER);
|
|
1247
1798
|
if (!rawTags || !rawTaggedAt) return {};
|
|
1248
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 {};
|
|
1249
1808
|
return {
|
|
1250
1809
|
tags: JSON.parse(decodeURIComponent(rawTags)) as string[],
|
|
1251
|
-
taggedAt
|
|
1810
|
+
taggedAt,
|
|
1252
1811
|
};
|
|
1253
1812
|
} catch {
|
|
1254
1813
|
return {};
|
|
@@ -1266,10 +1825,15 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1266
1825
|
tags: string[] | undefined,
|
|
1267
1826
|
taggedAt: number | undefined,
|
|
1268
1827
|
): Promise<boolean> {
|
|
1269
|
-
|
|
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;
|
|
1270
1834
|
const ctx = _getRequestContext();
|
|
1271
|
-
const memo = ctx ? getTagMarkerMemo(ctx) : undefined;
|
|
1272
|
-
const inflight = ctx ? getTagMarkerInflight(ctx) : undefined;
|
|
1835
|
+
const memo = ctx ? getTagMarkerMemo(ctx, this) : undefined;
|
|
1836
|
+
const inflight = ctx ? getTagMarkerInflight(ctx, this) : undefined;
|
|
1273
1837
|
try {
|
|
1274
1838
|
const markers = await Promise.all(
|
|
1275
1839
|
tags.map((tag) => this.readTagMarker(tag, memo, inflight)),
|
|
@@ -1340,34 +1904,81 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1340
1904
|
tag: string,
|
|
1341
1905
|
memo: Map<string, number | null> | undefined,
|
|
1342
1906
|
): Promise<number | null> {
|
|
1343
|
-
//
|
|
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.
|
|
1344
1924
|
if (this.tagCacheTtl > 0) {
|
|
1345
1925
|
try {
|
|
1346
1926
|
const cache = await this.getCache();
|
|
1347
|
-
const hit
|
|
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
|
+
);
|
|
1348
1938
|
if (hit) {
|
|
1349
|
-
const body = await
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
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
|
+
}
|
|
1353
1948
|
}
|
|
1354
1949
|
} catch {
|
|
1355
1950
|
// Fall through to KV on any L1 read error.
|
|
1356
1951
|
}
|
|
1357
1952
|
}
|
|
1358
1953
|
|
|
1359
|
-
// KV (global truth).
|
|
1360
|
-
|
|
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
|
+
}
|
|
1361
1970
|
const value = raw != null ? Number(raw) : null;
|
|
1362
|
-
|
|
1971
|
+
const resolved = memoize(value);
|
|
1363
1972
|
|
|
1364
|
-
// Populate L1 for subsequent reads in this colo (non-blocking).
|
|
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.
|
|
1365
1976
|
if (this.tagCacheTtl > 0) {
|
|
1366
|
-
const put = () => this.putTagMarkerL1(tag,
|
|
1977
|
+
const put = () => this.putTagMarkerL1(tag, resolved);
|
|
1367
1978
|
if (this.waitUntil) this.waitUntil(put);
|
|
1368
1979
|
else void put();
|
|
1369
1980
|
}
|
|
1370
|
-
return
|
|
1981
|
+
return resolved;
|
|
1371
1982
|
}
|
|
1372
1983
|
|
|
1373
1984
|
/**
|
|
@@ -1404,6 +2015,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1404
2015
|
private async putTagMarkerL1(
|
|
1405
2016
|
tag: string,
|
|
1406
2017
|
value: number | null,
|
|
2018
|
+
opts?: { critical?: boolean },
|
|
1407
2019
|
): Promise<void> {
|
|
1408
2020
|
if (this.tagCacheTtl <= 0) return;
|
|
1409
2021
|
try {
|
|
@@ -1418,8 +2030,29 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1418
2030
|
},
|
|
1419
2031
|
}),
|
|
1420
2032
|
);
|
|
1421
|
-
} catch {
|
|
1422
|
-
//
|
|
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
|
+
}
|
|
1423
2056
|
}
|
|
1424
2057
|
}
|
|
1425
2058
|
|
|
@@ -1446,7 +2079,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1446
2079
|
if (tags.length === 0) return;
|
|
1447
2080
|
const invalidatedAt = Date.now();
|
|
1448
2081
|
const ctx = _getRequestContext();
|
|
1449
|
-
const memo = ctx ? getTagMarkerMemo(ctx) : undefined;
|
|
2082
|
+
const memo = ctx ? getTagMarkerMemo(ctx, this) : undefined;
|
|
1450
2083
|
|
|
1451
2084
|
if (!this.kv && !this.onRevalidateTag) {
|
|
1452
2085
|
console.warn(
|
|
@@ -1461,11 +2094,12 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1461
2094
|
await Promise.all(
|
|
1462
2095
|
tags.map(async (tag) => {
|
|
1463
2096
|
const markerKey = this.tagMarkerKey(tag);
|
|
1464
|
-
|
|
2097
|
+
const markerKeyBytes = kvKeyByteLength(markerKey);
|
|
2098
|
+
if (markerKeyBytes > KV_MAX_KEY_BYTES) {
|
|
1465
2099
|
failedTags.add(tag);
|
|
1466
2100
|
errors.push(
|
|
1467
2101
|
new Error(
|
|
1468
|
-
`tag "${tag}" produces a ${
|
|
2102
|
+
`tag "${tag}" produces a ${markerKeyBytes}-byte KV ` +
|
|
1469
2103
|
`marker key, over the ${KV_MAX_KEY_BYTES}-byte limit`,
|
|
1470
2104
|
),
|
|
1471
2105
|
);
|
|
@@ -1485,20 +2119,27 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1485
2119
|
);
|
|
1486
2120
|
}
|
|
1487
2121
|
|
|
1488
|
-
// Write-through memo + L1 only for tags with a confirmed durable marker
|
|
1489
|
-
//
|
|
1490
|
-
//
|
|
1491
|
-
// memo
|
|
1492
|
-
//
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
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
|
+
}
|
|
1499
2140
|
}
|
|
2141
|
+
if (l1Writes.length > 0) await Promise.all(l1Writes);
|
|
1500
2142
|
}
|
|
1501
|
-
if (l1Writes.length > 0) await Promise.all(l1Writes);
|
|
1502
2143
|
|
|
1503
2144
|
// One batched eager purge of the lookup markers for the whole call. Fired
|
|
1504
2145
|
// regardless of KV write outcome (it is additive and uses pure string ops).
|
|
@@ -1535,39 +2176,78 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1535
2176
|
* Promotes hits to L1 via waitUntil.
|
|
1536
2177
|
* @internal
|
|
1537
2178
|
*/
|
|
1538
|
-
private async kvGetSegment(
|
|
2179
|
+
private async kvGetSegment(
|
|
2180
|
+
key: string,
|
|
2181
|
+
opts?: { suppressRevalidate?: boolean },
|
|
2182
|
+
): Promise<CacheGetResult | null> {
|
|
1539
2183
|
if (!this.kv) return null;
|
|
1540
2184
|
|
|
1541
2185
|
try {
|
|
1542
2186
|
const kvKey = this.toKVKey(key);
|
|
1543
|
-
const envelope
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
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
|
+
}
|
|
1550
2207
|
|
|
1551
2208
|
const now = Date.now();
|
|
1552
2209
|
|
|
1553
2210
|
// Hard-expired — treat as miss
|
|
1554
|
-
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
|
+
}
|
|
1555
2215
|
|
|
1556
2216
|
// Tag invalidation check (also covers the KV tier, not just L1).
|
|
1557
2217
|
if (
|
|
1558
2218
|
await this.isGloballyInvalidated(envelope.d.tags, envelope.d.taggedAt)
|
|
1559
2219
|
) {
|
|
2220
|
+
if (this.debug)
|
|
2221
|
+
this.emitDebug({ op: "get", key, outcome: "tag-invalidated" });
|
|
1560
2222
|
return null;
|
|
1561
2223
|
}
|
|
1562
2224
|
|
|
1563
|
-
|
|
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;
|
|
1564
2232
|
|
|
1565
2233
|
// Promote to L1 in background
|
|
1566
2234
|
this.promoteSegmentToL1(key, envelope);
|
|
1567
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
|
+
});
|
|
1568
2247
|
return { data: envelope.d, shouldRevalidate };
|
|
1569
2248
|
} catch (error) {
|
|
1570
2249
|
reportCacheError(error, "cache-read", "[CFCacheStore] kvGetSegment");
|
|
2250
|
+
if (this.debug) this.emitDebug({ op: "get", key, outcome: "error" });
|
|
1571
2251
|
return null;
|
|
1572
2252
|
}
|
|
1573
2253
|
}
|
|
@@ -1630,6 +2310,9 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1630
2310
|
"Content-Type": "application/json",
|
|
1631
2311
|
"Cache-Control": `public, max-age=${remainingTtl}`,
|
|
1632
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),
|
|
1633
2316
|
[CACHE_STATUS_HEADER]: "HIT",
|
|
1634
2317
|
// Preserve tags across KV->L1 promotion so the promoted entry
|
|
1635
2318
|
// stays tag-invalidatable.
|
|
@@ -1649,35 +2332,68 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1649
2332
|
* KV fallback for function cache reads.
|
|
1650
2333
|
* @internal
|
|
1651
2334
|
*/
|
|
1652
|
-
private async kvGetItem(
|
|
2335
|
+
private async kvGetItem(
|
|
2336
|
+
key: string,
|
|
2337
|
+
opts?: { suppressRevalidate?: boolean },
|
|
2338
|
+
): Promise<CacheItemResult | null> {
|
|
1653
2339
|
if (!this.kv) return null;
|
|
1654
2340
|
|
|
1655
2341
|
try {
|
|
1656
2342
|
const kvKey = this.toKVKey(`fn:${key}`);
|
|
1657
|
-
const envelope
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
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
|
+
}
|
|
1666
2362
|
|
|
1667
2363
|
const now = Date.now();
|
|
1668
2364
|
|
|
1669
|
-
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
|
+
}
|
|
1670
2370
|
|
|
1671
2371
|
// Tag invalidation check (also covers the KV tier, not just L1).
|
|
1672
2372
|
if (await this.isGloballyInvalidated(envelope.t, envelope.ta)) {
|
|
2373
|
+
if (this.debug)
|
|
2374
|
+
this.emitDebug({ op: "getItem", key, outcome: "tag-invalidated" });
|
|
1673
2375
|
return null;
|
|
1674
2376
|
}
|
|
1675
2377
|
|
|
1676
|
-
|
|
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;
|
|
1677
2382
|
|
|
1678
2383
|
// Promote to L1
|
|
1679
2384
|
this.promoteItemToL1(key, envelope);
|
|
1680
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
|
+
});
|
|
1681
2397
|
return {
|
|
1682
2398
|
value: envelope.v,
|
|
1683
2399
|
handles: envelope.h,
|
|
@@ -1686,6 +2402,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1686
2402
|
};
|
|
1687
2403
|
} catch (error) {
|
|
1688
2404
|
reportCacheError(error, "cache-read", "[CFCacheStore] kvGetItem");
|
|
2405
|
+
if (this.debug) this.emitDebug({ op: "getItem", key, outcome: "error" });
|
|
1689
2406
|
return null;
|
|
1690
2407
|
}
|
|
1691
2408
|
}
|
|
@@ -1717,6 +2434,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1717
2434
|
"Content-Type": "application/json",
|
|
1718
2435
|
"Cache-Control": `public, max-age=${remainingTtl}`,
|
|
1719
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),
|
|
1720
2439
|
[CACHE_STATUS_HEADER]: "HIT",
|
|
1721
2440
|
// Preserve tags across KV->L1 promotion (the item tier previously
|
|
1722
2441
|
// dropped them, permanently disabling tag invalidation here).
|
|
@@ -1742,15 +2461,32 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1742
2461
|
if (!this.kv) return null;
|
|
1743
2462
|
|
|
1744
2463
|
try {
|
|
1745
|
-
const kvKey = this.
|
|
1746
|
-
|
|
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>(
|
|
1747
2471
|
kvKey,
|
|
1748
2472
|
(e) =>
|
|
1749
2473
|
typeof e.b === "string" &&
|
|
1750
2474
|
typeof e.st === "number" &&
|
|
1751
2475
|
typeof e.e === "number" &&
|
|
1752
2476
|
typeof e.s === "number" &&
|
|
1753
|
-
|
|
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
|
+
),
|
|
1754
2490
|
"kvGetResponse",
|
|
1755
2491
|
);
|
|
1756
2492
|
if (!envelope) return null;
|
|
@@ -1772,6 +2508,12 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1772
2508
|
// so evict it and miss rather than re-failing every read until TTL.
|
|
1773
2509
|
let response: Response;
|
|
1774
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
|
+
);
|
|
1775
2517
|
const bodyBuffer = base64ToBuffer(envelope.b);
|
|
1776
2518
|
const headers = new Headers(envelope.hd);
|
|
1777
2519
|
response = new Response(bodyBuffer, {
|
|
@@ -1785,7 +2527,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1785
2527
|
"cache-corrupt",
|
|
1786
2528
|
"[CFCacheStore] kvGetResponse: corrupt response envelope, evicting",
|
|
1787
2529
|
);
|
|
1788
|
-
|
|
2530
|
+
this.scheduleKvEvict(kvKey, "kvGetResponse");
|
|
1789
2531
|
return null;
|
|
1790
2532
|
}
|
|
1791
2533
|
|
|
@@ -1824,13 +2566,15 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1824
2566
|
}
|
|
1825
2567
|
headers.set("Cache-Control", `public, max-age=${remainingTtl}`);
|
|
1826
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));
|
|
1827
2574
|
// Re-attach the internal tag headers (envelope.hd is client-facing
|
|
1828
2575
|
// and intentionally excludes them) so the promoted entry stays
|
|
1829
2576
|
// invalidatable.
|
|
1830
|
-
|
|
1831
|
-
for (const [name, value] of Object.entries(tagHeaders)) {
|
|
1832
|
-
headers.set(name, value);
|
|
1833
|
-
}
|
|
2577
|
+
this.setTagHeaders(headers, envelope.t, envelope.ta);
|
|
1834
2578
|
|
|
1835
2579
|
const bodyBuffer = base64ToBuffer(envelope.b);
|
|
1836
2580
|
const response = new Response(bodyBuffer, {
|
|
@@ -1847,27 +2591,3 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1847
2591
|
);
|
|
1848
2592
|
}
|
|
1849
2593
|
}
|
|
1850
|
-
|
|
1851
|
-
// ============================================================================
|
|
1852
|
-
// Base64 Helpers (binary-safe response body encoding for KV)
|
|
1853
|
-
// ============================================================================
|
|
1854
|
-
|
|
1855
|
-
/** Encode ArrayBuffer to base64 string. */
|
|
1856
|
-
function bufferToBase64(buffer: ArrayBuffer): string {
|
|
1857
|
-
const bytes = new Uint8Array(buffer);
|
|
1858
|
-
let binary = "";
|
|
1859
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
1860
|
-
binary += String.fromCharCode(bytes[i]!);
|
|
1861
|
-
}
|
|
1862
|
-
return btoa(binary);
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
/** Decode base64 string to ArrayBuffer. */
|
|
1866
|
-
function base64ToBuffer(base64: string): ArrayBuffer {
|
|
1867
|
-
const binary = atob(base64);
|
|
1868
|
-
const bytes = new Uint8Array(binary.length);
|
|
1869
|
-
for (let i = 0; i < binary.length; i++) {
|
|
1870
|
-
bytes[i] = binary.charCodeAt(i);
|
|
1871
|
-
}
|
|
1872
|
-
return bytes.buffer;
|
|
1873
|
-
}
|