@rangojs/router 0.0.0-experimental.a769fbe7 → 0.0.0-experimental.ac99d918
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -43
- package/dist/bin/rango.js +279 -102
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3313 -1160
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +62 -11
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +63 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +222 -30
- package/skills/caching/SKILL.md +263 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +250 -30
- package/skills/host-router/SKILL.md +124 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +49 -5
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +223 -9
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +77 -7
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +250 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +66 -5
- package/skills/router-setup/SKILL.md +38 -3
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +319 -27
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/vercel/SKILL.md +107 -0
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +1 -66
- 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/app-version.ts +14 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +81 -147
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +66 -14
- package/src/browser/navigation-client.ts +172 -109
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/partial-update.ts +79 -93
- package/src/browser/prefetch/cache.ts +180 -62
- package/src/browser/prefetch/fetch.ts +252 -39
- package/src/browser/prefetch/queue.ts +42 -8
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +72 -10
- package/src/browser/react/NavigationProvider.tsx +83 -31
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +49 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -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 +23 -69
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +22 -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 +46 -11
- 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 +52 -1
- package/src/browser/rsc-router.tsx +111 -24
- package/src/browser/scroll-restoration.ts +29 -19
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +176 -62
- package/src/browser/types.ts +60 -11
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +6 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +137 -32
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +333 -94
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +134 -32
- package/src/cache/cache-scope.ts +100 -74
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2256 -241
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +61 -20
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +22 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +108 -290
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +17 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/browser.ts +0 -1
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- 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 +69 -10
- package/src/index.ts +112 -14
- package/src/internal-debug.ts +2 -4
- 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/network-error-thrower.tsx +1 -6
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +413 -275
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +107 -32
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +50 -8
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +108 -9
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +46 -30
- package/src/router/intercept-resolution.ts +23 -23
- package/src/router/lazy-includes.ts +15 -52
- package/src/router/loader-resolution.ts +207 -30
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +40 -42
- package/src/router/match-api.ts +120 -204
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +161 -262
- package/src/router/match-middleware/cache-store.ts +3 -33
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +45 -14
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +87 -39
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -140
- package/src/router/middleware.ts +169 -140
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +109 -63
- package/src/router/prerender-match.ts +190 -54
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +63 -55
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +100 -35
- package/src/router/router-options.ts +91 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +119 -65
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +40 -37
- package/src/router/segment-resolution/revalidation.ts +329 -305
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +91 -46
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +128 -42
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +492 -409
- package/src/rsc/helpers.ts +162 -46
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +18 -3
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +28 -4
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +90 -63
- package/src/rsc/rsc-rendering.ts +53 -56
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +74 -69
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +22 -9
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +134 -0
- package/src/segment-system.tsx +208 -201
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +211 -52
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +26 -24
- package/src/server/loader-registry.ts +10 -28
- package/src/server/request-context.ts +289 -124
- package/src/ssr/index.tsx +22 -15
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +618 -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 +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -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 +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +124 -70
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +40 -11
- package/src/types/request-scope.ts +107 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +12 -7
- package/src/types/segments.ts +36 -15
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +192 -99
- 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 +51 -4
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin-types.ts +236 -6
- package/src/vite/plugins/cjs-to-esm.ts +8 -18
- 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 +11 -50
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
- package/src/vite/plugins/expose-internal-ids.ts +554 -317
- package/src/vite/plugins/performance-tracks.ts +65 -207
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +32 -23
- package/src/vite/plugins/virtual-entries.ts +46 -17
- package/src/vite/rango.ts +207 -125
- package/src/vite/router-discovery.ts +931 -133
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- 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/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 +27 -29
- package/src/vite/utils/shared-utils.ts +97 -44
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/debug-channel.ts +0 -93
- 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/router/middleware-cookies.ts +0 -55
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Allocation-light, linear-time source scanning for the build-time scanners.
|
|
2
|
+
//
|
|
3
|
+
// The router-file scanner, the HMR relevance check, and the unsupported-shape
|
|
4
|
+
// warning all need to know whether a token like `createRouter(` / `createLoader(`
|
|
5
|
+
// appears in REAL code versus inside a comment or string literal. Rather than
|
|
6
|
+
// build a full comment/string-stripped copy of the source (which on a large
|
|
7
|
+
// file allocates an O(n) string plus, naively, a per-char array), these helpers
|
|
8
|
+
// run the regex over the whole source ONCE (the engine sweeps left-to-right,
|
|
9
|
+
// O(n)) and classify each match's offset with a forward, O(1)-memory cursor that
|
|
10
|
+
// advances monotonically across the source.
|
|
11
|
+
//
|
|
12
|
+
// Time: O(n) — one native regex sweep plus one forward classification pass.
|
|
13
|
+
// Memory: O(1) for the boolean check; O(#matches) for the index list. No
|
|
14
|
+
// stripped copy and no per-char array are ever materialized.
|
|
15
|
+
//
|
|
16
|
+
// Pragmatic scanner, not a full tokenizer: regex literals are not special-cased
|
|
17
|
+
// (a target token inside one is implausible) and template interpolations are
|
|
18
|
+
// treated as opaque string content. One intentional consequence: a token whose
|
|
19
|
+
// match would only complete by treating an interleaved comment as whitespace
|
|
20
|
+
// (e.g. `createRouter /* x */ (`) is not detected — real calls never interleave
|
|
21
|
+
// a comment between the callee and its arguments.
|
|
22
|
+
|
|
23
|
+
// JS line terminators end a `//` comment: LF, CR, LS (U+2028), PS (U+2029).
|
|
24
|
+
function isLineTerminator(ch: string): boolean {
|
|
25
|
+
const c = ch.charCodeAt(0);
|
|
26
|
+
// LF, CR, LS (U+2028), PS (U+2029)
|
|
27
|
+
return c === 10 || c === 13 || c === 0x2028 || c === 0x2029;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build a classifier that answers "is offset `q` in code (not a comment or
|
|
32
|
+
* string)?" for STRICTLY INCREASING `q`. The internal cursor only moves forward,
|
|
33
|
+
* so a full left-to-right sequence of queries costs O(n) total with O(1) memory.
|
|
34
|
+
*/
|
|
35
|
+
function makeCodeClassifier(code: string): (q: number) => boolean {
|
|
36
|
+
const n = code.length;
|
|
37
|
+
let i = 0; // forward cursor: everything before `i` is already classified
|
|
38
|
+
let skipStart = -1; // last detected comment/string region (cache)
|
|
39
|
+
let skipEnd = -1;
|
|
40
|
+
|
|
41
|
+
return (q: number): boolean => {
|
|
42
|
+
if (q >= skipStart && q < skipEnd) return false; // q in the cached region
|
|
43
|
+
while (i < n && i <= q) {
|
|
44
|
+
const c = code[i];
|
|
45
|
+
const d = i + 1 < n ? code[i + 1] : "";
|
|
46
|
+
let end = -1;
|
|
47
|
+
if (c === "/" && d === "/") {
|
|
48
|
+
let j = i + 2;
|
|
49
|
+
while (j < n && !isLineTerminator(code[j])) j++;
|
|
50
|
+
end = j;
|
|
51
|
+
} else if (c === "/" && d === "*") {
|
|
52
|
+
let j = i + 2;
|
|
53
|
+
while (j < n && !(code[j] === "*" && code[j + 1] === "/")) j++;
|
|
54
|
+
end = Math.min(n, j + 2);
|
|
55
|
+
} else if (c === '"' || c === "'" || c === "`") {
|
|
56
|
+
let j = i + 1;
|
|
57
|
+
while (j < n) {
|
|
58
|
+
if (code[j] === "\\") {
|
|
59
|
+
j += 2;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (code[j] === c) {
|
|
63
|
+
j++;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
j++;
|
|
67
|
+
}
|
|
68
|
+
end = j;
|
|
69
|
+
}
|
|
70
|
+
if (end >= 0) {
|
|
71
|
+
// Comment/string region [i, end). `q >= i` here (loop condition).
|
|
72
|
+
if (q < end) {
|
|
73
|
+
skipStart = i;
|
|
74
|
+
skipEnd = end;
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
i = end;
|
|
78
|
+
} else {
|
|
79
|
+
i++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return true; // reached q in code mode
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Index of the first match of `pattern` that occurs in code (not in a comment
|
|
88
|
+
* or string), or -1. `pattern` MUST be a global (`/g`) regex. Single native
|
|
89
|
+
* regex sweep with early-exit; O(1) extra memory.
|
|
90
|
+
*/
|
|
91
|
+
export function firstCodeMatchIndex(code: string, pattern: RegExp): number {
|
|
92
|
+
const inCode = makeCodeClassifier(code);
|
|
93
|
+
pattern.lastIndex = 0;
|
|
94
|
+
let m: RegExpExecArray | null;
|
|
95
|
+
while ((m = pattern.exec(code)) !== null) {
|
|
96
|
+
if (inCode(m.index)) return m.index;
|
|
97
|
+
if (pattern.lastIndex <= m.index) pattern.lastIndex = m.index + 1;
|
|
98
|
+
}
|
|
99
|
+
return -1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Byte offsets of every match of `pattern` that occurs in code (not in a
|
|
104
|
+
* comment or string). `pattern` MUST be a global (`/g`) regex. Each offset is
|
|
105
|
+
* the match start — the same byte offset a raw `pattern.exec` reports. O(n)
|
|
106
|
+
* time, O(#matches) memory.
|
|
107
|
+
*/
|
|
108
|
+
export function codeMatchIndices(code: string, pattern: RegExp): number[] {
|
|
109
|
+
const inCode = makeCodeClassifier(code);
|
|
110
|
+
const indices: number[] = [];
|
|
111
|
+
pattern.lastIndex = 0;
|
|
112
|
+
let m: RegExpExecArray | null;
|
|
113
|
+
while ((m = pattern.exec(code)) !== null) {
|
|
114
|
+
if (inCode(m.index)) indices.push(m.index);
|
|
115
|
+
if (pattern.lastIndex <= m.index) pattern.lastIndex = m.index + 1;
|
|
116
|
+
}
|
|
117
|
+
return indices;
|
|
118
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import {
|
|
4
4
|
generateRouteTypesSource,
|
|
5
|
-
|
|
5
|
+
genFileTsPath,
|
|
6
|
+
resolveSearchSchemas,
|
|
6
7
|
} from "./generate-route-types.ts";
|
|
7
8
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
8
9
|
|
|
@@ -175,25 +176,13 @@ export async function discoverAndWriteRouteTypes(
|
|
|
175
176
|
);
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const filtered: Record<string, Record<string, string>> = {};
|
|
184
|
-
for (const name of Object.keys(routeManifest)) {
|
|
185
|
-
const schema = staticParsed.searchSchemas[name];
|
|
186
|
-
if (schema) filtered[name] = schema;
|
|
187
|
-
}
|
|
188
|
-
if (Object.keys(filtered).length > 0) {
|
|
189
|
-
routeSearchSchemas = filtered;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
179
|
+
routeSearchSchemas = resolveSearchSchemas(
|
|
180
|
+
Object.keys(routeManifest),
|
|
181
|
+
routeSearchSchemas,
|
|
182
|
+
sourceFile,
|
|
183
|
+
);
|
|
193
184
|
|
|
194
|
-
const
|
|
195
|
-
const routerBasename = basename(sourceFile).replace(/\.(tsx?|jsx?)$/, "");
|
|
196
|
-
const outPath = join(routerDir, `${routerBasename}.named-routes.gen.ts`);
|
|
185
|
+
const outPath = genFileTsPath(sourceFile);
|
|
197
186
|
|
|
198
187
|
const source = generateRouteTypesSource(
|
|
199
188
|
routeManifest,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache error reporting.
|
|
3
|
+
*
|
|
4
|
+
* Caches are best-effort: a read failure degrades to a miss (render fresh) and a
|
|
5
|
+
* write failure degrades to a no-op - they MUST NOT throw up and fail the
|
|
6
|
+
* request. But the failure must still be LOUD: it is logged to the console (so
|
|
7
|
+
* it is visible even in a background waitUntil task or when no hook is wired)
|
|
8
|
+
* AND routed through the router's onError callback (via the request context's
|
|
9
|
+
* deduped _reportBackgroundError) so consumers can observe cache degradation in
|
|
10
|
+
* their own telemetry.
|
|
11
|
+
*
|
|
12
|
+
* The one deliberate exception is the invalidation WRITE verb (updateTag ->
|
|
13
|
+
* store.invalidateTags): a failed durable marker write is rejected so an awaited
|
|
14
|
+
* updateTag() surfaces it (read-your-own-writes honesty). That is not a data
|
|
15
|
+
* read/write and does not go through this helper's swallow-and-degrade path.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { _getRequestContext } from "../server/request-context.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Minimal shape of a request context for error reporting. Passed explicitly by
|
|
22
|
+
* background tasks (waitUntil) where the ALS context is already gone, so the
|
|
23
|
+
* error can still reach the router's onError. Structural to avoid importing the
|
|
24
|
+
* full RequestContext type (request-context.ts imports CacheErrorCategory from
|
|
25
|
+
* here - a mutual type-only reference).
|
|
26
|
+
*/
|
|
27
|
+
export interface CacheErrorReporter {
|
|
28
|
+
_reportBackgroundError?: (
|
|
29
|
+
error: unknown,
|
|
30
|
+
category: CacheErrorCategory,
|
|
31
|
+
) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type CacheErrorCategory =
|
|
35
|
+
/** A read failed (transient infra: KV/Cache API error). Degrade to a miss. */
|
|
36
|
+
| "cache-read"
|
|
37
|
+
/** A write failed. Degrade to a no-op (entry simply not cached). */
|
|
38
|
+
| "cache-write"
|
|
39
|
+
/** A delete/eviction failed. Best-effort. */
|
|
40
|
+
| "cache-delete"
|
|
41
|
+
/**
|
|
42
|
+
* A STORED entry could not be parsed/deserialized (partial KV read, truncated
|
|
43
|
+
* Cache API body, malformed envelope/RSC payload). The entry is faulty and is
|
|
44
|
+
* evicted so subsequent reads do not keep failing on it. Distinct from
|
|
45
|
+
* cache-read so consumers can tell corruption from a transient outage.
|
|
46
|
+
*/
|
|
47
|
+
| "cache-corrupt"
|
|
48
|
+
/** A tag-invalidation side effect failed (e.g. the eager CDN purge hook). */
|
|
49
|
+
| "cache-invalidate"
|
|
50
|
+
/**
|
|
51
|
+
* A background stale-while-revalidate refresh failed (the `"use cache"`
|
|
52
|
+
* read-through path). The stale value was already served; the refresh that
|
|
53
|
+
* would have replaced it errored.
|
|
54
|
+
*/
|
|
55
|
+
| "stale-revalidation";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Report a non-fatal cache error loudly without failing the request: always logs
|
|
59
|
+
* (label + error) and, when a request context is available, routes the error
|
|
60
|
+
* through the router's onError callback. Never throws.
|
|
61
|
+
*
|
|
62
|
+
* `ctx` is for callers running in a detached background task (waitUntil), where
|
|
63
|
+
* the ALS request context is already gone (so `_getRequestContext()` is null):
|
|
64
|
+
* they capture the context up front and pass it here so onError still fires.
|
|
65
|
+
* Foreground callers omit it and fall back to the ALS context.
|
|
66
|
+
*/
|
|
67
|
+
export function reportCacheError(
|
|
68
|
+
error: unknown,
|
|
69
|
+
category: CacheErrorCategory,
|
|
70
|
+
label: string,
|
|
71
|
+
ctx?: CacheErrorReporter,
|
|
72
|
+
): void {
|
|
73
|
+
console.error(`${label}:`, error);
|
|
74
|
+
try {
|
|
75
|
+
const target = ctx ?? _getRequestContext();
|
|
76
|
+
target?._reportBackgroundError?.(error, category);
|
|
77
|
+
} catch {
|
|
78
|
+
// Reporting must never itself break the cache path.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Run a best-effort async cache task (typically scheduled via waitUntil), catching
|
|
84
|
+
* any rejection and routing it through reportCacheError so background cache work
|
|
85
|
+
* (non-blocking L1 writes, KV persistence, L1 promotion) reports failures via
|
|
86
|
+
* onError instead of throwing or silently swallowing. Never rejects.
|
|
87
|
+
*
|
|
88
|
+
* Pass `ctx` when the task runs detached (the ALS context is gone) and the
|
|
89
|
+
* failure should still reach onError; omit it to fall back to the ALS context.
|
|
90
|
+
*
|
|
91
|
+
* @example this.waitUntil(() => reportingAsync(() => cache.put(req, res), "cache-write", "[CFCacheStore] L1 write"))
|
|
92
|
+
*/
|
|
93
|
+
export async function reportingAsync(
|
|
94
|
+
task: () => Promise<unknown>,
|
|
95
|
+
category: CacheErrorCategory,
|
|
96
|
+
label: string,
|
|
97
|
+
ctx?: CacheErrorReporter,
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
try {
|
|
100
|
+
await task();
|
|
101
|
+
} catch (error) {
|
|
102
|
+
reportCacheError(error, category, label, ctx);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { CacheDefaults, SegmentCacheStore } from "./types.js";
|
|
10
10
|
import { _getRequestContext } from "../server/request-context.js";
|
|
11
11
|
import type { RequestContext } from "../server/request-context.js";
|
|
12
|
+
import { normalizeTags } from "./cache-tag.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Default TTL for route-level cache() DSL and loader cache.
|
|
@@ -68,24 +69,6 @@ export function computeExpiration(
|
|
|
68
69
|
return { staleAt, expiresAt };
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
// ============================================================================
|
|
72
|
-
// Cache Key Resolution
|
|
73
|
-
// ============================================================================
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Resolve cache key using the 3-tier priority:
|
|
77
|
-
* 1. keyFn (full override from route/loader cache options)
|
|
78
|
-
* 2. store.keyGenerator (modifies default key)
|
|
79
|
-
* 3. defaultKey (used when neither keyFn nor keyGenerator is provided)
|
|
80
|
-
*
|
|
81
|
-
* Errors from keyFn and store.keyGenerator propagate to the caller.
|
|
82
|
-
* Cache identity is correctness-critical: if explicit key logic throws,
|
|
83
|
-
* silently remapping to a different key could cause cache collisions or
|
|
84
|
-
* serve stale/wrong data. Callers must handle the error or let it surface.
|
|
85
|
-
*
|
|
86
|
-
* Uses _getRequestContext (non-throwing) so that calls outside ALS
|
|
87
|
-
* (e.g. build-time) gracefully fall back to defaultKey.
|
|
88
|
-
*/
|
|
89
72
|
export async function resolveCacheKey(
|
|
90
73
|
keyFn: ((ctx: RequestContext) => string | Promise<string>) | undefined,
|
|
91
74
|
store: SegmentCacheStore | null,
|
|
@@ -94,32 +77,89 @@ export async function resolveCacheKey(
|
|
|
94
77
|
): Promise<string> {
|
|
95
78
|
const requestCtx = _getRequestContext();
|
|
96
79
|
|
|
97
|
-
// Priority 1: Route/loader-level key function (full override)
|
|
98
80
|
if (keyFn && requestCtx) {
|
|
99
81
|
return await keyFn(requestCtx);
|
|
100
82
|
}
|
|
101
83
|
|
|
102
|
-
// Priority 2: Store-level keyGenerator (modifies default key)
|
|
103
84
|
if (store?.keyGenerator && requestCtx) {
|
|
104
85
|
return await store.keyGenerator(requestCtx, defaultKey);
|
|
105
86
|
}
|
|
106
87
|
|
|
107
|
-
// Priority 3: Default key (no custom key logic provided)
|
|
108
88
|
return defaultKey;
|
|
109
89
|
}
|
|
110
90
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
91
|
+
export function resolveTagsOption<TEnv>(
|
|
92
|
+
tags: string[] | ((ctx: RequestContext<TEnv>) => string[]) | undefined,
|
|
93
|
+
ctx: RequestContext<TEnv> | undefined,
|
|
94
|
+
label: string,
|
|
95
|
+
): string[] | undefined {
|
|
96
|
+
if (!tags) return undefined;
|
|
97
|
+
if (typeof tags === "function") {
|
|
98
|
+
if (!ctx) {
|
|
99
|
+
console.warn(
|
|
100
|
+
`[${label}] Dynamic tags function present but no request context; ` +
|
|
101
|
+
`caching without tags (this entry will not be tag-invalidatable).`,
|
|
102
|
+
);
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
return normalizeTagList(tags(ctx));
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error(
|
|
109
|
+
`[${label}] Tags function failed, caching without tags:`,
|
|
110
|
+
error,
|
|
111
|
+
);
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return normalizeTagList(tags);
|
|
116
|
+
}
|
|
114
117
|
|
|
115
118
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
+
* Normalize a resolved tags array so the WRITE path matches the invalidate path:
|
|
120
|
+
* updateTag()/revalidateTag()/cacheTag() all drop empty/whitespace-only tags via
|
|
121
|
+
* normalizeTag(). Without this, an empty tag attached at write time would enter
|
|
122
|
+
* the store index but could never be invalidated (the verbs normalize it away),
|
|
123
|
+
* and on CFCacheStore would also cost a wasted KV marker read per request.
|
|
124
|
+
* Returns undefined when nothing usable remains, keeping the entry header-free.
|
|
119
125
|
*/
|
|
126
|
+
function normalizeTagList(tags: string[]): string[] | undefined {
|
|
127
|
+
const out = normalizeTags(tags);
|
|
128
|
+
return out.length > 0 ? out : undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
120
131
|
export function resolveCacheStore(
|
|
121
132
|
explicitStore: SegmentCacheStore | undefined,
|
|
122
133
|
): SegmentCacheStore | null {
|
|
123
|
-
if (explicitStore)
|
|
134
|
+
if (explicitStore) {
|
|
135
|
+
registerExplicitTaggedStore(explicitStore);
|
|
136
|
+
return explicitStore;
|
|
137
|
+
}
|
|
124
138
|
return _getRequestContext()?._cacheStore ?? null;
|
|
125
139
|
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Upper bound on the per-handler explicit-store registry. A module-singleton
|
|
143
|
+
* store (the recommended pattern) dedupes to a single entry and never approaches
|
|
144
|
+
* this. The cap bounds the niche case of an explicit store constructed PER
|
|
145
|
+
* request/boundary (e.g. a ctx-bound CFCacheStore, which must take a per-request
|
|
146
|
+
* ctx): without it the registry - which intentionally persists across requests so
|
|
147
|
+
* a server action's updateTag() can reach stores a prior render registered -
|
|
148
|
+
* would accumulate one dead instance per request and fan invalidation out to
|
|
149
|
+
* finished execution contexts. LRU-touch on re-resolution keeps a live, re-used
|
|
150
|
+
* store from being evicted by that churn.
|
|
151
|
+
*/
|
|
152
|
+
const EXPLICIT_STORE_REGISTRY_CAP = 64;
|
|
153
|
+
|
|
154
|
+
function registerExplicitTaggedStore(store: SegmentCacheStore): void {
|
|
155
|
+
const set = _getRequestContext()?._explicitTaggedStores;
|
|
156
|
+
if (!set) return;
|
|
157
|
+
// LRU touch: move an already-present store to the most-recent position (Set
|
|
158
|
+
// preserves insertion order) so a store re-resolved every request stays live.
|
|
159
|
+
set.delete(store);
|
|
160
|
+
set.add(store);
|
|
161
|
+
if (set.size > EXPLICIT_STORE_REGISTRY_CAP) {
|
|
162
|
+
const oldest = set.values().next().value;
|
|
163
|
+
if (oldest !== undefined) set.delete(oldest);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
createClientTemporaryReferenceSet,
|
|
22
22
|
} from "@vitejs/plugin-rsc/rsc";
|
|
23
23
|
import { getRequestContext } from "../server/request-context.js";
|
|
24
|
+
import { isUnderTestRunner } from "../runtime-env.js";
|
|
24
25
|
import {
|
|
25
26
|
isTainted,
|
|
26
27
|
CACHED_FN_SYMBOL,
|
|
@@ -32,10 +33,21 @@ import {
|
|
|
32
33
|
export { isCachedFunction };
|
|
33
34
|
import { serializeResult, deserializeResult } from "./segment-codec.js";
|
|
34
35
|
import { createHandleStore } from "../server/handle-store.js";
|
|
35
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
restoreHandles,
|
|
38
|
+
encodeHandles,
|
|
39
|
+
decodeHandles,
|
|
40
|
+
} from "./handle-snapshot.js";
|
|
36
41
|
import { startHandleCapture, type HandleCapture } from "./handle-capture.js";
|
|
37
42
|
import { sortedSearchString } from "./cache-key-utils.js";
|
|
38
43
|
import { runBackground } from "./background-task.js";
|
|
44
|
+
import {
|
|
45
|
+
normalizeTags,
|
|
46
|
+
recordRequestTags,
|
|
47
|
+
runWithCacheTagScope,
|
|
48
|
+
} from "./cache-tag.js";
|
|
49
|
+
import { reportCacheError } from "./cache-error.js";
|
|
50
|
+
import type { CacheItemResult } from "./types.js";
|
|
39
51
|
|
|
40
52
|
/**
|
|
41
53
|
* Convert encodeReply result to a stable string key.
|
|
@@ -48,6 +60,10 @@ async function replyToCacheKey(encoded: string | FormData): Promise<string> {
|
|
|
48
60
|
return text;
|
|
49
61
|
}
|
|
50
62
|
|
|
63
|
+
// Cached-fn ids already warned about running uncached under a test runner, so
|
|
64
|
+
// the test-ergonomics warning fires once per fn rather than once per call.
|
|
65
|
+
const warnedUncachedUnderTest = new Set<string>();
|
|
66
|
+
|
|
51
67
|
// ============================================================================
|
|
52
68
|
// Core: registerCachedFunction
|
|
53
69
|
// ============================================================================
|
|
@@ -70,9 +86,38 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
70
86
|
const store = requestCtx?._cacheStore;
|
|
71
87
|
const resolvedProfileName = profileName || "default";
|
|
72
88
|
|
|
73
|
-
// Bypass: no store or no getItem support
|
|
89
|
+
// Bypass: no store or no getItem support. Still run inside a tag scope so a
|
|
90
|
+
// cacheTag() call inside the function degrades to a no-op rather than
|
|
91
|
+
// throwing "must be called inside a use cache function" - adopting cacheTag()
|
|
92
|
+
// must not hard-fail in apps/tests without an item-capable cache configured.
|
|
93
|
+
// Note: the INSIDE_CACHE_EXEC guard (cookies()/headers()/ctx.set() rejection)
|
|
94
|
+
// is intentionally NOT stamped here. It is a cached-path-only check; in the
|
|
95
|
+
// bypass the body actually executes, so the guarded side effects take effect
|
|
96
|
+
// and nothing is lost on a (non-existent) hit. Same applies to the
|
|
97
|
+
// non-serializable-args bypass below.
|
|
74
98
|
if (!store?.getItem) {
|
|
75
|
-
|
|
99
|
+
// Test-ergonomics guard: under a test runner, a "use cache" function that
|
|
100
|
+
// executes with no item-capable store seeded is exercising the UNCACHED
|
|
101
|
+
// path — a green test that proves nothing about caching. Warn once per fn
|
|
102
|
+
// id so the author knows to seed a cacheStore. Advisory (never throws), so
|
|
103
|
+
// a test that DELIBERATELY runs uncached is unaffected. Gated on the test
|
|
104
|
+
// runner (process.env.VITEST, not folded) so production never evaluates it.
|
|
105
|
+
if (isUnderTestRunner() && !warnedUncachedUnderTest.has(id)) {
|
|
106
|
+
warnedUncachedUnderTest.add(id);
|
|
107
|
+
console.warn(
|
|
108
|
+
`[rango] "use cache" function "${id}" executed but no cacheStore was ` +
|
|
109
|
+
`seeded; the cached path is NOT under test (it ran uncached). Pass ` +
|
|
110
|
+
`{ cacheStore, cacheProfiles } to runLoader/runMiddleware/renderHandler/` +
|
|
111
|
+
`runInRequestContext (or configure createRouter({ cache }) for dispatch) ` +
|
|
112
|
+
`to exercise it.`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const scoped = runWithCacheTagScope(() => fn.apply(this, args));
|
|
116
|
+
const result = await scoped.result;
|
|
117
|
+
// Still record the runtime tags into the request set so a cacheTag() in an
|
|
118
|
+
// uncached function tags the document, even with no item-capable store.
|
|
119
|
+
recordRequestTags(scoped.tags, requestCtx);
|
|
120
|
+
return result;
|
|
76
121
|
}
|
|
77
122
|
|
|
78
123
|
// Resolve profile strictly from request-scoped config (set by the
|
|
@@ -155,40 +200,57 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
155
200
|
cacheKey = `use-cache:${id}`;
|
|
156
201
|
}
|
|
157
202
|
} catch {
|
|
158
|
-
// Non-serializable args: run uncached
|
|
159
|
-
|
|
203
|
+
// Non-serializable args: run uncached (within a tag scope so cacheTag()
|
|
204
|
+
// still does not throw). Record runtime tags so the document union still
|
|
205
|
+
// sees them even though this call is not itself cached.
|
|
206
|
+
const scoped = runWithCacheTagScope(() => fn.apply(this, args));
|
|
207
|
+
const result = await scoped.result;
|
|
208
|
+
recordRequestTags(scoped.tags, requestCtx);
|
|
209
|
+
return result;
|
|
160
210
|
}
|
|
161
211
|
|
|
162
212
|
// Cache lookup
|
|
163
213
|
const cached = await store.getItem(cacheKey);
|
|
164
214
|
|
|
215
|
+
// Serve a cached entry on the hit path: deserialize the stored value,
|
|
216
|
+
// replay handle data (gated on tainted args), and surface the entry's tags
|
|
217
|
+
// to the request set (the function did not re-run, so its runtime cacheTag()
|
|
218
|
+
// tags are only available from the stored entry). Shared by the fresh-hit
|
|
219
|
+
// and stale-hit branches; the only divergence is the stale branch scheduling
|
|
220
|
+
// background revalidation, which it does after this returns.
|
|
221
|
+
const serveCached = async (entry: CacheItemResult): Promise<any> => {
|
|
222
|
+
const result = await deserializeResult(entry.value);
|
|
223
|
+
if (entry.handles && hasTaintedArgs) {
|
|
224
|
+
const handleStore = requestCtx?._handleStore;
|
|
225
|
+
if (handleStore) {
|
|
226
|
+
const r = await decodeHandles(entry.handles);
|
|
227
|
+
if (r) restoreHandles(r, handleStore);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
recordRequestTags(entry.tags, requestCtx);
|
|
231
|
+
return result;
|
|
232
|
+
};
|
|
233
|
+
|
|
165
234
|
if (cached && !cached.shouldRevalidate) {
|
|
166
235
|
// Fresh hit: deserialize and return
|
|
167
236
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// Deserialization failed, fall through to fresh execution
|
|
237
|
+
return await serveCached(cached);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
// The stored value is corrupt/partial (failed RSC deserialize). Report
|
|
240
|
+
// it, then fall through to fresh execution - the miss path below re-runs
|
|
241
|
+
// and setItem() overwrites the faulty entry under the same key (self-heal).
|
|
242
|
+
reportCacheError(
|
|
243
|
+
error,
|
|
244
|
+
"cache-corrupt",
|
|
245
|
+
`[use cache] "${id}" fresh-hit`,
|
|
246
|
+
);
|
|
179
247
|
}
|
|
180
248
|
}
|
|
181
249
|
|
|
182
250
|
if (cached?.shouldRevalidate) {
|
|
183
251
|
// Stale hit: return stale value, revalidate in background
|
|
184
252
|
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
|
-
}
|
|
253
|
+
const result = await serveCached(cached);
|
|
192
254
|
// Background revalidation — must capture handles if tainted args present.
|
|
193
255
|
// Use an isolated handle store so background pushes don't pollute the
|
|
194
256
|
// live response or throw LateHandlePushError on the completed store.
|
|
@@ -238,20 +300,41 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
238
300
|
}
|
|
239
301
|
|
|
240
302
|
try {
|
|
241
|
-
const
|
|
303
|
+
const scoped = runWithCacheTagScope(() => fn.apply(this, args));
|
|
304
|
+
const freshResult = await scoped.result;
|
|
242
305
|
bgStopCapture?.();
|
|
306
|
+
// Merge profile/DSL tags with runtime cacheTag() tags, read after
|
|
307
|
+
// awaiting so post-await cacheTag() calls are included. Normalize
|
|
308
|
+
// (drops empty profile tags, matching the invalidate path) + dedupe.
|
|
309
|
+
const freshTags = [
|
|
310
|
+
...new Set(
|
|
311
|
+
normalizeTags([...(profile.tags ?? []), ...scoped.tags]),
|
|
312
|
+
),
|
|
313
|
+
];
|
|
314
|
+
recordRequestTags(freshTags, requestCtx);
|
|
243
315
|
const serialized = await serializeResult(freshResult);
|
|
244
316
|
if (serialized !== null) {
|
|
317
|
+
const encodedHandles = bgCapture?.data
|
|
318
|
+
? await encodeHandles(bgCapture.data)
|
|
319
|
+
: undefined;
|
|
245
320
|
await store.setItem!(cacheKey, serialized, {
|
|
246
|
-
handles:
|
|
321
|
+
handles: encodedHandles,
|
|
247
322
|
ttl: profile.ttl,
|
|
248
323
|
swr: profile.swr,
|
|
249
|
-
tags:
|
|
324
|
+
tags: freshTags.length > 0 ? freshTags : undefined,
|
|
250
325
|
});
|
|
251
326
|
}
|
|
252
327
|
} catch (bgError) {
|
|
253
328
|
bgStopCapture?.();
|
|
254
|
-
requestCtx
|
|
329
|
+
// Pass requestCtx explicitly: this runs in a detached background
|
|
330
|
+
// task where the ALS context is gone, so onError can only fire if
|
|
331
|
+
// we hand it the context captured up front.
|
|
332
|
+
reportCacheError(
|
|
333
|
+
bgError,
|
|
334
|
+
"stale-revalidation",
|
|
335
|
+
"[use cache] background revalidation failed",
|
|
336
|
+
requestCtx,
|
|
337
|
+
);
|
|
255
338
|
} finally {
|
|
256
339
|
for (const arg of bgTaintedArgs) {
|
|
257
340
|
unstampCacheExec(arg as object);
|
|
@@ -263,8 +346,14 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
263
346
|
}
|
|
264
347
|
});
|
|
265
348
|
return result;
|
|
266
|
-
} catch {
|
|
267
|
-
//
|
|
349
|
+
} catch (error) {
|
|
350
|
+
// Stale value is corrupt/partial; report and fall through to a fresh
|
|
351
|
+
// execution, which overwrites the faulty entry under the same key.
|
|
352
|
+
reportCacheError(
|
|
353
|
+
error,
|
|
354
|
+
"cache-corrupt",
|
|
355
|
+
`[use cache] "${id}" stale-hit`,
|
|
356
|
+
);
|
|
268
357
|
}
|
|
269
358
|
}
|
|
270
359
|
|
|
@@ -297,8 +386,10 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
297
386
|
}
|
|
298
387
|
|
|
299
388
|
let result: any;
|
|
389
|
+
let scoped: ReturnType<typeof runWithCacheTagScope>;
|
|
300
390
|
try {
|
|
301
|
-
|
|
391
|
+
scoped = runWithCacheTagScope(() => fn.apply(this, args));
|
|
392
|
+
result = await scoped.result;
|
|
302
393
|
} finally {
|
|
303
394
|
// Decrement ref count; symbol is deleted when it reaches zero
|
|
304
395
|
for (const arg of taintedArgs) {
|
|
@@ -311,17 +402,28 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
311
402
|
stopCapture?.();
|
|
312
403
|
}
|
|
313
404
|
|
|
405
|
+
// Merge profile/DSL tags with runtime cacheTag() tags. Read scoped.tags
|
|
406
|
+
// after awaiting result so post-await cacheTag() calls are included.
|
|
407
|
+
// Normalize (drops empty profile tags, matching the invalidate path) + dedupe.
|
|
408
|
+
const allTags = [
|
|
409
|
+
...new Set(normalizeTags([...(profile.tags ?? []), ...scoped!.tags])),
|
|
410
|
+
];
|
|
411
|
+
recordRequestTags(allTags, requestCtx);
|
|
412
|
+
|
|
314
413
|
// Serialize and store — fully non-blocking when waitUntil is available.
|
|
315
414
|
// The response does not need to wait for serialization or the store write.
|
|
316
415
|
const cacheWrite = async () => {
|
|
317
416
|
try {
|
|
318
417
|
const serialized = await serializeResult(result);
|
|
319
418
|
if (serialized !== null) {
|
|
419
|
+
const encodedHandles = capture?.data
|
|
420
|
+
? await encodeHandles(capture.data)
|
|
421
|
+
: undefined;
|
|
320
422
|
await store.setItem!(cacheKey, serialized, {
|
|
321
|
-
handles:
|
|
423
|
+
handles: encodedHandles,
|
|
322
424
|
ttl: profile.ttl,
|
|
323
425
|
swr: profile.swr,
|
|
324
|
-
tags:
|
|
426
|
+
tags: allTags.length > 0 ? allTags : undefined,
|
|
325
427
|
});
|
|
326
428
|
}
|
|
327
429
|
} catch (writeError) {
|