@rangojs/router 0.0.0-experimental.80 → 0.0.0-experimental.80daa23e
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 -4
- package/README.md +318 -791
- 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 +927 -273
- 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/{src/network-error-thrower.tsx → dist/network-error-thrower.d.ts} +4 -10
- 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/__internal.d.ts +127 -0
- package/dist/types/bin/rango.d.ts +1 -0
- package/dist/types/browser/action-coordinator.d.ts +57 -0
- package/dist/types/browser/action-fence.d.ts +33 -0
- package/dist/types/browser/app-shell.d.ts +34 -0
- package/dist/types/browser/app-version.d.ts +6 -0
- package/dist/types/browser/connection-warmup.d.ts +31 -0
- package/dist/types/browser/cookie-name.d.ts +66 -0
- package/dist/types/browser/dev-discovery.d.ts +11 -0
- package/dist/types/browser/event-controller.d.ts +231 -0
- package/dist/types/browser/history-state.d.ts +26 -0
- package/dist/types/browser/index.d.ts +1 -0
- package/dist/types/browser/intercept-utils.d.ts +30 -0
- package/dist/types/browser/invalidate-client-cache.d.ts +17 -0
- package/dist/types/browser/link-interceptor.d.ts +53 -0
- package/dist/types/browser/logging.d.ts +33 -0
- package/dist/types/browser/merge-segment-loaders.d.ts +38 -0
- package/dist/types/browser/navigation-bridge.d.ts +39 -0
- package/dist/types/browser/navigation-client.d.ts +17 -0
- package/dist/types/browser/navigation-store-handle.d.ts +25 -0
- package/dist/types/browser/navigation-store.d.ts +85 -0
- package/dist/types/browser/navigation-transaction.d.ts +75 -0
- package/dist/types/browser/network-error-handler.d.ts +35 -0
- package/dist/types/browser/notify-listeners.d.ts +2 -0
- package/dist/types/browser/partial-update.d.ts +61 -0
- package/dist/types/browser/prefetch/cache.d.ts +224 -0
- package/dist/types/browser/prefetch/default-strategy.d.ts +31 -0
- package/dist/types/browser/prefetch/fetch.d.ts +52 -0
- package/dist/types/browser/prefetch/invalidation.d.ts +6 -0
- package/dist/types/browser/prefetch/loader.d.ts +12 -0
- package/dist/types/browser/prefetch/observer.d.ts +24 -0
- package/dist/types/browser/prefetch/policy.d.ts +13 -0
- package/dist/types/browser/prefetch/queue.d.ts +48 -0
- package/dist/types/browser/prefetch/resource-ready.d.ts +28 -0
- package/dist/types/browser/prefetch/runtime.d.ts +2 -0
- package/dist/types/browser/rango-state.d.ts +56 -0
- package/dist/types/browser/react/Link.d.ts +132 -0
- package/dist/types/browser/react/NavigationProvider.d.ts +88 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +78 -0
- package/dist/types/browser/react/context.d.ts +57 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +35 -0
- package/dist/types/browser/react/index.d.ts +1 -0
- package/dist/types/browser/react/location-state-shared.d.ts +162 -0
- package/dist/types/browser/react/location-state.d.ts +29 -0
- package/dist/types/browser/react/mount-context.d.ts +23 -0
- package/dist/types/browser/react/nonce-context.d.ts +14 -0
- package/dist/types/browser/react/shallow-equal.d.ts +5 -0
- package/dist/types/browser/react/use-action.d.ts +61 -0
- package/dist/types/browser/react/use-handle.d.ts +21 -0
- package/dist/types/browser/react/use-href.d.ts +32 -0
- package/dist/types/browser/react/use-link-status.d.ts +36 -0
- package/dist/types/browser/react/use-mount.d.ts +24 -0
- package/dist/types/browser/react/use-navigation.d.ts +15 -0
- package/dist/types/browser/react/use-params.d.ts +21 -0
- package/dist/types/browser/react/use-pathname.d.ts +13 -0
- package/dist/types/browser/react/use-reverse.d.ts +40 -0
- package/dist/types/browser/react/use-router.d.ts +30 -0
- package/dist/types/browser/react/use-search-params.d.ts +66 -0
- package/dist/types/browser/react/use-segments.d.ts +29 -0
- package/dist/types/browser/response-adapter.d.ts +58 -0
- package/dist/types/browser/rsc-router.d.ts +146 -0
- package/dist/types/browser/scroll-restoration.d.ts +103 -0
- package/dist/types/browser/segment-reconciler.d.ts +74 -0
- package/dist/types/browser/segment-structure-assert.d.ts +16 -0
- package/dist/types/browser/server-action-bridge.d.ts +29 -0
- package/dist/types/browser/types.d.ts +546 -0
- package/dist/types/browser/validate-redirect-origin.d.ts +28 -0
- package/dist/types/build/collect-fallback-refs.d.ts +5 -0
- package/dist/types/build/generate-manifest.d.ts +106 -0
- package/dist/types/build/generate-route-types.d.ts +8 -0
- package/dist/types/build/index.d.ts +21 -0
- package/dist/types/build/merge-full-manifests.d.ts +3 -0
- package/dist/types/build/prefix-tree-utils.d.ts +56 -0
- package/dist/types/build/route-trie.d.ts +20 -0
- package/dist/types/build/route-types/ast-helpers.d.ts +3 -0
- package/dist/types/build/route-types/ast-route-extraction.d.ts +13 -0
- package/dist/types/build/route-types/codegen.d.ts +16 -0
- package/dist/types/build/route-types/include-resolution.d.ts +74 -0
- package/dist/types/build/route-types/param-extraction.d.ts +13 -0
- package/dist/types/build/route-types/per-module-writer.d.ts +20 -0
- package/dist/types/build/route-types/router-processing.d.ts +81 -0
- package/dist/types/build/route-types/scan-filter.d.ts +17 -0
- package/dist/types/build/route-types/source-scan.d.ts +13 -0
- package/dist/types/build/runtime-discovery.d.ts +24 -0
- package/dist/types/cache/background-task.d.ts +21 -0
- package/dist/types/cache/cache-error.d.ts +71 -0
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/cache-key-utils.d.ts +43 -0
- package/dist/types/cache/cache-policy.d.ts +59 -0
- package/dist/types/cache/cache-runtime.d.ts +51 -0
- package/dist/types/cache/cache-scope.d.ts +215 -0
- package/dist/types/cache/cache-tag.d.ts +79 -0
- package/dist/types/cache/cf/cf-base64.d.ts +4 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +112 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +630 -0
- package/dist/types/cache/cf/cf-cache-types.d.ts +365 -0
- package/dist/types/cache/cf/cf-kv-utils.d.ts +43 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +15 -0
- package/dist/types/cache/cf/cf-zone-purge.d.ts +31 -0
- package/dist/types/cache/cf/index.d.ts +4 -0
- package/dist/types/cache/document-cache.d.ts +71 -0
- package/dist/types/cache/handle-capture.d.ts +23 -0
- package/dist/types/cache/handle-snapshot.d.ts +39 -0
- package/dist/types/cache/index.d.ts +9 -0
- package/dist/types/cache/memory-segment-store.d.ts +163 -0
- package/dist/types/cache/profile-registry.d.ts +40 -0
- package/dist/types/cache/read-through-swr.d.ts +61 -0
- package/dist/types/cache/search-params-filter.d.ts +64 -0
- package/dist/types/cache/segment-codec.d.ts +78 -0
- package/dist/types/cache/shell-snapshot.d.ts +186 -0
- package/dist/types/cache/tag-invalidation.d.ts +74 -0
- package/dist/types/cache/taint.d.ts +77 -0
- package/dist/types/cache/types.d.ts +522 -0
- package/dist/types/cache/vercel/index.d.ts +1 -0
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +268 -0
- package/dist/types/client-urls/client-root.d.ts +38 -0
- package/dist/types/client-urls/client-urls.d.ts +5 -0
- package/dist/types/client-urls/navigation.d.ts +38 -0
- package/dist/types/client-urls/revalidation-protocol.d.ts +25 -0
- package/dist/types/client-urls/server-projection.d.ts +71 -0
- package/dist/types/client-urls/types.d.ts +148 -0
- package/dist/types/client.d.ts +192 -0
- package/dist/types/client.rsc.d.ts +42 -0
- package/dist/types/cloudflare/index.d.ts +7 -0
- package/dist/types/cloudflare/tracing.d.ts +57 -0
- package/dist/types/component-utils.d.ts +46 -0
- package/dist/types/components/DefaultDocument.d.ts +13 -0
- package/dist/types/context-var.d.ts +84 -0
- package/dist/types/debug.d.ts +57 -0
- package/dist/types/decode-loader-results.d.ts +42 -0
- package/dist/types/default-error-boundary.d.ts +10 -0
- package/dist/types/defer.d.ts +89 -0
- package/dist/types/deps/browser.d.ts +1 -0
- package/dist/types/deps/html-stream-client.d.ts +1 -0
- package/dist/types/deps/html-stream-server.d.ts +1 -0
- package/dist/types/deps/rsc.d.ts +1 -0
- package/dist/types/deps/ssr.d.ts +1 -0
- package/dist/types/dev-discovery-protocol.d.ts +5 -0
- package/dist/types/encode-kv.d.ts +35 -0
- package/dist/types/errors.d.ts +227 -0
- package/dist/types/escape-script.d.ts +44 -0
- package/dist/types/handle.d.ts +93 -0
- package/dist/types/handles/MetaTags.d.ts +17 -0
- package/dist/types/handles/Scripts.d.ts +38 -0
- package/dist/types/handles/breadcrumbs.d.ts +43 -0
- package/dist/types/handles/deferred-resolution.d.ts +53 -0
- package/dist/types/handles/is-thenable.d.ts +10 -0
- package/dist/types/handles/meta.d.ts +43 -0
- package/dist/types/handles/script.d.ts +139 -0
- package/dist/types/host/cookie-handler.d.ts +8 -0
- package/dist/types/host/errors.d.ts +40 -0
- package/dist/types/host/index.d.ts +33 -0
- package/dist/types/host/pattern-matcher.d.ts +30 -0
- package/dist/types/host/router.d.ts +12 -0
- package/dist/types/host/testing.d.ts +41 -0
- package/dist/types/host/types.d.ts +148 -0
- package/dist/types/host/utils.d.ts +20 -0
- package/dist/types/href-client.d.ts +214 -0
- package/dist/types/index.d.ts +113 -0
- package/dist/types/index.rsc.d.ts +52 -0
- package/dist/types/internal-debug.d.ts +1 -0
- package/dist/types/loader-redirect.d.ts +27 -0
- package/dist/types/loader-store.d.ts +193 -0
- package/dist/types/loader.d.ts +18 -0
- package/dist/types/loader.rsc.d.ts +18 -0
- package/dist/types/missing-id-error.d.ts +1 -0
- package/dist/types/outlet-context.d.ts +32 -0
- package/dist/types/outlet-provider.d.ts +15 -0
- package/dist/types/prerender/build-shell-capture.d.ts +104 -0
- package/dist/types/prerender/param-hash.d.ts +6 -0
- package/dist/types/prerender/shell-manifest-key.d.ts +18 -0
- package/dist/types/prerender/store.d.ts +63 -0
- package/dist/types/prerender.d.ts +292 -0
- package/dist/types/redirect-origin.d.ts +71 -0
- package/dist/types/regex-escape.d.ts +6 -0
- package/dist/types/render-error-thrower.d.ts +13 -0
- package/dist/types/response-utils.d.ts +35 -0
- package/dist/types/reverse.d.ts +206 -0
- package/dist/types/root-error-boundary.d.ts +32 -0
- package/dist/types/route-content-wrapper.d.ts +84 -0
- package/dist/types/route-definition/dsl-helpers.d.ts +130 -0
- package/dist/types/route-definition/helper-factories.d.ts +22 -0
- package/dist/types/route-definition/helpers-types.d.ts +405 -0
- package/dist/types/route-definition/index.d.ts +7 -0
- package/dist/types/route-definition/redirect.d.ts +48 -0
- package/dist/types/route-definition/resolve-handler-use.d.ts +19 -0
- package/dist/types/route-definition/use-item-types.d.ts +1 -0
- package/dist/types/route-definition.d.ts +1 -0
- package/dist/types/route-map-builder.d.ts +102 -0
- package/dist/types/route-name.d.ts +27 -0
- package/dist/types/route-types.d.ts +172 -0
- package/dist/types/router/basename.d.ts +10 -0
- package/dist/types/router/content-negotiation.d.ts +91 -0
- package/dist/types/router/debug-manifest.d.ts +7 -0
- package/dist/types/router/error-handling.d.ts +110 -0
- package/dist/types/router/find-match.d.ts +19 -0
- package/dist/types/router/handler-context.d.ts +41 -0
- package/dist/types/router/instrument.d.ts +191 -0
- package/dist/types/router/intercept-resolution.d.ts +91 -0
- package/dist/types/router/lazy-includes.d.ts +26 -0
- package/dist/types/router/loader-resolution.d.ts +85 -0
- package/dist/types/router/logging.d.ts +41 -0
- package/dist/types/router/manifest.d.ts +8 -0
- package/dist/types/router/match-api.d.ts +19 -0
- package/dist/types/router/match-context.d.ts +184 -0
- package/dist/types/router/match-handlers.d.ts +48 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +113 -0
- package/dist/types/router/match-middleware/cache-lookup.d.ts +126 -0
- package/dist/types/router/match-middleware/cache-store.d.ts +112 -0
- package/dist/types/router/match-middleware/index.d.ts +80 -0
- package/dist/types/router/match-middleware/intercept-resolution.d.ts +116 -0
- package/dist/types/router/match-middleware/segment-resolution.d.ts +94 -0
- package/dist/types/router/match-pipelines.d.ts +103 -0
- package/dist/types/router/match-result.d.ts +114 -0
- package/dist/types/router/metrics.d.ts +6 -0
- package/dist/types/router/middleware-types.d.ts +74 -0
- package/dist/types/router/middleware.d.ts +116 -0
- package/dist/types/router/navigation-snapshot.d.ts +51 -0
- package/dist/types/router/params-util.d.ts +8 -0
- package/dist/types/router/parse-pattern.d.ts +38 -0
- package/dist/types/router/pattern-matching.d.ts +169 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +27 -0
- package/dist/types/router/prefetch-default.d.ts +28 -0
- package/dist/types/router/prefetch-limits.d.ts +20 -0
- package/dist/types/router/prerender-match.d.ts +53 -0
- package/dist/types/router/preview-match.d.ts +22 -0
- package/dist/types/router/request-classification.d.ts +104 -0
- package/dist/types/router/revalidation.d.ts +64 -0
- package/dist/types/router/route-snapshot.d.ts +112 -0
- package/dist/types/router/route-trie-builder.d.ts +77 -0
- package/dist/types/router/router-context.d.ts +137 -0
- package/dist/types/router/router-interfaces.d.ts +461 -0
- package/dist/types/router/router-options.d.ts +792 -0
- package/dist/types/router/router-registry.d.ts +15 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +55 -0
- package/dist/types/router/segment-resolution/helpers.d.ts +93 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +33 -0
- package/dist/types/router/segment-resolution/loader-mask.d.ts +44 -0
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +90 -0
- package/dist/types/router/segment-resolution/mask-nested.d.ts +53 -0
- package/dist/types/router/segment-resolution/revalidation.d.ts +85 -0
- package/dist/types/router/segment-resolution/static-store.d.ts +17 -0
- package/dist/types/router/segment-resolution/streamed-handler-telemetry.d.ts +16 -0
- package/dist/types/router/segment-resolution/view-transition-default.d.ts +27 -0
- package/dist/types/router/segment-resolution.d.ts +3 -0
- package/dist/types/router/segment-wrappers.d.ts +53 -0
- package/dist/types/router/state-cookie-name.d.ts +1 -0
- package/dist/types/router/substitute-pattern-params.d.ts +23 -0
- package/dist/types/router/telemetry-otel.d.ts +113 -0
- package/dist/types/router/telemetry.d.ts +216 -0
- package/dist/types/router/timeout.d.ts +107 -0
- package/dist/types/router/tracing.d.ts +139 -0
- package/dist/types/router/transition-when.d.ts +13 -0
- package/dist/types/router/trie-matching.d.ts +32 -0
- package/dist/types/router/types.d.ts +98 -0
- package/dist/types/router/url-params.d.ts +26 -0
- package/dist/types/router.d.ts +7 -0
- package/dist/types/rsc/capture-queue.d.ts +49 -0
- package/dist/types/rsc/full-payload.d.ts +22 -0
- package/dist/types/rsc/handler-context.d.ts +32 -0
- package/dist/types/rsc/handler.d.ts +9 -0
- package/dist/types/rsc/helpers.d.ts +127 -0
- package/dist/types/rsc/index.d.ts +17 -0
- package/dist/types/rsc/json-route-result.d.ts +20 -0
- package/dist/types/rsc/loader-fetch.d.ts +14 -0
- package/dist/types/rsc/manifest-init.d.ts +18 -0
- package/dist/types/rsc/nonce.d.ts +38 -0
- package/dist/types/rsc/origin-guard.d.ts +50 -0
- package/dist/types/rsc/progressive-enhancement.d.ts +19 -0
- package/dist/types/rsc/redirect-guard.d.ts +37 -0
- package/dist/types/rsc/render-pipeline.d.ts +148 -0
- package/dist/types/rsc/response-cache-serve.d.ts +46 -0
- package/dist/types/rsc/response-error.d.ts +19 -0
- package/dist/types/rsc/response-route-handler.d.ts +29 -0
- package/dist/types/rsc/routine-plan.d.ts +124 -0
- package/dist/types/rsc/rsc-rendering.d.ts +18 -0
- package/dist/types/rsc/runtime-warnings.d.ts +22 -0
- package/dist/types/rsc/server-action.d.ts +68 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +90 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +44 -0
- package/dist/types/rsc/shell-capture.d.ts +425 -0
- package/dist/types/rsc/shell-serve.d.ts +200 -0
- package/dist/types/rsc/ssr-setup.d.ts +73 -0
- package/dist/types/rsc/stream-idle.d.ts +60 -0
- package/dist/types/rsc/transition-gate.d.ts +23 -0
- package/dist/types/rsc/types.d.ts +353 -0
- package/dist/types/runtime-env.d.ts +1 -0
- package/dist/types/search-params.d.ts +125 -0
- package/dist/types/segment-content-promise.d.ts +13 -0
- package/dist/types/segment-fragments.d.ts +79 -0
- package/dist/types/segment-loader-promise.d.ts +22 -0
- package/dist/types/segment-system.d.ts +86 -0
- package/dist/types/serialize.d.ts +164 -0
- package/dist/types/server/context.d.ts +520 -0
- package/dist/types/server/cookie-parse.d.ts +10 -0
- package/dist/types/server/cookie-store.d.ts +107 -0
- package/dist/types/server/fetchable-loader-store.d.ts +20 -0
- package/dist/types/server/handle-store.d.ts +131 -0
- package/dist/types/server/loader-registry.d.ts +32 -0
- package/dist/types/server/request-context.d.ts +717 -0
- package/dist/types/server/root-layout.d.ts +3 -0
- package/dist/types/server.d.ts +16 -0
- package/dist/types/ssr/index.d.ts +271 -0
- package/dist/types/ssr/inject-rsc-eager.d.ts +3 -0
- package/dist/types/ssr/preinit-client-references.d.ts +71 -0
- package/dist/types/ssr/ssr-root.d.ts +84 -0
- package/dist/types/ssr-suspension-warning.d.ts +19 -0
- package/dist/types/static-handler.d.ts +71 -0
- package/dist/types/testing/cache-status.d.ts +63 -0
- package/dist/types/testing/collect-handle.d.ts +20 -0
- package/dist/types/testing/dispatch.d.ts +127 -0
- package/dist/types/testing/dom.entry.d.ts +15 -0
- package/dist/types/testing/e2e/fixture.d.ts +37 -0
- package/dist/types/testing/e2e/index.d.ts +31 -0
- package/dist/types/testing/e2e/matchers.d.ts +17 -0
- package/dist/types/testing/e2e/page-helpers.d.ts +86 -0
- package/dist/types/testing/e2e/parity.d.ts +111 -0
- package/dist/types/testing/e2e/server.d.ts +35 -0
- package/dist/types/testing/flight-matchers.d.ts +55 -0
- package/dist/types/testing/flight-normalize.d.ts +1 -0
- package/dist/types/testing/flight-tree.d.ts +192 -0
- package/dist/types/testing/flight.d.ts +115 -0
- package/dist/types/testing/flight.entry.d.ts +27 -0
- package/dist/types/testing/generated-routes.d.ts +66 -0
- package/dist/types/testing/index.d.ts +54 -0
- package/dist/types/testing/internal/context.d.ts +225 -0
- package/dist/types/testing/internal/flight-client-globals.d.ts +1 -0
- package/dist/types/testing/internal/seed-vars.d.ts +30 -0
- package/dist/types/testing/render-handler.d.ts +160 -0
- package/dist/types/testing/render-route.d.ts +265 -0
- package/dist/types/testing/run-loader.d.ts +195 -0
- package/dist/types/testing/run-middleware.d.ts +132 -0
- package/dist/types/testing/run-transition-when.d.ts +78 -0
- package/dist/types/testing/shell-status.d.ts +81 -0
- package/dist/types/testing/vitest-stubs/cloudflare-email.d.ts +6 -0
- package/dist/types/testing/vitest-stubs/cloudflare-workers.d.ts +13 -0
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +7 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/testing/vitest.d.ts +205 -0
- package/dist/types/theme/ThemeProvider.d.ts +13 -0
- package/dist/types/theme/ThemeScript.d.ts +45 -0
- package/dist/types/theme/constants.d.ts +39 -0
- package/dist/types/theme/index.d.ts +29 -0
- package/dist/types/theme/theme-context.d.ts +21 -0
- package/dist/types/theme/theme-script.d.ts +26 -0
- package/dist/types/theme/types.d.ts +162 -0
- package/dist/types/theme/use-theme.d.ts +8 -0
- package/dist/types/types/boundaries.d.ts +115 -0
- package/dist/types/types/cache-types.d.ts +191 -0
- package/dist/types/types/error-types.d.ts +114 -0
- package/dist/types/types/global-namespace.d.ts +90 -0
- package/dist/types/types/handler-context.d.ts +658 -0
- package/dist/types/types/index.d.ts +11 -0
- package/dist/types/types/loader-types.d.ts +240 -0
- package/dist/types/types/request-scope.d.ts +93 -0
- package/dist/types/types/route-config.d.ts +105 -0
- package/dist/types/types/route-entry.d.ts +98 -0
- package/dist/types/types/segments.d.ts +254 -0
- package/dist/types/types.d.ts +1 -0
- package/dist/types/urls/include-helper.d.ts +17 -0
- package/dist/types/urls/include-provider.d.ts +27 -0
- package/dist/types/urls/index.d.ts +6 -0
- package/dist/types/urls/path-helper-types.d.ts +207 -0
- package/dist/types/urls/path-helper.d.ts +12 -0
- package/dist/types/urls/pattern-types.d.ts +166 -0
- package/dist/types/urls/response-types.d.ts +67 -0
- package/dist/types/urls/type-extraction.d.ts +157 -0
- package/dist/types/urls/urls-function.d.ts +24 -0
- package/dist/types/urls.d.ts +1 -0
- package/dist/types/use-loader.d.ts +150 -0
- package/dist/types/vercel/index.d.ts +10 -0
- package/dist/types/vercel/tracing.d.ts +71 -0
- package/dist/types/vite/debug.d.ts +80 -0
- package/dist/types/vite/discovery/bundle-postprocess.d.ts +12 -0
- package/dist/types/vite/discovery/client-urls-projection.d.ts +53 -0
- package/dist/types/vite/discovery/dev-prerender-cache.d.ts +65 -0
- package/dist/types/vite/discovery/discover-routers.d.ts +17 -0
- package/dist/types/vite/discovery/discovery-errors.d.ts +113 -0
- package/dist/types/vite/discovery/gate-state.d.ts +79 -0
- package/dist/types/vite/discovery/prerender-collection.d.ts +24 -0
- package/dist/types/vite/discovery/route-types-writer.d.ts +32 -0
- package/dist/types/vite/discovery/self-gen-tracking.d.ts +22 -0
- package/dist/types/vite/discovery/shell-prerender-phase.d.ts +40 -0
- package/dist/types/vite/discovery/state.d.ts +170 -0
- package/dist/types/vite/discovery/virtual-module-codegen.d.ts +15 -0
- package/dist/types/vite/encryption-key.d.ts +2 -0
- package/dist/types/vite/index.d.ts +11 -0
- package/dist/types/vite/inject-client-debug.d.ts +56 -0
- package/dist/types/vite/plugin-types.d.ts +298 -0
- package/dist/types/vite/plugins/cjs-to-esm.d.ts +6 -0
- package/dist/types/vite/plugins/client-ref-dedup.d.ts +29 -0
- package/dist/types/vite/plugins/client-ref-hashing.d.ts +35 -0
- package/dist/types/vite/plugins/cloudflare-protocol-stub.d.ts +64 -0
- package/dist/types/vite/plugins/expose-action-id.d.ts +18 -0
- package/dist/types/vite/plugins/expose-id-utils.d.ts +37 -0
- package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +19 -0
- package/dist/types/vite/plugins/expose-ids/handler-transform.d.ts +10 -0
- package/dist/types/vite/plugins/expose-ids/loader-transform.d.ts +8 -0
- package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +13 -0
- package/dist/types/vite/plugins/expose-ids/types.d.ts +29 -0
- package/dist/types/vite/plugins/expose-internal-ids.d.ts +16 -0
- package/dist/types/vite/plugins/performance-tracks.d.ts +25 -0
- package/dist/types/vite/plugins/refresh-cmd.d.ts +20 -0
- package/dist/types/vite/plugins/server-ref-hashing.d.ts +24 -0
- package/dist/types/vite/plugins/server-reference-pattern.d.ts +1 -0
- package/dist/types/vite/plugins/use-cache-transform.d.ts +20 -0
- package/dist/types/vite/plugins/vercel-output.d.ts +85 -0
- package/dist/types/vite/plugins/version-injector.d.ts +21 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +19 -0
- package/dist/types/vite/plugins/virtual-entries.d.ts +36 -0
- package/dist/types/vite/plugins/virtual-stub-plugin.d.ts +7 -0
- package/dist/types/vite/rango.d.ts +29 -0
- package/dist/types/vite/router-discovery.d.ts +23 -0
- package/dist/types/vite/utils/ast-handler-extract.d.ts +64 -0
- package/dist/types/vite/utils/banner.d.ts +2 -0
- package/dist/types/vite/utils/bundle-analysis.d.ts +28 -0
- package/dist/types/vite/utils/client-chunks.d.ts +55 -0
- package/dist/types/vite/utils/directive-prologue.d.ts +16 -0
- package/dist/types/vite/utils/forward-user-plugins.d.ts +37 -0
- package/dist/types/vite/utils/manifest-utils.d.ts +7 -0
- package/dist/types/vite/utils/package-resolution.d.ts +6 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +32 -0
- package/dist/types/vite/utils/shared-utils.d.ts +67 -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 +10793 -4122
- 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/plugins/cloudflare-protocol-loader-hook.mjs +76 -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 +121 -38
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +117 -8
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +232 -34
- package/skills/caching/SKILL.md +375 -19
- package/skills/catalog.json +289 -0
- package/skills/client-urls/SKILL.md +339 -0
- package/skills/cloudflare/SKILL.md +153 -0
- package/skills/cloudflare/agents/openai.yaml +4 -0
- package/skills/cloudflare/references/d1-and-local-dev.md +131 -0
- package/skills/cloudflare/references/streaming-and-deploy.md +107 -0
- package/skills/cloudflare/references/webhooks-and-crypto.md +147 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +858 -0
- package/skills/composability/SKILL.md +124 -4
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +5 -11
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/deployment-caching/SKILL.md +176 -0
- package/skills/document-cache/SKILL.md +116 -58
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +12 -10
- package/skills/hooks/SKILL.md +73 -691
- package/skills/hooks/data.md +282 -0
- package/skills/hooks/handle-and-actions.md +110 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +62 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +172 -0
- package/skills/host-router/SKILL.md +129 -27
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +75 -19
- package/skills/layout/SKILL.md +40 -19
- package/skills/links/SKILL.md +283 -17
- package/skills/loader/SKILL.md +362 -11
- package/skills/middleware/SKILL.md +58 -13
- package/skills/migrate-nextjs/SKILL.md +336 -42
- package/skills/migrate-nextjs/backend-host-swap.md +120 -0
- package/skills/migrate-react-router/SKILL.md +95 -670
- package/skills/migrate-react-router/component-migration.md +201 -0
- package/skills/migrate-react-router/data-and-actions.md +236 -0
- package/skills/migrate-react-router/route-mapping.md +284 -0
- package/skills/mime-routes/SKILL.md +55 -18
- package/skills/observability/SKILL.md +231 -0
- package/skills/parallel/SKILL.md +72 -11
- package/skills/ppr/SKILL.md +918 -0
- package/skills/prerender/SKILL.md +137 -71
- package/skills/rango/SKILL.md +325 -26
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +180 -52
- package/skills/route/SKILL.md +126 -9
- package/skills/router-setup/SKILL.md +69 -11
- package/skills/scripts/SKILL.md +189 -0
- package/skills/server-actions/SKILL.md +776 -0
- package/skills/shell-manifest/SKILL.md +194 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +28 -4
- package/skills/testing/SKILL.md +131 -0
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +172 -0
- package/skills/testing/client-components.md +131 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +153 -0
- package/skills/testing/loader.md +130 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +85 -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/theme/SKILL.md +9 -6
- package/skills/typesafety/SKILL.md +45 -626
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +342 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +213 -0
- package/skills/use-cache/SKILL.md +121 -15
- package/skills/vercel/SKILL.md +149 -0
- package/skills/view-transitions/SKILL.md +341 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/bin/rango.ts +7 -3
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/dev-discovery.ts +66 -0
- package/src/browser/event-controller.ts +400 -168
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/link-interceptor.ts +469 -20
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +171 -27
- package/src/browser/navigation-client.ts +216 -90
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +167 -323
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/notify-listeners.ts +22 -0
- package/src/browser/partial-update.ts +243 -144
- package/src/browser/prefetch/cache.ts +325 -84
- package/src/browser/prefetch/default-strategy.ts +74 -0
- package/src/browser/prefetch/fetch.ts +411 -69
- package/src/browser/prefetch/invalidation.ts +30 -0
- package/src/browser/prefetch/loader.ts +111 -0
- package/src/browser/prefetch/observer.ts +50 -22
- package/src/browser/prefetch/queue.ts +61 -12
- package/src/browser/prefetch/runtime.ts +6 -0
- package/src/browser/rango-state.ts +178 -75
- package/src/browser/react/Link.tsx +160 -92
- package/src/browser/react/NavigationProvider.tsx +211 -121
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +4 -0
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +10 -5
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +62 -11
- package/src/browser/react/use-search-params.ts +140 -22
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +99 -8
- package/src/browser/rsc-router.tsx +248 -32
- package/src/browser/scroll-restoration.ts +37 -22
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +262 -65
- package/src/browser/types.ts +132 -34
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +213 -184
- package/src/build/generate-route-types.ts +3 -1
- package/src/build/index.ts +11 -3
- package/src/build/merge-full-manifests.ts +161 -0
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +43 -280
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +513 -64
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +33 -12
- package/src/build/route-types/router-processing.ts +312 -204
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +32 -29
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-exec-scope.ts +47 -0
- package/src/cache/cache-key-utils.ts +73 -15
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +566 -121
- package/src/cache/cache-scope.ts +383 -105
- package/src/cache/cache-tag.ts +149 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +134 -0
- package/src/cache/cf/cf-cache-store.ts +3083 -392
- package/src/cache/cf/cf-cache-types.ts +417 -0
- package/src/cache/cf/cf-kv-utils.ts +84 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +108 -0
- package/src/cache/cf/cf-zone-purge.ts +101 -0
- package/src/cache/cf/index.ts +11 -16
- package/src/cache/document-cache.ts +184 -53
- package/src/cache/handle-snapshot.ts +92 -1
- package/src/cache/index.ts +32 -20
- package/src/cache/memory-segment-store.ts +286 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +66 -16
- package/src/cache/search-params-filter.ts +118 -0
- package/src/cache/segment-codec.ts +80 -25
- package/src/cache/shell-snapshot.ts +516 -0
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +28 -9
- package/src/cache/types.ts +343 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1264 -0
- package/src/client-urls/client-root.tsx +168 -0
- package/src/client-urls/client-urls.ts +779 -0
- package/src/client-urls/navigation.ts +237 -0
- package/src/client-urls/revalidation-protocol.ts +56 -0
- package/src/client-urls/server-projection.ts +670 -0
- package/src/client-urls/types.ts +202 -0
- package/src/client.rsc.tsx +53 -21
- package/src/client.tsx +82 -67
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +112 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +18 -6
- package/src/decode-loader-results.ts +165 -0
- package/src/defer.ts +185 -0
- package/src/deps/ssr.ts +4 -2
- package/src/dev-discovery-protocol.ts +11 -0
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +44 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +67 -37
- package/src/handles/MetaTags.tsx +24 -53
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +35 -8
- package/src/handles/deferred-resolution.ts +134 -0
- package/src/handles/is-thenable.ts +16 -0
- package/src/handles/meta.ts +14 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +13 -22
- package/src/host/index.ts +9 -2
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +108 -12
- package/src/index.ts +106 -14
- package/src/internal-debug.ts +11 -10
- package/src/loader-redirect.tsx +64 -0
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +21 -1
- package/src/outlet-provider.tsx +20 -6
- package/src/prerender/build-shell-capture.ts +453 -0
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/shell-manifest-key.ts +20 -0
- package/src/prerender/store.ts +47 -38
- package/src/prerender.ts +81 -13
- package/src/redirect-origin.ts +172 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +62 -0
- package/src/reverse.ts +65 -39
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +120 -78
- package/src/route-definition/dsl-helpers.ts +342 -313
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +106 -62
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +60 -28
- package/src/route-definition/resolve-handler-use.ts +12 -1
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +102 -74
- package/src/route-types.ts +19 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +120 -30
- package/src/router/error-handling.ts +110 -29
- package/src/router/find-match.ts +129 -30
- package/src/router/handler-context.ts +58 -57
- package/src/router/instrument.ts +401 -0
- package/src/router/intercept-resolution.ts +98 -21
- package/src/router/lazy-includes.ts +83 -59
- package/src/router/loader-resolution.ts +429 -85
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +74 -40
- package/src/router/match-api.ts +94 -58
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +268 -171
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +316 -302
- package/src/router/match-middleware/cache-store.ts +134 -52
- 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 +76 -79
- package/src/router/metrics.ts +17 -36
- package/src/router/middleware-types.ts +20 -130
- package/src/router/middleware.ts +302 -172
- package/src/router/navigation-snapshot.ts +51 -60
- package/src/router/params-util.ts +23 -0
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +181 -150
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-default.ts +59 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +159 -69
- package/src/router/preview-match.ts +6 -2
- package/src/router/request-classification.ts +50 -69
- package/src/router/revalidation.ts +146 -83
- package/src/router/route-snapshot.ts +14 -3
- package/src/router/route-trie-builder.ts +334 -0
- package/src/router/router-context.ts +6 -29
- package/src/router/router-interfaces.ts +168 -38
- package/src/router/router-options.ts +235 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +219 -88
- package/src/router/segment-resolution/helpers.ts +96 -18
- package/src/router/segment-resolution/loader-cache.ts +208 -39
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +99 -0
- package/src/router/segment-resolution/revalidation.ts +318 -321
- package/src/router/segment-resolution/static-store.ts +55 -15
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +60 -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/substitute-pattern-params.ts +75 -0
- package/src/router/telemetry-otel.ts +171 -200
- package/src/router/telemetry.ts +106 -20
- package/src/router/timeout.ts +53 -22
- package/src/router/tracing.ts +235 -0
- package/src/router/transition-when.ts +76 -0
- package/src/router/trie-matching.ts +171 -59
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +57 -0
- package/src/router.ts +249 -79
- package/src/rsc/capture-queue.ts +218 -0
- package/src/rsc/full-payload.ts +77 -0
- package/src/rsc/handler-context.ts +4 -2
- package/src/rsc/handler.ts +570 -288
- package/src/rsc/helpers.ts +245 -47
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +116 -40
- package/src/rsc/manifest-init.ts +55 -56
- package/src/rsc/nonce.ts +26 -1
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +378 -83
- package/src/rsc/redirect-guard.ts +102 -0
- package/src/rsc/render-pipeline.ts +611 -0
- package/src/rsc/response-cache-serve.ts +259 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +80 -206
- package/src/rsc/routine-plan.ts +359 -0
- package/src/rsc/rsc-rendering.ts +1647 -164
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +421 -138
- package/src/rsc/shell-build-manifest.ts +343 -0
- package/src/rsc/shell-capture-constants.ts +45 -0
- package/src/rsc/shell-capture.ts +2135 -0
- package/src/rsc/shell-serve.ts +350 -0
- package/src/rsc/ssr-setup.ts +124 -16
- package/src/rsc/stream-idle.ts +137 -0
- package/src/rsc/transition-gate.ts +86 -0
- package/src/rsc/types.ts +125 -7
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-fragments.ts +165 -0
- package/src/segment-loader-promise.ts +49 -4
- package/src/segment-system.tsx +463 -153
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +426 -55
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +162 -15
- package/src/server/handle-store.ts +190 -70
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +824 -215
- package/src/server.ts +7 -2
- package/src/ssr/index.tsx +687 -184
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/preinit-client-references.ts +106 -0
- package/src/ssr/ssr-root.tsx +305 -0
- package/src/ssr-suspension-warning.ts +67 -0
- package/src/static-handler.ts +28 -15
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +46 -0
- package/src/testing/dispatch.ts +951 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +147 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +319 -0
- package/src/testing/e2e/parity.ts +400 -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 +199 -0
- package/src/testing/index.ts +121 -0
- package/src/testing/internal/context.ts +380 -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 +371 -0
- package/src/testing/render-route.tsx +667 -0
- package/src/testing/run-loader.ts +423 -0
- package/src/testing/run-middleware.ts +219 -0
- package/src/testing/run-transition-when.ts +197 -0
- package/src/testing/shell-status.ts +206 -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 +56 -84
- 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 +19 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +123 -64
- package/src/types/index.ts +4 -10
- package/src/types/loader-types.ts +76 -13
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +20 -52
- package/src/types/route-entry.ts +3 -6
- package/src/types/segments.ts +155 -14
- package/src/urls/include-helper.ts +38 -64
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +2 -11
- package/src/urls/path-helper-types.ts +88 -25
- package/src/urls/path-helper.ts +30 -110
- package/src/urls/pattern-types.ts +101 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +98 -154
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +379 -110
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +89 -0
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +18 -14
- package/src/vite/discovery/client-urls-projection.ts +322 -0
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +178 -151
- package/src/vite/discovery/discovery-errors.ts +255 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +125 -70
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/shell-prerender-phase.ts +397 -0
- package/src/vite/discovery/state.ts +104 -6
- package/src/vite/discovery/virtual-module-codegen.ts +226 -87
- package/src/vite/encryption-key.ts +29 -0
- package/src/vite/index.ts +12 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugin-types.ts +243 -10
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +296 -29
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +95 -120
- package/src/vite/plugins/expose-id-utils.ts +88 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +551 -486
- package/src/vite/plugins/performance-tracks.ts +26 -25
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/server-ref-hashing.ts +74 -0
- package/src/vite/plugins/server-reference-pattern.ts +10 -0
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +39 -29
- package/src/vite/plugins/version-plugin.ts +45 -40
- package/src/vite/plugins/virtual-entries.ts +193 -30
- package/src/vite/rango.ts +287 -140
- package/src/vite/router-discovery.ts +1639 -157
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +1 -59
- package/src/vite/utils/package-resolution.ts +21 -52
- package/src/vite/utils/prerender-utils.ts +96 -49
- package/src/vite/utils/shared-utils.ts +156 -43
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/shallow.ts +0 -40
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -34,61 +34,206 @@ import type {
|
|
|
34
34
|
CacheGetResult,
|
|
35
35
|
CacheItemResult,
|
|
36
36
|
CacheItemOptions,
|
|
37
|
+
ShellCacheEntry,
|
|
38
|
+
CacheReadError,
|
|
37
39
|
} from "../types.js";
|
|
40
|
+
import { CACHE_READ_ERROR } from "../types.js";
|
|
38
41
|
import {
|
|
39
42
|
_getRequestContext,
|
|
40
43
|
type RequestContext,
|
|
41
44
|
} from "../../server/request-context.js";
|
|
45
|
+
import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
|
|
42
46
|
import { VERSION } from "@rangojs/router:version";
|
|
47
|
+
import {
|
|
48
|
+
isPerClientSignalHeader,
|
|
49
|
+
stripPerClientSignals,
|
|
50
|
+
} from "../../browser/cookie-name.js";
|
|
43
51
|
import {
|
|
44
52
|
resolveTtl,
|
|
45
53
|
resolveSwrWindow,
|
|
46
54
|
DEFAULT_FUNCTION_TTL,
|
|
47
55
|
} from "../cache-policy.js";
|
|
56
|
+
import { reportCacheError, reportingAsync } from "../cache-error.js";
|
|
57
|
+
import type { CacheErrorCategory } from "../cache-error.js";
|
|
58
|
+
import { bufferToBase64, base64ToBuffer } from "./cf-base64.js";
|
|
59
|
+
import {
|
|
60
|
+
KV_KEY_PRESERVED_PREFIX_BYTES,
|
|
61
|
+
KV_MAX_KEY_BYTES,
|
|
62
|
+
KV_MIN_EXPIRATION_TTL,
|
|
63
|
+
kvKeyByteLength,
|
|
64
|
+
kvKeyDigest,
|
|
65
|
+
remainingCacheControl,
|
|
66
|
+
truncateToBytes,
|
|
67
|
+
} from "./cf-kv-utils.js";
|
|
68
|
+
import {
|
|
69
|
+
TAG_MARKER_CACHE_PREFIX,
|
|
70
|
+
TAG_MARKER_ABSENT,
|
|
71
|
+
getTagMarkerMemo,
|
|
72
|
+
getTagMarkerInflight,
|
|
73
|
+
} from "./cf-tag-marker-memo.js";
|
|
74
|
+
import { createCloudflareZonePurge } from "./cf-zone-purge.js";
|
|
48
75
|
|
|
49
76
|
// ============================================================================
|
|
50
77
|
// Constants
|
|
51
78
|
// ============================================================================
|
|
79
|
+
//
|
|
80
|
+
// Header names, KV prefixes, and timeout/interval defaults live in
|
|
81
|
+
// cf-cache-constants.ts so collaborator modules can share them without a
|
|
82
|
+
// circular import back to this class. They are re-exported below so existing
|
|
83
|
+
// import paths (`../cf-cache-store`, `./cf-cache-store.js`) still resolve.
|
|
84
|
+
import {
|
|
85
|
+
CACHE_STALE_AT_HEADER,
|
|
86
|
+
CACHE_STATUS_HEADER,
|
|
87
|
+
CACHE_TAGS_HEADER,
|
|
88
|
+
CACHE_TAGGED_AT_HEADER,
|
|
89
|
+
TAG_MARKER_PREFIX,
|
|
90
|
+
CACHE_REVALIDATING_AT_HEADER,
|
|
91
|
+
CACHE_EXPIRES_AT_HEADER,
|
|
92
|
+
CACHE_ORIG_CC_HEADER,
|
|
93
|
+
MAX_REVALIDATION_INTERVAL,
|
|
94
|
+
EDGE_LOOKUP_TIMEOUT_MS,
|
|
95
|
+
EDGE_READ_TIMEOUT_MS,
|
|
96
|
+
KV_READ_TIMEOUT_MS,
|
|
97
|
+
} from "./cf-cache-constants.js";
|
|
98
|
+
|
|
99
|
+
// Re-export the public constants so consumers/tests importing them from
|
|
100
|
+
// cf-cache-store keep working after the move.
|
|
101
|
+
export {
|
|
102
|
+
CACHE_STALE_AT_HEADER,
|
|
103
|
+
CACHE_STATUS_HEADER,
|
|
104
|
+
CACHE_TAGS_HEADER,
|
|
105
|
+
CACHE_TAGGED_AT_HEADER,
|
|
106
|
+
TAG_MARKER_PREFIX,
|
|
107
|
+
CACHE_REVALIDATING_AT_HEADER,
|
|
108
|
+
MAX_REVALIDATION_INTERVAL,
|
|
109
|
+
EDGE_LOOKUP_TIMEOUT_MS,
|
|
110
|
+
EDGE_READ_TIMEOUT_MS,
|
|
111
|
+
KV_READ_TIMEOUT_MS,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// The tag-marker prefix/sentinel and per-request memo helpers (with their
|
|
115
|
+
// module-singleton WeakMaps) live in cf-tag-marker-memo.ts; imported above.
|
|
52
116
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Per-request memo of the derived cache-key base URL.
|
|
119
|
+
*
|
|
120
|
+
* deriveBaseUrl() is a pure function of the live request URL, but keyToRequest
|
|
121
|
+
* calls it on EVERY cache operation (each segment/item get/set/delete, each
|
|
122
|
+
* KV->L1 promote, each tag-marker read), so a page composed of many cached
|
|
123
|
+
* entries re-parses the same request.url and re-runs the host validation tens
|
|
124
|
+
* of times. Keying by the request-context object collapses that to one derive
|
|
125
|
+
* per request. Keyed by ctx alone (not by store) because the derived value
|
|
126
|
+
* depends only on the request URL, not on which store asked.
|
|
127
|
+
*/
|
|
128
|
+
const derivedBaseUrlMemo = new WeakMap<object, string>();
|
|
55
129
|
|
|
56
|
-
|
|
57
|
-
|
|
130
|
+
// Pure KV helpers (key byte-length limits, expirationTtl floor, stale-path
|
|
131
|
+
// Cache-Control recompute) live in cf-kv-utils.ts; imported above.
|
|
58
132
|
|
|
59
133
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
134
|
+
* Stores (by namespace) already warned about tag machinery configured without a
|
|
135
|
+
* KV namespace, so the warning fires once per process rather than per request
|
|
136
|
+
* (CFCacheStore is constructed per request).
|
|
63
137
|
*/
|
|
64
|
-
|
|
138
|
+
const warnedNoKvReadInvalidation = new Set<string>();
|
|
65
139
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Stores (by namespace) already warned about a tagInvalidationTtl below KV's
|
|
142
|
+
* expirationTtl floor, so the floor warning fires once per process rather than
|
|
143
|
+
* once per request (CFCacheStore is constructed per request).
|
|
144
|
+
*/
|
|
145
|
+
const warnedTagInvalidationTtlFloor = new Set<string>();
|
|
69
146
|
|
|
70
147
|
/**
|
|
71
|
-
*
|
|
148
|
+
* Stores (by namespace) already warned that a TAGGED shell was written on a
|
|
149
|
+
* KV-less store without tagPurge: no markers and no purge means updateTag()
|
|
150
|
+
* cannot reach the shell (freshness is ttl/swr only). Once per isolate, not
|
|
151
|
+
* per capture (CFCacheStore is constructed per request).
|
|
72
152
|
*/
|
|
73
|
-
|
|
74
|
-
waitUntil(promise: Promise<any>): void;
|
|
75
|
-
passThroughOnException(): void;
|
|
76
|
-
}
|
|
153
|
+
const warnedShellTagsNoEviction = new Set<string>();
|
|
77
154
|
|
|
78
155
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
156
|
+
* Stores (by namespace) already warned that tag invalidation is writing KV
|
|
157
|
+
* markers with no expiry (tagInvalidationTtl unset), so the unbounded-growth
|
|
158
|
+
* warning fires once per process rather than once per invalidateTags call
|
|
159
|
+
* (CFCacheStore is constructed per request; invalidateTags runs per marker
|
|
160
|
+
* batch). Distinct from the floor warning: that one only fires for a positive
|
|
161
|
+
* below-floor value, never for the unset (no-expiry) default that this bounds.
|
|
81
162
|
*/
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
163
|
+
const warnedNoTagInvalidationTtl = new Set<string>();
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Stores (by namespace) already warned that an entry's tag set produced a
|
|
167
|
+
* Cache-Tag header over Cloudflare's aggregate limit, so the header was
|
|
168
|
+
* omitted (the entry stays cacheable and marker-invalidatable; it just cannot
|
|
169
|
+
* be evicted per-tag by a purge). Once per process, not per write.
|
|
170
|
+
*/
|
|
171
|
+
const warnedCacheTagHeaderOverflow = new Set<string>();
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Stores (by namespace) already warned that an over-limit tag set made an
|
|
175
|
+
* entry UNCACHEABLE in KV-less purge mode: with no Cache-Tag tokens a purge
|
|
176
|
+
* cannot evict it, and with no KV there is no marker fallback either, so
|
|
177
|
+
* caching it would serve stale until TTL while updateTag() reports success.
|
|
178
|
+
* Once per process, not per write.
|
|
179
|
+
*/
|
|
180
|
+
const warnedCacheTagOverflowUncacheable = new Set<string>();
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Max length of one emitted `rg:*` Cache-Tag token. Cloudflare caps a purge
|
|
184
|
+
* API tag value at 1,024 characters and the aggregate Cache-Tag header at
|
|
185
|
+
* 16 KB; an application tag is unbounded, so an over-long token is collapsed
|
|
186
|
+
* to a deterministic hash (see boundedTagToken) instead of being allowed to
|
|
187
|
+
* fail the whole L1 write. 256 keeps headers compact while leaving room for
|
|
188
|
+
* long-but-reasonable tag names under any namespace.
|
|
189
|
+
*/
|
|
190
|
+
const CACHE_TAG_TOKEN_MAX = 256;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Cloudflare's documented aggregate Cache-Tag header limit (16 KB). A tagged
|
|
194
|
+
* entry whose tokens would exceed it gets NO Cache-Tag header (plus a
|
|
195
|
+
* once-per-namespace warning) rather than a failed cache write; the read path
|
|
196
|
+
* then falls back to the marker check for that entry (see isL1Invalidated).
|
|
197
|
+
*/
|
|
198
|
+
const CACHE_TAG_HEADER_MAX_BYTES = 16 * 1024;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* FNV-1a 64-bit hash of a tag value, hex-encoded. Used to bound over-long
|
|
202
|
+
* Cache-Tag tokens: deterministic (write-time token === purge-time token) and
|
|
203
|
+
* collision-safe in the failure direction — a collision over-purges (an extra
|
|
204
|
+
* eviction, healed by the next render), never serves stale.
|
|
205
|
+
* @internal
|
|
206
|
+
*/
|
|
207
|
+
function fnv1a64(input: string): string {
|
|
208
|
+
let hash = 0xcbf29ce484222325n;
|
|
209
|
+
for (let i = 0; i < input.length; i++) {
|
|
210
|
+
hash ^= BigInt(input.charCodeAt(i));
|
|
211
|
+
hash = (hash * 0x100000001b3n) & 0xffffffffffffffffn;
|
|
212
|
+
}
|
|
213
|
+
return hash.toString(16).padStart(16, "0");
|
|
90
214
|
}
|
|
91
215
|
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// Types
|
|
218
|
+
// ============================================================================
|
|
219
|
+
//
|
|
220
|
+
// The shared public types (KVNamespace, CFCacheReadDebugEvent, CFCacheDebug,
|
|
221
|
+
// CFCacheStoreOptions) live in cf-cache-types.ts; imported and re-exported below
|
|
222
|
+
// so existing import paths still resolve. The private KV envelope interfaces
|
|
223
|
+
// stay here with the methods that read/write them.
|
|
224
|
+
import type {
|
|
225
|
+
KVNamespace,
|
|
226
|
+
CFCacheReadDebugEvent,
|
|
227
|
+
CFCacheDebug,
|
|
228
|
+
CFCacheStoreOptions,
|
|
229
|
+
} from "./cf-cache-types.js";
|
|
230
|
+
export type {
|
|
231
|
+
KVNamespace,
|
|
232
|
+
CFCacheReadDebugEvent,
|
|
233
|
+
CFCacheDebug,
|
|
234
|
+
CFCacheStoreOptions,
|
|
235
|
+
};
|
|
236
|
+
|
|
92
237
|
/**
|
|
93
238
|
* KV envelope for segment cache entries.
|
|
94
239
|
* @internal
|
|
@@ -109,132 +254,165 @@ interface KVSegmentEnvelope {
|
|
|
109
254
|
interface KVItemEnvelope {
|
|
110
255
|
/** RSC-serialized return value */
|
|
111
256
|
v: string;
|
|
112
|
-
/**
|
|
113
|
-
h?:
|
|
257
|
+
/** RSC-encoded handle data (see handle-snapshot.ts encodeHandles) */
|
|
258
|
+
h?: string;
|
|
114
259
|
/** When entry becomes stale (ms epoch) */
|
|
115
260
|
s: number;
|
|
116
261
|
/** When entry hard-expires (ms epoch) */
|
|
117
262
|
e: number;
|
|
263
|
+
/** Cache tags (for distributed tag invalidation) */
|
|
264
|
+
t?: string[];
|
|
265
|
+
/** Timestamp when tags were attached (ms epoch) */
|
|
266
|
+
ta?: number;
|
|
118
267
|
}
|
|
119
268
|
|
|
120
269
|
/**
|
|
121
|
-
* KV envelope for
|
|
270
|
+
* Coupled Cache API/KV envelope for PPR shell cache entries.
|
|
122
271
|
* @internal
|
|
123
272
|
*/
|
|
124
|
-
interface
|
|
125
|
-
/**
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
273
|
+
interface CFShellEnvelope {
|
|
274
|
+
/**
|
|
275
|
+
* base64-encoded prelude bytes. Absent iff `no` (navigationOnly entries
|
|
276
|
+
* store no document half — ShellCacheEntry.prelude).
|
|
277
|
+
*/
|
|
278
|
+
p?: string;
|
|
279
|
+
/** postponed state JSON, or null (DATA variant — no holes). Absent iff `no`. */
|
|
280
|
+
po?: string | null;
|
|
281
|
+
/** React.version captured at prerender time */
|
|
282
|
+
rv: string;
|
|
283
|
+
/** Build version captured at prerender time (ShellCacheEntry.buildVersion) */
|
|
284
|
+
bv?: string;
|
|
285
|
+
/** Capture-generation start time (ms epoch), used by tag marker checks. */
|
|
286
|
+
c: number;
|
|
133
287
|
/** When entry becomes stale (ms epoch) */
|
|
134
288
|
s: number;
|
|
135
289
|
/** When entry hard-expires (ms epoch) */
|
|
136
290
|
e: number;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Base URL for cache keys.
|
|
148
|
-
*
|
|
149
|
-
* If not provided, derives from request hostname via requestContext:
|
|
150
|
-
* - Production domains → uses `https://{hostname}/`
|
|
151
|
-
* - Dev/preview (localhost, workers.dev, pages.dev) → uses internal fallback URL
|
|
152
|
-
*/
|
|
153
|
-
baseUrl?: string;
|
|
154
|
-
|
|
155
|
-
/** Default cache options */
|
|
156
|
-
defaults?: CacheDefaults;
|
|
157
|
-
|
|
291
|
+
/** Cache tags (for distributed tag invalidation) */
|
|
292
|
+
t?: string[];
|
|
293
|
+
/** Timestamp when tags were attached (ms epoch) */
|
|
294
|
+
ta?: number;
|
|
295
|
+
/** initialTheme the capture render was built with (resume theme fidelity) */
|
|
296
|
+
i?: string;
|
|
297
|
+
/** Capture data snapshot: recorded cache-store hits/writes for HIT parity */
|
|
298
|
+
sn?: import("../types.js").ShellSnapshotRecord[];
|
|
158
299
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* new CFCacheStore({ ctx: env.ctx })
|
|
165
|
-
* ```
|
|
300
|
+
* ShellCacheEntry.docKey. Must round-trip: navigation-replay eligibility
|
|
301
|
+
* requires the exact canonical doc segment record named here — dropping the
|
|
302
|
+
* field reads back as "no consumable record" and every partial navigation
|
|
303
|
+
* reports `no-segment-snapshot` after a KV round trip (the memory store
|
|
304
|
+
* passes the entry by reference, so only envelope stores can lose it).
|
|
166
305
|
*/
|
|
167
|
-
|
|
168
|
-
|
|
306
|
+
dk?: string;
|
|
169
307
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* On writes, data is persisted to both L1 and KV.
|
|
175
|
-
*
|
|
176
|
-
* @example
|
|
177
|
-
* ```typescript
|
|
178
|
-
* new CFCacheStore({ ctx: env.ctx, kv: env.CACHE_KV })
|
|
179
|
-
* ```
|
|
308
|
+
* ShellCacheEntry.handlerLiveHoles. Must round-trip: the serve side arms the
|
|
309
|
+
* handler-free fast path on `!entry.handlerLiveHoles`, so dropping the flag
|
|
310
|
+
* here silently fast-pathed handler-live entries after a KV round trip —
|
|
311
|
+
* their holes only a handler re-run can fill.
|
|
180
312
|
*/
|
|
181
|
-
|
|
313
|
+
lh?: boolean;
|
|
314
|
+
/** ShellCacheEntry.transitionWhen; conditional transitions must re-run. */
|
|
315
|
+
tw?: true;
|
|
316
|
+
/** ShellCacheEntry.navigationOnly; its partial-context prelude is not document-safe. */
|
|
317
|
+
no?: true;
|
|
318
|
+
}
|
|
182
319
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
320
|
+
type CFShellDebugOutcome =
|
|
321
|
+
| "l1-hit"
|
|
322
|
+
| "l1-miss"
|
|
323
|
+
| "kv-hit"
|
|
324
|
+
| "kv-miss"
|
|
325
|
+
| "kv-promoted"
|
|
326
|
+
| "marker-invalidated"
|
|
327
|
+
| "l1-stored"
|
|
328
|
+
| "kv-stored"
|
|
329
|
+
| "write-invalidated";
|
|
330
|
+
|
|
331
|
+
interface CFShellDebugDetails {
|
|
332
|
+
tier?: "l1" | "kv";
|
|
333
|
+
reason?:
|
|
334
|
+
| "absent"
|
|
335
|
+
| "timeout"
|
|
336
|
+
| "error"
|
|
337
|
+
| "non-200"
|
|
338
|
+
| "corrupt"
|
|
339
|
+
| "malformed"
|
|
340
|
+
| "expired"
|
|
341
|
+
| "unavailable";
|
|
342
|
+
freshness?: "fresh" | "stale";
|
|
343
|
+
status?: number;
|
|
344
|
+
matchMs?: number;
|
|
345
|
+
bodyReadMs?: number;
|
|
346
|
+
markerMs?: number;
|
|
347
|
+
readMs?: number;
|
|
348
|
+
expiresAt?: number;
|
|
349
|
+
remainingTtl?: number;
|
|
350
|
+
}
|
|
191
351
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
)
|
|
352
|
+
/** Validate the coupled PPR shell envelope before any field reaches resume. */
|
|
353
|
+
function isShellEnvelope(value: unknown): value is CFShellEnvelope {
|
|
354
|
+
if (value == null || typeof value !== "object") return false;
|
|
355
|
+
const envelope = value as Partial<CFShellEnvelope>;
|
|
356
|
+
return (
|
|
357
|
+
// Document half: required unless the envelope is navigationOnly (`no`),
|
|
358
|
+
// which stores neither field. Tolerate a legacy navigationOnly envelope
|
|
359
|
+
// that still carries them.
|
|
360
|
+
(typeof envelope.p === "string" ||
|
|
361
|
+
(envelope.p === undefined && envelope.no === true)) &&
|
|
362
|
+
(envelope.po === null ||
|
|
363
|
+
typeof envelope.po === "string" ||
|
|
364
|
+
(envelope.po === undefined && envelope.no === true)) &&
|
|
365
|
+
typeof envelope.rv === "string" &&
|
|
366
|
+
(envelope.bv === undefined || typeof envelope.bv === "string") &&
|
|
367
|
+
typeof envelope.c === "number" &&
|
|
368
|
+
Number.isFinite(envelope.c) &&
|
|
369
|
+
typeof envelope.s === "number" &&
|
|
370
|
+
Number.isFinite(envelope.s) &&
|
|
371
|
+
typeof envelope.e === "number" &&
|
|
372
|
+
Number.isFinite(envelope.e) &&
|
|
373
|
+
(envelope.t === undefined ||
|
|
374
|
+
(Array.isArray(envelope.t) &&
|
|
375
|
+
envelope.t.every((tag) => typeof tag === "string"))) &&
|
|
376
|
+
(envelope.ta === undefined ||
|
|
377
|
+
(typeof envelope.ta === "number" && Number.isFinite(envelope.ta))) &&
|
|
378
|
+
(envelope.i === undefined || typeof envelope.i === "string") &&
|
|
379
|
+
(envelope.sn === undefined || Array.isArray(envelope.sn)) &&
|
|
380
|
+
(envelope.dk === undefined || typeof envelope.dk === "string") &&
|
|
381
|
+
(envelope.lh === undefined || typeof envelope.lh === "boolean") &&
|
|
382
|
+
(envelope.tw === undefined || envelope.tw === true) &&
|
|
383
|
+
(envelope.no === undefined || envelope.no === true)
|
|
384
|
+
);
|
|
225
385
|
}
|
|
226
386
|
|
|
227
387
|
/**
|
|
228
|
-
*
|
|
388
|
+
* KV envelope for document cache entries.
|
|
229
389
|
* @internal
|
|
230
390
|
*/
|
|
231
|
-
|
|
391
|
+
interface KVResponseEnvelope {
|
|
392
|
+
/** Response body as base64-encoded string (safe for binary payloads) */
|
|
393
|
+
b: string;
|
|
394
|
+
/** HTTP status code */
|
|
395
|
+
st: number;
|
|
396
|
+
/** HTTP status text */
|
|
397
|
+
stx: string;
|
|
398
|
+
/** Serialized headers as key-value pairs (client-facing; no internal headers) */
|
|
399
|
+
hd: [string, string][];
|
|
400
|
+
/** When entry becomes stale (ms epoch) */
|
|
401
|
+
s: number;
|
|
402
|
+
/** When entry hard-expires (ms epoch) */
|
|
403
|
+
e: number;
|
|
404
|
+
/** Cache tags (for distributed tag invalidation) */
|
|
405
|
+
t?: string[];
|
|
406
|
+
/** Timestamp when tags were attached (ms epoch) */
|
|
407
|
+
ta?: number;
|
|
408
|
+
}
|
|
232
409
|
|
|
233
410
|
// ============================================================================
|
|
234
411
|
// CFCacheStore Implementation
|
|
235
412
|
// ============================================================================
|
|
236
413
|
|
|
237
414
|
export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
415
|
+
readonly supportsPassiveShellReads: true = true;
|
|
238
416
|
readonly defaults?: CacheDefaults;
|
|
239
417
|
readonly keyGenerator?: (
|
|
240
418
|
ctx: RequestContext<TEnv>,
|
|
@@ -242,10 +420,20 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
242
420
|
) => string | Promise<string>;
|
|
243
421
|
|
|
244
422
|
private readonly namespace?: string;
|
|
245
|
-
private readonly
|
|
423
|
+
private readonly explicitBaseUrl?: string;
|
|
246
424
|
private readonly waitUntil?: (fn: () => Promise<void>) => void;
|
|
247
425
|
private readonly version?: string;
|
|
426
|
+
private readonly edgeLookupTimeoutMs: number;
|
|
427
|
+
private readonly edgeReadTimeoutMs: number;
|
|
428
|
+
private readonly kvReadTimeoutMs: number;
|
|
429
|
+
private readonly debug?: (event: CFCacheReadDebugEvent) => void;
|
|
248
430
|
private readonly kv?: KVNamespace;
|
|
431
|
+
/** True when constructed without KV: no durable tag history (see ctor). */
|
|
432
|
+
readonly tagHistoryInert?: boolean;
|
|
433
|
+
private readonly onRevalidateTag?: (tags: string[]) => Promise<void>;
|
|
434
|
+
private readonly tagPurge?: (cacheTags: string[]) => Promise<void>;
|
|
435
|
+
private readonly tagInvalidationTtl?: number;
|
|
436
|
+
private readonly tagCacheTtl: number;
|
|
249
437
|
|
|
250
438
|
constructor(options: CFCacheStoreOptions<TEnv>) {
|
|
251
439
|
if (!options.ctx) {
|
|
@@ -257,52 +445,256 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
257
445
|
}
|
|
258
446
|
|
|
259
447
|
this.namespace = options.namespace;
|
|
260
|
-
|
|
448
|
+
// Base URL is resolved lazily per cache operation (see resolveBaseUrl).
|
|
449
|
+
// The store is constructed before the per-request context ALS is entered
|
|
450
|
+
// (the cache factory runs ahead of runWithRequestContext in the handler),
|
|
451
|
+
// so deriving the host here would always miss the request and fall back to
|
|
452
|
+
// the internal host. Only the explicit override can be captured eagerly.
|
|
453
|
+
this.explicitBaseUrl = options.baseUrl;
|
|
261
454
|
this.defaults = options.defaults;
|
|
262
455
|
this.version = options.version ?? VERSION;
|
|
456
|
+
// Coalesce only finite numbers to the override; a non-finite value (NaN from
|
|
457
|
+
// `Number(env.UNSET)`, or Infinity) would otherwise sail past `?? DEFAULT`
|
|
458
|
+
// (which only replaces null/undefined) into setTimeout, where NaN/Infinity
|
|
459
|
+
// are spec-coerced to ~1ms and silently turn the budget into a near-100%
|
|
460
|
+
// false-miss on that tier. A genuine finite 0 or negative still passes
|
|
461
|
+
// through and disables the budget per the documented `<= 0` contract.
|
|
462
|
+
const finiteBudget = (
|
|
463
|
+
value: number | undefined,
|
|
464
|
+
fallback: number,
|
|
465
|
+
): number =>
|
|
466
|
+
typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
467
|
+
this.edgeLookupTimeoutMs = finiteBudget(
|
|
468
|
+
options.edgeLookupTimeoutMs,
|
|
469
|
+
EDGE_LOOKUP_TIMEOUT_MS,
|
|
470
|
+
);
|
|
471
|
+
this.edgeReadTimeoutMs = finiteBudget(
|
|
472
|
+
options.edgeReadTimeoutMs,
|
|
473
|
+
EDGE_READ_TIMEOUT_MS,
|
|
474
|
+
);
|
|
475
|
+
this.kvReadTimeoutMs = finiteBudget(
|
|
476
|
+
options.kvReadTimeoutMs,
|
|
477
|
+
KV_READ_TIMEOUT_MS,
|
|
478
|
+
);
|
|
479
|
+
this.debug =
|
|
480
|
+
options.debug === true
|
|
481
|
+
? (event) =>
|
|
482
|
+
console.log(`[CFCacheStore:debug] ${JSON.stringify(event)}`)
|
|
483
|
+
: typeof options.debug === "function"
|
|
484
|
+
? options.debug
|
|
485
|
+
: undefined;
|
|
263
486
|
this.keyGenerator = options.keyGenerator;
|
|
264
487
|
this.waitUntil = (fn) => options.ctx.waitUntil(fn());
|
|
265
488
|
this.kv = options.kv;
|
|
489
|
+
// Without KV, isTagsInvalidatedSince has no durable history — it answers
|
|
490
|
+
// from the per-request memo at best. Runtime shells tolerate that (purge
|
|
491
|
+
// eviction + ttl/swr bound the staleness), but an immutable TAGGED
|
|
492
|
+
// build-manifest shell must not serve on such a store: nothing could ever
|
|
493
|
+
// evict it (purge cannot delete a build asset), so the manifest gate
|
|
494
|
+
// declines on this flag (shell-build-manifest.ts).
|
|
495
|
+
this.tagHistoryInert = options.kv ? undefined : true;
|
|
496
|
+
this.onRevalidateTag = options.onRevalidateTag;
|
|
497
|
+
// tagPurge accepts a ready purge function or a credentials object; the
|
|
498
|
+
// object form is normalized through the built-in zone purge client, which
|
|
499
|
+
// validates zoneId/apiToken eagerly so an unset env var fails at
|
|
500
|
+
// construction instead of on the first updateTag().
|
|
501
|
+
this.tagPurge =
|
|
502
|
+
typeof options.tagPurge === "function"
|
|
503
|
+
? options.tagPurge
|
|
504
|
+
: options.tagPurge
|
|
505
|
+
? createCloudflareZonePurge(options.tagPurge)
|
|
506
|
+
: undefined;
|
|
507
|
+
// tagInvalidationTtl feeds KV's expirationTtl, which CF rejects below
|
|
508
|
+
// KV_MIN_EXPIRATION_TTL (60s) -- a too-small finite value would make EVERY
|
|
509
|
+
// marker write throw and break ALL invalidation. Floor it (and warn once);
|
|
510
|
+
// a non-finite/non-positive value falls back to the no-expiry default
|
|
511
|
+
// (markers persist) rather than silently sailing a NaN into expirationTtl.
|
|
512
|
+
// KV-less the option is dead config — no markers to expire, and the
|
|
513
|
+
// retention cap it used to imply is KV-conditional (putShell) — so it is
|
|
514
|
+
// dropped without the KV-floor validation/warning, which would misdirect.
|
|
515
|
+
this.tagInvalidationTtl = options.kv
|
|
516
|
+
? this.sanitizeTagInvalidationTtl(options.tagInvalidationTtl)
|
|
517
|
+
: undefined;
|
|
518
|
+
// tagCacheTtl gates the L1 marker cache via `> 0`. A non-finite value (NaN
|
|
519
|
+
// from `Number(env.UNSET)`) is not null/undefined, so `?? 0` would let it
|
|
520
|
+
// through and silently disable the cache while reading as "configured".
|
|
521
|
+
// finiteBudget coerces non-finite/null/undefined to 0; the `> 0` guard then
|
|
522
|
+
// collapses a finite non-positive value to the documented 0 = disabled.
|
|
523
|
+
const tagCacheTtl = finiteBudget(options.tagCacheTtl, 0);
|
|
524
|
+
this.tagCacheTtl = tagCacheTtl > 0 ? tagCacheTtl : 0;
|
|
525
|
+
|
|
526
|
+
// Read-side tag invalidation requires KV: isGloballyInvalidated() compares an
|
|
527
|
+
// entry's taggedAt against the per-tag KV marker and short-circuits to "not
|
|
528
|
+
// invalidated" when no KV namespace is configured. A consumer who wires the
|
|
529
|
+
// tag machinery (tagCacheTtl for L1 markers, or onRevalidateTag for CDN purge)
|
|
530
|
+
// but omits kv gets only the purge fired - marker writes are skipped without
|
|
531
|
+
// kv - yet every tagged read still serves stale data with no other signal.
|
|
532
|
+
// Surface that misconfiguration. Exception: with tagPurge configured (purge
|
|
533
|
+
// mode) L1 eviction is the purge itself, so a KV-less store is a supported
|
|
534
|
+
// L1-only configuration, not a silent no-op.
|
|
535
|
+
if (
|
|
536
|
+
!this.kv &&
|
|
537
|
+
!this.tagPurge &&
|
|
538
|
+
(this.tagCacheTtl > 0 || this.onRevalidateTag)
|
|
539
|
+
) {
|
|
540
|
+
this.warnOncePerNamespace(
|
|
541
|
+
warnedNoKvReadInvalidation,
|
|
542
|
+
`[CFCacheStore] tagCacheTtl/onRevalidateTag is configured without a KV ` +
|
|
543
|
+
`namespace, so tag invalidation has NO read-side effect: tagged reads ` +
|
|
544
|
+
`are never treated as invalidated and serve stale data. Configure ` +
|
|
545
|
+
`{ kv } for distributed tag invalidation.`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Warn about a namespace-scoped misconfiguration once per namespace per
|
|
552
|
+
* isolate. `seen` is the module-level Set for that message family -- Sets
|
|
553
|
+
* are module-level (not instance fields) so re-constructed stores in the
|
|
554
|
+
* same isolate don't re-warn.
|
|
555
|
+
* @internal
|
|
556
|
+
*/
|
|
557
|
+
private warnOncePerNamespace(seen: Set<string>, message: string): void {
|
|
558
|
+
const id = this.namespace ?? "default";
|
|
559
|
+
if (seen.has(id)) return;
|
|
560
|
+
seen.add(id);
|
|
561
|
+
console.warn(message);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Validate a consumer-supplied tagInvalidationTtl against CF KV's expirationTtl
|
|
566
|
+
* floor. A finite value below KV_MIN_EXPIRATION_TTL is raised to it (with a
|
|
567
|
+
* one-time warning) so invalidation keeps working instead of every marker
|
|
568
|
+
* write throwing; a non-finite or non-positive value returns undefined (the
|
|
569
|
+
* no-expiry default). The warning still notes the sizing rule: the TTL must
|
|
570
|
+
* exceed the largest entry TTL+SWR or invalidated entries can resurrect.
|
|
571
|
+
* @internal
|
|
572
|
+
*/
|
|
573
|
+
private sanitizeTagInvalidationTtl(
|
|
574
|
+
value: number | undefined,
|
|
575
|
+
): number | undefined {
|
|
576
|
+
if (value == null) return undefined;
|
|
577
|
+
if (!Number.isFinite(value) || value <= 0) return undefined;
|
|
578
|
+
if (value < KV_MIN_EXPIRATION_TTL) {
|
|
579
|
+
this.warnOncePerNamespace(
|
|
580
|
+
warnedTagInvalidationTtlFloor,
|
|
581
|
+
`[CFCacheStore] tagInvalidationTtl ${value} is below Cloudflare KV's ` +
|
|
582
|
+
`${KV_MIN_EXPIRATION_TTL}s expirationTtl floor; raising to ` +
|
|
583
|
+
`${KV_MIN_EXPIRATION_TTL}. It must still exceed your largest entry ` +
|
|
584
|
+
`TTL+SWR or invalidated entries can resurrect when the marker expires.`,
|
|
585
|
+
);
|
|
586
|
+
return KV_MIN_EXPIRATION_TTL;
|
|
587
|
+
}
|
|
588
|
+
return value;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Emit a debug event if `debug` is enabled. Swallows sink errors so a faulty
|
|
593
|
+
* debug callback can never break a cache read.
|
|
594
|
+
* @internal
|
|
595
|
+
*/
|
|
596
|
+
private emitDebug(event: CFCacheReadDebugEvent): void {
|
|
597
|
+
if (!this.debug) return;
|
|
598
|
+
try {
|
|
599
|
+
this.debug(event);
|
|
600
|
+
} catch {
|
|
601
|
+
// A broken debug sink must not affect the request.
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** Build-time-gated shell tier trace for deployed cross-colo diagnostics. */
|
|
606
|
+
private debugShell(
|
|
607
|
+
key: string,
|
|
608
|
+
outcome: CFShellDebugOutcome,
|
|
609
|
+
details: CFShellDebugDetails = {},
|
|
610
|
+
): void {
|
|
611
|
+
if (!INTERNAL_RANGO_DEBUG) return;
|
|
612
|
+
const request = _getRequestContext()?.request as
|
|
613
|
+
| (Request & { cf?: { colo?: unknown } })
|
|
614
|
+
| undefined;
|
|
615
|
+
const ray = request?.headers.get("cf-ray") ?? undefined;
|
|
616
|
+
const cfColo = request?.cf?.colo;
|
|
617
|
+
const colo =
|
|
618
|
+
typeof cfColo === "string"
|
|
619
|
+
? cfColo
|
|
620
|
+
: ray?.includes("-")
|
|
621
|
+
? ray.slice(ray.lastIndexOf("-") + 1)
|
|
622
|
+
: undefined;
|
|
623
|
+
console.log(
|
|
624
|
+
`[CFCacheStore][shell] ${JSON.stringify({
|
|
625
|
+
key,
|
|
626
|
+
outcome,
|
|
627
|
+
at: Date.now(),
|
|
628
|
+
ray,
|
|
629
|
+
colo,
|
|
630
|
+
...details,
|
|
631
|
+
})}`,
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Resolve the cache-key base URL for the current cache operation.
|
|
637
|
+
* Prefers an explicit `baseUrl` option; otherwise derives it from the live
|
|
638
|
+
* request. Called per operation (from keyToRequest), which runs inside the
|
|
639
|
+
* request-context ALS, so deriveBaseUrl sees the request and can use the
|
|
640
|
+
* production host instead of the internal fallback.
|
|
641
|
+
* @internal
|
|
642
|
+
*/
|
|
643
|
+
private resolveBaseUrl(): string {
|
|
644
|
+
return this.explicitBaseUrl ?? this.deriveBaseUrl();
|
|
266
645
|
}
|
|
267
646
|
|
|
268
647
|
/**
|
|
269
648
|
* Derive base URL from request hostname via requestContext.
|
|
270
649
|
* Uses internal fallback for dev/preview environments and untrusted hostnames.
|
|
650
|
+
* Must run inside the request context (invoked lazily via resolveBaseUrl).
|
|
271
651
|
* @internal
|
|
272
652
|
*/
|
|
273
653
|
private deriveBaseUrl(): string {
|
|
274
|
-
const fallback = "https://rsc-
|
|
654
|
+
const fallback = "https://rsc-dummy-host-1.com/";
|
|
275
655
|
|
|
276
656
|
const ctx = _getRequestContext();
|
|
277
657
|
if (!ctx?.request) {
|
|
278
658
|
return fallback;
|
|
279
659
|
}
|
|
280
660
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
hostname === "localhost" ||
|
|
288
|
-
hostname === "127.0.0.1" ||
|
|
289
|
-
hostname.endsWith(".workers.dev") ||
|
|
290
|
-
hostname.endsWith(".pages.dev")
|
|
291
|
-
) {
|
|
292
|
-
return fallback;
|
|
293
|
-
}
|
|
661
|
+
// The result is deterministic per request, but keyToRequest calls this on
|
|
662
|
+
// every cache operation; memoize per request context (see derivedBaseUrlMemo).
|
|
663
|
+
const memoized = derivedBaseUrlMemo.get(ctx);
|
|
664
|
+
if (memoized !== undefined) {
|
|
665
|
+
return memoized;
|
|
666
|
+
}
|
|
294
667
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
668
|
+
const derived = ((): string => {
|
|
669
|
+
try {
|
|
670
|
+
const url = new URL(ctx.request.url);
|
|
671
|
+
const hostname = url.hostname;
|
|
672
|
+
|
|
673
|
+
// Use fallback for dev/preview environments
|
|
674
|
+
if (
|
|
675
|
+
hostname === "localhost" ||
|
|
676
|
+
hostname === "127.0.0.1" ||
|
|
677
|
+
hostname.endsWith(".workers.dev") ||
|
|
678
|
+
hostname.endsWith(".pages.dev")
|
|
679
|
+
) {
|
|
680
|
+
return fallback;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Validate hostname: must be a valid domain (alphanumeric, hyphens, dots)
|
|
684
|
+
// to prevent host header injection into cache keys
|
|
685
|
+
if (!/^[a-zA-Z0-9.-]+$/.test(hostname) || hostname.length > 253) {
|
|
686
|
+
return fallback;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Use actual hostname for production
|
|
690
|
+
return `https://${hostname}/`;
|
|
691
|
+
} catch {
|
|
298
692
|
return fallback;
|
|
299
693
|
}
|
|
694
|
+
})();
|
|
300
695
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
} catch {
|
|
304
|
-
return fallback;
|
|
305
|
-
}
|
|
696
|
+
derivedBaseUrlMemo.set(ctx, derived);
|
|
697
|
+
return derived;
|
|
306
698
|
}
|
|
307
699
|
|
|
308
700
|
/**
|
|
@@ -316,10 +708,297 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
316
708
|
return caches.default;
|
|
317
709
|
}
|
|
318
710
|
|
|
711
|
+
/**
|
|
712
|
+
* Race an async cache read against a latency budget. Shared by all three read
|
|
713
|
+
* tiers (L1 match, L1 body, L2/KV) so the timeout policy lives in one place:
|
|
714
|
+
* on timeout it returns `{ value: undefined, timedOut: true }` and logs
|
|
715
|
+
* `${label} exceeded ${budgetMs}ms; treating as miss`; the abandoned read is
|
|
716
|
+
* left to settle in the background (late rejection swallowed) rather than
|
|
717
|
+
* aborted, since the underlying CF primitives expose no cancellation. A budget
|
|
718
|
+
* <= 0 disables the bound and awaits the read directly. `read` is a thunk so
|
|
719
|
+
* the disabled path and the raced path start the read identically.
|
|
720
|
+
* @internal
|
|
721
|
+
*/
|
|
722
|
+
private async readWithTimeout<T>(
|
|
723
|
+
read: () => Promise<T>,
|
|
724
|
+
budgetMs: number,
|
|
725
|
+
label: string,
|
|
726
|
+
): Promise<{ value: T | undefined; timedOut: boolean }> {
|
|
727
|
+
if (budgetMs <= 0) return { value: await read(), timedOut: false };
|
|
728
|
+
|
|
729
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
730
|
+
const timeout = new Promise<{ timedOut: true }>((resolve) => {
|
|
731
|
+
timer = setTimeout(() => resolve({ timedOut: true }), budgetMs);
|
|
732
|
+
});
|
|
733
|
+
try {
|
|
734
|
+
const readPromise = read();
|
|
735
|
+
// The losing branch keeps running; ensure a late rejection can't surface
|
|
736
|
+
// as an unhandled rejection once we've stopped awaiting it.
|
|
737
|
+
readPromise.catch(() => {});
|
|
738
|
+
const result = await Promise.race([
|
|
739
|
+
readPromise.then((value) => ({ timedOut: false as const, value })),
|
|
740
|
+
timeout,
|
|
741
|
+
]);
|
|
742
|
+
if (result.timedOut) {
|
|
743
|
+
console.warn(
|
|
744
|
+
`[CFCacheStore] ${label} exceeded ${budgetMs}ms; treating as miss`,
|
|
745
|
+
);
|
|
746
|
+
return { value: undefined, timedOut: true };
|
|
747
|
+
}
|
|
748
|
+
return { value: result.value, timedOut: false };
|
|
749
|
+
} finally {
|
|
750
|
+
if (timer) clearTimeout(timer);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Read from the L1 edge cache under the edgeLookupTimeoutMs budget. A `match`
|
|
756
|
+
* slower than the budget is abandoned and reported as a miss
|
|
757
|
+
* (`{ response: undefined, timedOut: true }`) so a degraded colo cannot stall
|
|
758
|
+
* the request; callers fall through to their normal miss path (L2/KV or
|
|
759
|
+
* render). The `timedOut` flag lets callers distinguish an abandoned slow
|
|
760
|
+
* match from a genuine miss for debug reporting; `error` is set when the
|
|
761
|
+
* `match` itself rejected (a transient L1 infra error) so the caller can
|
|
762
|
+
* report it as cache-read while still degrading to L2/KV -- distinct from a
|
|
763
|
+
* genuine miss (no entry), which sets neither flag.
|
|
764
|
+
* @internal
|
|
765
|
+
*/
|
|
766
|
+
private async matchWithTimeout(
|
|
767
|
+
cache: Cache,
|
|
768
|
+
request: Request,
|
|
769
|
+
): Promise<{
|
|
770
|
+
response: Response | undefined;
|
|
771
|
+
timedOut: boolean;
|
|
772
|
+
error?: unknown;
|
|
773
|
+
}> {
|
|
774
|
+
let matchError: unknown;
|
|
775
|
+
const { value, timedOut } = await this.readWithTimeout(
|
|
776
|
+
// A fast match rejection is caught at the thunk and reported as a miss
|
|
777
|
+
// (response undefined), so the caller falls through to L2/KV rather than
|
|
778
|
+
// escaping to the outer catch -- symmetric with the body-read thunk. The
|
|
779
|
+
// error is captured (not swallowed) so the caller can surface it via
|
|
780
|
+
// onError as a cache-read degradation.
|
|
781
|
+
() =>
|
|
782
|
+
cache.match(request).catch((e) => {
|
|
783
|
+
matchError = e;
|
|
784
|
+
return undefined;
|
|
785
|
+
}),
|
|
786
|
+
this.edgeLookupTimeoutMs,
|
|
787
|
+
"edge cache lookup",
|
|
788
|
+
);
|
|
789
|
+
return { response: value, timedOut, error: matchError };
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Read and JSON-parse a matched L1 Response's body under the edgeReadTimeoutMs
|
|
794
|
+
* budget. CF resolves `match()` with a lazily-streamed body, so the latency
|
|
795
|
+
* tail surfaces here -- after matchWithTimeout has already passed -- not in the
|
|
796
|
+
* match itself. On timeout `undefined` is returned so the caller falls through
|
|
797
|
+
* to L2/KV or render.
|
|
798
|
+
* @internal
|
|
799
|
+
*/
|
|
800
|
+
private async readJsonWithTimeout<T>(
|
|
801
|
+
response: Response,
|
|
802
|
+
): Promise<{ value: T | undefined; errored: boolean; error?: unknown }> {
|
|
803
|
+
// A FAST json() rejection (a corrupt body, or a foreign 200 non-JSON
|
|
804
|
+
// response that collided on this key) is caught at the thunk and turned into
|
|
805
|
+
// a miss, so the caller falls through to L2/KV exactly like a body-timeout
|
|
806
|
+
// -- instead of escaping to get()/getItem()'s outer catch, which returns
|
|
807
|
+
// null WITHOUT ever consulting KV. The catch lives here, not in
|
|
808
|
+
// readWithTimeout, so the L2/KV tier keeps propagating a genuine kv.get
|
|
809
|
+
// rejection to its own error sink. The `errored` flag lets the caller emit a
|
|
810
|
+
// distinct "body-error" debug outcome rather than masquerading as a timeout.
|
|
811
|
+
// On a TIMEOUT the json() promise is still pending, so the catch has not
|
|
812
|
+
// fired: errored stays false and the outcome is correctly a body-timeout. A
|
|
813
|
+
// late rejection after the timeout only mutates the closure flag, which the
|
|
814
|
+
// already-returned object no longer reads.
|
|
815
|
+
let errored = false;
|
|
816
|
+
let error: unknown;
|
|
817
|
+
const { value } = await this.readWithTimeout<T | undefined>(
|
|
818
|
+
() =>
|
|
819
|
+
(response.json() as Promise<T>).catch((e) => {
|
|
820
|
+
errored = true;
|
|
821
|
+
error = e;
|
|
822
|
+
return undefined;
|
|
823
|
+
}),
|
|
824
|
+
this.edgeReadTimeoutMs,
|
|
825
|
+
"edge cache body read",
|
|
826
|
+
);
|
|
827
|
+
return { value, errored, error };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Self-heal a corrupt L1 entry, then return the fall-through result. Reports
|
|
832
|
+
* the corruption as cache-corrupt (so an onError consumer sees it distinctly
|
|
833
|
+
* from a transient outage), runs the caller's L2/KV fall-through, and evicts
|
|
834
|
+
* the faulty per-colo entry ONLY when that fall-through found no good copy.
|
|
835
|
+
*
|
|
836
|
+
* The conditional evict is the load-bearing detail: when KV DOES serve a copy,
|
|
837
|
+
* kvGet* has already scheduled a same-key promote (`cache.put`); an eager
|
|
838
|
+
* `cache.delete` here would race that put with no CF Cache API ordering
|
|
839
|
+
* guarantee and could clobber the freshly-restored entry. So in that case we
|
|
840
|
+
* lean on #558's heal-by-overwrite (the non-suppressed fall-through promotes /
|
|
841
|
+
* a fresh render re-`set`s over the bad entry) and skip the delete. Only when
|
|
842
|
+
* this request's fall-through found no copy (=== null) is the eager evict
|
|
843
|
+
* scheduled -- useful then, since nothing else will overwrite the poison entry.
|
|
844
|
+
* A null fall-through can also be a KV-read TIMEOUT rather than a genuine miss:
|
|
845
|
+
* a concurrent request that read KV successfully may be promoting the same key,
|
|
846
|
+
* and this evict could race it. That is benign -- the worst case is one wasted
|
|
847
|
+
* colo-local promote, never a wrong served value, and the next read self-heals
|
|
848
|
+
* -- so we accept it rather than suppressing the evict on a timeout (which
|
|
849
|
+
* would strand the poison entry when KV really is empty). The evict is
|
|
850
|
+
* non-blocking (waitUntil) so it never adds latency to the degraded read.
|
|
851
|
+
* @internal
|
|
852
|
+
*/
|
|
853
|
+
private async healCorruptL1<T>(
|
|
854
|
+
cache: Cache,
|
|
855
|
+
request: Request,
|
|
856
|
+
error: unknown,
|
|
857
|
+
label: string,
|
|
858
|
+
fallThrough: () => Promise<T | null>,
|
|
859
|
+
): Promise<T | null> {
|
|
860
|
+
reportCacheError(
|
|
861
|
+
error ?? new Error("corrupt/partial L1 body"),
|
|
862
|
+
"cache-corrupt",
|
|
863
|
+
`[CFCacheStore] ${label}: corrupt L1 body`,
|
|
864
|
+
);
|
|
865
|
+
const result = await fallThrough();
|
|
866
|
+
if (result === null) {
|
|
867
|
+
const evict = (): Promise<void> =>
|
|
868
|
+
reportingAsync(
|
|
869
|
+
() => cache.delete(request),
|
|
870
|
+
"cache-delete",
|
|
871
|
+
`[CFCacheStore] ${label}: evict corrupt L1`,
|
|
872
|
+
);
|
|
873
|
+
if (this.waitUntil) this.waitUntil(evict);
|
|
874
|
+
else void evict();
|
|
875
|
+
}
|
|
876
|
+
return result;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Re-put a stale L1 entry marked REVALIDATING, so concurrent requests serve it
|
|
881
|
+
* without each triggering a revalidation. Shared by get()/getItem().
|
|
882
|
+
*
|
|
883
|
+
* The write is NON-BLOCKING (waitUntil) and best-effort by design:
|
|
884
|
+
* - It runs in waitUntil, so it never adds the put latency to the served stale
|
|
885
|
+
* read and a put failure can never turn that good read into a miss. The put
|
|
886
|
+
* is still initiated synchronously (this.waitUntil invokes its callback
|
|
887
|
+
* immediately), so concurrent readers see the marker land at the same time an
|
|
888
|
+
* awaited write would -- awaiting only blocks the current request.
|
|
889
|
+
* - The background revalidation's fresh set() is gated behind a full re-render,
|
|
890
|
+
* so it lands well after this put; a stale-clobbers-fresh race would require
|
|
891
|
+
* this single put to be slower than that entire render+set, and self-heals
|
|
892
|
+
* within MAX_REVALIDATION_INTERVAL.
|
|
893
|
+
*
|
|
894
|
+
* Cache-Control is recomputed to the REMAINING ttl from the stored hard-expiry
|
|
895
|
+
* deadline (see remainingCacheControl), not copied from the original
|
|
896
|
+
* full-window header -- copying it would restart CF retention on every re-arm
|
|
897
|
+
* and pin a perpetually-failing entry past hard-expiry. A legacy/tampered entry
|
|
898
|
+
* without a valid deadline floors to max-age=1 and self-heals via KV.
|
|
899
|
+
* @internal
|
|
900
|
+
*/
|
|
901
|
+
private markRevalidating(
|
|
902
|
+
cache: Cache,
|
|
903
|
+
request: Request,
|
|
904
|
+
sourceHeaders: Headers,
|
|
905
|
+
status: number,
|
|
906
|
+
body: string,
|
|
907
|
+
): void {
|
|
908
|
+
const reputNow = Date.now();
|
|
909
|
+
const headers = new Headers(sourceHeaders);
|
|
910
|
+
headers.set(CACHE_STATUS_HEADER, "REVALIDATING");
|
|
911
|
+
headers.set(CACHE_REVALIDATING_AT_HEADER, String(reputNow));
|
|
912
|
+
headers.set("Cache-Control", remainingCacheControl(headers, reputNow));
|
|
913
|
+
const markerResponse = new Response(body, { status, headers });
|
|
914
|
+
const write = async (): Promise<void> => {
|
|
915
|
+
try {
|
|
916
|
+
await cache.put(request, markerResponse);
|
|
917
|
+
} catch {
|
|
918
|
+
// Best-effort: a failed marker write must not affect the served read;
|
|
919
|
+
// the entry simply re-arms on the next stale read.
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
if (this.waitUntil) this.waitUntil(write);
|
|
923
|
+
else void write();
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Document-tier counterpart of markRevalidating for getResponse's herd guard.
|
|
928
|
+
* The segment/item tiers JSON-parse the body, so they re-put with a string
|
|
929
|
+
* body; document bodies are streamed verbatim, so we re-put with a CLONED
|
|
930
|
+
* response body (`response.clone()`) supplied by the caller -- the original
|
|
931
|
+
* body still streams to the client while the marker carries the clone. Same
|
|
932
|
+
* REVALIDATING status header, same revalidating-at stamp, same
|
|
933
|
+
* remainingCacheControl re-put math as markRevalidating, so the document tier
|
|
934
|
+
* suppresses concurrent revalidation for the identical MAX_REVALIDATION_INTERVAL
|
|
935
|
+
* window the segment tier does. Best-effort and non-blocking: a failed marker
|
|
936
|
+
* write must not affect the served stale read.
|
|
937
|
+
* @internal
|
|
938
|
+
*/
|
|
939
|
+
private markResponseRevalidating(
|
|
940
|
+
cache: Cache,
|
|
941
|
+
request: Request,
|
|
942
|
+
clonedResponse: Response,
|
|
943
|
+
): void {
|
|
944
|
+
const reputNow = Date.now();
|
|
945
|
+
const headers = new Headers(clonedResponse.headers);
|
|
946
|
+
headers.set(CACHE_STATUS_HEADER, "REVALIDATING");
|
|
947
|
+
headers.set(CACHE_REVALIDATING_AT_HEADER, String(reputNow));
|
|
948
|
+
headers.set("Cache-Control", remainingCacheControl(headers, reputNow));
|
|
949
|
+
const markerResponse = new Response(clonedResponse.body, {
|
|
950
|
+
status: clonedResponse.status,
|
|
951
|
+
statusText: clonedResponse.statusText,
|
|
952
|
+
headers,
|
|
953
|
+
});
|
|
954
|
+
const write = async (): Promise<void> => {
|
|
955
|
+
try {
|
|
956
|
+
await cache.put(request, markerResponse);
|
|
957
|
+
} catch {
|
|
958
|
+
// Best-effort: see markRevalidating.
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
if (this.waitUntil) this.waitUntil(write);
|
|
962
|
+
else void write();
|
|
963
|
+
}
|
|
964
|
+
|
|
319
965
|
// ============================================================================
|
|
320
966
|
// Segment Cache Methods
|
|
321
967
|
// ============================================================================
|
|
322
968
|
|
|
969
|
+
/**
|
|
970
|
+
* Guard the segment tier against a `keyGenerator` that returns a key colliding
|
|
971
|
+
* with a reserved tag-marker namespace: `__tag__/` (the KV marker key) or
|
|
972
|
+
* `__tagmarker__/` (the L1 Cache API marker request). The item/doc tiers are
|
|
973
|
+
* internally prefixed (`fn:`/`doc:`) so only the bare segment key can collide;
|
|
974
|
+
* a collision would let a segment write clobber - or a segment read/delete
|
|
975
|
+
* evict - a live tag marker, silently breaking invalidation. Report loudly
|
|
976
|
+
* (so a misconfigured keyGenerator surfaces immediately) and treat the segment
|
|
977
|
+
* operation as a miss/no-op rather than corrupting the marker namespace.
|
|
978
|
+
* @internal
|
|
979
|
+
*/
|
|
980
|
+
private isReservedSegmentKey(
|
|
981
|
+
key: string,
|
|
982
|
+
category: CacheErrorCategory,
|
|
983
|
+
): boolean {
|
|
984
|
+
const reserved = key.startsWith(TAG_MARKER_PREFIX)
|
|
985
|
+
? TAG_MARKER_PREFIX
|
|
986
|
+
: key.startsWith(TAG_MARKER_CACHE_PREFIX)
|
|
987
|
+
? TAG_MARKER_CACHE_PREFIX
|
|
988
|
+
: null;
|
|
989
|
+
if (!reserved) return false;
|
|
990
|
+
reportCacheError(
|
|
991
|
+
new Error(
|
|
992
|
+
`segment key "${key}" collides with the reserved "${reserved}" ` +
|
|
993
|
+
`tag-marker namespace; the operation is ignored. Fix the store ` +
|
|
994
|
+
`keyGenerator so it does not produce keys with this prefix.`,
|
|
995
|
+
),
|
|
996
|
+
category,
|
|
997
|
+
"[CFCacheStore] reserved key",
|
|
998
|
+
);
|
|
999
|
+
return true;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
323
1002
|
/**
|
|
324
1003
|
* Get cached entry data by key.
|
|
325
1004
|
*
|
|
@@ -331,50 +1010,237 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
331
1010
|
* On L1 miss, falls back to KV (L2) if configured.
|
|
332
1011
|
* KV hits are promoted to L1 in the background.
|
|
333
1012
|
*/
|
|
334
|
-
async get(key: string): Promise<CacheGetResult | null> {
|
|
1013
|
+
async get(key: string): Promise<CacheGetResult | null | CacheReadError> {
|
|
1014
|
+
if (this.isReservedSegmentKey(key, "cache-read")) return null;
|
|
335
1015
|
try {
|
|
336
1016
|
const cache = await this.getCache();
|
|
337
1017
|
const request = this.keyToRequest(key);
|
|
338
|
-
const
|
|
1018
|
+
const matchStart = Date.now();
|
|
1019
|
+
const {
|
|
1020
|
+
response,
|
|
1021
|
+
timedOut,
|
|
1022
|
+
error: matchError,
|
|
1023
|
+
} = await this.matchWithTimeout(cache, request);
|
|
1024
|
+
const matchMs = Date.now() - matchStart;
|
|
339
1025
|
|
|
340
1026
|
if (!response) {
|
|
1027
|
+
if (matchError) {
|
|
1028
|
+
// A match REJECTION is reported as cache-read and still degrades to
|
|
1029
|
+
// L2/KV -- a real KV value (or KV's own CACHE_READ_ERROR) stands on
|
|
1030
|
+
// its own. But a null KV result (unconfigured namespace, kv-miss,
|
|
1031
|
+
// kv-timeout) is NOT proof of absence under a rejected L1 match:
|
|
1032
|
+
// the only real signal this read produced is the failure, so
|
|
1033
|
+
// surface CACHE_READ_ERROR instead of a replayable miss (the PPR
|
|
1034
|
+
// seeded fallback must render uncached, not substitute the doc
|
|
1035
|
+
// record for a partition the store could not actually read).
|
|
1036
|
+
reportCacheError(
|
|
1037
|
+
matchError,
|
|
1038
|
+
"cache-read",
|
|
1039
|
+
"[CFCacheStore] get L1 match",
|
|
1040
|
+
);
|
|
1041
|
+
if (this.debug)
|
|
1042
|
+
this.emitDebug({
|
|
1043
|
+
op: "get",
|
|
1044
|
+
key,
|
|
1045
|
+
// Distinct from a genuine absence: surface it as match-error so
|
|
1046
|
+
// debug agrees with the cache-read already routed to onError,
|
|
1047
|
+
// instead of masquerading as l1-miss.
|
|
1048
|
+
outcome: "match-error",
|
|
1049
|
+
matchMs,
|
|
1050
|
+
});
|
|
1051
|
+
const kvResult = await this.kvGetSegment(key);
|
|
1052
|
+
return kvResult ?? CACHE_READ_ERROR;
|
|
1053
|
+
}
|
|
1054
|
+
// An abandoned slow match (timedOut) keeps the fail-open latency-budget
|
|
1055
|
+
// policy: degrade to L2/KV, and a KV null stays a miss.
|
|
1056
|
+
if (this.debug)
|
|
1057
|
+
this.emitDebug({
|
|
1058
|
+
op: "get",
|
|
1059
|
+
key,
|
|
1060
|
+
outcome: timedOut ? "match-timeout" : "l1-miss",
|
|
1061
|
+
matchMs,
|
|
1062
|
+
});
|
|
341
1063
|
return this.kvGetSegment(key);
|
|
342
1064
|
}
|
|
343
1065
|
|
|
1066
|
+
// A non-200 entry (a cached error response, or a foreign response that
|
|
1067
|
+
// landed on this key) is not valid segment data; treat it as a miss
|
|
1068
|
+
// rather than JSON-parsing garbage and serving it as a hit.
|
|
1069
|
+
if (response.status !== 200) {
|
|
1070
|
+
if (this.debug)
|
|
1071
|
+
this.emitDebug({
|
|
1072
|
+
op: "get",
|
|
1073
|
+
key,
|
|
1074
|
+
outcome: "non-200",
|
|
1075
|
+
status: response.status,
|
|
1076
|
+
matchMs,
|
|
1077
|
+
});
|
|
1078
|
+
// Degraded fall-through: suppress revalidation so a broken L1 entry hit
|
|
1079
|
+
// concurrently serves KV-stale, not a herd. See kvGetSegment.
|
|
1080
|
+
return this.kvGetSegment(key, { suppressRevalidate: true });
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Tag invalidation: an entry whose tags were invalidated after it was
|
|
1084
|
+
// cached is treated as a miss, so the next render re-populates it. We
|
|
1085
|
+
// return null (re-render locally) rather than falling through to KV. In
|
|
1086
|
+
// the common case the L1 entry and its KV twin were written together with
|
|
1087
|
+
// the same taggedAt, so kvGetSegment's own tag check would miss too and a
|
|
1088
|
+
// fall-through is pure cost. The tiers CAN diverge -- another colo may have
|
|
1089
|
+
// already re-rendered and written a fresher KV envelope -- in which case a
|
|
1090
|
+
// fall-through could serve that copy instead of re-rendering here.
|
|
1091
|
+
// Capturing that cross-colo optimization is a deferred follow-up, not a
|
|
1092
|
+
// correctness gap: this colo's next read after its own re-render self-heals.
|
|
1093
|
+
const tagInfo = this.readTagInfo(response.headers);
|
|
1094
|
+
// Measure the marker-resolution tail (memo -> L1 marker cache -> KV) only
|
|
1095
|
+
// when debug is on, so the hot path pays nothing. It is the serial read
|
|
1096
|
+
// that sits between matchMs and bodyReadMs for a tagged entry.
|
|
1097
|
+
const markerStart = this.debug ? Date.now() : 0;
|
|
1098
|
+
const invalidated = await this.isL1Invalidated(
|
|
1099
|
+
tagInfo.tags,
|
|
1100
|
+
tagInfo.taggedAt,
|
|
1101
|
+
response.headers,
|
|
1102
|
+
);
|
|
1103
|
+
const markerMs = this.debug ? Date.now() - markerStart : undefined;
|
|
1104
|
+
if (invalidated) {
|
|
1105
|
+
if (this.debug)
|
|
1106
|
+
this.emitDebug({
|
|
1107
|
+
op: "get",
|
|
1108
|
+
key,
|
|
1109
|
+
outcome: "tag-invalidated",
|
|
1110
|
+
status: response.status,
|
|
1111
|
+
matchMs,
|
|
1112
|
+
markerMs,
|
|
1113
|
+
});
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
344
1117
|
// Read status headers
|
|
345
1118
|
const status = response.headers.get(CACHE_STATUS_HEADER);
|
|
346
|
-
const age = Number(response.headers.get("age") ?? "0");
|
|
347
1119
|
const staleAt = Number(
|
|
348
1120
|
response.headers.get(CACHE_STALE_AT_HEADER) ?? "0",
|
|
349
1121
|
);
|
|
1122
|
+
const revalidatingAt = Number(
|
|
1123
|
+
response.headers.get(CACHE_REVALIDATING_AT_HEADER) ?? "0",
|
|
1124
|
+
);
|
|
350
1125
|
|
|
351
|
-
const
|
|
1126
|
+
const now = Date.now();
|
|
1127
|
+
const isStale = staleAt > 0 && now > staleAt;
|
|
1128
|
+
// Recency comes from our explicit revalidating-at stamp, not CF's `Age`
|
|
1129
|
+
// header (see CACHE_REVALIDATING_AT_HEADER). An absent/zero stamp counts
|
|
1130
|
+
// as "not recent" so a dropped revalidation re-arms instead of pinning.
|
|
352
1131
|
const isRevalidating =
|
|
353
|
-
status === "REVALIDATING" &&
|
|
1132
|
+
status === "REVALIDATING" &&
|
|
1133
|
+
revalidatingAt > 0 &&
|
|
1134
|
+
now - revalidatingAt < MAX_REVALIDATION_INTERVAL * 1000;
|
|
1135
|
+
|
|
1136
|
+
// Single emitter for the post-header L1 outcomes. Undefined (so the event
|
|
1137
|
+
// object is never allocated) when debug is off; the informational-only
|
|
1138
|
+
// `age` header is read lazily inside for the same reason.
|
|
1139
|
+
const debugRead = this.debug
|
|
1140
|
+
? (
|
|
1141
|
+
outcome: CFCacheReadDebugEvent["outcome"],
|
|
1142
|
+
bodyReadMs: number,
|
|
1143
|
+
shouldRevalidate?: boolean,
|
|
1144
|
+
) =>
|
|
1145
|
+
this.emitDebug({
|
|
1146
|
+
op: "get",
|
|
1147
|
+
key,
|
|
1148
|
+
outcome,
|
|
1149
|
+
status: response.status,
|
|
1150
|
+
cacheStatus: status,
|
|
1151
|
+
staleAt,
|
|
1152
|
+
revalidatingAt,
|
|
1153
|
+
ageHeader: response.headers.get("age"),
|
|
1154
|
+
isStale,
|
|
1155
|
+
isRevalidating,
|
|
1156
|
+
shouldRevalidate,
|
|
1157
|
+
matchMs,
|
|
1158
|
+
markerMs,
|
|
1159
|
+
bodyReadMs,
|
|
1160
|
+
})
|
|
1161
|
+
: undefined;
|
|
354
1162
|
|
|
355
1163
|
// Case 1: Fresh or already being revalidated - just return data
|
|
356
1164
|
if (!isStale || isRevalidating) {
|
|
357
|
-
const
|
|
1165
|
+
const bodyStart = Date.now();
|
|
1166
|
+
const {
|
|
1167
|
+
value: data,
|
|
1168
|
+
errored,
|
|
1169
|
+
error,
|
|
1170
|
+
} = await this.readJsonWithTimeout<CachedEntryData>(response);
|
|
1171
|
+
const bodyReadMs = Date.now() - bodyStart;
|
|
1172
|
+
if (data === undefined) {
|
|
1173
|
+
debugRead?.(errored ? "body-error" : "body-timeout", bodyReadMs);
|
|
1174
|
+
// A body-ERROR (corrupt/foreign body) self-heals via healCorruptL1:
|
|
1175
|
+
// report cache-corrupt, fall through to L2/KV (which overwrites the
|
|
1176
|
+
// bad entry), and evict only if KV had no good copy to promote. A
|
|
1177
|
+
// body-TIMEOUT is a degraded read of a likely-valid entry: leave it
|
|
1178
|
+
// intact and suppress revalidation so a stalling colo cannot herd.
|
|
1179
|
+
if (errored)
|
|
1180
|
+
return this.healCorruptL1(cache, request, error, "get", () =>
|
|
1181
|
+
this.kvGetSegment(key, { suppressRevalidate: false }),
|
|
1182
|
+
);
|
|
1183
|
+
return this.kvGetSegment(key, { suppressRevalidate: true });
|
|
1184
|
+
}
|
|
1185
|
+
debugRead?.(
|
|
1186
|
+
isRevalidating ? "l1-revalidating-guarded" : "l1-fresh",
|
|
1187
|
+
bodyReadMs,
|
|
1188
|
+
false,
|
|
1189
|
+
);
|
|
358
1190
|
return { data, shouldRevalidate: false };
|
|
359
1191
|
}
|
|
360
1192
|
|
|
361
|
-
// Case 2: Stale and needs revalidation
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
1193
|
+
// Case 2: Stale and needs revalidation.
|
|
1194
|
+
// Read the body under the edge-read budget BEFORE writing the REVALIDATING
|
|
1195
|
+
// marker. CF can resolve match() fast but stall the body stream; the prior
|
|
1196
|
+
// approach teed the stream and awaited cache.put(b1) first, which blocked
|
|
1197
|
+
// on that same stalled stream so the read budget could never fire on a
|
|
1198
|
+
// stale hit. Reading first bounds the stall and lets us skip marking an
|
|
1199
|
+
// entry we could not even read.
|
|
1200
|
+
const bodyStart = Date.now();
|
|
1201
|
+
const {
|
|
1202
|
+
value: data,
|
|
1203
|
+
errored,
|
|
1204
|
+
error,
|
|
1205
|
+
} = await this.readJsonWithTimeout<CachedEntryData>(response);
|
|
1206
|
+
const bodyReadMs = Date.now() - bodyStart;
|
|
1207
|
+
if (data === undefined) {
|
|
1208
|
+
debugRead?.(errored ? "body-error" : "body-timeout", bodyReadMs);
|
|
1209
|
+
// Heal + conditionally evict a body-error, suppress a body-timeout; see
|
|
1210
|
+
// Case 1.
|
|
1211
|
+
if (errored)
|
|
1212
|
+
return this.healCorruptL1(
|
|
1213
|
+
cache,
|
|
1214
|
+
request,
|
|
1215
|
+
error,
|
|
1216
|
+
"get(revalidating)",
|
|
1217
|
+
() => this.kvGetSegment(key, { suppressRevalidate: false }),
|
|
1218
|
+
);
|
|
1219
|
+
return this.kvGetSegment(key, { suppressRevalidate: true });
|
|
1220
|
+
}
|
|
366
1221
|
|
|
367
|
-
//
|
|
368
|
-
|
|
1222
|
+
// Mark REVALIDATING so concurrent requests don't all revalidate, then
|
|
1223
|
+
// return the stale data. The marker write is non-blocking and best-effort
|
|
1224
|
+
// (see markRevalidating) -- it must not add latency to, or fail, the served
|
|
1225
|
+
// stale read.
|
|
1226
|
+
this.markRevalidating(
|
|
1227
|
+
cache,
|
|
369
1228
|
request,
|
|
370
|
-
|
|
1229
|
+
response.headers,
|
|
1230
|
+
response.status,
|
|
1231
|
+
JSON.stringify(data),
|
|
371
1232
|
);
|
|
372
1233
|
|
|
373
|
-
|
|
1234
|
+
debugRead?.("l1-stale-revalidate", bodyReadMs, true);
|
|
374
1235
|
return { data, shouldRevalidate: true };
|
|
375
1236
|
} catch (error) {
|
|
376
|
-
|
|
377
|
-
|
|
1237
|
+
// reportCacheError logs and routes to onError (cache-read); the debug
|
|
1238
|
+
// emit is the separate wrangler-tail signal. Keep both observability paths.
|
|
1239
|
+
reportCacheError(error, "cache-read", "[CFCacheStore] get");
|
|
1240
|
+
if (this.debug) this.emitDebug({ op: "get", key, outcome: "error" });
|
|
1241
|
+
// Distinct from a miss so the PPR replay composition renders uncached
|
|
1242
|
+
// instead of substituting the seeded doc record (CACHE_READ_ERROR).
|
|
1243
|
+
return CACHE_READ_ERROR;
|
|
378
1244
|
}
|
|
379
1245
|
}
|
|
380
1246
|
|
|
@@ -389,6 +1255,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
389
1255
|
ttl: number,
|
|
390
1256
|
swr?: number,
|
|
391
1257
|
): Promise<void> {
|
|
1258
|
+
if (this.isReservedSegmentKey(key, "cache-write")) return;
|
|
1259
|
+
if (this.skipUncacheableTagSet(data.tags)) return;
|
|
392
1260
|
try {
|
|
393
1261
|
const cache = await this.getCache();
|
|
394
1262
|
const request = this.keyToRequest(key);
|
|
@@ -398,32 +1266,60 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
398
1266
|
const totalTtl = ttl + swrWindow;
|
|
399
1267
|
const staleAt = Date.now() + ttl * 1000;
|
|
400
1268
|
|
|
401
|
-
|
|
1269
|
+
// Stamp the tag timestamp at write time and carry it (with the tags)
|
|
1270
|
+
// into both the L1 body and the KV envelope so reads can run the
|
|
1271
|
+
// invalidation check.
|
|
1272
|
+
const taggedAt =
|
|
1273
|
+
Array.isArray(data.tags) && data.tags.length > 0
|
|
1274
|
+
? Date.now()
|
|
1275
|
+
: undefined;
|
|
1276
|
+
const dataToStore: CachedEntryData = taggedAt
|
|
1277
|
+
? { ...data, taggedAt }
|
|
1278
|
+
: data;
|
|
1279
|
+
|
|
1280
|
+
// Serialize the segment payload exactly once: L1 stores the JSON body
|
|
1281
|
+
// directly, and kvSetSegment embeds the same string as envelope.d so the
|
|
1282
|
+
// (potentially large) Flight/segment tree is not walked a second time.
|
|
1283
|
+
const body = JSON.stringify(dataToStore);
|
|
402
1284
|
const response = new Response(body, {
|
|
403
1285
|
headers: {
|
|
404
1286
|
"Content-Type": "application/json",
|
|
405
1287
|
"Cache-Control": `public, max-age=${totalTtl}`,
|
|
406
1288
|
[CACHE_STALE_AT_HEADER]: String(staleAt),
|
|
1289
|
+
// Absolute hard-expiry deadline so a stale-path re-put can recompute a
|
|
1290
|
+
// shrinking max-age instead of restarting retention (see
|
|
1291
|
+
// remainingCacheControl / CACHE_EXPIRES_AT_HEADER).
|
|
1292
|
+
[CACHE_EXPIRES_AT_HEADER]: String(staleAt + swrWindow * 1000),
|
|
407
1293
|
[CACHE_STATUS_HEADER]: "HIT",
|
|
1294
|
+
...this.tagHeaderEntries(dataToStore.tags, taggedAt),
|
|
408
1295
|
},
|
|
409
1296
|
});
|
|
410
1297
|
|
|
411
1298
|
const putPromise = cache.put(request, response);
|
|
412
1299
|
|
|
413
1300
|
if (this.waitUntil) {
|
|
414
|
-
// Non-blocking write
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
1301
|
+
// Non-blocking write. These store-level background tasks intentionally
|
|
1302
|
+
// omit the reportingAsync ctx argument: the store is a request-agnostic
|
|
1303
|
+
// singleton and this.waitUntil is the execution context's, not a single
|
|
1304
|
+
// request's, so a failure is reported console-loud only (it cannot be
|
|
1305
|
+
// attributed to one request's onError). The request-scoped tag verbs
|
|
1306
|
+
// (revalidateTag / stale-revalidation) DO thread their captured ctx.
|
|
1307
|
+
this.waitUntil(() =>
|
|
1308
|
+
reportingAsync(
|
|
1309
|
+
() => putPromise,
|
|
1310
|
+
"cache-write",
|
|
1311
|
+
"[CFCacheStore] L1 write",
|
|
1312
|
+
),
|
|
1313
|
+
);
|
|
418
1314
|
} else {
|
|
419
1315
|
// Blocking fallback
|
|
420
1316
|
await putPromise;
|
|
421
1317
|
}
|
|
422
1318
|
|
|
423
|
-
// L2: persist to KV
|
|
424
|
-
this.kvSetSegment(key,
|
|
1319
|
+
// L2: persist to KV (reuses `body` as envelope.d)
|
|
1320
|
+
this.kvSetSegment(key, body, staleAt, totalTtl, swrWindow);
|
|
425
1321
|
} catch (error) {
|
|
426
|
-
|
|
1322
|
+
reportCacheError(error, "cache-write", "[CFCacheStore] set");
|
|
427
1323
|
}
|
|
428
1324
|
}
|
|
429
1325
|
|
|
@@ -431,25 +1327,26 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
431
1327
|
* Delete a cached entry from L1 and L2.
|
|
432
1328
|
*/
|
|
433
1329
|
async delete(key: string): Promise<boolean> {
|
|
1330
|
+
if (this.isReservedSegmentKey(key, "cache-delete")) return false;
|
|
434
1331
|
try {
|
|
435
1332
|
const cache = await this.getCache();
|
|
436
1333
|
const result = await cache.delete(this.keyToRequest(key));
|
|
437
1334
|
|
|
438
1335
|
// L2: delete from KV
|
|
439
1336
|
if (this.kv && this.waitUntil) {
|
|
440
|
-
const kvKey = this.toKVKey(key);
|
|
441
|
-
this.waitUntil(
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
1337
|
+
const kvKey = await this.toKVKey(key);
|
|
1338
|
+
this.waitUntil(() =>
|
|
1339
|
+
reportingAsync(
|
|
1340
|
+
() => this.kv!.delete(kvKey),
|
|
1341
|
+
"cache-delete",
|
|
1342
|
+
"[CFCacheStore] delete L2",
|
|
1343
|
+
),
|
|
1344
|
+
);
|
|
448
1345
|
}
|
|
449
1346
|
|
|
450
1347
|
return result;
|
|
451
1348
|
} catch (error) {
|
|
452
|
-
|
|
1349
|
+
reportCacheError(error, "cache-delete", "[CFCacheStore] delete");
|
|
453
1350
|
return false;
|
|
454
1351
|
}
|
|
455
1352
|
}
|
|
@@ -469,26 +1366,132 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
469
1366
|
try {
|
|
470
1367
|
const cache = await this.getCache();
|
|
471
1368
|
const request = this.keyToRequest(`doc:${key}`);
|
|
472
|
-
|
|
1369
|
+
// The document path is outside the debug surface (op is only get/getItem),
|
|
1370
|
+
// so the match-timeout flag is not surfaced as an event here -- though
|
|
1371
|
+
// matchWithTimeout still warns on a slow match. A miss or timeout falls
|
|
1372
|
+
// through to the KV document path and then render.
|
|
1373
|
+
const { response, error: matchError } = await this.matchWithTimeout(
|
|
1374
|
+
cache,
|
|
1375
|
+
request,
|
|
1376
|
+
);
|
|
473
1377
|
|
|
474
1378
|
if (!response || response.status !== 200) {
|
|
1379
|
+
// A transient L1 match rejection (matchError set; only ever set when
|
|
1380
|
+
// response is undefined) is surfaced as cache-read before degrading to
|
|
1381
|
+
// L2/KV -- matching get()/getItem(). A genuine miss or a non-200 hit
|
|
1382
|
+
// carries no matchError and reports nothing.
|
|
1383
|
+
if (matchError)
|
|
1384
|
+
reportCacheError(
|
|
1385
|
+
matchError,
|
|
1386
|
+
"cache-read",
|
|
1387
|
+
"[CFCacheStore] getResponse L1 match",
|
|
1388
|
+
);
|
|
475
1389
|
return this.kvGetResponse(key);
|
|
476
1390
|
}
|
|
477
1391
|
|
|
1392
|
+
// Tag invalidation check (treat invalidated entry as a miss).
|
|
1393
|
+
const tagInfo = this.readTagInfo(response.headers);
|
|
1394
|
+
if (
|
|
1395
|
+
await this.isL1Invalidated(
|
|
1396
|
+
tagInfo.tags,
|
|
1397
|
+
tagInfo.taggedAt,
|
|
1398
|
+
response.headers,
|
|
1399
|
+
)
|
|
1400
|
+
) {
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
478
1404
|
// Check staleness
|
|
479
1405
|
const staleAt = Number(response.headers.get(CACHE_STALE_AT_HEADER) || 0);
|
|
480
|
-
const
|
|
1406
|
+
const now = Date.now();
|
|
1407
|
+
const isStale = staleAt > 0 && now > staleAt;
|
|
1408
|
+
|
|
1409
|
+
// Thundering-herd guard, mirroring the segment (get) and item (getItem)
|
|
1410
|
+
// tiers. Without it, every concurrent stale reader returned
|
|
1411
|
+
// shouldRevalidate=true and document-cache.ts scheduled a fresh render for
|
|
1412
|
+
// each one. Recency comes from our own revalidating-at stamp, not CF's Age
|
|
1413
|
+
// header (see CACHE_REVALIDATING_AT_HEADER); an absent/zero stamp counts as
|
|
1414
|
+
// "not recent" so a dropped revalidation re-arms instead of pinning.
|
|
1415
|
+
const status = response.headers.get(CACHE_STATUS_HEADER);
|
|
1416
|
+
const revalidatingAt = Number(
|
|
1417
|
+
response.headers.get(CACHE_REVALIDATING_AT_HEADER) ?? "0",
|
|
1418
|
+
);
|
|
1419
|
+
const isRevalidating =
|
|
1420
|
+
status === "REVALIDATING" &&
|
|
1421
|
+
revalidatingAt > 0 &&
|
|
1422
|
+
now - revalidatingAt < MAX_REVALIDATION_INTERVAL * 1000;
|
|
1423
|
+
|
|
1424
|
+
// L1 document bodies are streamed through verbatim - unlike the segment/
|
|
1425
|
+
// item tiers (which JSON-parse and so structurally detect corruption) and
|
|
1426
|
+
// the KV doc tier (validated in kvGetResponse, KV being the real partial-
|
|
1427
|
+
// read vector). Integrity here relies on the Cache API: cache.put stores a
|
|
1428
|
+
// response atomically or fails, so a truncated body is not served back. We
|
|
1429
|
+
// deliberately do NOT buffer+hash the body to re-verify it: that would
|
|
1430
|
+
// defeat streaming the document and add a full read to every cache hit.
|
|
1431
|
+
|
|
1432
|
+
if (isStale && !isRevalidating) {
|
|
1433
|
+
// First stale reader within the window: mark REVALIDATING (non-blocking,
|
|
1434
|
+
// best-effort) so concurrent readers below see the guard and suppress,
|
|
1435
|
+
// then return shouldRevalidate=true so this caller revalidates. Clone the
|
|
1436
|
+
// matched response for the marker since its original body must still
|
|
1437
|
+
// stream to the client.
|
|
1438
|
+
this.markResponseRevalidating(cache, request, response.clone());
|
|
1439
|
+
return {
|
|
1440
|
+
response: this.toClientResponse(response),
|
|
1441
|
+
shouldRevalidate: true,
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
481
1444
|
|
|
1445
|
+
// Fresh, or stale-but-already-REVALIDATING: serve without scheduling a
|
|
1446
|
+
// (re-)revalidation. A recent marker already has a render in flight.
|
|
482
1447
|
return {
|
|
483
|
-
response,
|
|
484
|
-
shouldRevalidate:
|
|
1448
|
+
response: this.toClientResponse(response),
|
|
1449
|
+
shouldRevalidate: false,
|
|
485
1450
|
};
|
|
486
1451
|
} catch (error) {
|
|
487
|
-
|
|
1452
|
+
reportCacheError(error, "cache-read", "[CFCacheStore] getResponse");
|
|
488
1453
|
return null;
|
|
489
1454
|
}
|
|
490
1455
|
}
|
|
491
1456
|
|
|
1457
|
+
/**
|
|
1458
|
+
* Strip internal edge headers and restore the author's Cache-Control before a
|
|
1459
|
+
* cached document Response is served to a client. L1 entries carry the
|
|
1460
|
+
* internal staleness/status headers and a rewritten Cache-Control; none of
|
|
1461
|
+
* those should reach the browser or an upstream CDN.
|
|
1462
|
+
*/
|
|
1463
|
+
private toClientResponse(response: Response): Response {
|
|
1464
|
+
const headers = new Headers(response.headers);
|
|
1465
|
+
const originalCacheControl = headers.get(CACHE_ORIG_CC_HEADER);
|
|
1466
|
+
if (originalCacheControl !== null) {
|
|
1467
|
+
headers.set("Cache-Control", originalCacheControl);
|
|
1468
|
+
} else {
|
|
1469
|
+
headers.delete("Cache-Control");
|
|
1470
|
+
}
|
|
1471
|
+
headers.delete(CACHE_ORIG_CC_HEADER);
|
|
1472
|
+
headers.delete(CACHE_STALE_AT_HEADER);
|
|
1473
|
+
headers.delete(CACHE_STATUS_HEADER);
|
|
1474
|
+
headers.delete(CACHE_TAGS_HEADER);
|
|
1475
|
+
headers.delete(CACHE_TAGGED_AT_HEADER);
|
|
1476
|
+
// Remove OUR namespaced tokens from Cache-Tag while preserving any the
|
|
1477
|
+
// document author set (setTagHeaders appended ours onto theirs). The
|
|
1478
|
+
// author's tags may be load-bearing for their own CDN purging; ours are
|
|
1479
|
+
// internal storage bookkeeping and must not leak to clients.
|
|
1480
|
+
this.stripInternalCacheTags(headers);
|
|
1481
|
+
// Internal stale-path bookkeeping (hard-expiry deadline + REVALIDATING
|
|
1482
|
+
// stamp). Carried on doc L1 entries for the herd guard; never serve them.
|
|
1483
|
+
headers.delete(CACHE_EXPIRES_AT_HEADER);
|
|
1484
|
+
headers.delete(CACHE_REVALIDATING_AT_HEADER);
|
|
1485
|
+
// Finding #3 (read side): strip per-client signals a pre-fix or
|
|
1486
|
+
// pinned-version L1 entry may carry. See the read-side note in the design doc.
|
|
1487
|
+
stripPerClientSignals(headers);
|
|
1488
|
+
return new Response(response.body, {
|
|
1489
|
+
status: response.status,
|
|
1490
|
+
statusText: response.statusText,
|
|
1491
|
+
headers,
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
|
|
492
1495
|
/**
|
|
493
1496
|
* Store a Response with TTL and optional SWR window (for document-level caching).
|
|
494
1497
|
* When KV is configured, also persists to L2.
|
|
@@ -498,7 +1501,9 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
498
1501
|
response: Response,
|
|
499
1502
|
ttl: number,
|
|
500
1503
|
swr?: number,
|
|
1504
|
+
tags?: string[],
|
|
501
1505
|
): Promise<void> {
|
|
1506
|
+
if (this.skipUncacheableTagSet(tags)) return;
|
|
502
1507
|
try {
|
|
503
1508
|
const cache = await this.getCache();
|
|
504
1509
|
const request = this.keyToRequest(`doc:${key}`);
|
|
@@ -507,6 +1512,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
507
1512
|
const swrWindow = resolveSwrWindow(swr, this.defaults);
|
|
508
1513
|
const totalTtl = ttl + swrWindow;
|
|
509
1514
|
const staleAt = Date.now() + ttl * 1000;
|
|
1515
|
+
const taggedAt =
|
|
1516
|
+
Array.isArray(tags) && tags.length > 0 ? Date.now() : undefined;
|
|
510
1517
|
|
|
511
1518
|
// Clone body for potential KV write before consuming it for L1
|
|
512
1519
|
const [l1Body, kvBody] = this.kv
|
|
@@ -515,10 +1522,27 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
515
1522
|
: [null, null]
|
|
516
1523
|
: [response.body, null];
|
|
517
1524
|
|
|
518
|
-
// Clone and add cache headers
|
|
1525
|
+
// Clone and add cache headers. The author's Cache-Control is stashed and
|
|
1526
|
+
// replaced with a long max-age so the CF Cache API holds the entry across
|
|
1527
|
+
// the SWR window; getResponse restores the original before serving.
|
|
519
1528
|
const headers = new Headers(response.headers);
|
|
1529
|
+
// Finding #3: never persist a per-client signal in the shared L1 entry
|
|
1530
|
+
// (the platform's Set-Cookie rejection is unverified and ignores the
|
|
1531
|
+
// directive anyway). See stripPerClientSignals.
|
|
1532
|
+
stripPerClientSignals(headers);
|
|
1533
|
+
const originalCacheControl = response.headers.get("Cache-Control");
|
|
1534
|
+
if (originalCacheControl !== null) {
|
|
1535
|
+
headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
|
|
1536
|
+
}
|
|
520
1537
|
headers.set("Cache-Control", `public, max-age=${totalTtl}`);
|
|
521
1538
|
headers.set(CACHE_STALE_AT_HEADER, String(staleAt));
|
|
1539
|
+
// Absolute hard-expiry deadline so a stale-path REVALIDATING re-put can
|
|
1540
|
+
// recompute a shrinking max-age (remainingCacheControl) instead of
|
|
1541
|
+
// restarting retention. Mirrors set()/setItem(). Stripped by
|
|
1542
|
+
// toClientResponse before serving.
|
|
1543
|
+
headers.set(CACHE_EXPIRES_AT_HEADER, String(staleAt + swrWindow * 1000));
|
|
1544
|
+
// Internal tag headers (stripped by toClientResponse before serving).
|
|
1545
|
+
this.setTagHeaders(headers, tags, taggedAt);
|
|
522
1546
|
|
|
523
1547
|
const toCache = new Response(l1Body, {
|
|
524
1548
|
status: response.status,
|
|
@@ -530,9 +1554,13 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
530
1554
|
|
|
531
1555
|
if (this.waitUntil) {
|
|
532
1556
|
// Non-blocking write
|
|
533
|
-
this.waitUntil(
|
|
534
|
-
|
|
535
|
-
|
|
1557
|
+
this.waitUntil(() =>
|
|
1558
|
+
reportingAsync(
|
|
1559
|
+
() => putPromise,
|
|
1560
|
+
"cache-write",
|
|
1561
|
+
"[CFCacheStore] L1 write",
|
|
1562
|
+
),
|
|
1563
|
+
);
|
|
536
1564
|
} else {
|
|
537
1565
|
// Blocking fallback
|
|
538
1566
|
await putPromise;
|
|
@@ -540,35 +1568,43 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
540
1568
|
|
|
541
1569
|
// L2: persist to KV (KV requires expirationTtl >= 60s)
|
|
542
1570
|
if (this.kv && this.waitUntil && totalTtl >= 60) {
|
|
543
|
-
const kvKey = this.
|
|
1571
|
+
const kvKey = await this.toDocKVKey(key);
|
|
1572
|
+
// Finding #3: never persist a per-client signal in the KV envelope.
|
|
544
1573
|
const headersArray: [string, string][] = [];
|
|
545
|
-
response.headers.forEach((v, k) =>
|
|
1574
|
+
response.headers.forEach((v, k) => {
|
|
1575
|
+
if (isPerClientSignalHeader(k)) return;
|
|
1576
|
+
headersArray.push([k, v]);
|
|
1577
|
+
});
|
|
546
1578
|
// Read body as ArrayBuffer and encode to base64 to preserve binary payloads
|
|
547
1579
|
const bodyBuf = kvBody
|
|
548
1580
|
? await new Response(kvBody).arrayBuffer()
|
|
549
1581
|
: new ArrayBuffer(0);
|
|
550
1582
|
const bodyBase64 = bufferToBase64(bodyBuf);
|
|
551
1583
|
|
|
552
|
-
this.waitUntil(
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
1584
|
+
this.waitUntil(() =>
|
|
1585
|
+
reportingAsync(
|
|
1586
|
+
() => {
|
|
1587
|
+
const envelope: KVResponseEnvelope = {
|
|
1588
|
+
b: bodyBase64,
|
|
1589
|
+
st: response.status,
|
|
1590
|
+
stx: response.statusText,
|
|
1591
|
+
hd: headersArray,
|
|
1592
|
+
s: staleAt,
|
|
1593
|
+
e: staleAt + swrWindow * 1000,
|
|
1594
|
+
t: tags,
|
|
1595
|
+
ta: taggedAt,
|
|
1596
|
+
};
|
|
1597
|
+
return this.kv!.put(kvKey, JSON.stringify(envelope), {
|
|
1598
|
+
expirationTtl: totalTtl,
|
|
1599
|
+
});
|
|
1600
|
+
},
|
|
1601
|
+
"cache-write",
|
|
1602
|
+
"[CFCacheStore] kvPutResponse",
|
|
1603
|
+
),
|
|
1604
|
+
);
|
|
569
1605
|
}
|
|
570
1606
|
} catch (error) {
|
|
571
|
-
|
|
1607
|
+
reportCacheError(error, "cache-write", "[CFCacheStore] putResponse");
|
|
572
1608
|
}
|
|
573
1609
|
}
|
|
574
1610
|
|
|
@@ -585,48 +1621,174 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
585
1621
|
try {
|
|
586
1622
|
const cache = await this.getCache();
|
|
587
1623
|
const request = this.keyToRequest(`fn:${key}`);
|
|
588
|
-
const
|
|
1624
|
+
const matchStart = Date.now();
|
|
1625
|
+
const {
|
|
1626
|
+
response,
|
|
1627
|
+
timedOut,
|
|
1628
|
+
error: matchError,
|
|
1629
|
+
} = await this.matchWithTimeout(cache, request);
|
|
1630
|
+
const matchMs = Date.now() - matchStart;
|
|
1631
|
+
|
|
1632
|
+
if (!response) {
|
|
1633
|
+
// Transient match error reported cache-read; still degrades to L2/KV.
|
|
1634
|
+
if (matchError)
|
|
1635
|
+
reportCacheError(
|
|
1636
|
+
matchError,
|
|
1637
|
+
"cache-read",
|
|
1638
|
+
"[CFCacheStore] getItem L1 match",
|
|
1639
|
+
);
|
|
1640
|
+
if (this.debug)
|
|
1641
|
+
this.emitDebug({
|
|
1642
|
+
op: "getItem",
|
|
1643
|
+
key,
|
|
1644
|
+
// match-error (rejection) vs l1-miss (absence); see get().
|
|
1645
|
+
outcome: matchError
|
|
1646
|
+
? "match-error"
|
|
1647
|
+
: timedOut
|
|
1648
|
+
? "match-timeout"
|
|
1649
|
+
: "l1-miss",
|
|
1650
|
+
matchMs,
|
|
1651
|
+
});
|
|
1652
|
+
return this.kvGetItem(key);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// Non-200 entry is not a valid cached function result; treat as a miss.
|
|
1656
|
+
if (response.status !== 200) {
|
|
1657
|
+
if (this.debug)
|
|
1658
|
+
this.emitDebug({
|
|
1659
|
+
op: "getItem",
|
|
1660
|
+
key,
|
|
1661
|
+
outcome: "non-200",
|
|
1662
|
+
status: response.status,
|
|
1663
|
+
matchMs,
|
|
1664
|
+
});
|
|
1665
|
+
// Degraded fall-through: suppress revalidation so a broken L1 entry hit
|
|
1666
|
+
// concurrently serves KV-stale instead of spawning a herd (see get()).
|
|
1667
|
+
return this.kvGetItem(key, { suppressRevalidate: true });
|
|
1668
|
+
}
|
|
589
1669
|
|
|
590
|
-
|
|
1670
|
+
// Tag invalidation check (treat invalidated entry as a miss). Measure the
|
|
1671
|
+
// marker-resolution tail only under debug (see get()).
|
|
1672
|
+
const tagInfo = this.readTagInfo(response.headers);
|
|
1673
|
+
const markerStart = this.debug ? Date.now() : 0;
|
|
1674
|
+
const invalidated = await this.isL1Invalidated(
|
|
1675
|
+
tagInfo.tags,
|
|
1676
|
+
tagInfo.taggedAt,
|
|
1677
|
+
response.headers,
|
|
1678
|
+
);
|
|
1679
|
+
const markerMs = this.debug ? Date.now() - markerStart : undefined;
|
|
1680
|
+
if (invalidated) {
|
|
1681
|
+
if (this.debug)
|
|
1682
|
+
this.emitDebug({
|
|
1683
|
+
op: "getItem",
|
|
1684
|
+
key,
|
|
1685
|
+
outcome: "tag-invalidated",
|
|
1686
|
+
status: response.status,
|
|
1687
|
+
matchMs,
|
|
1688
|
+
markerMs,
|
|
1689
|
+
});
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
591
1692
|
|
|
592
1693
|
const staleAt = Number(
|
|
593
1694
|
response.headers.get(CACHE_STALE_AT_HEADER) ?? "0",
|
|
594
1695
|
);
|
|
595
1696
|
const status = response.headers.get(CACHE_STATUS_HEADER);
|
|
596
|
-
const
|
|
1697
|
+
const revalidatingAt = Number(
|
|
1698
|
+
response.headers.get(CACHE_REVALIDATING_AT_HEADER) ?? "0",
|
|
1699
|
+
);
|
|
597
1700
|
|
|
598
|
-
const
|
|
1701
|
+
const now = Date.now();
|
|
1702
|
+
const isStale = staleAt > 0 && now > staleAt;
|
|
1703
|
+
// Recency from our explicit stamp, not CF's `Age` header (see get()).
|
|
599
1704
|
const isRevalidating =
|
|
600
|
-
status === "REVALIDATING" &&
|
|
601
|
-
|
|
602
|
-
|
|
1705
|
+
status === "REVALIDATING" &&
|
|
1706
|
+
revalidatingAt > 0 &&
|
|
1707
|
+
now - revalidatingAt < MAX_REVALIDATION_INTERVAL * 1000;
|
|
1708
|
+
|
|
1709
|
+
// Single emitter for the post-header L1 outcomes (see get()). Undefined
|
|
1710
|
+
// when debug is off, so the event object is never allocated on the hot
|
|
1711
|
+
// path; the informational-only `age` header is read lazily inside.
|
|
1712
|
+
const debugRead = this.debug
|
|
1713
|
+
? (
|
|
1714
|
+
outcome: CFCacheReadDebugEvent["outcome"],
|
|
1715
|
+
bodyReadMs: number,
|
|
1716
|
+
shouldRevalidate?: boolean,
|
|
1717
|
+
) =>
|
|
1718
|
+
this.emitDebug({
|
|
1719
|
+
op: "getItem",
|
|
1720
|
+
key,
|
|
1721
|
+
outcome,
|
|
1722
|
+
status: response.status,
|
|
1723
|
+
cacheStatus: status,
|
|
1724
|
+
staleAt,
|
|
1725
|
+
revalidatingAt,
|
|
1726
|
+
ageHeader: response.headers.get("age"),
|
|
1727
|
+
isStale,
|
|
1728
|
+
isRevalidating,
|
|
1729
|
+
shouldRevalidate,
|
|
1730
|
+
matchMs,
|
|
1731
|
+
markerMs,
|
|
1732
|
+
bodyReadMs,
|
|
1733
|
+
})
|
|
1734
|
+
: undefined;
|
|
1735
|
+
|
|
1736
|
+
const bodyStart = Date.now();
|
|
1737
|
+
const {
|
|
1738
|
+
value: data,
|
|
1739
|
+
errored,
|
|
1740
|
+
error,
|
|
1741
|
+
} = await this.readJsonWithTimeout<{
|
|
603
1742
|
value: string;
|
|
604
|
-
handles?:
|
|
605
|
-
};
|
|
1743
|
+
handles?: string;
|
|
1744
|
+
}>(response);
|
|
1745
|
+
const bodyReadMs = Date.now() - bodyStart;
|
|
1746
|
+
if (data === undefined) {
|
|
1747
|
+
debugRead?.(errored ? "body-error" : "body-timeout", bodyReadMs);
|
|
1748
|
+
// Heal + conditionally evict a body-error, suppress a body-timeout; see
|
|
1749
|
+
// get().
|
|
1750
|
+
if (errored)
|
|
1751
|
+
return this.healCorruptL1(cache, request, error, "getItem", () =>
|
|
1752
|
+
this.kvGetItem(key, { suppressRevalidate: false }),
|
|
1753
|
+
);
|
|
1754
|
+
return this.kvGetItem(key, { suppressRevalidate: true });
|
|
1755
|
+
}
|
|
606
1756
|
|
|
607
1757
|
if (!isStale || isRevalidating) {
|
|
1758
|
+
debugRead?.(
|
|
1759
|
+
isRevalidating ? "l1-revalidating-guarded" : "l1-fresh",
|
|
1760
|
+
bodyReadMs,
|
|
1761
|
+
false,
|
|
1762
|
+
);
|
|
608
1763
|
return {
|
|
609
1764
|
value: data.value,
|
|
610
1765
|
handles: data.handles,
|
|
611
1766
|
shouldRevalidate: false,
|
|
1767
|
+
tags: tagInfo.tags,
|
|
612
1768
|
};
|
|
613
1769
|
}
|
|
614
1770
|
|
|
615
|
-
// Stale and needs revalidation
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
1771
|
+
// Stale and needs revalidation -- mark REVALIDATING (non-blocking,
|
|
1772
|
+
// best-effort, remaining-ttl) and return the stale value. See get() /
|
|
1773
|
+
// markRevalidating for the full rationale.
|
|
1774
|
+
this.markRevalidating(
|
|
1775
|
+
cache,
|
|
619
1776
|
request,
|
|
620
|
-
|
|
1777
|
+
response.headers,
|
|
1778
|
+
200,
|
|
1779
|
+
JSON.stringify(data),
|
|
621
1780
|
);
|
|
622
1781
|
|
|
1782
|
+
debugRead?.("l1-stale-revalidate", bodyReadMs, true);
|
|
623
1783
|
return {
|
|
624
1784
|
value: data.value,
|
|
625
1785
|
handles: data.handles,
|
|
626
1786
|
shouldRevalidate: true,
|
|
1787
|
+
tags: tagInfo.tags,
|
|
627
1788
|
};
|
|
628
1789
|
} catch (error) {
|
|
629
|
-
|
|
1790
|
+
reportCacheError(error, "cache-read", "[CFCacheStore] getItem");
|
|
1791
|
+
if (this.debug) this.emitDebug({ op: "getItem", key, outcome: "error" });
|
|
630
1792
|
return null;
|
|
631
1793
|
}
|
|
632
1794
|
}
|
|
@@ -640,6 +1802,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
640
1802
|
value: string,
|
|
641
1803
|
options?: CacheItemOptions,
|
|
642
1804
|
): Promise<void> {
|
|
1805
|
+
if (this.skipUncacheableTagSet(options?.tags)) return;
|
|
643
1806
|
try {
|
|
644
1807
|
const cache = await this.getCache();
|
|
645
1808
|
const request = this.keyToRequest(`fn:${key}`);
|
|
@@ -649,64 +1812,562 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
649
1812
|
const totalTtl = ttl + swrWindow;
|
|
650
1813
|
const staleAt = Date.now() + ttl * 1000;
|
|
651
1814
|
|
|
652
|
-
const
|
|
1815
|
+
const tags = options?.tags;
|
|
1816
|
+
const taggedAt =
|
|
1817
|
+
Array.isArray(tags) && tags.length > 0 ? Date.now() : undefined;
|
|
1818
|
+
|
|
1819
|
+
// Serialize value/handles once; L1 body and KV envelope.v/h share the
|
|
1820
|
+
// escaped strings so a large RSC payload is not re-escaped for L2.
|
|
1821
|
+
const valueJson = JSON.stringify(value);
|
|
1822
|
+
const handlesJson =
|
|
1823
|
+
options?.handles !== undefined
|
|
1824
|
+
? JSON.stringify(options.handles)
|
|
1825
|
+
: undefined;
|
|
1826
|
+
const body =
|
|
1827
|
+
handlesJson !== undefined
|
|
1828
|
+
? `{"value":${valueJson},"handles":${handlesJson}}`
|
|
1829
|
+
: `{"value":${valueJson}}`;
|
|
653
1830
|
const response = new Response(body, {
|
|
654
1831
|
headers: {
|
|
655
1832
|
"Content-Type": "application/json",
|
|
656
1833
|
"Cache-Control": `public, max-age=${totalTtl}`,
|
|
657
1834
|
[CACHE_STALE_AT_HEADER]: String(staleAt),
|
|
1835
|
+
// Absolute hard-expiry deadline; see set() / remainingCacheControl.
|
|
1836
|
+
[CACHE_EXPIRES_AT_HEADER]: String(staleAt + swrWindow * 1000),
|
|
658
1837
|
[CACHE_STATUS_HEADER]: "HIT",
|
|
1838
|
+
...this.tagHeaderEntries(tags, taggedAt),
|
|
659
1839
|
},
|
|
660
1840
|
});
|
|
661
1841
|
|
|
662
1842
|
const putPromise = cache.put(request, response);
|
|
663
1843
|
|
|
664
1844
|
if (this.waitUntil) {
|
|
665
|
-
this.waitUntil(
|
|
666
|
-
|
|
667
|
-
|
|
1845
|
+
this.waitUntil(() =>
|
|
1846
|
+
reportingAsync(
|
|
1847
|
+
() => putPromise,
|
|
1848
|
+
"cache-write",
|
|
1849
|
+
"[CFCacheStore] L1 write",
|
|
1850
|
+
),
|
|
1851
|
+
);
|
|
668
1852
|
} else {
|
|
669
1853
|
await putPromise;
|
|
670
1854
|
}
|
|
671
1855
|
|
|
672
|
-
// L2: persist to KV (KV requires expirationTtl >= 60s)
|
|
1856
|
+
// L2: persist to KV (KV requires expirationTtl >= 60s). Wire shape matches
|
|
1857
|
+
// JSON.stringify(KVItemEnvelope); field names differ from L1 so we assemble
|
|
1858
|
+
// from the pre-escaped value/handles pieces rather than re-stringifying.
|
|
673
1859
|
if (this.kv && this.waitUntil && totalTtl >= 60) {
|
|
674
|
-
const kvKey = this.toKVKey(`fn:${key}`);
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1860
|
+
const kvKey = await this.toKVKey(`fn:${key}`);
|
|
1861
|
+
const expiresAt = staleAt + swrWindow * 1000;
|
|
1862
|
+
let envelopeJson = `{"v":${valueJson}`;
|
|
1863
|
+
if (handlesJson !== undefined) envelopeJson += `,"h":${handlesJson}`;
|
|
1864
|
+
envelopeJson += `,"s":${staleAt},"e":${expiresAt}`;
|
|
1865
|
+
if (tags !== undefined) envelopeJson += `,"t":${JSON.stringify(tags)}`;
|
|
1866
|
+
if (taggedAt !== undefined) envelopeJson += `,"ta":${taggedAt}`;
|
|
1867
|
+
envelopeJson += `}`;
|
|
1868
|
+
this.waitUntil(() =>
|
|
1869
|
+
reportingAsync(
|
|
1870
|
+
() =>
|
|
1871
|
+
this.kv!.put(kvKey, envelopeJson, {
|
|
1872
|
+
expirationTtl: totalTtl,
|
|
1873
|
+
}),
|
|
1874
|
+
"cache-write",
|
|
1875
|
+
"[CFCacheStore] kvSetItem",
|
|
1876
|
+
),
|
|
1877
|
+
);
|
|
690
1878
|
}
|
|
691
1879
|
} catch (error) {
|
|
692
|
-
|
|
1880
|
+
reportCacheError(error, "cache-write", "[CFCacheStore] setItem");
|
|
693
1881
|
}
|
|
694
1882
|
}
|
|
695
1883
|
|
|
696
1884
|
// ============================================================================
|
|
697
|
-
//
|
|
1885
|
+
// Shell Cache Methods (PPR shell resume) — Cache API L1 + optional KV L2
|
|
698
1886
|
// ============================================================================
|
|
1887
|
+
//
|
|
1888
|
+
// With KV it is the durable, cross-colo shell tier: writes populate both
|
|
1889
|
+
// tiers, a valid KV hit promotes the same coupled envelope into L1, and
|
|
1890
|
+
// shell L1 hits deliberately keep the KV generation-marker check even in
|
|
1891
|
+
// purge mode. A shell's taggedAt is its CAPTURE START, not its write time:
|
|
1892
|
+
// an invalidation can purge while an older capture is still running, then
|
|
1893
|
+
// that capture can land after the purge. The marker check rejects that
|
|
1894
|
+
// resurrection.
|
|
1895
|
+
//
|
|
1896
|
+
// Without KV the family is L1-only (edge-only ppr): every colo captures and
|
|
1897
|
+
// serves its own shell from the Cache API. Tag eviction then needs purge
|
|
1898
|
+
// mode (tagPurge) — shell L1 entries carry the same namespaced Cache-Tag
|
|
1899
|
+
// tokens as the data families, read-your-own-writes comes from the
|
|
1900
|
+
// per-request marker memo, and the capture resurrection race narrows to
|
|
1901
|
+
// cross-request timing bounded by ttl+swr (the data families' documented
|
|
1902
|
+
// purge-mode stance). KV-less WITHOUT tagPurge still caches: freshness is
|
|
1903
|
+
// ttl/swr only, and a tagged write warns once that invalidation cannot
|
|
1904
|
+
// reach it (see warnShellTagsNoEvictionOnce).
|
|
699
1905
|
|
|
700
1906
|
/**
|
|
701
|
-
*
|
|
702
|
-
*
|
|
1907
|
+
* Warn once per isolate that a TAGGED shell landed on a store with no
|
|
1908
|
+
* eviction path for it: no KV (markers) and no tagPurge (purge-by-tag).
|
|
1909
|
+
* The shell still caches and expires by ttl+swr, but updateTag()/
|
|
1910
|
+
* revalidateTag() cannot reach it — silent staleness a consumer who tagged
|
|
1911
|
+
* the route clearly did not intend. Fired from putShell (not the
|
|
1912
|
+
* constructor) so an untagged edge-only ppr config stays warning-free.
|
|
703
1913
|
* @internal
|
|
704
1914
|
*/
|
|
705
|
-
private
|
|
1915
|
+
private warnShellTagsNoEvictionOnce(): void {
|
|
1916
|
+
this.warnOncePerNamespace(
|
|
1917
|
+
warnedShellTagsNoEviction,
|
|
1918
|
+
`[CFCacheStore] a ppr shell with tags was stored on a KV-less store ` +
|
|
1919
|
+
`without tagPurge: tag invalidation cannot evict it (no KV markers, ` +
|
|
1920
|
+
`no purge-by-tag), so updateTag()/revalidateTag() will not reach ` +
|
|
1921
|
+
`this shell — it serves until ttl+swr expiry. Configure { kv } for ` +
|
|
1922
|
+
`marker invalidation or { tagPurge } for purge-by-tag eviction; ` +
|
|
1923
|
+
`untagged ppr routes (ttl/swr freshness) are unaffected.`,
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
/**
|
|
1928
|
+
* Generation gate for shell writes and the capture scheduler
|
|
1929
|
+
* (isTagsInvalidatedSince). With KV it is the durable marker cascade.
|
|
1930
|
+
* Without KV there are no markers: in purge mode the per-request memo is
|
|
1931
|
+
* the only signal — a capture racing THIS request's updateTag() is still
|
|
1932
|
+
* rejected (read-your-own-writes), while cross-request races are bounded
|
|
1933
|
+
* by ttl+swr exactly like the data families' purge-mode writes. Without
|
|
1934
|
+
* either, fail open (ttl/swr-only semantics, warned at putShell).
|
|
1935
|
+
* @internal
|
|
1936
|
+
*/
|
|
1937
|
+
private async isShellGenerationInvalidated(
|
|
1938
|
+
tags: string[] | undefined,
|
|
1939
|
+
since: number | undefined,
|
|
1940
|
+
): Promise<boolean> {
|
|
1941
|
+
if (this.kv) return this.isGloballyInvalidated(tags, since);
|
|
1942
|
+
if (!this.tagPurge || !Array.isArray(tags) || tags.length === 0 || !since)
|
|
1943
|
+
return false;
|
|
1944
|
+
const ctx = _getRequestContext();
|
|
1945
|
+
if (!ctx) return false;
|
|
1946
|
+
const memo = getTagMarkerMemo(ctx, this);
|
|
1947
|
+
for (const tag of tags) {
|
|
1948
|
+
const marker = memo.get(tag);
|
|
1949
|
+
if (marker != null && marker >= since) return true;
|
|
1950
|
+
}
|
|
1951
|
+
return false;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
/**
|
|
1955
|
+
* Get a cached PPR shell entry from Cache API, falling through to KV and
|
|
1956
|
+
* promoting a valid KV hit. Both tiers store one envelope so the prelude,
|
|
1957
|
+
* postponed state, snapshot, versions, and generation metadata cannot mix.
|
|
1958
|
+
* SWR remains a plain staleness flag; the capture scheduler's module-level
|
|
1959
|
+
* in-flight set is the recapture stampede guard.
|
|
1960
|
+
*/
|
|
1961
|
+
async getShell(
|
|
1962
|
+
key: string,
|
|
1963
|
+
): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
|
|
1964
|
+
try {
|
|
1965
|
+
const cache = await this.getCache();
|
|
1966
|
+
const request = this.keyToRequest(`shell:${key}`);
|
|
1967
|
+
const matchStartedAt = INTERNAL_RANGO_DEBUG ? Date.now() : 0;
|
|
1968
|
+
const {
|
|
1969
|
+
response,
|
|
1970
|
+
timedOut,
|
|
1971
|
+
error: matchError,
|
|
1972
|
+
} = await this.matchWithTimeout(cache, request);
|
|
1973
|
+
const matchMs = INTERNAL_RANGO_DEBUG
|
|
1974
|
+
? Date.now() - matchStartedAt
|
|
1975
|
+
: undefined;
|
|
1976
|
+
|
|
1977
|
+
if (!response) {
|
|
1978
|
+
if (matchError) {
|
|
1979
|
+
reportCacheError(
|
|
1980
|
+
matchError,
|
|
1981
|
+
"cache-read",
|
|
1982
|
+
"[CFCacheStore] getShell L1 match",
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
this.debugShell(key, "l1-miss", {
|
|
1986
|
+
reason: matchError ? "error" : timedOut ? "timeout" : "absent",
|
|
1987
|
+
matchMs,
|
|
1988
|
+
});
|
|
1989
|
+
return this.kvGetShell(key);
|
|
1990
|
+
}
|
|
1991
|
+
if (response.status !== 200) {
|
|
1992
|
+
this.debugShell(key, "l1-miss", {
|
|
1993
|
+
reason: "non-200",
|
|
1994
|
+
status: response.status,
|
|
1995
|
+
matchMs,
|
|
1996
|
+
});
|
|
1997
|
+
return this.kvGetShell(key);
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
const bodyStartedAt = INTERNAL_RANGO_DEBUG ? Date.now() : 0;
|
|
2001
|
+
const { value, errored, error } =
|
|
2002
|
+
await this.readJsonWithTimeout<unknown>(response);
|
|
2003
|
+
const bodyReadMs = INTERNAL_RANGO_DEBUG
|
|
2004
|
+
? Date.now() - bodyStartedAt
|
|
2005
|
+
: undefined;
|
|
2006
|
+
if (value === undefined) {
|
|
2007
|
+
this.debugShell(key, "l1-miss", {
|
|
2008
|
+
reason: errored ? "corrupt" : "timeout",
|
|
2009
|
+
matchMs,
|
|
2010
|
+
bodyReadMs,
|
|
2011
|
+
});
|
|
2012
|
+
if (errored) {
|
|
2013
|
+
return this.healCorruptL1(cache, request, error, "getShell", () =>
|
|
2014
|
+
this.kvGetShell(key),
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
2017
|
+
return this.kvGetShell(key);
|
|
2018
|
+
}
|
|
2019
|
+
if (!isShellEnvelope(value)) {
|
|
2020
|
+
this.debugShell(key, "l1-miss", {
|
|
2021
|
+
reason: "malformed",
|
|
2022
|
+
matchMs,
|
|
2023
|
+
bodyReadMs,
|
|
2024
|
+
});
|
|
2025
|
+
return this.healCorruptL1(
|
|
2026
|
+
cache,
|
|
2027
|
+
request,
|
|
2028
|
+
new Error("malformed/partial L1 shell envelope"),
|
|
2029
|
+
"getShell",
|
|
2030
|
+
() => this.kvGetShell(key),
|
|
2031
|
+
);
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
const now = Date.now();
|
|
2035
|
+
if (now > value.e) {
|
|
2036
|
+
this.debugShell(key, "l1-miss", {
|
|
2037
|
+
reason: "expired",
|
|
2038
|
+
matchMs,
|
|
2039
|
+
bodyReadMs,
|
|
2040
|
+
expiresAt: value.e,
|
|
2041
|
+
});
|
|
2042
|
+
return this.kvGetShell(key);
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
// Unlike other L1 families, shells with KV always check the durable
|
|
2046
|
+
// generation marker — see the capture-start/purge race documented
|
|
2047
|
+
// above. Without KV there are no markers: L1-only shells adopt the
|
|
2048
|
+
// data families' purge-mode read semantics (a hit that survived the
|
|
2049
|
+
// purge is trusted; the per-request memo masks this request's own
|
|
2050
|
+
// updateTag() writes; entries a purge cannot reach fall back to the
|
|
2051
|
+
// marker check, which fails open KV-less).
|
|
2052
|
+
const markerStartedAt = INTERNAL_RANGO_DEBUG ? Date.now() : 0;
|
|
2053
|
+
const invalidated = this.kv
|
|
2054
|
+
? await this.isGloballyInvalidated(value.t, value.ta)
|
|
2055
|
+
: await this.isL1Invalidated(value.t, value.ta, response.headers);
|
|
2056
|
+
const markerMs = INTERNAL_RANGO_DEBUG
|
|
2057
|
+
? Date.now() - markerStartedAt
|
|
2058
|
+
: undefined;
|
|
2059
|
+
if (invalidated) {
|
|
2060
|
+
this.debugShell(key, "marker-invalidated", {
|
|
2061
|
+
tier: "l1",
|
|
2062
|
+
matchMs,
|
|
2063
|
+
bodyReadMs,
|
|
2064
|
+
markerMs,
|
|
2065
|
+
});
|
|
2066
|
+
return null;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
const shouldRevalidate = value.s > 0 && now > value.s;
|
|
2070
|
+
this.debugShell(key, "l1-hit", {
|
|
2071
|
+
freshness: shouldRevalidate ? "stale" : "fresh",
|
|
2072
|
+
matchMs,
|
|
2073
|
+
bodyReadMs,
|
|
2074
|
+
markerMs,
|
|
2075
|
+
expiresAt: value.e,
|
|
2076
|
+
});
|
|
2077
|
+
return {
|
|
2078
|
+
entry: this.shellEnvelopeToEntry(value),
|
|
2079
|
+
shouldRevalidate,
|
|
2080
|
+
};
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
reportCacheError(error, "cache-read", "[CFCacheStore] getShell");
|
|
2083
|
+
this.debugShell(key, "l1-miss", { reason: "error" });
|
|
2084
|
+
return this.kvGetShell(key);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
/**
|
|
2089
|
+
* Store a PPR shell envelope in Cache API and, when KV is configured and
|
|
2090
|
+
* the retention meets its 60-second floor, KV. The shared write is
|
|
2091
|
+
* registered with waitUntil and awaited so invalidation rejection can be
|
|
2092
|
+
* acknowledged to the capture scheduler. Short-lived shells remain useful
|
|
2093
|
+
* in L1 even though KV rejects them; a KV-less store is L1-only by design
|
|
2094
|
+
* (edge-only ppr — see the section comment).
|
|
2095
|
+
*/
|
|
2096
|
+
async putShell(
|
|
2097
|
+
key: string,
|
|
2098
|
+
entry: ShellCacheEntry,
|
|
2099
|
+
ttlSeconds?: number,
|
|
2100
|
+
swrSeconds?: number,
|
|
2101
|
+
tags?: string[],
|
|
2102
|
+
): Promise<"stored" | "invalidated" | "uncacheable" | void> {
|
|
2103
|
+
if (!this.waitUntil) return;
|
|
2104
|
+
// Same write gate as the data families: in KV-less purge mode an
|
|
2105
|
+
// over-limit tag set has NO eviction path (no tokens, no markers), so the
|
|
2106
|
+
// shell is not cached rather than becoming un-invalidatable. Unlike the
|
|
2107
|
+
// void-returning data puts, this is ACKNOWLEDGED — the capture scheduler
|
|
2108
|
+
// must back the key off (every write would refuse identically), not
|
|
2109
|
+
// treat the capture as stored and re-render on every MISS.
|
|
2110
|
+
if (this.skipUncacheableTagSet(tags)) return "uncacheable";
|
|
2111
|
+
if (!this.kv && !this.tagPurge && Array.isArray(tags) && tags.length > 0) {
|
|
2112
|
+
this.warnShellTagsNoEvictionOnce();
|
|
2113
|
+
}
|
|
2114
|
+
try {
|
|
2115
|
+
const ttl = resolveTtl(ttlSeconds, this.defaults, DEFAULT_FUNCTION_TTL);
|
|
2116
|
+
const swrWindow = resolveSwrWindow(swrSeconds, this.defaults);
|
|
2117
|
+
const totalTtl = ttl + swrWindow;
|
|
2118
|
+
|
|
2119
|
+
// The tagInvalidationTtl cap exists so a tagged entry can never outlive
|
|
2120
|
+
// its KV markers (an expired marker would resurrect it). Without KV
|
|
2121
|
+
// there are no markers to outlive — capping would just hard-expire the
|
|
2122
|
+
// shell below its declared ttl+swr — so the cap is KV-conditional.
|
|
2123
|
+
const retentionTtl =
|
|
2124
|
+
tags && tags.length > 0 && this.kv && this.tagInvalidationTtl
|
|
2125
|
+
? Math.min(totalTtl, this.tagInvalidationTtl)
|
|
2126
|
+
: totalTtl;
|
|
2127
|
+
const now = Date.now();
|
|
2128
|
+
const staleAt = now + ttl * 1000;
|
|
2129
|
+
const expiresAt = now + retentionTtl * 1000;
|
|
2130
|
+
const taggedAt =
|
|
2131
|
+
Array.isArray(tags) && tags.length > 0 ? entry.createdAt : undefined;
|
|
2132
|
+
|
|
2133
|
+
const writeKv = !!this.kv && retentionTtl >= KV_MIN_EXPIRATION_TTL;
|
|
2134
|
+
const kvKey = writeKv ? await this.toKVKey(`shell:${key}`) : null;
|
|
2135
|
+
|
|
2136
|
+
const write = (async (): Promise<"stored" | "invalidated" | void> => {
|
|
2137
|
+
if (
|
|
2138
|
+
tags &&
|
|
2139
|
+
tags.length > 0 &&
|
|
2140
|
+
(await this.isShellGenerationInvalidated(tags, entry.createdAt))
|
|
2141
|
+
) {
|
|
2142
|
+
this.debugShell(key, "write-invalidated");
|
|
2143
|
+
return "invalidated";
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
const envelope: CFShellEnvelope = {
|
|
2147
|
+
// Presence-keyed: a navigationOnly entry has no document half, and an
|
|
2148
|
+
// omitted key (vs an explicit undefined) also keeps it out of the
|
|
2149
|
+
// serialized JSON.
|
|
2150
|
+
...(entry.prelude !== undefined ? { p: entry.prelude } : {}),
|
|
2151
|
+
...(entry.postponed !== undefined ? { po: entry.postponed } : {}),
|
|
2152
|
+
rv: entry.reactVersion,
|
|
2153
|
+
bv: entry.buildVersion,
|
|
2154
|
+
c: entry.createdAt,
|
|
2155
|
+
s: staleAt,
|
|
2156
|
+
e: expiresAt,
|
|
2157
|
+
t: tags,
|
|
2158
|
+
ta: taggedAt,
|
|
2159
|
+
i: entry.initialTheme,
|
|
2160
|
+
sn: entry.snapshot,
|
|
2161
|
+
dk: entry.docKey,
|
|
2162
|
+
lh: entry.handlerLiveHoles,
|
|
2163
|
+
tw: entry.transitionWhen,
|
|
2164
|
+
no: entry.navigationOnly,
|
|
2165
|
+
};
|
|
2166
|
+
const body = JSON.stringify(envelope);
|
|
2167
|
+
const writes: Promise<boolean>[] = [
|
|
2168
|
+
(async () => {
|
|
2169
|
+
try {
|
|
2170
|
+
const cache = await this.getCache();
|
|
2171
|
+
await cache.put(
|
|
2172
|
+
this.keyToRequest(`shell:${key}`),
|
|
2173
|
+
this.shellEnvelopeResponse(body, envelope),
|
|
2174
|
+
);
|
|
2175
|
+
this.debugShell(key, "l1-stored", {
|
|
2176
|
+
expiresAt: envelope.e,
|
|
2177
|
+
});
|
|
2178
|
+
return true;
|
|
2179
|
+
} catch (error) {
|
|
2180
|
+
reportCacheError(
|
|
2181
|
+
error,
|
|
2182
|
+
"cache-write",
|
|
2183
|
+
"[CFCacheStore] putShell L1",
|
|
2184
|
+
);
|
|
2185
|
+
return false;
|
|
2186
|
+
}
|
|
2187
|
+
})(),
|
|
2188
|
+
];
|
|
2189
|
+
if (writeKv && kvKey !== null) {
|
|
2190
|
+
writes.push(
|
|
2191
|
+
(async () => {
|
|
2192
|
+
try {
|
|
2193
|
+
await this.kv!.put(kvKey, body, {
|
|
2194
|
+
expirationTtl: retentionTtl,
|
|
2195
|
+
});
|
|
2196
|
+
this.debugShell(key, "kv-stored", {
|
|
2197
|
+
expiresAt: envelope.e,
|
|
2198
|
+
});
|
|
2199
|
+
return true;
|
|
2200
|
+
} catch (error) {
|
|
2201
|
+
reportCacheError(
|
|
2202
|
+
error,
|
|
2203
|
+
"cache-write",
|
|
2204
|
+
"[CFCacheStore] putShell L2",
|
|
2205
|
+
);
|
|
2206
|
+
return false;
|
|
2207
|
+
}
|
|
2208
|
+
})(),
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
return (await Promise.all(writes)).some(Boolean) ? "stored" : undefined;
|
|
2212
|
+
})();
|
|
2213
|
+
this.waitUntil(async () => {
|
|
2214
|
+
await write;
|
|
2215
|
+
});
|
|
2216
|
+
return await write;
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
reportCacheError(error, "cache-write", "[CFCacheStore] putShell");
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
/** Rebuild the public shell entry from its validated storage envelope. */
|
|
2223
|
+
private shellEnvelopeToEntry(envelope: CFShellEnvelope): ShellCacheEntry {
|
|
2224
|
+
return {
|
|
2225
|
+
prelude: envelope.p,
|
|
2226
|
+
postponed: envelope.po,
|
|
2227
|
+
reactVersion: envelope.rv,
|
|
2228
|
+
buildVersion: envelope.bv,
|
|
2229
|
+
initialTheme: envelope.i,
|
|
2230
|
+
snapshot: envelope.sn,
|
|
2231
|
+
docKey: envelope.dk,
|
|
2232
|
+
handlerLiveHoles: envelope.lh,
|
|
2233
|
+
transitionWhen: envelope.tw,
|
|
2234
|
+
navigationOnly: envelope.no,
|
|
2235
|
+
createdAt: envelope.c,
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
/** Build the Cache API representation of the coupled shell envelope. */
|
|
2240
|
+
private shellEnvelopeResponse(
|
|
2241
|
+
body: string,
|
|
2242
|
+
envelope: CFShellEnvelope,
|
|
2243
|
+
): Response {
|
|
2244
|
+
const remainingTtl = Math.max(
|
|
2245
|
+
1,
|
|
2246
|
+
Math.floor((envelope.e - Date.now()) / 1000),
|
|
2247
|
+
);
|
|
2248
|
+
return new Response(body, {
|
|
2249
|
+
headers: {
|
|
2250
|
+
"Content-Type": "application/json",
|
|
2251
|
+
"Cache-Control": `public, max-age=${remainingTtl}`,
|
|
2252
|
+
[CACHE_STALE_AT_HEADER]: String(envelope.s),
|
|
2253
|
+
[CACHE_EXPIRES_AT_HEADER]: String(envelope.e),
|
|
2254
|
+
[CACHE_STATUS_HEADER]: "HIT",
|
|
2255
|
+
...this.tagHeaderEntries(envelope.t, envelope.ta),
|
|
2256
|
+
},
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
/** KV shell fallback with corruption checks and background L1 promotion. */
|
|
2261
|
+
private async kvGetShell(
|
|
2262
|
+
key: string,
|
|
2263
|
+
): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
|
|
2264
|
+
if (!this.kv) return null;
|
|
2265
|
+
try {
|
|
2266
|
+
const readStartedAt = INTERNAL_RANGO_DEBUG ? Date.now() : 0;
|
|
2267
|
+
const kvKey = await this.toKVKey(`shell:${key}`);
|
|
2268
|
+
const { value: envelope, timedOut } =
|
|
2269
|
+
await this.kvGetOrEvict<CFShellEnvelope>(
|
|
2270
|
+
kvKey,
|
|
2271
|
+
isShellEnvelope,
|
|
2272
|
+
"getShell",
|
|
2273
|
+
);
|
|
2274
|
+
const readMs = INTERNAL_RANGO_DEBUG
|
|
2275
|
+
? Date.now() - readStartedAt
|
|
2276
|
+
: undefined;
|
|
2277
|
+
if (timedOut || !envelope) {
|
|
2278
|
+
this.debugShell(key, "kv-miss", {
|
|
2279
|
+
reason: timedOut ? "timeout" : "unavailable",
|
|
2280
|
+
readMs,
|
|
2281
|
+
});
|
|
2282
|
+
return null;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
const now = Date.now();
|
|
2286
|
+
if (now > envelope.e) {
|
|
2287
|
+
this.debugShell(key, "kv-miss", {
|
|
2288
|
+
reason: "expired",
|
|
2289
|
+
readMs,
|
|
2290
|
+
expiresAt: envelope.e,
|
|
2291
|
+
});
|
|
2292
|
+
return null;
|
|
2293
|
+
}
|
|
2294
|
+
const markerStartedAt = INTERNAL_RANGO_DEBUG ? Date.now() : 0;
|
|
2295
|
+
const invalidated = await this.isGloballyInvalidated(
|
|
2296
|
+
envelope.t,
|
|
2297
|
+
envelope.ta,
|
|
2298
|
+
);
|
|
2299
|
+
const markerMs = INTERNAL_RANGO_DEBUG
|
|
2300
|
+
? Date.now() - markerStartedAt
|
|
2301
|
+
: undefined;
|
|
2302
|
+
if (invalidated) {
|
|
2303
|
+
this.debugShell(key, "marker-invalidated", {
|
|
2304
|
+
tier: "kv",
|
|
2305
|
+
readMs,
|
|
2306
|
+
markerMs,
|
|
2307
|
+
});
|
|
2308
|
+
return null;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
const shouldRevalidate = envelope.s > 0 && now > envelope.s;
|
|
2312
|
+
this.debugShell(key, "kv-hit", {
|
|
2313
|
+
freshness: shouldRevalidate ? "stale" : "fresh",
|
|
2314
|
+
readMs,
|
|
2315
|
+
markerMs,
|
|
2316
|
+
expiresAt: envelope.e,
|
|
2317
|
+
});
|
|
2318
|
+
this.promoteShellToL1(key, envelope);
|
|
2319
|
+
return {
|
|
2320
|
+
entry: this.shellEnvelopeToEntry(envelope),
|
|
2321
|
+
shouldRevalidate,
|
|
2322
|
+
};
|
|
2323
|
+
} catch (error) {
|
|
2324
|
+
reportCacheError(error, "cache-read", "[CFCacheStore] kvGetShell");
|
|
2325
|
+
this.debugShell(key, "kv-miss", { reason: "error" });
|
|
2326
|
+
return null;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
/** Promote a valid KV shell into the per-colo Cache API tier. */
|
|
2331
|
+
private promoteShellToL1(key: string, envelope: CFShellEnvelope): void {
|
|
2332
|
+
if (!this.waitUntil) return;
|
|
2333
|
+
this.waitUntil(() =>
|
|
2334
|
+
reportingAsync(
|
|
2335
|
+
async () => {
|
|
2336
|
+
if (Date.now() > envelope.e) return;
|
|
2337
|
+
const cache = await this.getCache();
|
|
2338
|
+
const body = JSON.stringify(envelope);
|
|
2339
|
+
await cache.put(
|
|
2340
|
+
this.keyToRequest(`shell:${key}`),
|
|
2341
|
+
this.shellEnvelopeResponse(body, envelope),
|
|
2342
|
+
);
|
|
2343
|
+
this.debugShell(key, "kv-promoted", {
|
|
2344
|
+
remainingTtl: Math.max(
|
|
2345
|
+
1,
|
|
2346
|
+
Math.floor((envelope.e - Date.now()) / 1000),
|
|
2347
|
+
),
|
|
2348
|
+
expiresAt: envelope.e,
|
|
2349
|
+
});
|
|
2350
|
+
},
|
|
2351
|
+
"cache-write",
|
|
2352
|
+
"[CFCacheStore] promoteShellToL1",
|
|
2353
|
+
),
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
// ============================================================================
|
|
2358
|
+
// Key Helpers
|
|
2359
|
+
// ============================================================================
|
|
2360
|
+
|
|
2361
|
+
/**
|
|
2362
|
+
* Convert string key to Request object for CF Cache API.
|
|
2363
|
+
* Includes version in URL if specified (for cache invalidation on code changes).
|
|
2364
|
+
* @internal
|
|
2365
|
+
*/
|
|
2366
|
+
private keyToRequest(key: string): Request {
|
|
706
2367
|
const encodedKey = encodeURIComponent(key);
|
|
707
2368
|
// Include version in URL path to invalidate cache when version changes
|
|
708
2369
|
const versionPath = this.version ? `v/${this.version}/` : "";
|
|
709
|
-
return new Request(`${this.
|
|
2370
|
+
return new Request(`${this.resolveBaseUrl()}${versionPath}${encodedKey}`, {
|
|
710
2371
|
method: "GET",
|
|
711
2372
|
});
|
|
712
2373
|
}
|
|
@@ -714,11 +2375,868 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
714
2375
|
/**
|
|
715
2376
|
* Convert string key to KV key string.
|
|
716
2377
|
* Uses same version prefix as Cache API for consistent invalidation.
|
|
2378
|
+
*
|
|
2379
|
+
* Single chokepoint for EVERY KV family (segments, items, shells, documents
|
|
2380
|
+
* via toDocKVKey, tag markers via tagMarkerKey): a composed key over
|
|
2381
|
+
* Cloudflare KV's 512-byte limit is normalized to a preserved readable
|
|
2382
|
+
* prefix plus a SHA-256-derived 128-bit digest of the FULL key. Without
|
|
2383
|
+
* this, kv.put/get reject with `414 ... exceeds key length limit of 512`
|
|
2384
|
+
* and the entry silently never reaches L2 (observed in production for
|
|
2385
|
+
* "use cache" items whose serialized args — e.g. a CMS query object — blow
|
|
2386
|
+
* the cap; autobarn pilot). Keys are opaque storage identifiers, so
|
|
2387
|
+
* normalization is semantics-preserving as long as distinct logical keys
|
|
2388
|
+
* stay distinct: colliding requires an identical 400-byte prefix AND a
|
|
2389
|
+
* 128-bit SHA-256 collision. Deterministic, so every family's read, write,
|
|
2390
|
+
* and delete paths agree on the stored key.
|
|
717
2391
|
* @internal
|
|
718
2392
|
*/
|
|
719
|
-
private toKVKey(key: string): string {
|
|
2393
|
+
private async toKVKey(key: string): Promise<string> {
|
|
720
2394
|
const versionPath = this.version ? `v/${this.version}/` : "";
|
|
721
|
-
|
|
2395
|
+
const composed = `${versionPath}${key}`;
|
|
2396
|
+
if (kvKeyByteLength(composed) <= KV_MAX_KEY_BYTES) return composed;
|
|
2397
|
+
const prefix = truncateToBytes(composed, KV_KEY_PRESERVED_PREFIX_BYTES);
|
|
2398
|
+
return `${prefix}~${await kvKeyDigest(composed)}`;
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
/**
|
|
2402
|
+
* Host token for the current request, used to namespace the document KV key.
|
|
2403
|
+
* Derived from the same resolveBaseUrl() that namespaces the L1 (Cache API)
|
|
2404
|
+
* tier, so a doc entry's KV twin lands under the identical host bucket.
|
|
2405
|
+
* Falls back to "_" if the base URL cannot be parsed (it always carries a
|
|
2406
|
+
* trailing-slash origin, so parsing succeeds in practice).
|
|
2407
|
+
* @internal
|
|
2408
|
+
*/
|
|
2409
|
+
private docKVHost(): string {
|
|
2410
|
+
try {
|
|
2411
|
+
return new URL(this.resolveBaseUrl()).host || "_";
|
|
2412
|
+
} catch {
|
|
2413
|
+
return "_";
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
/**
|
|
2418
|
+
* Convert a document key to its host-namespaced KV key. The L1 tier already
|
|
2419
|
+
* namespaces document entries by host via keyToRequest/resolveBaseUrl, but the
|
|
2420
|
+
* KV fallback keyed only on `doc:{key}`, so two hosts serving the same path
|
|
2421
|
+
* could collide on the KV tier (one host serving another's cached document).
|
|
2422
|
+
* Prefixing the host closes that cross-host collision. Deterministic per
|
|
2423
|
+
* (host, key). Segment/fn/tag-marker KV keys keep toKVKey unchanged: tag
|
|
2424
|
+
* markers are intentionally global (invalidation must cross hosts), and the
|
|
2425
|
+
* document tier is the one with a request-host context here.
|
|
2426
|
+
* @internal
|
|
2427
|
+
*/
|
|
2428
|
+
private toDocKVKey(key: string): Promise<string> {
|
|
2429
|
+
return this.toKVKey(`h/${this.docKVHost()}/doc:${key}`);
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
/**
|
|
2433
|
+
* Best-effort delete of a single KV key, reporting (not swallowing) a delete
|
|
2434
|
+
* failure as cache-delete. Used by the corrupt-entry self-heal paths.
|
|
2435
|
+
* @internal
|
|
2436
|
+
*/
|
|
2437
|
+
private async evictKvKey(kvKey: string, label: string): Promise<void> {
|
|
2438
|
+
try {
|
|
2439
|
+
await this.kv!.delete(kvKey);
|
|
2440
|
+
} catch (error) {
|
|
2441
|
+
reportCacheError(
|
|
2442
|
+
error,
|
|
2443
|
+
"cache-delete",
|
|
2444
|
+
`[CFCacheStore] ${label}: evict failed`,
|
|
2445
|
+
);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
/**
|
|
2450
|
+
* Schedule a corrupt-entry KV eviction as a NON-BLOCKING background task
|
|
2451
|
+
* (waitUntil) instead of awaiting it on the request path. The corrupt read has
|
|
2452
|
+
* already resolved to a miss; awaiting an unbounded kv.delete here would re-add
|
|
2453
|
+
* exactly the multi-second stall the read budgets exist to prevent when the KV
|
|
2454
|
+
* namespace is degraded. evictKvKey never rejects (it reports its own failure),
|
|
2455
|
+
* so the fire-and-forget fallback is safe when no waitUntil is available.
|
|
2456
|
+
* @internal
|
|
2457
|
+
*/
|
|
2458
|
+
private scheduleKvEvict(kvKey: string, label: string): void {
|
|
2459
|
+
const evict = (): Promise<void> => this.evictKvKey(kvKey, label);
|
|
2460
|
+
if (this.waitUntil) this.waitUntil(evict);
|
|
2461
|
+
else void evict();
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
/**
|
|
2465
|
+
* KV-get a JSON envelope, EVICTING the key only when it is genuinely corrupt.
|
|
2466
|
+
*
|
|
2467
|
+
* Reads as { type: "text" }, NOT { type: "json" }, on purpose: the "json" form
|
|
2468
|
+
* fuses the network read and the JSON parse, so a transient KV outage (5xx/429/
|
|
2469
|
+
* network blip) is indistinguishable from a malformed body and would delete a
|
|
2470
|
+
* still-good cross-colo entry - a self-inflicted miss storm. Reading text lets a
|
|
2471
|
+
* transient read error propagate to the caller's outer catch (reported
|
|
2472
|
+
* cache-read, the entry left intact); only a JSON.parse failure on a body that
|
|
2473
|
+
* WAS successfully read - or an envelope that parses but fails `validate`
|
|
2474
|
+
* (fields missing from a truncated write) - is true corruption that evicts +
|
|
2475
|
+
* reports cache-corrupt. A MISSING key (kv.get -> null) is a normal miss.
|
|
2476
|
+
* @internal
|
|
2477
|
+
*/
|
|
2478
|
+
private async kvGetOrEvict<T>(
|
|
2479
|
+
kvKey: string,
|
|
2480
|
+
validate: (envelope: T) => boolean,
|
|
2481
|
+
label: string,
|
|
2482
|
+
): Promise<{ value: T | null; timedOut: boolean }> {
|
|
2483
|
+
// Bound the read with the KV latency budget (inherited from #558) so a
|
|
2484
|
+
// degraded namespace cannot pin the request. readWithTimeout reports
|
|
2485
|
+
// timedOut on budget expiry; a transient read REJECTION (5xx/429/network)
|
|
2486
|
+
// instead propagates out to the caller's outer catch (reported cache-read,
|
|
2487
|
+
// the entry left intact) -- deliberately NOT caught as corruption.
|
|
2488
|
+
const { value: raw, timedOut } = await this.readWithTimeout<unknown>(
|
|
2489
|
+
() => this.kv!.get(kvKey, { type: "text" }),
|
|
2490
|
+
this.kvReadTimeoutMs,
|
|
2491
|
+
"KV read",
|
|
2492
|
+
);
|
|
2493
|
+
if (timedOut) return { value: null, timedOut: true };
|
|
2494
|
+
if (raw == null) return { value: null, timedOut: false }; // missing = miss
|
|
2495
|
+
|
|
2496
|
+
// Real CF KV with { type: "text" } returns a string: parse + structurally
|
|
2497
|
+
// validate it; a parse/validate failure on a successfully-read body is the
|
|
2498
|
+
// only true corruption (evict + cache-corrupt). A KV binding that already
|
|
2499
|
+
// returns a parsed object (some shims/tests) is used as-is.
|
|
2500
|
+
let envelope: T;
|
|
2501
|
+
if (typeof raw === "string") {
|
|
2502
|
+
try {
|
|
2503
|
+
envelope = JSON.parse(raw) as T;
|
|
2504
|
+
} catch (error) {
|
|
2505
|
+
reportCacheError(
|
|
2506
|
+
error,
|
|
2507
|
+
"cache-corrupt",
|
|
2508
|
+
`[CFCacheStore] ${label}: corrupt JSON in KV, evicting`,
|
|
2509
|
+
);
|
|
2510
|
+
this.scheduleKvEvict(kvKey, label);
|
|
2511
|
+
return { value: null, timedOut: false };
|
|
2512
|
+
}
|
|
2513
|
+
} else {
|
|
2514
|
+
envelope = raw as T;
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// A body that parses to null or a primitive ('null', '42', 'true', '"x"')
|
|
2518
|
+
// is not a valid envelope. Guard it BEFORE validate(): the property-reading
|
|
2519
|
+
// validators throw on a null/primitive rather than returning false, which
|
|
2520
|
+
// would escape to the caller's outer catch as a transient cache-read and
|
|
2521
|
+
// leave the bad key un-evicted (re-failing every read until its KV TTL). The
|
|
2522
|
+
// typeof check short-circuits validate() so it only ever runs on an object.
|
|
2523
|
+
if (
|
|
2524
|
+
envelope == null ||
|
|
2525
|
+
typeof envelope !== "object" ||
|
|
2526
|
+
!validate(envelope)
|
|
2527
|
+
) {
|
|
2528
|
+
reportCacheError(
|
|
2529
|
+
new Error("malformed/partial KV envelope"),
|
|
2530
|
+
"cache-corrupt",
|
|
2531
|
+
`[CFCacheStore] ${label}: malformed envelope, evicting`,
|
|
2532
|
+
);
|
|
2533
|
+
this.scheduleKvEvict(kvKey, label);
|
|
2534
|
+
return { value: null, timedOut: false };
|
|
2535
|
+
}
|
|
2536
|
+
return { value: envelope, timedOut: false };
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// ============================================================================
|
|
2540
|
+
// Tag Invalidation (single-store: markers live in this.kv)
|
|
2541
|
+
// ============================================================================
|
|
2542
|
+
|
|
2543
|
+
/** KV key for a tag's invalidation marker. */
|
|
2544
|
+
private tagMarkerKey(tag: string): Promise<string> {
|
|
2545
|
+
return this.toKVKey(`${TAG_MARKER_PREFIX}${tag}`);
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
/**
|
|
2549
|
+
* Header entries carrying an entry's tags (JSON-encoded, comma-safe) and the
|
|
2550
|
+
* timestamp they were attached. Returns an empty object when there are no
|
|
2551
|
+
* tags so untagged entries stay header-free and skip the invalidation check.
|
|
2552
|
+
*
|
|
2553
|
+
* Also stamps the namespaced `Cache-Tag` header (see entryCacheTags) so a
|
|
2554
|
+
* Cloudflare purge-by-tag can evict the entry — the mechanism purge mode
|
|
2555
|
+
* (tagPurge) relies on. Written unconditionally (not only in purge mode):
|
|
2556
|
+
* it costs a small header and makes existing entries purgeable the moment a
|
|
2557
|
+
* consumer turns purge mode on, with no re-render needed.
|
|
2558
|
+
*/
|
|
2559
|
+
private tagHeaderEntries(
|
|
2560
|
+
tags: string[] | undefined,
|
|
2561
|
+
taggedAt: number | undefined,
|
|
2562
|
+
): Record<string, string> {
|
|
2563
|
+
if (!Array.isArray(tags) || tags.length === 0 || !taggedAt) return {};
|
|
2564
|
+
const entries: Record<string, string> = {
|
|
2565
|
+
// encodeURIComponent so the value is pure ASCII: HTTP header values are
|
|
2566
|
+
// ByteStrings, but JSON.stringify leaves codepoints > U+00FF (emoji/CJK)
|
|
2567
|
+
// verbatim, which makes new Response({ headers }) throw and the outer
|
|
2568
|
+
// try/catch silently drop the whole entry from cache. Decoded in
|
|
2569
|
+
// readTagInfo. The L1 marker Cache-Tag path encodes for the same reason.
|
|
2570
|
+
[CACHE_TAGS_HEADER]: encodeURIComponent(JSON.stringify(tags)),
|
|
2571
|
+
[CACHE_TAGGED_AT_HEADER]: String(taggedAt),
|
|
2572
|
+
};
|
|
2573
|
+
// Over Cloudflare's aggregate Cache-Tag limit the header is OMITTED — the
|
|
2574
|
+
// entry still caches and stays marker-invalidatable; it just cannot be
|
|
2575
|
+
// purge-evicted per tag (isL1Invalidated falls back to the marker check
|
|
2576
|
+
// for such entries, so purge mode stays correct). Emitting a header over
|
|
2577
|
+
// the limit would instead risk failing the whole L1 write. The KV-LESS
|
|
2578
|
+
// purge-mode combination never reaches here: there the marker fallback
|
|
2579
|
+
// has no KV to consult, so skipUncacheableTagSet rejects the write first.
|
|
2580
|
+
const cacheTag = this.entryCacheTagHeader(tags);
|
|
2581
|
+
if (cacheTag !== null) {
|
|
2582
|
+
entries["Cache-Tag"] = cacheTag;
|
|
2583
|
+
} else {
|
|
2584
|
+
this.warnOncePerNamespace(
|
|
2585
|
+
warnedCacheTagHeaderOverflow,
|
|
2586
|
+
`[CFCacheStore] an entry's ${tags.length} tags produce a Cache-Tag ` +
|
|
2587
|
+
`header over Cloudflare's ${CACHE_TAG_HEADER_MAX_BYTES}-byte ` +
|
|
2588
|
+
`limit; the header was omitted. The entry stays cacheable and ` +
|
|
2589
|
+
`marker-invalidatable, but a purge-by-tag cannot evict it (purge ` +
|
|
2590
|
+
`mode falls back to the marker check for it). Reduce the number ` +
|
|
2591
|
+
`of tags per entry.`,
|
|
2592
|
+
);
|
|
2593
|
+
}
|
|
2594
|
+
return entries;
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
/**
|
|
2598
|
+
* Joined entry Cache-Tag header value for `tags`, or null when it would
|
|
2599
|
+
* exceed Cloudflare's aggregate header limit. Tokens are pure ASCII
|
|
2600
|
+
* (encodeURIComponent output), so .length is bytes.
|
|
2601
|
+
* @internal
|
|
2602
|
+
*/
|
|
2603
|
+
private entryCacheTagHeader(tags: string[]): string | null {
|
|
2604
|
+
const joined = this.entryCacheTags(tags).join(",");
|
|
2605
|
+
return joined.length <= CACHE_TAG_HEADER_MAX_BYTES ? joined : null;
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
/**
|
|
2609
|
+
* Write-path gate for the one configuration where an over-limit tag set has
|
|
2610
|
+
* NO invalidation path: purge mode WITHOUT KV. There the entry Cache-Tag
|
|
2611
|
+
* tokens are the only eviction mechanism, and a tag set whose header
|
|
2612
|
+
* overflows CACHE_TAG_HEADER_MAX_BYTES gets no tokens — a purge could never
|
|
2613
|
+
* evict the entry and there is no KV marker fallback, so it would serve
|
|
2614
|
+
* stale until TTL while updateTag() reports success. Returns true (and
|
|
2615
|
+
* warns once) so the caller SKIPS caching: the route simply renders fresh,
|
|
2616
|
+
* which is the fail-safe direction. With KV configured the omitted-header
|
|
2617
|
+
* entry falls back to the marker check (see tagHeaderEntries), and KV-less
|
|
2618
|
+
* MARKER mode keeps its documented no-read-side-invalidation semantics —
|
|
2619
|
+
* neither is gated.
|
|
2620
|
+
* @internal
|
|
2621
|
+
*/
|
|
2622
|
+
private skipUncacheableTagSet(tags: string[] | undefined): boolean {
|
|
2623
|
+
if (!this.tagPurge || this.kv) return false;
|
|
2624
|
+
if (!Array.isArray(tags) || tags.length === 0) return false;
|
|
2625
|
+
if (this.entryCacheTagHeader(tags) !== null) return false;
|
|
2626
|
+
this.warnOncePerNamespace(
|
|
2627
|
+
warnedCacheTagOverflowUncacheable,
|
|
2628
|
+
`[CFCacheStore] an entry's ${tags.length} tags produce a Cache-Tag ` +
|
|
2629
|
+
`header over Cloudflare's ${CACHE_TAG_HEADER_MAX_BYTES}-byte limit. ` +
|
|
2630
|
+
`In purge mode without KV those tokens are the only invalidation ` +
|
|
2631
|
+
`path, so the entry was NOT cached (it renders fresh instead of ` +
|
|
2632
|
+
`becoming un-invalidatable). Reduce the number of tags per entry, ` +
|
|
2633
|
+
`or configure { kv } to get the marker fallback.`,
|
|
2634
|
+
);
|
|
2635
|
+
return true;
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
/**
|
|
2639
|
+
* Merge the internal tag headers onto an existing Headers instance. The
|
|
2640
|
+
* from-scratch paths spread tagHeaderEntries() into an object-literal init;
|
|
2641
|
+
* the document put/promote paths build a Headers first, so they .set() each
|
|
2642
|
+
* entry instead — except `Cache-Tag`, which is APPENDED: a document author
|
|
2643
|
+
* may have set their own Cache-Tag, and clobbering it would break their CDN
|
|
2644
|
+
* purging. Append produces the comma-merged list CF expects.
|
|
2645
|
+
*/
|
|
2646
|
+
private setTagHeaders(
|
|
2647
|
+
headers: Headers,
|
|
2648
|
+
tags: string[] | undefined,
|
|
2649
|
+
taggedAt: number | undefined,
|
|
2650
|
+
): void {
|
|
2651
|
+
for (const [name, value] of Object.entries(
|
|
2652
|
+
this.tagHeaderEntries(tags, taggedAt),
|
|
2653
|
+
)) {
|
|
2654
|
+
if (name === "Cache-Tag") headers.append(name, value);
|
|
2655
|
+
else headers.set(name, value);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
/** Read an entry's tags/taggedAt back from its headers. */
|
|
2660
|
+
private readTagInfo(headers: Headers): {
|
|
2661
|
+
tags?: string[];
|
|
2662
|
+
taggedAt?: number;
|
|
2663
|
+
} {
|
|
2664
|
+
const rawTags = headers.get(CACHE_TAGS_HEADER);
|
|
2665
|
+
const rawTaggedAt = headers.get(CACHE_TAGGED_AT_HEADER);
|
|
2666
|
+
if (!rawTags || !rawTaggedAt) return {};
|
|
2667
|
+
try {
|
|
2668
|
+
const taggedAt = Number(rawTaggedAt);
|
|
2669
|
+
// A corrupt/non-numeric tagged-at header yields NaN. isGloballyInvalidated
|
|
2670
|
+
// short-circuits on a falsy taggedAt (NaN is falsy), so returning
|
|
2671
|
+
// { taggedAt: NaN } would make the entry permanently NON-invalidatable -
|
|
2672
|
+
// a revalidateTag could never evict it. Treat a non-finite stamp the same
|
|
2673
|
+
// as the missing-header case (untagged): drop both tags and taggedAt so the
|
|
2674
|
+
// entry is re-rendered/re-tagged rather than silently un-invalidatable.
|
|
2675
|
+
if (!Number.isFinite(taggedAt)) return {};
|
|
2676
|
+
return {
|
|
2677
|
+
tags: JSON.parse(decodeURIComponent(rawTags)) as string[],
|
|
2678
|
+
taggedAt,
|
|
2679
|
+
};
|
|
2680
|
+
} catch {
|
|
2681
|
+
return {};
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
/**
|
|
2686
|
+
* Whether an entry tagged at `taggedAt` with `tags` has been invalidated since.
|
|
2687
|
+
* Reads the per-tag invalidation markers from KV and returns true if any tag's
|
|
2688
|
+
* latest invalidation is at or after taggedAt (>= so a same-millisecond
|
|
2689
|
+
* invalidate wins, favouring freshness over staleness). Fails open: KV errors
|
|
2690
|
+
* never turn a hit into a wrongful miss-storm beyond this single read.
|
|
2691
|
+
*/
|
|
2692
|
+
private async isGloballyInvalidated(
|
|
2693
|
+
tags: string[] | undefined,
|
|
2694
|
+
taggedAt: number | undefined,
|
|
2695
|
+
): Promise<boolean> {
|
|
2696
|
+
// Array.isArray (not just truthiness): a non-array tags value - direct store
|
|
2697
|
+
// misuse like setItem(k, v, { tags: "products" }), or a skewed KV envelope -
|
|
2698
|
+
// must fail safe to "not invalidated" rather than throwing `.map` on every
|
|
2699
|
+
// read (which the outer catch would mis-report as a transient cache-read).
|
|
2700
|
+
if (!this.kv || !Array.isArray(tags) || tags.length === 0 || !taggedAt)
|
|
2701
|
+
return false;
|
|
2702
|
+
const ctx = _getRequestContext();
|
|
2703
|
+
const memo = ctx ? getTagMarkerMemo(ctx, this) : undefined;
|
|
2704
|
+
const inflight = ctx ? getTagMarkerInflight(ctx, this) : undefined;
|
|
2705
|
+
try {
|
|
2706
|
+
const markers = await Promise.all(
|
|
2707
|
+
tags.map((tag) => this.readTagMarker(tag, memo, inflight)),
|
|
2708
|
+
);
|
|
2709
|
+
for (const marker of markers) {
|
|
2710
|
+
if (marker != null && marker >= taggedAt) return true;
|
|
2711
|
+
}
|
|
2712
|
+
return false;
|
|
2713
|
+
} catch (error) {
|
|
2714
|
+
reportCacheError(
|
|
2715
|
+
error,
|
|
2716
|
+
"cache-read",
|
|
2717
|
+
"[CFCacheStore] tag invalidation check",
|
|
2718
|
+
);
|
|
2719
|
+
return false;
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
/**
|
|
2724
|
+
* Tag-invalidation check for an L1 (Cache API) hit. In purge mode (tagPurge
|
|
2725
|
+
* configured), invalidateTags() evicts L1 entries via a Cloudflare
|
|
2726
|
+
* purge-by-tag call, so a hit that SURVIVED is trusted without a per-read
|
|
2727
|
+
* marker lookup — that skipped lookup is the entire point of purge mode.
|
|
2728
|
+
* Only the per-request memo is consulted (synchronous, no KV read) so a
|
|
2729
|
+
* request that ran updateTag() still masks its own entries during the purge
|
|
2730
|
+
* propagation window (read-your-own-writes).
|
|
2731
|
+
*
|
|
2732
|
+
* The trust is conditional on the entry actually CARRYING this store's
|
|
2733
|
+
* entry Cache-Tag tokens (`headers`): an entry a purge cannot reach — one
|
|
2734
|
+
* written before the tokens existed, or whose tag set overflowed the
|
|
2735
|
+
* Cache-Tag header limit — keeps the full marker check, or purge mode
|
|
2736
|
+
* would serve it stale until TTL with no eviction path.
|
|
2737
|
+
*
|
|
2738
|
+
* Without tagPurge this is the full marker cascade. KV-tier reads and
|
|
2739
|
+
* shells always use isGloballyInvalidated directly: purge cannot reach KV,
|
|
2740
|
+
* so the markers stay their invalidation mechanism.
|
|
2741
|
+
* @internal
|
|
2742
|
+
*/
|
|
2743
|
+
private async isL1Invalidated(
|
|
2744
|
+
tags: string[] | undefined,
|
|
2745
|
+
taggedAt: number | undefined,
|
|
2746
|
+
headers: Headers,
|
|
2747
|
+
): Promise<boolean> {
|
|
2748
|
+
if (!this.tagPurge) return this.isGloballyInvalidated(tags, taggedAt);
|
|
2749
|
+
if (!Array.isArray(tags) || tags.length === 0 || !taggedAt) return false;
|
|
2750
|
+
if (!this.hasEntryCacheTags(headers)) {
|
|
2751
|
+
return this.isGloballyInvalidated(tags, taggedAt);
|
|
2752
|
+
}
|
|
2753
|
+
const ctx = _getRequestContext();
|
|
2754
|
+
if (!ctx) return false;
|
|
2755
|
+
const memo = getTagMarkerMemo(ctx, this);
|
|
2756
|
+
for (const tag of tags) {
|
|
2757
|
+
const marker = memo.get(tag);
|
|
2758
|
+
if (marker != null && marker >= taggedAt) return true;
|
|
2759
|
+
}
|
|
2760
|
+
return false;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
/** Synthetic Cache API request for a tag's L1-cached invalidation marker. */
|
|
2764
|
+
private tagMarkerRequest(tag: string): Request {
|
|
2765
|
+
return this.keyToRequest(`${TAG_MARKER_CACHE_PREFIX}${tag}`);
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
/**
|
|
2769
|
+
* Read a tag's latest invalidation timestamp (or null if never invalidated)
|
|
2770
|
+
* through the cascade: per-request memo -> per-colo L1 cache (only when
|
|
2771
|
+
* tagCacheTtl > 0) -> KV (the global truth). The memo is always consulted
|
|
2772
|
+
* first so it stays authoritative within a request (read-your-own-writes),
|
|
2773
|
+
* and every KV/L1 result is written back into the memo. A Cache API miss
|
|
2774
|
+
* always falls through to KV; absence is represented by a cached sentinel,
|
|
2775
|
+
* never by a miss.
|
|
2776
|
+
*
|
|
2777
|
+
* Concurrent reads of the same tag within a request share one in-flight read
|
|
2778
|
+
* (the resolved-value memo only collapses sequential reads; parallel segment
|
|
2779
|
+
* loading would otherwise issue one KV read per concurrent reader).
|
|
2780
|
+
* @internal
|
|
2781
|
+
*/
|
|
2782
|
+
private async readTagMarker(
|
|
2783
|
+
tag: string,
|
|
2784
|
+
memo: Map<string, number | null> | undefined,
|
|
2785
|
+
inflight: Map<string, Promise<number | null>> | undefined,
|
|
2786
|
+
): Promise<number | null> {
|
|
2787
|
+
if (memo && memo.has(tag)) return memo.get(tag) ?? null;
|
|
2788
|
+
|
|
2789
|
+
// Collapse concurrent (not-yet-resolved) reads of this tag onto one promise.
|
|
2790
|
+
if (inflight) {
|
|
2791
|
+
const pending = inflight.get(tag);
|
|
2792
|
+
if (pending) return pending;
|
|
2793
|
+
const read = this.fetchTagMarker(tag, memo);
|
|
2794
|
+
inflight.set(tag, read);
|
|
2795
|
+
try {
|
|
2796
|
+
return await read;
|
|
2797
|
+
} finally {
|
|
2798
|
+
// Resolved values now live in the memo; drop the in-flight entry.
|
|
2799
|
+
inflight.delete(tag);
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
return this.fetchTagMarker(tag, memo);
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
/**
|
|
2807
|
+
* Uncached body of readTagMarker: L1 (per-colo Cache API, opt-in via
|
|
2808
|
+
* tagCacheTtl) -> KV. Writes the resolved value back into the memo.
|
|
2809
|
+
* @internal
|
|
2810
|
+
*/
|
|
2811
|
+
private async fetchTagMarker(
|
|
2812
|
+
tag: string,
|
|
2813
|
+
memo: Map<string, number | null> | undefined,
|
|
2814
|
+
): Promise<number | null> {
|
|
2815
|
+
// Write the resolved marker into the memo WITHOUT clobbering a value a
|
|
2816
|
+
// concurrent invalidateTags() wrote during our await. The router resolves
|
|
2817
|
+
// sibling slots in parallel, so a slot's updateTag() can land the
|
|
2818
|
+
// authoritative invalidatedAt into the memo while this read is still in
|
|
2819
|
+
// flight; overwriting it with our (pre-invalidation) read result would break
|
|
2820
|
+
// read-your-own-writes for the rest of the request. If the tag was memoized
|
|
2821
|
+
// mid-read, that value wins and is returned. Without a memo, the read result
|
|
2822
|
+
// stands as-is.
|
|
2823
|
+
const memoize = (read: number | null): number | null => {
|
|
2824
|
+
if (memo && memo.has(tag)) return memo.get(tag) ?? null;
|
|
2825
|
+
memo?.set(tag, read);
|
|
2826
|
+
return read;
|
|
2827
|
+
};
|
|
2828
|
+
|
|
2829
|
+
// L1 (per-colo) marker cache - opt-in via tagCacheTtl. Bounded by the same
|
|
2830
|
+
// edge budgets as data reads (inherited from #558) so a degraded colo cannot
|
|
2831
|
+
// stall a tagged read; a miss, timeout, or error all fall through to KV.
|
|
2832
|
+
if (this.tagCacheTtl > 0) {
|
|
2833
|
+
try {
|
|
2834
|
+
const cache = await this.getCache();
|
|
2835
|
+
const { response: hit, error: matchError } =
|
|
2836
|
+
await this.matchWithTimeout(cache, this.tagMarkerRequest(tag));
|
|
2837
|
+
// A transient match REJECTION is captured (not thrown) by
|
|
2838
|
+
// matchWithTimeout; surface it as cache-read like the data read paths
|
|
2839
|
+
// before falling through to KV, rather than silently dropping it.
|
|
2840
|
+
if (matchError)
|
|
2841
|
+
reportCacheError(
|
|
2842
|
+
matchError,
|
|
2843
|
+
"cache-read",
|
|
2844
|
+
"[CFCacheStore] tag marker L1 match",
|
|
2845
|
+
);
|
|
2846
|
+
if (hit) {
|
|
2847
|
+
const { value: body } = await this.readWithTimeout(
|
|
2848
|
+
() => hit.text(),
|
|
2849
|
+
this.edgeReadTimeoutMs,
|
|
2850
|
+
"tag marker L1 body read",
|
|
2851
|
+
);
|
|
2852
|
+
if (body !== undefined) {
|
|
2853
|
+
const value = body === TAG_MARKER_ABSENT ? null : Number(body);
|
|
2854
|
+
return memoize(value);
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
} catch {
|
|
2858
|
+
// Fall through to KV on any L1 read error.
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
// KV (global truth), bounded by the KV budget. On TIMEOUT fail OPEN: treat
|
|
2863
|
+
// the marker as absent (-> entry not invalidated -> served) so a degraded
|
|
2864
|
+
// namespace cannot pin every tagged read behind a slow global lookup. A
|
|
2865
|
+
// transient REJECTION instead propagates to isGloballyInvalidated's catch
|
|
2866
|
+
// (reported cache-read), which also fails open. Either way one slow tag
|
|
2867
|
+
// never amplifies into a per-segment stall.
|
|
2868
|
+
const { value: raw, timedOut } = await this.readWithTimeout<string | null>(
|
|
2869
|
+
async () => this.kv!.get(await this.tagMarkerKey(tag), { type: "text" }),
|
|
2870
|
+
this.kvReadTimeoutMs,
|
|
2871
|
+
"tag marker KV read",
|
|
2872
|
+
);
|
|
2873
|
+
if (timedOut) {
|
|
2874
|
+
// Memoize the fail-open result so the rest of this request is consistent
|
|
2875
|
+
// (and does not re-pay the timeout per segment sharing the tag).
|
|
2876
|
+
return memoize(null);
|
|
2877
|
+
}
|
|
2878
|
+
const value = raw != null ? Number(raw) : null;
|
|
2879
|
+
const resolved = memoize(value);
|
|
2880
|
+
|
|
2881
|
+
// Populate L1 for subsequent reads in this colo (non-blocking). Use the
|
|
2882
|
+
// resolved (memo-aware) value so a marker invalidated mid-read is not
|
|
2883
|
+
// re-cached stale into this colo's L1.
|
|
2884
|
+
if (this.tagCacheTtl > 0) {
|
|
2885
|
+
const put = () => this.putTagMarkerL1(tag, resolved);
|
|
2886
|
+
if (this.waitUntil) this.waitUntil(put);
|
|
2887
|
+
else void put();
|
|
2888
|
+
}
|
|
2889
|
+
return resolved;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
/**
|
|
2893
|
+
* Namespace token used inside every `rg:*` Cache-Tag. encodeURIComponent'd
|
|
2894
|
+
* like the tag values: a raw namespace containing a comma would split into
|
|
2895
|
+
* bogus tokens inside the comma-delimited Cache-Tag header — breaking both
|
|
2896
|
+
* stripInternalCacheTags (internal tokens would leak to clients) and the
|
|
2897
|
+
* purge match (the purged tag string never equals any stored token).
|
|
2898
|
+
* @internal
|
|
2899
|
+
*/
|
|
2900
|
+
private nsToken(): string {
|
|
2901
|
+
return encodeURIComponent(this.namespace ?? "default");
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
/**
|
|
2905
|
+
* Cloudflare Cache-Tags written on a tag's L1 marker entry, namespaced per
|
|
2906
|
+
* store so purges never collide with other Cache-Tags in the zone. Three
|
|
2907
|
+
* tiers, broad to specific:
|
|
2908
|
+
* rg:{ns} - everything this store cached (deploy/nuclear reset)
|
|
2909
|
+
* rg:{ns}:lk - all tag-lookup markers
|
|
2910
|
+
* rg:{ns}:lk:{tag} - this tag's lookup (the normal updateTag purge target)
|
|
2911
|
+
* The namespace and tag value are encodeURIComponent'd so commas/spaces
|
|
2912
|
+
* can't corrupt the comma-delimited Cache-Tag header.
|
|
2913
|
+
* @internal
|
|
2914
|
+
*/
|
|
2915
|
+
private lookupCacheTags(tag: string): string[] {
|
|
2916
|
+
const ns = this.nsToken();
|
|
2917
|
+
return [`rg:${ns}`, `rg:${ns}:lk`, this.lookupPurgeTag(tag)];
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
/**
|
|
2921
|
+
* Build one `{prefix}{tag}` Cache-Tag token, bounded to CACHE_TAG_TOKEN_MAX:
|
|
2922
|
+
* an over-long encoded tag collapses to `{prefix}h:{fnv1a64(tag)}` so a
|
|
2923
|
+
* legally-long application tag can never blow Cloudflare's per-tag purge
|
|
2924
|
+
* limit (1,024 chars) or bloat the header. Deterministic, so the write-time
|
|
2925
|
+
* token and the invalidate-time purge token always agree.
|
|
2926
|
+
* @internal
|
|
2927
|
+
*/
|
|
2928
|
+
private boundedTagToken(prefix: string, tag: string): string {
|
|
2929
|
+
const token = `${prefix}${encodeURIComponent(tag)}`;
|
|
2930
|
+
if (token.length <= CACHE_TAG_TOKEN_MAX) return token;
|
|
2931
|
+
return `${prefix}h:${fnv1a64(tag)}`;
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
/** The specific Cache-Tag a consumer purges to evict tag `tag`'s lookup. */
|
|
2935
|
+
private lookupPurgeTag(tag: string): string {
|
|
2936
|
+
return this.boundedTagToken(`rg:${this.nsToken()}:lk:`, tag);
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
/**
|
|
2940
|
+
* Cloudflare Cache-Tags written on a tagged DATA entry (segment/item/doc),
|
|
2941
|
+
* mirroring the lookup-marker tiers but under `:e` (entry):
|
|
2942
|
+
* rg:{ns} - everything this store cached (deploy/nuclear reset)
|
|
2943
|
+
* rg:{ns}:e - all data entries
|
|
2944
|
+
* rg:{ns}:e:{t} - entries carrying tag `t` (the purge-mode invalidation
|
|
2945
|
+
* target; see CFCacheStoreOptions.tagPurge)
|
|
2946
|
+
* Namespace and tag value encodeURIComponent'd like the lookup tier.
|
|
2947
|
+
* @internal
|
|
2948
|
+
*/
|
|
2949
|
+
private entryCacheTags(tags: string[]): string[] {
|
|
2950
|
+
const ns = this.nsToken();
|
|
2951
|
+
return [
|
|
2952
|
+
`rg:${ns}`,
|
|
2953
|
+
`rg:${ns}:e`,
|
|
2954
|
+
...tags.map((tag) => this.entryPurgeTag(tag)),
|
|
2955
|
+
];
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
/** The specific Cache-Tag purged to evict entries carrying tag `tag`. */
|
|
2959
|
+
private entryPurgeTag(tag: string): string {
|
|
2960
|
+
return this.boundedTagToken(`rg:${this.nsToken()}:e:`, tag);
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
/**
|
|
2964
|
+
* Whether an L1 entry's stored headers carry this store's entry Cache-Tag
|
|
2965
|
+
* tokens — i.e. whether a purge-by-tag can actually evict it. False for an
|
|
2966
|
+
* entry written before this feature existed, or one whose tag set exceeded
|
|
2967
|
+
* CACHE_TAG_HEADER_MAX_BYTES (header omitted). Purge mode only trusts
|
|
2968
|
+
* entries a purge can reach; the rest keep the marker check.
|
|
2969
|
+
* @internal
|
|
2970
|
+
*/
|
|
2971
|
+
private hasEntryCacheTags(headers: Headers): boolean {
|
|
2972
|
+
const raw = headers.get("Cache-Tag");
|
|
2973
|
+
if (raw === null) return false;
|
|
2974
|
+
const tier = `rg:${this.nsToken()}:e`;
|
|
2975
|
+
return raw.split(",").some((token) => {
|
|
2976
|
+
const trimmed = token.trim();
|
|
2977
|
+
return trimmed === tier || trimmed.startsWith(`${tier}:`);
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
/**
|
|
2982
|
+
* Drop this store's namespaced tokens (`rg:{ns}` and `rg:{ns}:*`) from a
|
|
2983
|
+
* Cache-Tag header, keeping author-set tokens intact. Deletes the header
|
|
2984
|
+
* when nothing remains. Serve-path counterpart of setTagHeaders' append.
|
|
2985
|
+
* An author token that exactly equals a reserved `rg:{ns}` tier is stripped
|
|
2986
|
+
* too — `rg:` is this store's documented-reserved Cache-Tag prefix.
|
|
2987
|
+
* @internal
|
|
2988
|
+
*/
|
|
2989
|
+
private stripInternalCacheTags(headers: Headers): void {
|
|
2990
|
+
const raw = headers.get("Cache-Tag");
|
|
2991
|
+
if (raw === null) return;
|
|
2992
|
+
const ns = this.nsToken();
|
|
2993
|
+
const kept = raw
|
|
2994
|
+
.split(",")
|
|
2995
|
+
.map((token) => token.trim())
|
|
2996
|
+
.filter(
|
|
2997
|
+
(token) =>
|
|
2998
|
+
token.length > 0 &&
|
|
2999
|
+
token !== `rg:${ns}` &&
|
|
3000
|
+
!token.startsWith(`rg:${ns}:`),
|
|
3001
|
+
);
|
|
3002
|
+
if (kept.length > 0) headers.set("Cache-Tag", kept.join(","));
|
|
3003
|
+
else headers.delete("Cache-Tag");
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
/**
|
|
3007
|
+
* Write a tag marker value into the per-colo L1 Cache API with tagCacheTtl.
|
|
3008
|
+
* `null` is stored as the TAG_MARKER_ABSENT sentinel so "no marker yet" is
|
|
3009
|
+
* cacheable (most tags are never invalidated - that is where the read savings
|
|
3010
|
+
* come from). The entry also carries a namespaced Cache-Tag so an external
|
|
3011
|
+
* purge-by-tag (via onRevalidateTag) can evict it across colos promptly,
|
|
3012
|
+
* rather than waiting out tagCacheTtl. Best-effort.
|
|
3013
|
+
* @internal
|
|
3014
|
+
*/
|
|
3015
|
+
private async putTagMarkerL1(
|
|
3016
|
+
tag: string,
|
|
3017
|
+
value: number | null,
|
|
3018
|
+
opts?: { critical?: boolean },
|
|
3019
|
+
): Promise<void> {
|
|
3020
|
+
if (this.tagCacheTtl <= 0) return;
|
|
3021
|
+
try {
|
|
3022
|
+
const cache = await this.getCache();
|
|
3023
|
+
const body = value != null ? String(value) : TAG_MARKER_ABSENT;
|
|
3024
|
+
await cache.put(
|
|
3025
|
+
this.tagMarkerRequest(tag),
|
|
3026
|
+
new Response(body, {
|
|
3027
|
+
headers: {
|
|
3028
|
+
"Cache-Control": `public, max-age=${this.tagCacheTtl}`,
|
|
3029
|
+
"Cache-Tag": this.lookupCacheTags(tag).join(","),
|
|
3030
|
+
},
|
|
3031
|
+
}),
|
|
3032
|
+
);
|
|
3033
|
+
} catch (error) {
|
|
3034
|
+
// The read-path populate is best-effort: a failed populate just means the
|
|
3035
|
+
// next read consults KV. The invalidation WRITE-THROUGH (critical) is not
|
|
3036
|
+
// - silently swallowing it would leave this colo's stale marker (often the
|
|
3037
|
+
// ABSENT sentinel) authoritative for tagCacheTtl while updateTag reports
|
|
3038
|
+
// success. Surface it, and best-effort delete the L1 marker so the next
|
|
3039
|
+
// read re-reads KV, which already holds the fresh marker (written before
|
|
3040
|
+
// this write-through in invalidateTags).
|
|
3041
|
+
if (opts?.critical) {
|
|
3042
|
+
reportCacheError(
|
|
3043
|
+
error,
|
|
3044
|
+
"cache-invalidate",
|
|
3045
|
+
"[CFCacheStore] tag marker L1 write-through",
|
|
3046
|
+
);
|
|
3047
|
+
await reportingAsync(
|
|
3048
|
+
async () => {
|
|
3049
|
+
const cache = await this.getCache();
|
|
3050
|
+
await cache.delete(this.tagMarkerRequest(tag));
|
|
3051
|
+
},
|
|
3052
|
+
"cache-delete",
|
|
3053
|
+
"[CFCacheStore] tag marker L1 evict after failed write-through",
|
|
3054
|
+
);
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
/**
|
|
3060
|
+
* Invalidate every entry tagged with any of `tags`. Receives the whole batch
|
|
3061
|
+
* from one updateTag()/revalidateTag() call so the eager-purge hook fires
|
|
3062
|
+
* ONCE (one CDN purge request, not one per tag). For each tag: records the KV
|
|
3063
|
+
* marker (the durable cross-colo truth that reads compare taggedAt against),
|
|
3064
|
+
* writes the fresh marker straight into this colo's L1 (write-through, NOT
|
|
3065
|
+
* delete - a delete would let the next read re-read a not-yet-converged KV
|
|
3066
|
+
* value and re-arm the stale window), and memoizes it for same-request
|
|
3067
|
+
* read-your-own-writes. In purge mode (tagPurge) it then AWAITS the
|
|
3068
|
+
* consumer's purge-by-tag call with the entry Cache-Tags — the eviction the
|
|
3069
|
+
* per-read marker skip on L1 hits relies on. Finally fires onRevalidateTag
|
|
3070
|
+
* with the namespaced lookup Cache-Tags so a consumer purge evicts the
|
|
3071
|
+
* cached lookups in other colos promptly (otherwise they converge within
|
|
3072
|
+
* tagCacheTtl).
|
|
3073
|
+
*
|
|
3074
|
+
* Durable-write integrity: the in-memory write-through (memo + L1) for a tag
|
|
3075
|
+
* runs ONLY after that tag's KV marker write is confirmed. If any KV write
|
|
3076
|
+
* fails (transient error; over-limit keys are normalized by toKVKey rather
|
|
3077
|
+
* than rejected), this rejects with the
|
|
3078
|
+
* failed tags so an awaiting updateTag() surfaces the failure instead of
|
|
3079
|
+
* silently reporting success while other requests/colos serve stale data. The
|
|
3080
|
+
* eager purge still fires for the whole batch first (it is additive).
|
|
3081
|
+
*/
|
|
3082
|
+
/**
|
|
3083
|
+
* Shell tag-generation gate (SegmentCacheStore.isTagsInvalidatedSince): the
|
|
3084
|
+
* SAME KV markers used by runtime envelopes also evict immutable build shells
|
|
3085
|
+
* and captures whose write races updateTag(). Thin public wrapper over the
|
|
3086
|
+
* shell generation check (marker >= since, fail open); KV-less it degrades
|
|
3087
|
+
* to the per-request memo in purge mode and to false otherwise — see
|
|
3088
|
+
* isShellGenerationInvalidated.
|
|
3089
|
+
*/
|
|
3090
|
+
async isTagsInvalidatedSince(
|
|
3091
|
+
tags: string[],
|
|
3092
|
+
sinceMs: number,
|
|
3093
|
+
): Promise<boolean> {
|
|
3094
|
+
return this.isShellGenerationInvalidated(tags, sinceMs);
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
async invalidateTags(tags: string[]): Promise<void> {
|
|
3098
|
+
if (tags.length === 0) return;
|
|
3099
|
+
const invalidatedAt = Date.now();
|
|
3100
|
+
const ctx = _getRequestContext();
|
|
3101
|
+
const memo = ctx ? getTagMarkerMemo(ctx, this) : undefined;
|
|
3102
|
+
|
|
3103
|
+
if (!this.kv && !this.onRevalidateTag && !this.tagPurge) {
|
|
3104
|
+
console.warn(
|
|
3105
|
+
`[CFCacheStore] invalidateTags had no effect: configure a KV namespace ` +
|
|
3106
|
+
`for distributed invalidation, a tagPurge hook for purge-by-tag ` +
|
|
3107
|
+
`eviction, or an onRevalidateTag hook.`,
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
const failedTags = new Set<string>();
|
|
3112
|
+
const errors: unknown[] = [];
|
|
3113
|
+
if (this.kv) {
|
|
3114
|
+
// Markers written with no expiry (tagInvalidationTtl unset) never expire,
|
|
3115
|
+
// so high-cardinality tags accumulate KV keys unboundedly with no reaper.
|
|
3116
|
+
// Warn once per namespace at the batch entry point (not per marker write,
|
|
3117
|
+
// which would fire once per tag). Kept separate from the floor warning:
|
|
3118
|
+
// that path only fires for a positive below-floor value, never the unset
|
|
3119
|
+
// default sanitizeTagInvalidationTtl passes through as undefined.
|
|
3120
|
+
if (!this.tagInvalidationTtl) {
|
|
3121
|
+
this.warnOncePerNamespace(
|
|
3122
|
+
warnedNoTagInvalidationTtl,
|
|
3123
|
+
`[CFCacheStore] invalidateTags is writing KV markers with no expiry ` +
|
|
3124
|
+
`(tagInvalidationTtl is unset): high-cardinality tags accumulate KV ` +
|
|
3125
|
+
`keys unboundedly (storage + list-scan cost) with no reaper. Set ` +
|
|
3126
|
+
`tagInvalidationTtl above your largest entry TTL+SWR to bound marker ` +
|
|
3127
|
+
`growth; setting it too small resurrects invalidated entries.`,
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
3130
|
+
await Promise.all(
|
|
3131
|
+
tags.map(async (tag) => {
|
|
3132
|
+
// An over-limit tag no longer rejects: tagMarkerKey normalizes
|
|
3133
|
+
// through toKVKey, and the marker read path derives the key the same
|
|
3134
|
+
// way, so oversized tags invalidate correctly instead of erroring.
|
|
3135
|
+
const markerKey = await this.tagMarkerKey(tag);
|
|
3136
|
+
try {
|
|
3137
|
+
await this.kv!.put(markerKey, String(invalidatedAt), {
|
|
3138
|
+
...(this.tagInvalidationTtl
|
|
3139
|
+
? { expirationTtl: this.tagInvalidationTtl }
|
|
3140
|
+
: {}),
|
|
3141
|
+
});
|
|
3142
|
+
} catch (error) {
|
|
3143
|
+
failedTags.add(tag);
|
|
3144
|
+
errors.push(error);
|
|
3145
|
+
}
|
|
3146
|
+
}),
|
|
3147
|
+
);
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
// Write-through memo + L1 only for tags with a confirmed durable marker, and
|
|
3151
|
+
// only when KV is configured. Markers are read exclusively through
|
|
3152
|
+
// isGloballyInvalidated(), which short-circuits to "not invalidated" when
|
|
3153
|
+
// !this.kv; writing memo/L1 markers without KV would be dead state no read
|
|
3154
|
+
// path ever consults — EXCEPT the memo in purge mode: isL1Invalidated()
|
|
3155
|
+
// consults it (and only it) on every L1 hit, so a KV-less purge-mode store
|
|
3156
|
+
// still writes the memo for same-request read-your-own-writes. The
|
|
3157
|
+
// onRevalidateTag purge below still fires regardless (it is additive and
|
|
3158
|
+
// external to the marker cascade). The memo write is synchronous
|
|
3159
|
+
// (read-your-own-writes); the L1 Cache API writes are independent, so fan
|
|
3160
|
+
// them out in parallel rather than awaiting each.
|
|
3161
|
+
const lookupMarkerCacheActive = Boolean(this.kv) && this.tagCacheTtl > 0;
|
|
3162
|
+
if (this.kv || this.tagPurge) {
|
|
3163
|
+
const l1Writes: Promise<void>[] = [];
|
|
3164
|
+
for (const tag of tags) {
|
|
3165
|
+
if (failedTags.has(tag)) continue;
|
|
3166
|
+
memo?.set(tag, invalidatedAt);
|
|
3167
|
+
if (lookupMarkerCacheActive) {
|
|
3168
|
+
l1Writes.push(
|
|
3169
|
+
this.putTagMarkerL1(tag, invalidatedAt, { critical: true }),
|
|
3170
|
+
);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
if (l1Writes.length > 0) await Promise.all(l1Writes);
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
// Purge mode: evict the tagged L1 entries across every colo via the
|
|
3177
|
+
// consumer's purge-by-tag call. AWAITED, and a failure is correctness-
|
|
3178
|
+
// bearing (unlike onRevalidateTag): with the per-read marker lookup skipped
|
|
3179
|
+
// on L1 hits, a dropped purge leaves L1 serving stale until TTL — so it
|
|
3180
|
+
// surfaces through updateTag() like a failed marker write. Fired for the
|
|
3181
|
+
// whole batch regardless of marker outcome (purging is additive and
|
|
3182
|
+
// idempotent; a retry re-runs both). Lookup Cache-Tags ride along when the
|
|
3183
|
+
// L1 marker cache is active (tagCacheTtl > 0 AND kv — without kv no lookup
|
|
3184
|
+
// entries are ever written) so a single purge call also converges other
|
|
3185
|
+
// colos' cached lookups (no separate onRevalidateTag needed).
|
|
3186
|
+
let purgeError: unknown;
|
|
3187
|
+
if (this.tagPurge) {
|
|
3188
|
+
const purgeTags = tags.flatMap((tag) =>
|
|
3189
|
+
lookupMarkerCacheActive
|
|
3190
|
+
? [this.entryPurgeTag(tag), this.lookupPurgeTag(tag)]
|
|
3191
|
+
: [this.entryPurgeTag(tag)],
|
|
3192
|
+
);
|
|
3193
|
+
try {
|
|
3194
|
+
await this.tagPurge(purgeTags);
|
|
3195
|
+
} catch (error) {
|
|
3196
|
+
purgeError = error ?? new Error("tagPurge rejected");
|
|
3197
|
+
reportCacheError(
|
|
3198
|
+
purgeError,
|
|
3199
|
+
"cache-invalidate",
|
|
3200
|
+
"[CFCacheStore] tagPurge hook",
|
|
3201
|
+
);
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
// One batched eager purge of the lookup markers for the whole call. Fired
|
|
3206
|
+
// regardless of KV write outcome (it is additive and uses pure string ops).
|
|
3207
|
+
if (this.onRevalidateTag) {
|
|
3208
|
+
try {
|
|
3209
|
+
await this.onRevalidateTag(tags.map((tag) => this.lookupPurgeTag(tag)));
|
|
3210
|
+
} catch (error) {
|
|
3211
|
+
reportCacheError(
|
|
3212
|
+
error,
|
|
3213
|
+
"cache-invalidate",
|
|
3214
|
+
"[CFCacheStore] onRevalidateTag hook",
|
|
3215
|
+
);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
if (failedTags.size > 0 || purgeError) {
|
|
3220
|
+
const parts: string[] = [];
|
|
3221
|
+
if (failedTags.size > 0) {
|
|
3222
|
+
parts.push(
|
|
3223
|
+
`${failedTags.size}/${tags.length} tag marker write(s) failed: ` +
|
|
3224
|
+
`${[...failedTags].join(", ")}`,
|
|
3225
|
+
);
|
|
3226
|
+
}
|
|
3227
|
+
if (purgeError) {
|
|
3228
|
+
parts.push(
|
|
3229
|
+
`the tagPurge purge-by-tag call failed (tagged L1 entries stay ` +
|
|
3230
|
+
`stale until TTL)`,
|
|
3231
|
+
);
|
|
3232
|
+
}
|
|
3233
|
+
const err = new Error(
|
|
3234
|
+
`[CFCacheStore] ${parts.join("; ")}. Those tags may still serve ` +
|
|
3235
|
+
`stale data across requests/colos; retry the invalidation.`,
|
|
3236
|
+
);
|
|
3237
|
+
(err as Error & { cause?: unknown }).cause = errors[0] ?? purgeError;
|
|
3238
|
+
throw err;
|
|
3239
|
+
}
|
|
722
3240
|
}
|
|
723
3241
|
|
|
724
3242
|
// ============================================================================
|
|
@@ -731,63 +3249,122 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
731
3249
|
* Promotes hits to L1 via waitUntil.
|
|
732
3250
|
* @internal
|
|
733
3251
|
*/
|
|
734
|
-
private async kvGetSegment(
|
|
3252
|
+
private async kvGetSegment(
|
|
3253
|
+
key: string,
|
|
3254
|
+
opts?: { suppressRevalidate?: boolean },
|
|
3255
|
+
): Promise<CacheGetResult | null | CacheReadError> {
|
|
735
3256
|
if (!this.kv) return null;
|
|
736
3257
|
|
|
737
3258
|
try {
|
|
738
|
-
const kvKey = this.toKVKey(key);
|
|
739
|
-
const
|
|
740
|
-
|
|
3259
|
+
const kvKey = await this.toKVKey(key);
|
|
3260
|
+
const { value: envelope, timedOut } =
|
|
3261
|
+
await this.kvGetOrEvict<KVSegmentEnvelope>(
|
|
3262
|
+
kvKey,
|
|
3263
|
+
(e) =>
|
|
3264
|
+
typeof e.e === "number" && typeof e.s === "number" && e.d != null,
|
|
3265
|
+
"kvGetSegment",
|
|
3266
|
+
);
|
|
3267
|
+
if (timedOut) {
|
|
3268
|
+
// Abandoned slow KV read: no envelope, so no promote-to-L1. Distinct
|
|
3269
|
+
// from a genuine kv-miss so the degradation is visible on wrangler tail.
|
|
3270
|
+
if (this.debug)
|
|
3271
|
+
this.emitDebug({ op: "get", key, outcome: "kv-timeout" });
|
|
3272
|
+
return null;
|
|
3273
|
+
}
|
|
3274
|
+
if (!envelope) {
|
|
3275
|
+
// Missing key, or a corrupt entry already evicted + reported by
|
|
3276
|
+
// kvGetOrEvict. Either way a miss.
|
|
3277
|
+
if (this.debug) this.emitDebug({ op: "get", key, outcome: "kv-miss" });
|
|
3278
|
+
return null;
|
|
3279
|
+
}
|
|
741
3280
|
|
|
742
|
-
const envelope = raw as KVSegmentEnvelope;
|
|
743
3281
|
const now = Date.now();
|
|
744
3282
|
|
|
745
3283
|
// Hard-expired — treat as miss
|
|
746
|
-
if (now > envelope.e)
|
|
3284
|
+
if (now > envelope.e) {
|
|
3285
|
+
if (this.debug) this.emitDebug({ op: "get", key, outcome: "kv-miss" });
|
|
3286
|
+
return null;
|
|
3287
|
+
}
|
|
747
3288
|
|
|
748
|
-
|
|
3289
|
+
// Tag invalidation check (also covers the KV tier, not just L1).
|
|
3290
|
+
if (
|
|
3291
|
+
await this.isGloballyInvalidated(envelope.d.tags, envelope.d.taggedAt)
|
|
3292
|
+
) {
|
|
3293
|
+
if (this.debug)
|
|
3294
|
+
this.emitDebug({ op: "get", key, outcome: "tag-invalidated" });
|
|
3295
|
+
return null;
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
// When this is a degraded L1 fall-through (body-timeout / non-200), the
|
|
3299
|
+
// caller asks us to suppress revalidation: KV has no REVALIDATING herd
|
|
3300
|
+
// guard, so N concurrent degraded reads would otherwise each spawn a
|
|
3301
|
+
// render exactly when the colo is already struggling. We still serve the
|
|
3302
|
+
// stale data and still promote to L1; only the revalidation is withheld.
|
|
3303
|
+
const stale = now > envelope.s;
|
|
3304
|
+
const shouldRevalidate = stale && !opts?.suppressRevalidate;
|
|
749
3305
|
|
|
750
3306
|
// Promote to L1 in background
|
|
751
3307
|
this.promoteSegmentToL1(key, envelope);
|
|
752
3308
|
|
|
3309
|
+
if (this.debug)
|
|
3310
|
+
this.emitDebug({
|
|
3311
|
+
op: "get",
|
|
3312
|
+
key,
|
|
3313
|
+
outcome: !stale
|
|
3314
|
+
? "kv-fresh"
|
|
3315
|
+
: opts?.suppressRevalidate
|
|
3316
|
+
? "kv-stale-suppressed"
|
|
3317
|
+
: "kv-stale",
|
|
3318
|
+
shouldRevalidate,
|
|
3319
|
+
});
|
|
753
3320
|
return { data: envelope.d, shouldRevalidate };
|
|
754
3321
|
} catch (error) {
|
|
755
|
-
|
|
756
|
-
|
|
3322
|
+
reportCacheError(error, "cache-read", "[CFCacheStore] kvGetSegment");
|
|
3323
|
+
if (this.debug) this.emitDebug({ op: "get", key, outcome: "error" });
|
|
3324
|
+
// A KV failure is NOT proof of absence: returning null classified it a
|
|
3325
|
+
// real miss and let the PPR seeded fallback substitute the doc record
|
|
3326
|
+
// for a key partition the store could not actually read. Same sentinel
|
|
3327
|
+
// as get()'s own catch — lookupRouteDetailed classifies it `error` and
|
|
3328
|
+
// the render stays uncached. (A kvGetOrEvict TIMEOUT above stays null
|
|
3329
|
+
// by design: it is a bounded-latency degrade of a likely-healthy read,
|
|
3330
|
+
// and serving the equivalent seeded record there is the fallback
|
|
3331
|
+
// working as intended, not a masked failure.)
|
|
3332
|
+
return CACHE_READ_ERROR;
|
|
757
3333
|
}
|
|
758
3334
|
}
|
|
759
3335
|
|
|
760
3336
|
/**
|
|
761
3337
|
* Write segment data to KV.
|
|
3338
|
+
*
|
|
3339
|
+
* `dataJson` is the already-serialized CachedEntryData body also stored in
|
|
3340
|
+
* L1 — embedded as envelope.d without a second JSON.stringify walk.
|
|
762
3341
|
* @internal
|
|
763
3342
|
*/
|
|
764
3343
|
private kvSetSegment(
|
|
765
3344
|
key: string,
|
|
766
|
-
|
|
3345
|
+
dataJson: string,
|
|
767
3346
|
staleAt: number,
|
|
768
3347
|
totalTtl: number,
|
|
3348
|
+
swrWindow: number,
|
|
769
3349
|
): void {
|
|
770
3350
|
// KV requires expirationTtl >= 60s. Skip write for short-lived entries.
|
|
771
3351
|
if (!this.kv || !this.waitUntil || totalTtl < 60) return;
|
|
772
3352
|
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
console.error("[CFCacheStore] KV set failed:", error);
|
|
789
|
-
}
|
|
790
|
-
});
|
|
3353
|
+
const expiresAt = staleAt + swrWindow * 1000;
|
|
3354
|
+
// Same wire shape as JSON.stringify({ d, s, e }) — dataJson is already
|
|
3355
|
+
// valid JSON for CachedEntryData, so embedding it avoids re-walking the tree.
|
|
3356
|
+
const envelopeJson = `{"d":${dataJson},"s":${staleAt},"e":${expiresAt}}`;
|
|
3357
|
+
|
|
3358
|
+
this.waitUntil(() =>
|
|
3359
|
+
reportingAsync(
|
|
3360
|
+
async () =>
|
|
3361
|
+
this.kv!.put(await this.toKVKey(key), envelopeJson, {
|
|
3362
|
+
expirationTtl: totalTtl,
|
|
3363
|
+
}),
|
|
3364
|
+
"cache-write",
|
|
3365
|
+
"[CFCacheStore] kvSetSegment",
|
|
3366
|
+
),
|
|
3367
|
+
);
|
|
791
3368
|
}
|
|
792
3369
|
|
|
793
3370
|
/**
|
|
@@ -797,58 +3374,115 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
797
3374
|
private promoteSegmentToL1(key: string, envelope: KVSegmentEnvelope): void {
|
|
798
3375
|
if (!this.waitUntil) return;
|
|
799
3376
|
|
|
800
|
-
this.waitUntil(
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
3377
|
+
this.waitUntil(() =>
|
|
3378
|
+
reportingAsync(
|
|
3379
|
+
async () => {
|
|
3380
|
+
const now = Date.now();
|
|
3381
|
+
const remainingTtl = Math.max(
|
|
3382
|
+
1,
|
|
3383
|
+
Math.floor((envelope.e - now) / 1000),
|
|
3384
|
+
);
|
|
3385
|
+
const cache = await this.getCache();
|
|
3386
|
+
const request = this.keyToRequest(key);
|
|
3387
|
+
|
|
3388
|
+
const response = new Response(JSON.stringify(envelope.d), {
|
|
3389
|
+
headers: {
|
|
3390
|
+
"Content-Type": "application/json",
|
|
3391
|
+
"Cache-Control": `public, max-age=${remainingTtl}`,
|
|
3392
|
+
[CACHE_STALE_AT_HEADER]: String(envelope.s),
|
|
3393
|
+
// Carry the hard-expiry deadline so a promoted entry that later
|
|
3394
|
+
// goes stale re-puts with the correct remaining ttl (see set()).
|
|
3395
|
+
[CACHE_EXPIRES_AT_HEADER]: String(envelope.e),
|
|
3396
|
+
[CACHE_STATUS_HEADER]: "HIT",
|
|
3397
|
+
// Preserve tags across KV->L1 promotion so the promoted entry
|
|
3398
|
+
// stays tag-invalidatable.
|
|
3399
|
+
...this.tagHeaderEntries(envelope.d.tags, envelope.d.taggedAt),
|
|
3400
|
+
},
|
|
3401
|
+
});
|
|
3402
|
+
|
|
3403
|
+
await cache.put(request, response);
|
|
3404
|
+
},
|
|
3405
|
+
"cache-write",
|
|
3406
|
+
"[CFCacheStore] promoteSegmentToL1",
|
|
3407
|
+
),
|
|
3408
|
+
);
|
|
821
3409
|
}
|
|
822
3410
|
|
|
823
3411
|
/**
|
|
824
3412
|
* KV fallback for function cache reads.
|
|
825
3413
|
* @internal
|
|
826
3414
|
*/
|
|
827
|
-
private async kvGetItem(
|
|
3415
|
+
private async kvGetItem(
|
|
3416
|
+
key: string,
|
|
3417
|
+
opts?: { suppressRevalidate?: boolean },
|
|
3418
|
+
): Promise<CacheItemResult | null> {
|
|
828
3419
|
if (!this.kv) return null;
|
|
829
3420
|
|
|
830
3421
|
try {
|
|
831
|
-
const kvKey = this.toKVKey(`fn:${key}`);
|
|
832
|
-
const
|
|
833
|
-
|
|
3422
|
+
const kvKey = await this.toKVKey(`fn:${key}`);
|
|
3423
|
+
const { value: envelope, timedOut } =
|
|
3424
|
+
await this.kvGetOrEvict<KVItemEnvelope>(
|
|
3425
|
+
kvKey,
|
|
3426
|
+
(e) =>
|
|
3427
|
+
typeof e.v === "string" &&
|
|
3428
|
+
typeof e.e === "number" &&
|
|
3429
|
+
typeof e.s === "number",
|
|
3430
|
+
"kvGetItem",
|
|
3431
|
+
);
|
|
3432
|
+
if (timedOut) {
|
|
3433
|
+
if (this.debug)
|
|
3434
|
+
this.emitDebug({ op: "getItem", key, outcome: "kv-timeout" });
|
|
3435
|
+
return null;
|
|
3436
|
+
}
|
|
3437
|
+
if (!envelope) {
|
|
3438
|
+
if (this.debug)
|
|
3439
|
+
this.emitDebug({ op: "getItem", key, outcome: "kv-miss" });
|
|
3440
|
+
return null;
|
|
3441
|
+
}
|
|
834
3442
|
|
|
835
|
-
const envelope = raw as KVItemEnvelope;
|
|
836
3443
|
const now = Date.now();
|
|
837
3444
|
|
|
838
|
-
if (now > envelope.e)
|
|
3445
|
+
if (now > envelope.e) {
|
|
3446
|
+
if (this.debug)
|
|
3447
|
+
this.emitDebug({ op: "getItem", key, outcome: "kv-miss" });
|
|
3448
|
+
return null;
|
|
3449
|
+
}
|
|
839
3450
|
|
|
840
|
-
|
|
3451
|
+
// Tag invalidation check (also covers the KV tier, not just L1).
|
|
3452
|
+
if (await this.isGloballyInvalidated(envelope.t, envelope.ta)) {
|
|
3453
|
+
if (this.debug)
|
|
3454
|
+
this.emitDebug({ op: "getItem", key, outcome: "tag-invalidated" });
|
|
3455
|
+
return null;
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
// Degraded fall-through suppresses revalidation (no KV herd guard); see
|
|
3459
|
+
// kvGetSegment. Still serves stale and still promotes.
|
|
3460
|
+
const stale = now > envelope.s;
|
|
3461
|
+
const shouldRevalidate = stale && !opts?.suppressRevalidate;
|
|
841
3462
|
|
|
842
3463
|
// Promote to L1
|
|
843
3464
|
this.promoteItemToL1(key, envelope);
|
|
844
3465
|
|
|
3466
|
+
if (this.debug)
|
|
3467
|
+
this.emitDebug({
|
|
3468
|
+
op: "getItem",
|
|
3469
|
+
key,
|
|
3470
|
+
outcome: !stale
|
|
3471
|
+
? "kv-fresh"
|
|
3472
|
+
: opts?.suppressRevalidate
|
|
3473
|
+
? "kv-stale-suppressed"
|
|
3474
|
+
: "kv-stale",
|
|
3475
|
+
shouldRevalidate,
|
|
3476
|
+
});
|
|
845
3477
|
return {
|
|
846
3478
|
value: envelope.v,
|
|
847
3479
|
handles: envelope.h,
|
|
848
3480
|
shouldRevalidate,
|
|
3481
|
+
tags: envelope.t,
|
|
849
3482
|
};
|
|
850
3483
|
} catch (error) {
|
|
851
|
-
|
|
3484
|
+
reportCacheError(error, "cache-read", "[CFCacheStore] kvGetItem");
|
|
3485
|
+
if (this.debug) this.emitDebug({ op: "getItem", key, outcome: "error" });
|
|
852
3486
|
return null;
|
|
853
3487
|
}
|
|
854
3488
|
}
|
|
@@ -860,28 +3494,41 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
860
3494
|
private promoteItemToL1(key: string, envelope: KVItemEnvelope): void {
|
|
861
3495
|
if (!this.waitUntil) return;
|
|
862
3496
|
|
|
863
|
-
this.waitUntil(
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
3497
|
+
this.waitUntil(() =>
|
|
3498
|
+
reportingAsync(
|
|
3499
|
+
async () => {
|
|
3500
|
+
const now = Date.now();
|
|
3501
|
+
const remainingTtl = Math.max(
|
|
3502
|
+
1,
|
|
3503
|
+
Math.floor((envelope.e - now) / 1000),
|
|
3504
|
+
);
|
|
3505
|
+
const cache = await this.getCache();
|
|
3506
|
+
const request = this.keyToRequest(`fn:${key}`);
|
|
3507
|
+
|
|
3508
|
+
const body = JSON.stringify({
|
|
3509
|
+
value: envelope.v,
|
|
3510
|
+
handles: envelope.h,
|
|
3511
|
+
});
|
|
3512
|
+
const response = new Response(body, {
|
|
3513
|
+
headers: {
|
|
3514
|
+
"Content-Type": "application/json",
|
|
3515
|
+
"Cache-Control": `public, max-age=${remainingTtl}`,
|
|
3516
|
+
[CACHE_STALE_AT_HEADER]: String(envelope.s),
|
|
3517
|
+
// Carry the hard-expiry deadline; see promoteSegmentToL1 / set().
|
|
3518
|
+
[CACHE_EXPIRES_AT_HEADER]: String(envelope.e),
|
|
3519
|
+
[CACHE_STATUS_HEADER]: "HIT",
|
|
3520
|
+
// Preserve tags across KV->L1 promotion (the item tier previously
|
|
3521
|
+
// dropped them, permanently disabling tag invalidation here).
|
|
3522
|
+
...this.tagHeaderEntries(envelope.t, envelope.ta),
|
|
3523
|
+
},
|
|
3524
|
+
});
|
|
3525
|
+
|
|
3526
|
+
await cache.put(request, response);
|
|
3527
|
+
},
|
|
3528
|
+
"cache-write",
|
|
3529
|
+
"[CFCacheStore] promoteItemToL1",
|
|
3530
|
+
),
|
|
3531
|
+
);
|
|
885
3532
|
}
|
|
886
3533
|
|
|
887
3534
|
/**
|
|
@@ -894,32 +3541,82 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
894
3541
|
if (!this.kv) return null;
|
|
895
3542
|
|
|
896
3543
|
try {
|
|
897
|
-
const kvKey = this.
|
|
898
|
-
|
|
899
|
-
|
|
3544
|
+
const kvKey = await this.toDocKVKey(key);
|
|
3545
|
+
// The document path is debug-silent (op is only get/getItem): a KV-read
|
|
3546
|
+
// timeout here is bounded for resilience parity (kvGetOrEvict applies the
|
|
3547
|
+
// budget) but emits no kv-timeout event, so its absence from the debug
|
|
3548
|
+
// stream is expected. A null envelope is a miss -- missing key, a budget
|
|
3549
|
+
// timeout, or a corrupt entry already evicted + reported by kvGetOrEvict.
|
|
3550
|
+
const { value: envelope } = await this.kvGetOrEvict<KVResponseEnvelope>(
|
|
3551
|
+
kvKey,
|
|
3552
|
+
(e) =>
|
|
3553
|
+
typeof e.b === "string" &&
|
|
3554
|
+
typeof e.st === "number" &&
|
|
3555
|
+
typeof e.e === "number" &&
|
|
3556
|
+
typeof e.s === "number" &&
|
|
3557
|
+
// stx is optional but, if present, must be a string (feeds Response).
|
|
3558
|
+
(e.stx === undefined || typeof e.stx === "string") &&
|
|
3559
|
+
// hd must be an array of [name, value] string tuples; a malformed
|
|
3560
|
+
// shape would otherwise throw in `new Headers(hd)`. Validate it here
|
|
3561
|
+
// so a faulty envelope is a fail-open MISS, never a thrown read.
|
|
3562
|
+
Array.isArray(e.hd) &&
|
|
3563
|
+
e.hd.every(
|
|
3564
|
+
(entry) =>
|
|
3565
|
+
Array.isArray(entry) &&
|
|
3566
|
+
entry.length === 2 &&
|
|
3567
|
+
typeof entry[0] === "string" &&
|
|
3568
|
+
typeof entry[1] === "string",
|
|
3569
|
+
),
|
|
3570
|
+
"kvGetResponse",
|
|
3571
|
+
);
|
|
3572
|
+
if (!envelope) return null;
|
|
900
3573
|
|
|
901
|
-
const envelope = raw as KVResponseEnvelope;
|
|
902
3574
|
const now = Date.now();
|
|
903
3575
|
|
|
904
3576
|
if (now > envelope.e) return null;
|
|
905
3577
|
|
|
3578
|
+
// Tag invalidation check (also covers the KV tier, not just L1).
|
|
3579
|
+
if (await this.isGloballyInvalidated(envelope.t, envelope.ta)) {
|
|
3580
|
+
return null;
|
|
3581
|
+
}
|
|
3582
|
+
|
|
906
3583
|
const shouldRevalidate = now > envelope.s;
|
|
907
3584
|
|
|
908
|
-
// Reconstruct Response
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
3585
|
+
// Reconstruct Response: decode base64 -> binary, rebuild headers/status.
|
|
3586
|
+
// Corrupt/partial base64 throws in atob; malformed `hd` or an out-of-range
|
|
3587
|
+
// `st` throws in new Headers/new Response. Any of these is a faulty entry,
|
|
3588
|
+
// so evict it and miss rather than re-failing every read until TTL.
|
|
3589
|
+
let response: Response;
|
|
3590
|
+
try {
|
|
3591
|
+
// Finding #3 (read side): strip per-client signals a stale envelope may
|
|
3592
|
+
// carry. Inside the try so a malformed `hd` evicts (not throws through);
|
|
3593
|
+
// mutates `hd` in place so promoteResponseToL1 re-seeds from it too.
|
|
3594
|
+
envelope.hd = envelope.hd.filter(
|
|
3595
|
+
([name]) => !isPerClientSignalHeader(name),
|
|
3596
|
+
);
|
|
3597
|
+
const bodyBuffer = base64ToBuffer(envelope.b);
|
|
3598
|
+
const headers = new Headers(envelope.hd);
|
|
3599
|
+
response = new Response(bodyBuffer, {
|
|
3600
|
+
status: envelope.st,
|
|
3601
|
+
statusText: envelope.stx,
|
|
3602
|
+
headers,
|
|
3603
|
+
});
|
|
3604
|
+
} catch (error) {
|
|
3605
|
+
reportCacheError(
|
|
3606
|
+
error,
|
|
3607
|
+
"cache-corrupt",
|
|
3608
|
+
"[CFCacheStore] kvGetResponse: corrupt response envelope, evicting",
|
|
3609
|
+
);
|
|
3610
|
+
this.scheduleKvEvict(kvKey, "kvGetResponse");
|
|
3611
|
+
return null;
|
|
3612
|
+
}
|
|
916
3613
|
|
|
917
3614
|
// Promote to L1
|
|
918
3615
|
this.promoteResponseToL1(key, envelope);
|
|
919
3616
|
|
|
920
3617
|
return { response, shouldRevalidate };
|
|
921
3618
|
} catch (error) {
|
|
922
|
-
|
|
3619
|
+
reportCacheError(error, "cache-read", "[CFCacheStore] kvGetResponse");
|
|
923
3620
|
return null;
|
|
924
3621
|
}
|
|
925
3622
|
}
|
|
@@ -931,52 +3628,46 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
931
3628
|
private promoteResponseToL1(key: string, envelope: KVResponseEnvelope): void {
|
|
932
3629
|
if (!this.waitUntil) return;
|
|
933
3630
|
|
|
934
|
-
this.waitUntil(
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
function base64ToBuffer(base64: string): ArrayBuffer {
|
|
976
|
-
const binary = atob(base64);
|
|
977
|
-
const bytes = new Uint8Array(binary.length);
|
|
978
|
-
for (let i = 0; i < binary.length; i++) {
|
|
979
|
-
bytes[i] = binary.charCodeAt(i);
|
|
3631
|
+
this.waitUntil(() =>
|
|
3632
|
+
reportingAsync(
|
|
3633
|
+
async () => {
|
|
3634
|
+
const now = Date.now();
|
|
3635
|
+
const remainingTtl = Math.max(
|
|
3636
|
+
1,
|
|
3637
|
+
Math.floor((envelope.e - now) / 1000),
|
|
3638
|
+
);
|
|
3639
|
+
const cache = await this.getCache();
|
|
3640
|
+
const request = this.keyToRequest(`doc:${key}`);
|
|
3641
|
+
|
|
3642
|
+
const headers = new Headers(envelope.hd);
|
|
3643
|
+
const originalCacheControl = headers.get("Cache-Control");
|
|
3644
|
+
if (originalCacheControl !== null) {
|
|
3645
|
+
headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
|
|
3646
|
+
}
|
|
3647
|
+
headers.set("Cache-Control", `public, max-age=${remainingTtl}`);
|
|
3648
|
+
headers.set(CACHE_STALE_AT_HEADER, String(envelope.s));
|
|
3649
|
+
// Carry the hard-expiry deadline so the document herd guard's
|
|
3650
|
+
// markResponseRevalidating re-put can compute the remaining window
|
|
3651
|
+
// (matches promoteSegmentToL1/promoteItemToL1); without it a stale
|
|
3652
|
+
// re-put would floor to max-age=1 and churn the KV-promoted twin.
|
|
3653
|
+
headers.set(CACHE_EXPIRES_AT_HEADER, String(envelope.e));
|
|
3654
|
+
// Re-attach the internal tag headers (envelope.hd is client-facing
|
|
3655
|
+
// and intentionally excludes them) so the promoted entry stays
|
|
3656
|
+
// invalidatable.
|
|
3657
|
+
this.setTagHeaders(headers, envelope.t, envelope.ta);
|
|
3658
|
+
|
|
3659
|
+
const bodyBuffer = base64ToBuffer(envelope.b);
|
|
3660
|
+
const response = new Response(bodyBuffer, {
|
|
3661
|
+
status: envelope.st,
|
|
3662
|
+
statusText: envelope.stx,
|
|
3663
|
+
headers,
|
|
3664
|
+
});
|
|
3665
|
+
|
|
3666
|
+
await cache.put(request, response);
|
|
3667
|
+
},
|
|
3668
|
+
"cache-write",
|
|
3669
|
+
"[CFCacheStore] promoteResponseToL1",
|
|
3670
|
+
),
|
|
3671
|
+
);
|
|
980
3672
|
}
|
|
981
|
-
return bytes.buffer;
|
|
982
3673
|
}
|