@rangojs/router 0.0.0-experimental.79 → 0.0.0-experimental.7c7e4327
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 +301 -797
- package/dist/bin/rango.js +603 -145
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3750 -1160
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +96 -24
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +85 -6
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +228 -33
- package/skills/caching/SKILL.md +336 -19
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +110 -4
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +5 -3
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +87 -56
- 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 +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -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 +247 -17
- package/skills/loader/SKILL.md +248 -10
- package/skills/middleware/SKILL.md +25 -13
- package/skills/migrate-nextjs/SKILL.md +205 -20
- package/skills/migrate-react-router/SKILL.md +59 -670
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +29 -2
- package/skills/observability/SKILL.md +202 -0
- package/skills/parallel/SKILL.md +40 -10
- package/skills/ppr/SKILL.md +616 -0
- package/skills/prerender/SKILL.md +72 -60
- package/skills/rango/SKILL.md +318 -26
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +138 -49
- package/skills/route/SKILL.md +117 -9
- package/skills/router-setup/SKILL.md +44 -9
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +776 -0
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +28 -4
- package/skills/testing/SKILL.md +130 -0
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +131 -0
- package/skills/testing/loader.md +128 -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 +1 -1
- package/skills/typesafety/SKILL.md +45 -626
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +74 -15
- package/skills/vercel/SKILL.md +128 -0
- package/skills/view-transitions/SKILL.md +337 -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/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/event-controller.ts +252 -158
- 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/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +94 -25
- package/src/browser/navigation-client.ts +144 -79
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +161 -73
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +183 -144
- package/src/browser/prefetch/cache.ts +242 -77
- package/src/browser/prefetch/fetch.ts +325 -69
- package/src/browser/prefetch/queue.ts +61 -12
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +58 -20
- package/src/browser/react/NavigationProvider.tsx +202 -120
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- 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 +32 -7
- 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 +25 -3
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +99 -8
- package/src/browser/rsc-router.tsx +145 -28
- 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 +236 -65
- package/src/browser/types.ts +102 -9
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +203 -154
- package/src/build/generate-route-types.ts +3 -1
- package/src/build/index.ts +11 -3
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +152 -21
- 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 +456 -62
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +128 -51
- 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 +13 -21
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +58 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +421 -58
- package/src/cache/cache-scope.ts +187 -96
- 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 +127 -0
- package/src/cache/cf/cf-cache-store.ts +2202 -372
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +126 -41
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +243 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +13 -21
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +180 -99
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1127 -0
- package/src/client.rsc.tsx +41 -21
- package/src/client.tsx +33 -61
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +108 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +18 -6
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +185 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +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 +127 -0
- package/src/handles/is-thenable.ts +18 -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 +97 -12
- package/src/index.ts +98 -14
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +78 -10
- package/src/redirect-origin.ts +114 -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 +19 -77
- package/src/route-definition/dsl-helpers.ts +304 -309
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +87 -59
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- 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 +41 -20
- package/src/route-types.ts +19 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +73 -25
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +129 -30
- package/src/router/handler-context.ts +27 -42
- package/src/router/instrument.ts +355 -0
- package/src/router/intercept-resolution.ts +39 -20
- package/src/router/lazy-includes.ts +82 -59
- package/src/router/loader-resolution.ts +167 -72
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +74 -40
- package/src/router/match-api.ts +80 -55
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +211 -165
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +159 -285
- package/src/router/match-middleware/cache-store.ts +64 -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 +69 -79
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -134
- package/src/router/middleware.ts +298 -172
- package/src/router/navigation-snapshot.ts +7 -56
- 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-limits.ts +37 -0
- package/src/router/prerender-match.ts +112 -67
- package/src/router/preview-match.ts +6 -2
- package/src/router/request-classification.ts +50 -69
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +14 -3
- package/src/router/router-context.ts +6 -29
- package/src/router/router-interfaces.ts +115 -36
- package/src/router/router-options.ts +166 -5
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +131 -86
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +139 -39
- package/src/router/segment-resolution/loader-mask.ts +67 -0
- package/src/router/segment-resolution/loader-snapshot.ts +251 -0
- package/src/router/segment-resolution/revalidation.ts +272 -320
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +56 -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 +160 -200
- package/src/router/telemetry.ts +105 -20
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +215 -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 +157 -71
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +291 -217
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +29 -42
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +124 -13
- package/src/rsc/redirect-guard.ts +100 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +99 -189
- package/src/rsc/rsc-rendering.ts +421 -76
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +282 -116
- package/src/rsc/shell-capture.ts +1158 -0
- package/src/rsc/shell-serve.ts +150 -0
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +53 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-loader-promise.ts +49 -4
- package/src/segment-system.tsx +350 -149
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +208 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +152 -5
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +395 -176
- package/src/ssr/index.tsx +458 -178
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +46 -0
- package/src/testing/dispatch.ts +813 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +199 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +357 -0
- package/src/testing/render-route.tsx +584 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/run-transition-when.ts +164 -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 +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +110 -62
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +20 -52
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +135 -14
- package/src/urls/include-helper.ts +19 -64
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +2 -11
- package/src/urls/path-helper-types.ts +63 -17
- package/src/urls/path-helper.ts +22 -106
- package/src/urls/pattern-types.ts +72 -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 +292 -107
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +127 -86
- 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 +96 -68
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +45 -1
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +4 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugin-types.ts +210 -10
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- 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 +48 -95
- 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 +505 -486
- package/src/vite/plugins/performance-tracks.ts +26 -25
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- 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 +40 -29
- package/src/vite/plugins/version-plugin.ts +37 -40
- package/src/vite/plugins/virtual-entries.ts +138 -27
- package/src/vite/rango.ts +236 -138
- package/src/vite/router-discovery.ts +927 -136
- 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 +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +71 -43
- package/src/vite/utils/shared-utils.ts +142 -43
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -20,7 +20,11 @@ import {
|
|
|
20
20
|
encodeReply,
|
|
21
21
|
createClientTemporaryReferenceSet,
|
|
22
22
|
} from "@vitejs/plugin-rsc/rsc";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
getRequestContext,
|
|
25
|
+
runWithRequestContext,
|
|
26
|
+
} from "../server/request-context.js";
|
|
27
|
+
import { isUnderTestRunner } from "../runtime-env.js";
|
|
24
28
|
import {
|
|
25
29
|
isTainted,
|
|
26
30
|
CACHED_FN_SYMBOL,
|
|
@@ -32,22 +36,177 @@ import {
|
|
|
32
36
|
export { isCachedFunction };
|
|
33
37
|
import { serializeResult, deserializeResult } from "./segment-codec.js";
|
|
34
38
|
import { createHandleStore } from "../server/handle-store.js";
|
|
35
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
restoreHandles,
|
|
41
|
+
encodeHandles,
|
|
42
|
+
decodeHandles,
|
|
43
|
+
} from "./handle-snapshot.js";
|
|
36
44
|
import { startHandleCapture, type HandleCapture } from "./handle-capture.js";
|
|
37
45
|
import { sortedSearchString } from "./cache-key-utils.js";
|
|
46
|
+
import { encodeKV } from "../encode-kv.js";
|
|
38
47
|
import { runBackground } from "./background-task.js";
|
|
48
|
+
import {
|
|
49
|
+
normalizeTags,
|
|
50
|
+
recordRequestTags,
|
|
51
|
+
runWithCacheTagScope,
|
|
52
|
+
} from "./cache-tag.js";
|
|
53
|
+
import { reportCacheError } from "./cache-error.js";
|
|
54
|
+
import type { CacheItemResult } from "./types.js";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* DJB2 hash returning an 8-char hex string. Deterministic across runtimes
|
|
58
|
+
* (no crypto import — cache-runtime runs on the edge). Mirrors prerender's
|
|
59
|
+
* param-hash djb2Hex so binary key parts hash consistently.
|
|
60
|
+
*/
|
|
61
|
+
function djb2HexBytes(bytes: Uint8Array): string {
|
|
62
|
+
let hash = 5381;
|
|
63
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
64
|
+
hash = ((hash << 5) + hash + bytes[i]!) >>> 0;
|
|
65
|
+
}
|
|
66
|
+
return hash.toString(16).padStart(8, "0");
|
|
67
|
+
}
|
|
39
68
|
|
|
40
69
|
/**
|
|
41
70
|
* Convert encodeReply result to a stable string key.
|
|
42
|
-
*
|
|
71
|
+
*
|
|
72
|
+
* encodeReply may return a string or FormData. A plain string is already
|
|
73
|
+
* deterministic for a given arg set, so return it verbatim. FormData (emitted
|
|
74
|
+
* whenever a key arg is a typed array / Blob / File / a large object React
|
|
75
|
+
* lazily chunks) carries a per-call RANDOM multipart boundary
|
|
76
|
+
* (`formdata-undici-<random>`); stringifying the whole body via
|
|
77
|
+
* `new Response(formData).text()` would therefore produce a DIFFERENT key on
|
|
78
|
+
* every call, so the cached function would always miss and the store would
|
|
79
|
+
* accumulate one duplicate entry per call (unbounded growth).
|
|
80
|
+
*
|
|
81
|
+
* Instead derive the key from the entries themselves, independent of the
|
|
82
|
+
* boundary: iterate in sorted-key order and, for each value, emit a
|
|
83
|
+
* boundary-free token — `s:<value>` for strings, `b:<size>:<type>:<name>:<hash>`
|
|
84
|
+
* for Blob/File (bytes folded via djb2 so distinct payloads of equal
|
|
85
|
+
* size/type/name still differ). Strings carry an `s:` type tag so a string whose
|
|
86
|
+
* value happens to equal a blob token (e.g. the literal `b:4::a:b:<hash>`) cannot
|
|
87
|
+
* collide with an actual Blob/File entry under the same FormData key. The
|
|
88
|
+
* user-controlled `type`/`name` are percent-encoded before joining so an embedded
|
|
89
|
+
* `:` cannot shift the field boundaries and collide two distinct files (e.g.
|
|
90
|
+
* {name:"a:b",type:""} vs {name:"b",type:":a"}). The result is stable across
|
|
91
|
+
* identical arg sets.
|
|
43
92
|
*/
|
|
44
|
-
async function replyToCacheKey(
|
|
93
|
+
export async function replyToCacheKey(
|
|
94
|
+
encoded: string | FormData,
|
|
95
|
+
): Promise<string> {
|
|
45
96
|
if (typeof encoded === "string") return encoded;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
97
|
+
|
|
98
|
+
// Snapshot entries synchronously (forEach avoids relying on FormData's
|
|
99
|
+
// iterator typings), then fold any Blob/File bytes asynchronously.
|
|
100
|
+
const raw: [string, FormDataEntryValue][] = [];
|
|
101
|
+
encoded.forEach((value, key) => {
|
|
102
|
+
raw.push([key, value]);
|
|
103
|
+
});
|
|
104
|
+
const pairs: [string, string][] = [];
|
|
105
|
+
for (const [key, value] of raw) {
|
|
106
|
+
if (typeof value === "string") {
|
|
107
|
+
// Type-tag strings with `s:` so a string equal to a blob token (e.g.
|
|
108
|
+
// `b:4::a:b:<hash>`) cannot collide with a Blob/File entry under the same
|
|
109
|
+
// key (which carries the `b:` tag below).
|
|
110
|
+
pairs.push([key, "s:" + value]);
|
|
111
|
+
} else {
|
|
112
|
+
// Blob/File: fold the bytes into a deterministic, boundary-free token.
|
|
113
|
+
// Percent-encode the user-controlled type/name so an embedded `:` cannot
|
|
114
|
+
// shift the `:`-delimited field boundaries and collide distinct files.
|
|
115
|
+
const buf = await value.arrayBuffer();
|
|
116
|
+
const hash = djb2HexBytes(new Uint8Array(buf));
|
|
117
|
+
const name = "name" in value ? value.name : "";
|
|
118
|
+
const encType = encodeURIComponent(value.type);
|
|
119
|
+
const encName = encodeURIComponent(name);
|
|
120
|
+
pairs.push([key, `b:${value.size}:${encType}:${encName}:${hash}`]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return encodeKV(pairs, { sort: true });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Cached-fn ids already warned about running uncached under a test runner, so
|
|
127
|
+
// the test-ergonomics warning fires once per fn rather than once per call.
|
|
128
|
+
const warnedUncachedUnderTest = new Set<string>();
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Fast-path cache-key builder for JSON-safe key args. Returns a deterministic
|
|
132
|
+
* string (object keys recursively sorted so insertion order can't change the
|
|
133
|
+
* key) when EVERY part is a primitive or a plain object/array of the same, and
|
|
134
|
+
* `undefined` otherwise so the caller falls back to the Flight reply encoder.
|
|
135
|
+
*
|
|
136
|
+
* Strings are always quoted via JSON.stringify, so they can never collide with
|
|
137
|
+
* the bareword encodings of null/true/false/undefined/numbers. Anything the
|
|
138
|
+
* reply encoder must handle instead — functions, symbols, bigint, non-finite
|
|
139
|
+
* numbers, Dates, Maps, class instances, promises, and React elements (whose
|
|
140
|
+
* `$$typeof` symbol value trips the symbol reject) — yields `undefined`.
|
|
141
|
+
*/
|
|
142
|
+
function jsonSafeKeyPart(value: unknown): string | undefined {
|
|
143
|
+
if (value === null) return "null";
|
|
144
|
+
switch (typeof value) {
|
|
145
|
+
case "string":
|
|
146
|
+
return JSON.stringify(value);
|
|
147
|
+
case "boolean":
|
|
148
|
+
return value ? "true" : "false";
|
|
149
|
+
case "number":
|
|
150
|
+
return Number.isFinite(value) ? String(value) : undefined;
|
|
151
|
+
case "undefined":
|
|
152
|
+
return "undefined";
|
|
153
|
+
case "object": {
|
|
154
|
+
if (Array.isArray(value)) {
|
|
155
|
+
const parts: string[] = [];
|
|
156
|
+
for (const item of value) {
|
|
157
|
+
const encoded = jsonSafeKeyPart(item);
|
|
158
|
+
if (encoded === undefined) return undefined;
|
|
159
|
+
parts.push(encoded);
|
|
160
|
+
}
|
|
161
|
+
return `[${parts.join(",")}]`;
|
|
162
|
+
}
|
|
163
|
+
// Only plain objects (Object.prototype or null proto) are fast-path safe.
|
|
164
|
+
// Dates, Maps, class instances, promises, etc. carry a different prototype
|
|
165
|
+
// and fall back to the encoder.
|
|
166
|
+
const proto = Object.getPrototypeOf(value);
|
|
167
|
+
if (proto !== Object.prototype && proto !== null) return undefined;
|
|
168
|
+
const obj = value as Record<string, unknown>;
|
|
169
|
+
const parts: string[] = [];
|
|
170
|
+
for (const key of Object.keys(obj).sort()) {
|
|
171
|
+
const encoded = jsonSafeKeyPart(obj[key]);
|
|
172
|
+
if (encoded === undefined) return undefined;
|
|
173
|
+
parts.push(`${JSON.stringify(key)}:${encoded}`);
|
|
174
|
+
}
|
|
175
|
+
return `{${parts.join(",")}}`;
|
|
176
|
+
}
|
|
177
|
+
default:
|
|
178
|
+
// bigint, symbol, function
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
49
181
|
}
|
|
50
182
|
|
|
183
|
+
/**
|
|
184
|
+
* The serialized product of one "use cache" execution: exactly what the store
|
|
185
|
+
* write persists. Followers that dedup onto an in-flight execution (see
|
|
186
|
+
* inFlightExecutions) await this and serve it as a synthetic cache hit — each
|
|
187
|
+
* deserializing its OWN copy of `serialized` and replaying `handles`/`tags`
|
|
188
|
+
* against its OWN request. A deserialized result object is never shared.
|
|
189
|
+
*/
|
|
190
|
+
interface CacheEnvelope {
|
|
191
|
+
/** RSC-serialized return value (never null — a null serialize rejects). */
|
|
192
|
+
serialized: string;
|
|
193
|
+
/** Merged profile/DSL + runtime cacheTag() tags. */
|
|
194
|
+
tags: string[];
|
|
195
|
+
/** RSC-encoded handle blob captured during execution, if any. */
|
|
196
|
+
handles?: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* In-flight "use cache" executions keyed by cache key. When N concurrent calls
|
|
201
|
+
* miss on the same key, only the first (the leader) runs the function; the rest
|
|
202
|
+
* await its {@link CacheEnvelope} and serve it as a synthetic hit rather than
|
|
203
|
+
* re-running the function and re-writing the store. Isolate-scoped (module
|
|
204
|
+
* singleton), and cleared for a key as soon as the leader settles: a rejected
|
|
205
|
+
* leader (function threw, or the result was not serializable) propagates to
|
|
206
|
+
* current waiters, which then retry fresh.
|
|
207
|
+
*/
|
|
208
|
+
const inFlightExecutions = new Map<string, Promise<CacheEnvelope>>();
|
|
209
|
+
|
|
51
210
|
// ============================================================================
|
|
52
211
|
// Core: registerCachedFunction
|
|
53
212
|
// ============================================================================
|
|
@@ -70,9 +229,38 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
70
229
|
const store = requestCtx?._cacheStore;
|
|
71
230
|
const resolvedProfileName = profileName || "default";
|
|
72
231
|
|
|
73
|
-
// Bypass: no store or no getItem support
|
|
232
|
+
// Bypass: no store or no getItem support. Still run inside a tag scope so a
|
|
233
|
+
// cacheTag() call inside the function degrades to a no-op rather than
|
|
234
|
+
// throwing "must be called inside a use cache function" - adopting cacheTag()
|
|
235
|
+
// must not hard-fail in apps/tests without an item-capable cache configured.
|
|
236
|
+
// Note: the INSIDE_CACHE_EXEC guard (cookies()/headers()/ctx.set() rejection)
|
|
237
|
+
// is intentionally NOT stamped here. It is a cached-path-only check; in the
|
|
238
|
+
// bypass the body actually executes, so the guarded side effects take effect
|
|
239
|
+
// and nothing is lost on a (non-existent) hit. Same applies to the
|
|
240
|
+
// non-serializable-args bypass below.
|
|
74
241
|
if (!store?.getItem) {
|
|
75
|
-
|
|
242
|
+
// Test-ergonomics guard: under a test runner, a "use cache" function that
|
|
243
|
+
// executes with no item-capable store seeded is exercising the UNCACHED
|
|
244
|
+
// path — a green test that proves nothing about caching. Warn once per fn
|
|
245
|
+
// id so the author knows to seed a cacheStore. Advisory (never throws), so
|
|
246
|
+
// a test that DELIBERATELY runs uncached is unaffected. Gated on the test
|
|
247
|
+
// runner (process.env.VITEST, not folded) so production never evaluates it.
|
|
248
|
+
if (isUnderTestRunner() && !warnedUncachedUnderTest.has(id)) {
|
|
249
|
+
warnedUncachedUnderTest.add(id);
|
|
250
|
+
console.warn(
|
|
251
|
+
`[rango] "use cache" function "${id}" executed but no cacheStore was ` +
|
|
252
|
+
`seeded; the cached path is NOT under test (it ran uncached). Pass ` +
|
|
253
|
+
`{ cacheStore, cacheProfiles } to runLoader/runMiddleware/renderHandler/` +
|
|
254
|
+
`runInRequestContext (or configure createRouter({ cache }) for dispatch) ` +
|
|
255
|
+
`to exercise it.`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
const scoped = runWithCacheTagScope(() => fn.apply(this, args));
|
|
259
|
+
const result = await scoped.result;
|
|
260
|
+
// Still record the runtime tags into the request set so a cacheTag() in an
|
|
261
|
+
// uncached function tags the document, even with no item-capable store.
|
|
262
|
+
recordRequestTags(scoped.tags, requestCtx);
|
|
263
|
+
return result;
|
|
76
264
|
}
|
|
77
265
|
|
|
78
266
|
// Resolve profile strictly from request-scoped config (set by the
|
|
@@ -145,57 +333,95 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
145
333
|
let cacheKey: string;
|
|
146
334
|
try {
|
|
147
335
|
if (keyArgs.length > 0) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
336
|
+
// Fast path: when every key arg is JSON-safe, build the key with a
|
|
337
|
+
// deterministic stable-stringify and skip encodeReply (the Flight reply
|
|
338
|
+
// encoder runs on EVERY call, including hits). The `:j:` namespace keeps
|
|
339
|
+
// these keys disjoint from the encoder path so the two can never collide
|
|
340
|
+
// for one fn id. Entries cached under the old encoder key for JSON-safe
|
|
341
|
+
// args cold-start once after this upgrade.
|
|
342
|
+
const fastKey = jsonSafeKeyPart(keyArgs);
|
|
343
|
+
if (fastKey !== undefined) {
|
|
344
|
+
cacheKey = `use-cache:${id}:j:${fastKey}`;
|
|
345
|
+
} else {
|
|
346
|
+
const tempRefs = createClientTemporaryReferenceSet();
|
|
347
|
+
const encoded = await encodeReply(keyArgs as unknown[], {
|
|
348
|
+
temporaryReferences: tempRefs,
|
|
349
|
+
});
|
|
350
|
+
const argsKey = await replyToCacheKey(encoded);
|
|
351
|
+
cacheKey = `use-cache:${id}:${argsKey}`;
|
|
352
|
+
}
|
|
154
353
|
} else {
|
|
155
354
|
cacheKey = `use-cache:${id}`;
|
|
156
355
|
}
|
|
157
356
|
} catch {
|
|
158
|
-
// Non-serializable args: run uncached
|
|
159
|
-
|
|
357
|
+
// Non-serializable args: run uncached (within a tag scope so cacheTag()
|
|
358
|
+
// still does not throw). Record runtime tags so the document union still
|
|
359
|
+
// sees them even though this call is not itself cached.
|
|
360
|
+
const scoped = runWithCacheTagScope(() => fn.apply(this, args));
|
|
361
|
+
const result = await scoped.result;
|
|
362
|
+
recordRequestTags(scoped.tags, requestCtx);
|
|
363
|
+
return result;
|
|
160
364
|
}
|
|
161
365
|
|
|
162
366
|
// Cache lookup
|
|
163
367
|
const cached = await store.getItem(cacheKey);
|
|
164
368
|
|
|
369
|
+
// Serve a cached entry on the hit path: deserialize the stored value,
|
|
370
|
+
// replay handle data (gated on tainted args), and surface the entry's tags
|
|
371
|
+
// to the request set (the function did not re-run, so its runtime cacheTag()
|
|
372
|
+
// tags are only available from the stored entry). Shared by the fresh-hit
|
|
373
|
+
// and stale-hit branches; the only divergence is the stale branch scheduling
|
|
374
|
+
// background revalidation, which it does after this returns.
|
|
375
|
+
const serveCached = async (entry: CacheItemResult): Promise<any> => {
|
|
376
|
+
const result = await deserializeResult(entry.value);
|
|
377
|
+
if (entry.handles && hasTaintedArgs) {
|
|
378
|
+
const handleStore = requestCtx?._handleStore;
|
|
379
|
+
if (handleStore) {
|
|
380
|
+
const r = await decodeHandles(entry.handles);
|
|
381
|
+
if (r) restoreHandles(r, handleStore);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
recordRequestTags(entry.tags, requestCtx);
|
|
385
|
+
return result;
|
|
386
|
+
};
|
|
387
|
+
|
|
165
388
|
if (cached && !cached.shouldRevalidate) {
|
|
166
389
|
// Fresh hit: deserialize and return
|
|
167
390
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// Deserialization failed, fall through to fresh execution
|
|
391
|
+
return await serveCached(cached);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
// The stored value is corrupt/partial (failed RSC deserialize). Report
|
|
394
|
+
// it, then fall through to fresh execution - the miss path below re-runs
|
|
395
|
+
// and setItem() overwrites the faulty entry under the same key (self-heal).
|
|
396
|
+
reportCacheError(
|
|
397
|
+
error,
|
|
398
|
+
"cache-corrupt",
|
|
399
|
+
`[use cache] "${id}" fresh-hit`,
|
|
400
|
+
);
|
|
179
401
|
}
|
|
180
402
|
}
|
|
181
403
|
|
|
182
|
-
|
|
404
|
+
// foregroundOnAction (opt-in; see CacheProfile.foregroundOnAction): during an
|
|
405
|
+
// action's revalidation render, a stale entry falls through to the foreground
|
|
406
|
+
// miss path below instead of SWR. The flag is set by revalidateAfterAction.
|
|
407
|
+
const foregroundOnActionRevalidation =
|
|
408
|
+
requestCtx?._inActionRevalidation === true &&
|
|
409
|
+
profile.foregroundOnAction === true;
|
|
410
|
+
if (cached?.shouldRevalidate && !foregroundOnActionRevalidation) {
|
|
183
411
|
// Stale hit: return stale value, revalidate in background
|
|
184
412
|
try {
|
|
185
|
-
const result = await
|
|
186
|
-
if (cached.handles && hasTaintedArgs) {
|
|
187
|
-
const handleStore = requestCtx?._handleStore;
|
|
188
|
-
if (handleStore) {
|
|
189
|
-
restoreHandles(cached.handles, handleStore);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
413
|
+
const result = await serveCached(cached);
|
|
192
414
|
// Background revalidation — must capture handles if tainted args present.
|
|
193
415
|
// Use an isolated handle store so background pushes don't pollute the
|
|
194
416
|
// live response or throw LateHandlePushError on the completed store.
|
|
195
417
|
// Same isolation pattern as route-level background-revalidation.ts.
|
|
196
418
|
runBackground(requestCtx, async () => {
|
|
197
|
-
//
|
|
198
|
-
//
|
|
419
|
+
// The closure-captured requestCtx is reused for the framework's own
|
|
420
|
+
// reads (handle store swap, error reporting) AND, below, to
|
|
421
|
+
// re-establish the request-context ALS around the user fn. ALS context
|
|
422
|
+
// may be gone inside waitUntil: on workerd a waitUntil task runs
|
|
423
|
+
// detached from the request's I/O context, so getRequestContext()
|
|
424
|
+
// inside the cached body would otherwise throw.
|
|
199
425
|
let originalHandleStore:
|
|
200
426
|
| ReturnType<typeof createHandleStore>
|
|
201
427
|
| undefined;
|
|
@@ -238,20 +464,49 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
238
464
|
}
|
|
239
465
|
|
|
240
466
|
try {
|
|
241
|
-
|
|
467
|
+
// Re-establish the request-context ALS so a "use cache" body that
|
|
468
|
+
// reads the ambient getRequestContext() (e.g.
|
|
469
|
+
// getRequestContext().env.ApiKey) resolves during the background
|
|
470
|
+
// revalidation instead of throwing "called outside of a request
|
|
471
|
+
// context". runWithRequestContext sets the store for fn's
|
|
472
|
+
// synchronous kickoff; its async continuations inherit it.
|
|
473
|
+
const scoped = runWithRequestContext(requestCtx, () =>
|
|
474
|
+
runWithCacheTagScope(() => fn.apply(this, args)),
|
|
475
|
+
);
|
|
476
|
+
const freshResult = await scoped.result;
|
|
242
477
|
bgStopCapture?.();
|
|
478
|
+
// Merge profile/DSL tags with runtime cacheTag() tags, read after
|
|
479
|
+
// awaiting so post-await cacheTag() calls are included. Normalize
|
|
480
|
+
// (drops empty profile tags, matching the invalidate path) + dedupe.
|
|
481
|
+
const freshTags = [
|
|
482
|
+
...new Set(
|
|
483
|
+
normalizeTags([...(profile.tags ?? []), ...scoped.tags]),
|
|
484
|
+
),
|
|
485
|
+
];
|
|
486
|
+
recordRequestTags(freshTags, requestCtx);
|
|
243
487
|
const serialized = await serializeResult(freshResult);
|
|
244
488
|
if (serialized !== null) {
|
|
489
|
+
const encodedHandles = bgCapture?.data
|
|
490
|
+
? await encodeHandles(bgCapture.data)
|
|
491
|
+
: undefined;
|
|
245
492
|
await store.setItem!(cacheKey, serialized, {
|
|
246
|
-
handles:
|
|
493
|
+
handles: encodedHandles,
|
|
247
494
|
ttl: profile.ttl,
|
|
248
495
|
swr: profile.swr,
|
|
249
|
-
tags:
|
|
496
|
+
tags: freshTags.length > 0 ? freshTags : undefined,
|
|
250
497
|
});
|
|
251
498
|
}
|
|
252
499
|
} catch (bgError) {
|
|
253
500
|
bgStopCapture?.();
|
|
254
|
-
requestCtx
|
|
501
|
+
// Pass requestCtx explicitly: this runs in a detached background
|
|
502
|
+
// task where the ALS context is gone, so onError can only fire if
|
|
503
|
+
// we hand it the context captured up front.
|
|
504
|
+
reportCacheError(
|
|
505
|
+
bgError,
|
|
506
|
+
"stale-revalidation",
|
|
507
|
+
"[use cache] background revalidation failed",
|
|
508
|
+
requestCtx,
|
|
509
|
+
);
|
|
255
510
|
} finally {
|
|
256
511
|
for (const arg of bgTaintedArgs) {
|
|
257
512
|
unstampCacheExec(arg as object);
|
|
@@ -263,12 +518,76 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
263
518
|
}
|
|
264
519
|
});
|
|
265
520
|
return result;
|
|
521
|
+
} catch (error) {
|
|
522
|
+
// Stale value is corrupt/partial; report and fall through to a fresh
|
|
523
|
+
// execution, which overwrites the faulty entry under the same key.
|
|
524
|
+
reportCacheError(
|
|
525
|
+
error,
|
|
526
|
+
"cache-corrupt",
|
|
527
|
+
`[use cache] "${id}" stale-hit`,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Cache miss.
|
|
533
|
+
//
|
|
534
|
+
// In-flight dedup: if a concurrent call for this key is already executing,
|
|
535
|
+
// await its envelope and serve it as a synthetic hit rather than re-running
|
|
536
|
+
// the function. Each follower deserializes its OWN copy, replays handles
|
|
537
|
+
// against its OWN handle store (gated on ITS hasTaintedArgs), and records
|
|
538
|
+
// tags into its OWN request — no deserialized result is shared across
|
|
539
|
+
// requests. The store write stays exactly once (the leader's).
|
|
540
|
+
const existing = inFlightExecutions.get(cacheKey);
|
|
541
|
+
if (existing) {
|
|
542
|
+
let envelope: CacheEnvelope | undefined;
|
|
543
|
+
try {
|
|
544
|
+
envelope = await existing;
|
|
266
545
|
} catch {
|
|
267
|
-
//
|
|
546
|
+
// Leader rejected (function threw or its result was not serializable);
|
|
547
|
+
// its map entry is already cleared, so fall through to a fresh run.
|
|
548
|
+
envelope = undefined;
|
|
549
|
+
}
|
|
550
|
+
if (envelope) {
|
|
551
|
+
try {
|
|
552
|
+
return await serveCached({
|
|
553
|
+
value: envelope.serialized,
|
|
554
|
+
handles: envelope.handles,
|
|
555
|
+
tags: envelope.tags,
|
|
556
|
+
shouldRevalidate: false,
|
|
557
|
+
});
|
|
558
|
+
} catch (error) {
|
|
559
|
+
reportCacheError(
|
|
560
|
+
error,
|
|
561
|
+
"cache-corrupt",
|
|
562
|
+
`[use cache] "${id}" inflight-hit`,
|
|
563
|
+
);
|
|
564
|
+
// Fall through to a fresh execution below.
|
|
565
|
+
}
|
|
268
566
|
}
|
|
269
567
|
}
|
|
270
568
|
|
|
271
|
-
//
|
|
569
|
+
// This call becomes the leader. Register a deferred envelope so concurrent
|
|
570
|
+
// callers dedup onto it; it is resolved/rejected exactly once below (or on a
|
|
571
|
+
// function throw). clearSelf only deletes the map slot if it still holds
|
|
572
|
+
// THIS promise, so a fall-through retry (rare rejected-leader path) can't
|
|
573
|
+
// evict a newer leader's entry.
|
|
574
|
+
let resolveEnvelope!: (env: CacheEnvelope) => void;
|
|
575
|
+
let rejectEnvelope!: (err: unknown) => void;
|
|
576
|
+
const envelopePromise = new Promise<CacheEnvelope>((res, rej) => {
|
|
577
|
+
resolveEnvelope = res;
|
|
578
|
+
rejectEnvelope = rej;
|
|
579
|
+
});
|
|
580
|
+
// Followers attach their own catch; guard the map's own reference so a
|
|
581
|
+
// rejected envelope with no waiter is not an unhandled rejection.
|
|
582
|
+
envelopePromise.catch(() => {});
|
|
583
|
+
inFlightExecutions.set(cacheKey, envelopePromise);
|
|
584
|
+
const clearSelf = (): void => {
|
|
585
|
+
if (inFlightExecutions.get(cacheKey) === envelopePromise) {
|
|
586
|
+
inFlightExecutions.delete(cacheKey);
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
// execute, serialize, store
|
|
272
591
|
const handleStore = hasTaintedArgs ? requestCtx?._handleStore : undefined;
|
|
273
592
|
let capture: HandleCapture | undefined;
|
|
274
593
|
let stopCapture: (() => void) | undefined;
|
|
@@ -297,8 +616,16 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
297
616
|
}
|
|
298
617
|
|
|
299
618
|
let result: any;
|
|
619
|
+
let scoped: ReturnType<typeof runWithCacheTagScope>;
|
|
300
620
|
try {
|
|
301
|
-
|
|
621
|
+
scoped = runWithCacheTagScope(() => fn.apply(this, args));
|
|
622
|
+
result = await scoped.result;
|
|
623
|
+
} catch (execError) {
|
|
624
|
+
// The function threw: drop the in-flight entry and reject any waiters so
|
|
625
|
+
// they retry fresh, then propagate to this caller.
|
|
626
|
+
clearSelf();
|
|
627
|
+
rejectEnvelope(execError);
|
|
628
|
+
throw execError;
|
|
302
629
|
} finally {
|
|
303
630
|
// Decrement ref count; symbol is deleted when it reaches zero
|
|
304
631
|
for (const arg of taintedArgs) {
|
|
@@ -311,25 +638,61 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
311
638
|
stopCapture?.();
|
|
312
639
|
}
|
|
313
640
|
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
|
|
641
|
+
// Merge profile/DSL tags with runtime cacheTag() tags. Read scoped.tags
|
|
642
|
+
// after awaiting result so post-await cacheTag() calls are included.
|
|
643
|
+
// Normalize (drops empty profile tags, matching the invalidate path) + dedupe.
|
|
644
|
+
const allTags = [
|
|
645
|
+
...new Set(normalizeTags([...(profile.tags ?? []), ...scoped!.tags])),
|
|
646
|
+
];
|
|
647
|
+
recordRequestTags(allTags, requestCtx);
|
|
648
|
+
|
|
649
|
+
// Serialize + encode handles ONCE, resolve the in-flight envelope so any
|
|
650
|
+
// concurrent followers can serve a synthetic hit, then persist to the store.
|
|
651
|
+
// Fully non-blocking when waitUntil is available — the leader's response
|
|
652
|
+
// waits on neither serialization nor the store write.
|
|
653
|
+
const finalizeAndWrite = async (): Promise<void> => {
|
|
654
|
+
let serialized: string | null;
|
|
655
|
+
let encodedHandles: string | undefined;
|
|
317
656
|
try {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
await
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
657
|
+
serialized = await serializeResult(result);
|
|
658
|
+
encodedHandles = capture?.data
|
|
659
|
+
? await encodeHandles(capture.data)
|
|
660
|
+
: undefined;
|
|
661
|
+
} catch (buildError) {
|
|
662
|
+
// Serialize/handle-encode failed: no envelope for followers (they run
|
|
663
|
+
// fresh) and nothing to write.
|
|
664
|
+
clearSelf();
|
|
665
|
+
rejectEnvelope(buildError);
|
|
666
|
+
requestCtx?._reportBackgroundError?.(buildError, "cache-write");
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
clearSelf();
|
|
670
|
+
if (serialized === null) {
|
|
671
|
+
// Non-serializable result: no store write (matches the prior silent
|
|
672
|
+
// skip); reject so any waiter falls through to a fresh execution.
|
|
673
|
+
rejectEnvelope(
|
|
674
|
+
new Error(
|
|
675
|
+
`[use cache] "${id}" result is not serializable; not cached`,
|
|
676
|
+
),
|
|
677
|
+
);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
// Hand followers the envelope before the store write so a slow/failed
|
|
681
|
+
// write never stalls them.
|
|
682
|
+
resolveEnvelope({ serialized, tags: allTags, handles: encodedHandles });
|
|
683
|
+
try {
|
|
684
|
+
await store.setItem!(cacheKey, serialized, {
|
|
685
|
+
handles: encodedHandles,
|
|
686
|
+
ttl: profile.ttl,
|
|
687
|
+
swr: profile.swr,
|
|
688
|
+
tags: allTags.length > 0 ? allTags : undefined,
|
|
689
|
+
});
|
|
327
690
|
} catch (writeError) {
|
|
328
691
|
requestCtx?._reportBackgroundError?.(writeError, "cache-write");
|
|
329
692
|
}
|
|
330
693
|
};
|
|
331
694
|
|
|
332
|
-
await runBackground(requestCtx,
|
|
695
|
+
await runBackground(requestCtx, finalizeAndWrite, true);
|
|
333
696
|
|
|
334
697
|
return result;
|
|
335
698
|
};
|