@rangojs/router 0.16.0 → 0.18.0
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 +255 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -0
- package/dist/types/browser/navigation-transaction.d.ts +6 -0
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/scroll-restoration.d.ts +7 -0
- package/dist/types/cache/cache-exec-scope.d.ts +18 -1
- package/dist/types/cache/cache-key-utils.d.ts +17 -2
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +132 -19
- package/dist/types/cache/cache-tag.d.ts +92 -7
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +54 -13
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +110 -21
- package/dist/types/cache/tag-invalidation.d.ts +30 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +220 -44
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- package/dist/types/client.d.ts +4 -4
- package/dist/types/client.rsc.d.ts +4 -5
- package/dist/types/components/DefaultDocument.d.ts +5 -2
- package/dist/types/context-var.d.ts +6 -5
- package/dist/types/escape-script.d.ts +1 -1
- package/dist/types/handles/MetaTags.d.ts +5 -4
- package/dist/types/handles/Scripts.d.ts +8 -7
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/handles/script.d.ts +17 -16
- package/dist/types/href-client.d.ts +1 -1
- package/dist/types/html.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +3 -2
- package/dist/types/loader-store.d.ts +7 -4
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/reverse.d.ts +36 -4
- package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
- package/dist/types/route-definition/helpers-types.d.ts +33 -9
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +1 -1
- package/dist/types/router/debug-manifest.d.ts +3 -2
- package/dist/types/router/error-handling.d.ts +20 -0
- package/dist/types/router/handler-context.d.ts +6 -0
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +9 -3
- package/dist/types/router/router-context.d.ts +3 -1
- package/dist/types/router/router-interfaces.d.ts +24 -2
- package/dist/types/router/router-options.d.ts +8 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
- package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
- package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
- package/dist/types/router/segment-wrappers.d.ts +1 -1
- package/dist/types/rsc/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
- package/dist/types/rsc/shell-capture.d.ts +151 -19
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +17 -0
- package/dist/types/server/context.d.ts +139 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +182 -53
- package/dist/types/ssr/index.d.ts +24 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/generated-routes.d.ts +2 -2
- package/dist/types/testing/internal/context.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +32 -6
- package/dist/types/testing/run-loader.d.ts +18 -3
- package/dist/types/testing/serve-shell-request.d.ts +132 -0
- package/dist/types/testing/shell-status.d.ts +23 -10
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
- package/dist/types/testing/vitest.d.ts +35 -1
- package/dist/types/theme/ThemeScript.d.ts +3 -3
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +28 -1
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/pattern-types.d.ts +22 -18
- package/dist/types/urls/response-types.d.ts +3 -12
- package/dist/types/use-loader.d.ts +12 -3
- package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
- package/dist/types/vite/rango.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1420 -1348
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +75 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +306 -124
- package/skills/caching/SKILL.md +530 -98
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +65 -38
- package/skills/cloudflare/SKILL.md +87 -20
- package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
- package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
- package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
- package/skills/comparison/SKILL.md +5 -0
- package/skills/comparison/references/framework-comparison.md +75 -57
- package/skills/composability/SKILL.md +38 -29
- package/skills/css/SKILL.md +10 -13
- package/skills/debug-manifest/SKILL.md +74 -31
- package/skills/defer-hydration/SKILL.md +52 -31
- package/skills/deployment-caching/SKILL.md +11 -3
- package/skills/document-cache/SKILL.md +98 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +55 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +34 -17
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +39 -15
- package/skills/hooks/urls.md +7 -5
- package/skills/host-router/SKILL.md +64 -12
- package/skills/i18n/SKILL.md +60 -34
- package/skills/intercept/SKILL.md +161 -26
- package/skills/layout/SKILL.md +106 -50
- package/skills/links/SKILL.md +51 -19
- package/skills/loader/SKILL.md +386 -128
- package/skills/middleware/SKILL.md +129 -26
- package/skills/migrate-nextjs/SKILL.md +156 -70
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +51 -39
- package/skills/migrate-react-router/component-migration.md +28 -7
- package/skills/migrate-react-router/data-and-actions.md +21 -6
- package/skills/migrate-react-router/route-mapping.md +19 -10
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +92 -14
- package/skills/parallel/SKILL.md +88 -47
- package/skills/ppr/SKILL.md +866 -379
- package/skills/prerender/SKILL.md +210 -108
- package/skills/rango/SKILL.md +115 -65
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +170 -60
- package/skills/router-setup/SKILL.md +420 -240
- package/skills/scripts/SKILL.md +39 -21
- package/skills/server-actions/SKILL.md +231 -59
- package/skills/shell-manifest/SKILL.md +28 -14
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +73 -33
- package/skills/testing/cache-prerender.md +60 -33
- package/skills/testing/client-components.md +59 -6
- package/skills/testing/e2e-parity.md +12 -2
- package/skills/testing/flight.md +19 -11
- package/skills/testing/handles.md +8 -5
- package/skills/testing/loader.md +26 -6
- package/skills/testing/middleware.md +8 -3
- package/skills/testing/predicates.md +120 -0
- package/skills/testing/render-handler.md +70 -22
- package/skills/testing/response-routes.md +11 -4
- package/skills/testing/reverse-and-types.md +20 -14
- package/skills/testing/serve-shell-request.md +188 -0
- package/skills/testing/server-actions.md +12 -3
- package/skills/testing/server-tree.md +9 -0
- package/skills/testing/setup.md +9 -5
- package/skills/theme/SKILL.md +155 -30
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +137 -50
- package/skills/typesafety/generated-files-and-cli.md +26 -14
- package/skills/typesafety/params-and-search.md +44 -19
- package/skills/typesafety/route-types.md +50 -14
- package/skills/use-cache/SKILL.md +224 -83
- package/skills/vercel/SKILL.md +35 -13
- package/skills/view-transitions/SKILL.md +42 -21
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- package/src/browser/index.ts +2 -2
- package/src/browser/navigation-bridge.ts +3 -4
- package/src/browser/navigation-transaction.ts +22 -2
- package/src/browser/partial-update.ts +35 -25
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +6 -1
- package/src/browser/scroll-restoration.ts +53 -6
- package/src/cache/cache-exec-scope.ts +37 -4
- package/src/cache/cache-key-utils.ts +23 -8
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +264 -68
- package/src/cache/cache-scope.ts +419 -43
- package/src/cache/cache-tag.ts +375 -11
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +990 -348
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +133 -28
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +108 -22
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +225 -44
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- package/src/client.rsc.tsx +10 -9
- package/src/client.tsx +5 -4
- package/src/components/DefaultDocument.tsx +7 -2
- package/src/context-var.ts +6 -5
- package/src/escape-script.ts +1 -1
- package/src/handles/MetaTags.tsx +13 -11
- package/src/handles/Scripts.tsx +11 -10
- package/src/handles/meta.ts +2 -2
- package/src/handles/script.ts +17 -16
- package/src/href-client.ts +1 -1
- package/src/html.ts +12 -0
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +1 -0
- package/src/loader-store.ts +7 -4
- package/src/prerender/build-shell-capture.ts +73 -22
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +121 -33
- package/src/route-definition/helpers-types.ts +33 -9
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +0 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +71 -28
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +141 -43
- package/src/router/loader-resolution.ts +115 -39
- package/src/router/match-api.ts +40 -26
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +110 -10
- package/src/router/match-middleware/cache-store.ts +48 -101
- package/src/router/middleware-types.ts +9 -6
- package/src/router/middleware.ts +75 -13
- package/src/router/prerender-match.ts +59 -19
- package/src/router/route-snapshot.ts +11 -1
- package/src/router/router-context.ts +2 -0
- package/src/router/router-interfaces.ts +25 -2
- package/src/router/router-options.ts +8 -5
- package/src/router/segment-resolution/fresh.ts +62 -21
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +407 -76
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +208 -56
- package/src/router/segment-resolution/mask-nested.ts +138 -44
- package/src/router/segment-resolution/revalidation.ts +61 -16
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/full-payload.ts +25 -1
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-cache-serve.ts +9 -6
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +608 -225
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +73 -6
- package/src/rsc/shell-capture.ts +1068 -296
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +18 -0
- package/src/server/context.ts +300 -32
- package/src/server/cookie-store.ts +87 -61
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +297 -67
- package/src/ssr/index.tsx +136 -44
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/generated-routes.ts +2 -2
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/internal/flight-client-globals.ts +7 -5
- package/src/testing/render-route.tsx +85 -24
- package/src/testing/run-loader.ts +68 -6
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +28 -1
- package/src/types/loader-types.ts +8 -6
- package/src/urls/pattern-types.ts +22 -18
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
- package/src/vite/router-discovery.ts +14 -4
|
@@ -35,14 +35,14 @@ import {
|
|
|
35
35
|
|
|
36
36
|
export { isCachedFunction };
|
|
37
37
|
import { serializeResult, deserializeResult } from "./segment-codec.js";
|
|
38
|
-
import { createHandleStore } from "../server/handle-store.js";
|
|
39
38
|
import {
|
|
40
|
-
|
|
39
|
+
appendHandles,
|
|
41
40
|
encodeHandles,
|
|
42
41
|
decodeHandles,
|
|
43
42
|
} from "./handle-snapshot.js";
|
|
44
|
-
import { startHandleCapture,
|
|
45
|
-
import {
|
|
43
|
+
import { startHandleCapture, useCacheRecordKey } from "./handle-capture.js";
|
|
44
|
+
import { isHandle } from "../handle.js";
|
|
45
|
+
import { cacheKeyBase, sortedSearchString } from "./cache-key-utils.js";
|
|
46
46
|
import { encodeKV } from "../encode-kv.js";
|
|
47
47
|
import { runBackground } from "./background-task.js";
|
|
48
48
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
@@ -51,9 +51,16 @@ import {
|
|
|
51
51
|
recordRequestTags,
|
|
52
52
|
runWithCacheTagScope,
|
|
53
53
|
} from "./cache-tag.js";
|
|
54
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
createCacheExecScope,
|
|
56
|
+
isInCacheExecChain,
|
|
57
|
+
runWithCacheExecScope,
|
|
58
|
+
type CacheExecScope,
|
|
59
|
+
} from "./cache-exec-scope.js";
|
|
55
60
|
import { reportCacheError } from "./cache-error.js";
|
|
61
|
+
import { currentCacheSeq, invalidatedSince } from "./tag-invalidation.js";
|
|
56
62
|
import type { CacheItemResult } from "./types.js";
|
|
63
|
+
import type { InternalHandlerContext } from "../types.js";
|
|
57
64
|
|
|
58
65
|
/**
|
|
59
66
|
* DJB2 hash returning an 8-char hex string. Deterministic across runtimes
|
|
@@ -125,10 +132,74 @@ export async function replyToCacheKey(
|
|
|
125
132
|
return encodeKV(pairs, { sort: true });
|
|
126
133
|
}
|
|
127
134
|
|
|
135
|
+
/**
|
|
136
|
+
* An unescaped temporary-reference token in an encodeReply part. A user
|
|
137
|
+
* string "$T" encodes as "$$T", a quote inside a string is escaped, and an
|
|
138
|
+
* object key "$T" is followed by ":".
|
|
139
|
+
*/
|
|
140
|
+
const TEMPORARY_REFERENCE_TOKEN = /(?<!\\)"\$T"(?!:)/g;
|
|
141
|
+
|
|
142
|
+
const REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element");
|
|
143
|
+
const CLIENT_REFERENCE_TYPE = Symbol.for("react.client.reference");
|
|
144
|
+
const SERVER_REFERENCE_TYPE = Symbol.for("react.server.reference");
|
|
145
|
+
|
|
146
|
+
function countTemporaryReferences(encoded: string | FormData): number {
|
|
147
|
+
if (typeof encoded === "string") {
|
|
148
|
+
return encoded.match(TEMPORARY_REFERENCE_TOKEN)?.length ?? 0;
|
|
149
|
+
}
|
|
150
|
+
let count = 0;
|
|
151
|
+
encoded.forEach((value) => {
|
|
152
|
+
if (typeof value === "string") {
|
|
153
|
+
count += value.match(TEMPORARY_REFERENCE_TOKEN)?.length ?? 0;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
return count;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** A React element, or a client or server reference function. */
|
|
160
|
+
function isRenderSlot(value: unknown): boolean {
|
|
161
|
+
if (typeof value === "object" && value !== null) {
|
|
162
|
+
return (value as { $$typeof?: unknown }).$$typeof === REACT_ELEMENT_TYPE;
|
|
163
|
+
}
|
|
164
|
+
if (typeof value === "function") {
|
|
165
|
+
const tag = (value as { $$typeof?: unknown }).$$typeof;
|
|
166
|
+
return tag === CLIENT_REFERENCE_TYPE || tag === SERVER_REFERENCE_TYPE;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Whether encodeReply wrote "$T" for a value that cannot be keyed. Given a
|
|
173
|
+
* temporary-reference set it does not throw on a value it cannot serialize:
|
|
174
|
+
* it writes "$T", so distinct arguments would share one key (issue #924).
|
|
175
|
+
* React elements and client/server references are render slots, left out of
|
|
176
|
+
* the key on purpose (the interleave route, e2e/use-cache-streaming.test.ts);
|
|
177
|
+
* a function, a symbol or a class instance is not.
|
|
178
|
+
*
|
|
179
|
+
* The set is the Map encodeReply fills: one entry per "$T" it writes, plus
|
|
180
|
+
* every object it visits. So more tokens than slot entries means a non-slot
|
|
181
|
+
* "$T", with no traversal of the arguments. Runs only on the encoder path.
|
|
182
|
+
*/
|
|
183
|
+
function hasUnkeyableReference(
|
|
184
|
+
encoded: string | FormData,
|
|
185
|
+
tempRefs: unknown,
|
|
186
|
+
): boolean {
|
|
187
|
+
const tokens = countTemporaryReferences(encoded);
|
|
188
|
+
if (tokens === 0) return false;
|
|
189
|
+
let slots = 0;
|
|
190
|
+
for (const value of (tempRefs as Map<string, unknown>).values()) {
|
|
191
|
+
if (isRenderSlot(value)) slots++;
|
|
192
|
+
}
|
|
193
|
+
return tokens > slots;
|
|
194
|
+
}
|
|
195
|
+
|
|
128
196
|
// Cached-fn ids already warned about running uncached under a test runner, so
|
|
129
197
|
// the test-ergonomics warning fires once per fn rather than once per call.
|
|
130
198
|
const warnedUncachedUnderTest = new Set<string>();
|
|
131
199
|
|
|
200
|
+
// Cached-fn ids already warned about arguments that cannot be keyed (dev only).
|
|
201
|
+
const warnedUnkeyableArgs = new Set<string>();
|
|
202
|
+
|
|
132
203
|
/**
|
|
133
204
|
* Fast-path cache-key builder for JSON-safe key args. Returns a deterministic
|
|
134
205
|
* string (object keys recursively sorted so insertion order can't change the
|
|
@@ -222,7 +293,10 @@ interface CacheEnvelope {
|
|
|
222
293
|
*/
|
|
223
294
|
interface InFlightExecution {
|
|
224
295
|
promise: Promise<CacheEnvelope>;
|
|
296
|
+
/** Date.now() at registration, for IN_FLIGHT_LEADER_MAX_WAIT_MS. */
|
|
225
297
|
registeredAt: number;
|
|
298
|
+
/** currentCacheSeq() at registration, for invalidatedSince (#973). */
|
|
299
|
+
startSeq: number;
|
|
226
300
|
}
|
|
227
301
|
|
|
228
302
|
const inFlightExecutions = new Map<string, InFlightExecution>();
|
|
@@ -264,6 +338,30 @@ function raceLeader(
|
|
|
264
338
|
});
|
|
265
339
|
}
|
|
266
340
|
|
|
341
|
+
/**
|
|
342
|
+
* A stale refresh's view of a handler ctx: loader reads run on their own
|
|
343
|
+
* executor (setupLoaderAccess _runLoaderIsolated), and an inner HIT replays in
|
|
344
|
+
* full without claiming. The refresh diverts its pushes, so the request's
|
|
345
|
+
* memoized run of a loader would take that loader's live pushes off the page,
|
|
346
|
+
* and a claim would starve the page's own replay of it.
|
|
347
|
+
*/
|
|
348
|
+
function refreshView(arg: unknown): unknown {
|
|
349
|
+
const ctx = arg as InternalHandlerContext<any, any>;
|
|
350
|
+
const runIsolated = isTainted(arg) ? ctx._runLoaderIsolated : undefined;
|
|
351
|
+
if (!runIsolated) return arg;
|
|
352
|
+
const runs = new Map<string, Promise<any>>();
|
|
353
|
+
const use = (item: any) => {
|
|
354
|
+
if (isHandle(item)) return ctx.use(item);
|
|
355
|
+
let run = runs.get(item.$$id);
|
|
356
|
+
if (!run) runs.set(item.$$id, (run = runIsolated(item)));
|
|
357
|
+
return run;
|
|
358
|
+
};
|
|
359
|
+
return Object.create(ctx, {
|
|
360
|
+
use: { value: use },
|
|
361
|
+
_claimLoaderPushes: { value: undefined },
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
267
365
|
// ============================================================================
|
|
268
366
|
// Core: registerCachedFunction
|
|
269
367
|
// ============================================================================
|
|
@@ -333,17 +431,33 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
333
431
|
);
|
|
334
432
|
}
|
|
335
433
|
|
|
336
|
-
// Separate
|
|
337
|
-
// For
|
|
338
|
-
// searchParams), extract serializable values
|
|
339
|
-
// different routes, param combinations, and query
|
|
340
|
-
// distinct cache entries.
|
|
434
|
+
// Separate request-scoped args (ctx, a Request, the request's env) from
|
|
435
|
+
// key-generating args. For those that carry route context (ctx params,
|
|
436
|
+
// pathname, searchParams; a Request's URL), extract serializable values
|
|
437
|
+
// into the key so different routes, param combinations, and query
|
|
438
|
+
// variants produce distinct cache entries.
|
|
341
439
|
const keyArgs: unknown[] = [];
|
|
342
440
|
let hasTaintedArgs = false;
|
|
441
|
+
// The calling segment, read synchronously as ctx.use(Handle) does: a HIT
|
|
442
|
+
// replays into it. A route's layouts and page share one handler ctx and
|
|
443
|
+
// so one key; the recorded ids are the first caller's. Its ctx's claim
|
|
444
|
+
// keeps a replayed loader's pushes to one copy per request.
|
|
445
|
+
let callerSegmentId: string | undefined;
|
|
446
|
+
let claimLoaderPushes: ((loaderId: string) => boolean) | undefined;
|
|
447
|
+
// A fetchable loader's request body (loader-fetch.ts) is an input the
|
|
448
|
+
// route fields leave out: such a call runs uncached (#940).
|
|
449
|
+
let hasBodyCtx = false;
|
|
343
450
|
for (const arg of args) {
|
|
344
451
|
if (isTainted(arg)) {
|
|
345
452
|
hasTaintedArgs = true;
|
|
346
453
|
const ctx = arg as any;
|
|
454
|
+
if (ctx.body !== undefined || ctx.formData !== undefined) {
|
|
455
|
+
hasBodyCtx = true;
|
|
456
|
+
}
|
|
457
|
+
if (callerSegmentId === undefined) {
|
|
458
|
+
callerSegmentId = ctx._currentSegmentId;
|
|
459
|
+
claimLoaderPushes = ctx._claimLoaderPushes;
|
|
460
|
+
}
|
|
347
461
|
if (ctx.params && typeof ctx.params === "object") {
|
|
348
462
|
// Include host to prevent cross-host cache collisions (same
|
|
349
463
|
// pattern as route-level cache-scope.ts key generation).
|
|
@@ -373,6 +487,22 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
373
487
|
}
|
|
374
488
|
}
|
|
375
489
|
}
|
|
490
|
+
} else if (arg instanceof Request) {
|
|
491
|
+
// A raw Request (ctx.request) is request-scoped like ctx: fold in its
|
|
492
|
+
// URL with the same host-namespacing and search normalization.
|
|
493
|
+
const url = new URL(arg.url);
|
|
494
|
+
keyArgs.push(
|
|
495
|
+
cacheKeyBase(
|
|
496
|
+
url.host,
|
|
497
|
+
url.pathname,
|
|
498
|
+
url.searchParams,
|
|
499
|
+
undefined,
|
|
500
|
+
requestCtx?._searchParamsFilter,
|
|
501
|
+
),
|
|
502
|
+
);
|
|
503
|
+
} else if (arg != null && arg === requestCtx?.env) {
|
|
504
|
+
// The request's env is left out: constant per deployment, and its
|
|
505
|
+
// bindings are not serializable.
|
|
376
506
|
} else {
|
|
377
507
|
keyArgs.push(arg);
|
|
378
508
|
}
|
|
@@ -383,7 +513,7 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
383
513
|
// gone. Throw early rather than silently dropping handle side effects.
|
|
384
514
|
if (hasTaintedArgs && !requestCtx?._handleStore) {
|
|
385
515
|
throw new Error(
|
|
386
|
-
`[use cache] "${id}" receives a
|
|
516
|
+
`[use cache] "${id}" receives a ctx argument but the ` +
|
|
387
517
|
`HandleStore is not available. This typically happens when a "use cache" ` +
|
|
388
518
|
`function with ctx runs outside the request context (e.g., during late ` +
|
|
389
519
|
`streaming after AsyncLocalStorage context is lost). Move the "use cache" ` +
|
|
@@ -391,10 +521,16 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
391
521
|
`use the route-level cache() DSL instead.`,
|
|
392
522
|
);
|
|
393
523
|
}
|
|
524
|
+
// Records group pushes by owning loader (useCacheRecordKey) and replay
|
|
525
|
+
// into the caller's segment; a caller without one records segment ids.
|
|
526
|
+
const ownerKeyed = callerSegmentId !== undefined;
|
|
394
527
|
|
|
395
528
|
// Generate cache key
|
|
396
529
|
let cacheKey: string;
|
|
397
530
|
try {
|
|
531
|
+
if (hasBodyCtx) {
|
|
532
|
+
throw new Error("ctx carries a request body");
|
|
533
|
+
}
|
|
398
534
|
if (keyArgs.length > 0) {
|
|
399
535
|
// Fast path: when every key arg is JSON-safe, build the key with a
|
|
400
536
|
// deterministic stable-stringify and skip encodeReply (the Flight reply
|
|
@@ -410,6 +546,9 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
410
546
|
const encoded = await encodeReply(keyArgs as unknown[], {
|
|
411
547
|
temporaryReferences: tempRefs,
|
|
412
548
|
});
|
|
549
|
+
if (hasUnkeyableReference(encoded, tempRefs)) {
|
|
550
|
+
throw new Error("unserializable key argument");
|
|
551
|
+
}
|
|
413
552
|
const argsKey = await replyToCacheKey(encoded);
|
|
414
553
|
cacheKey = `use-cache:${id}:${argsKey}`;
|
|
415
554
|
}
|
|
@@ -420,6 +559,19 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
420
559
|
// Non-serializable args: run uncached (within a tag scope so cacheTag()
|
|
421
560
|
// still does not throw). Record runtime tags so the document union still
|
|
422
561
|
// sees them even though this call is not itself cached.
|
|
562
|
+
if (
|
|
563
|
+
process.env.NODE_ENV !== "production" &&
|
|
564
|
+
!warnedUnkeyableArgs.has(id)
|
|
565
|
+
) {
|
|
566
|
+
warnedUnkeyableArgs.add(id);
|
|
567
|
+
console.warn(
|
|
568
|
+
`[use cache] "${id}" ran uncached: an argument, or a value nested in ` +
|
|
569
|
+
`one, cannot be serialized into the cache key (a function, class ` +
|
|
570
|
+
`instance or symbol, or a loader ctx carrying a request body). ` +
|
|
571
|
+
`Pass serializable values; ctx, a Request, the request's env and ` +
|
|
572
|
+
`React elements are handled.`,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
423
575
|
const scoped = runWithCacheTagScope(() => fn.apply(this, args));
|
|
424
576
|
const result = await scoped.result;
|
|
425
577
|
recordRequestTags(scoped.tags, requestCtx);
|
|
@@ -441,7 +593,9 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
441
593
|
const handleStore = requestCtx?._handleStore;
|
|
442
594
|
if (handleStore) {
|
|
443
595
|
const r = await decodeHandles(entry.handles);
|
|
444
|
-
if (r)
|
|
596
|
+
if (r) {
|
|
597
|
+
appendHandles(r, handleStore, callerSegmentId, claimLoaderPushes);
|
|
598
|
+
}
|
|
445
599
|
}
|
|
446
600
|
}
|
|
447
601
|
recordRequestTags(entry.tags, requestCtx);
|
|
@@ -474,34 +628,29 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
474
628
|
// Stale hit: return stale value, revalidate in background
|
|
475
629
|
try {
|
|
476
630
|
const result = await serveCached(cached);
|
|
631
|
+
const liveStore = hasTaintedArgs ? requestCtx?._handleStore : undefined;
|
|
632
|
+
const recordKey = ownerKeyed ? useCacheRecordKey() : undefined;
|
|
477
633
|
// Background revalidation — must capture handles if tainted args present.
|
|
478
634
|
runBackground(requestCtx, async () => {
|
|
479
|
-
// The background body
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
// the
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
635
|
+
// The background body's handle pushes belong to the refreshed entry
|
|
636
|
+
// only: the foreground already replayed the stale entry's. They
|
|
637
|
+
// reach the REQUEST's store (a handler ctx.use(Handle) binds it at
|
|
638
|
+
// setupLoaderAccess, so a derived context with its own store never
|
|
639
|
+
// sees them), so the capture accepts only this body's chain and
|
|
640
|
+
// diverts what it records away from the live store. The foreground
|
|
641
|
+
// is still rendering here (runBackground starts on the next
|
|
642
|
+
// microtask), and its concurrent pushes are neither recorded nor
|
|
643
|
+
// diverted (issue #684, plan 010). A root scope: the task is
|
|
644
|
+
// detached, so an outer "use cache" body that scheduled it must not
|
|
645
|
+
// record the refresh's pushes.
|
|
646
|
+
const bgScope: CacheExecScope = {};
|
|
647
|
+
const bgCapture = liveStore
|
|
648
|
+
? startHandleCapture(liveStore, {
|
|
649
|
+
accept: () => isInCacheExecChain(bgScope),
|
|
650
|
+
divert: true,
|
|
651
|
+
key: recordKey,
|
|
496
652
|
})
|
|
497
|
-
:
|
|
498
|
-
let bgCapture: HandleCapture | undefined;
|
|
499
|
-
let bgStopCapture: (() => void) | undefined;
|
|
500
|
-
if (bgHandleStore) {
|
|
501
|
-
const c = startHandleCapture(bgHandleStore);
|
|
502
|
-
bgCapture = c.capture;
|
|
503
|
-
bgStopCapture = c.stop;
|
|
504
|
-
}
|
|
653
|
+
: undefined;
|
|
505
654
|
|
|
506
655
|
// Tainted args are NOT stamped here, in contrast to the foreground
|
|
507
656
|
// miss path below. The args include the live HandlerContext the
|
|
@@ -522,23 +671,27 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
522
671
|
// getRequestContext().env.ApiKey) resolves during the background
|
|
523
672
|
// revalidation instead of throwing "called outside of a request
|
|
524
673
|
// context". runWithRequestContext sets the store for fn's
|
|
525
|
-
// synchronous kickoff; its async continuations inherit it.
|
|
526
|
-
// DERIVED context goes in, so ambient _handleStore reads inside
|
|
527
|
-
// the body resolve to the isolated store.
|
|
674
|
+
// synchronous kickoff; its async continuations inherit it.
|
|
528
675
|
//
|
|
529
676
|
// The span opens inside the request ALS and wraps the WHOLE task —
|
|
530
677
|
// fn kickoff through the store write — so its platform spans nest
|
|
531
678
|
// under rango.background and a slow or failing setItem is part of
|
|
532
679
|
// the traced revalidation, not an untraced tail.
|
|
533
|
-
await runWithRequestContext(
|
|
680
|
+
await runWithRequestContext(requestCtx, () =>
|
|
534
681
|
observePhase(
|
|
535
682
|
PHASES.background("use-cache-revalidation"),
|
|
536
683
|
async () => {
|
|
684
|
+
const refreshArgs = args.map(refreshView);
|
|
685
|
+
// The refresh reads its data from here on (#973).
|
|
686
|
+
const startSeq = currentCacheSeq();
|
|
537
687
|
const scoped = runWithCacheTagScope(() =>
|
|
538
|
-
runWithCacheExecScope(
|
|
688
|
+
runWithCacheExecScope(
|
|
689
|
+
() => fn.apply(this, refreshArgs),
|
|
690
|
+
bgScope,
|
|
691
|
+
),
|
|
539
692
|
);
|
|
540
693
|
const freshResult = await scoped.result;
|
|
541
|
-
|
|
694
|
+
bgCapture?.stop();
|
|
542
695
|
// Merge profile/DSL tags with runtime cacheTag() tags, read
|
|
543
696
|
// after awaiting so post-await cacheTag() calls are included.
|
|
544
697
|
// Normalize (drops empty profile tags, matching the
|
|
@@ -549,11 +702,30 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
549
702
|
),
|
|
550
703
|
];
|
|
551
704
|
recordRequestTags(freshTags, requestCtx);
|
|
552
|
-
const
|
|
705
|
+
const flightErrors: unknown[] = [];
|
|
706
|
+
const onFlightError = (error: unknown): void => {
|
|
707
|
+
flightErrors.push(error);
|
|
708
|
+
};
|
|
709
|
+
const serialized = await serializeResult(
|
|
710
|
+
freshResult,
|
|
711
|
+
onFlightError,
|
|
712
|
+
);
|
|
553
713
|
if (serialized !== null) {
|
|
554
|
-
const encodedHandles = bgCapture
|
|
555
|
-
? await encodeHandles(
|
|
714
|
+
const encodedHandles = bgCapture
|
|
715
|
+
? await encodeHandles(
|
|
716
|
+
bgCapture.capture.data,
|
|
717
|
+
onFlightError,
|
|
718
|
+
)
|
|
556
719
|
: undefined;
|
|
720
|
+
// An error row would replace the stale entry; the catch
|
|
721
|
+
// below reports it and the stale entry keeps serving.
|
|
722
|
+
if (flightErrors.length > 0) throw flightErrors[0];
|
|
723
|
+
// The request invalidated one of the refreshed value's
|
|
724
|
+
// tags after the refresh started: the value may predate
|
|
725
|
+
// it, and written now it would outlive it.
|
|
726
|
+
if (invalidatedSince(requestCtx, freshTags, startSeq)) {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
557
729
|
await store.setItem!(cacheKey, serialized, {
|
|
558
730
|
handles: encodedHandles,
|
|
559
731
|
ttl: profile.ttl,
|
|
@@ -565,7 +737,7 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
565
737
|
),
|
|
566
738
|
);
|
|
567
739
|
} catch (bgError) {
|
|
568
|
-
|
|
740
|
+
bgCapture?.stop();
|
|
569
741
|
// Pass requestCtx explicitly: this runs in a detached background
|
|
570
742
|
// task where the ALS context is gone, so onError can only fire if
|
|
571
743
|
// we hand it the context captured up front.
|
|
@@ -576,8 +748,6 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
576
748
|
requestCtx,
|
|
577
749
|
);
|
|
578
750
|
}
|
|
579
|
-
// No finally: nothing shared was mutated — the derived context and
|
|
580
|
-
// its handle store are garbage after the task settles.
|
|
581
751
|
});
|
|
582
752
|
return result;
|
|
583
753
|
} catch (error) {
|
|
@@ -599,8 +769,8 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
599
769
|
// against its OWN handle store (gated on ITS hasTaintedArgs), and records
|
|
600
770
|
// tags into its OWN request — no deserialized result is shared across
|
|
601
771
|
// requests. The store write stays exactly once (the leader's).
|
|
602
|
-
|
|
603
|
-
|
|
772
|
+
let existing = inFlightExecutions.get(cacheKey);
|
|
773
|
+
while (existing) {
|
|
604
774
|
const remainingMs =
|
|
605
775
|
IN_FLIGHT_LEADER_MAX_WAIT_MS - (Date.now() - existing.registeredAt);
|
|
606
776
|
const raced =
|
|
@@ -622,8 +792,12 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
622
792
|
"cache-read",
|
|
623
793
|
`[use cache] "${id}" inflight-timeout`,
|
|
624
794
|
);
|
|
625
|
-
//
|
|
626
|
-
}
|
|
795
|
+
break; // This call becomes the leader below.
|
|
796
|
+
}
|
|
797
|
+
// Leader rejected: its map entry is already cleared; run fresh.
|
|
798
|
+
if (!raced) break;
|
|
799
|
+
// Checked after the wait, so an invalidation made meanwhile counts too.
|
|
800
|
+
if (!invalidatedSince(requestCtx, raced.tags, existing.startSeq)) {
|
|
627
801
|
try {
|
|
628
802
|
return await serveCached({
|
|
629
803
|
value: raced.serialized,
|
|
@@ -637,11 +811,16 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
637
811
|
"cache-corrupt",
|
|
638
812
|
`[use cache] "${id}" inflight-hit`,
|
|
639
813
|
);
|
|
640
|
-
//
|
|
814
|
+
break; // Run fresh below.
|
|
641
815
|
}
|
|
642
816
|
}
|
|
643
|
-
//
|
|
644
|
-
//
|
|
817
|
+
// This request invalidated one of the leader's tags after it started
|
|
818
|
+
// (#973): its value may predate the invalidation. The leader cleared
|
|
819
|
+
// its entry before resolving, so an entry now is a newer execution
|
|
820
|
+
// (another caller that made this same decision): join it rather than
|
|
821
|
+
// start one more.
|
|
822
|
+
const next = inFlightExecutions.get(cacheKey);
|
|
823
|
+
existing = next !== existing ? next : undefined;
|
|
645
824
|
}
|
|
646
825
|
|
|
647
826
|
// This call becomes the leader. Register a deferred envelope so concurrent
|
|
@@ -658,9 +837,11 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
658
837
|
// Followers attach their own catch; guard the map's own reference so a
|
|
659
838
|
// rejected envelope with no waiter is not an unhandled rejection.
|
|
660
839
|
envelopePromise.catch(() => {});
|
|
840
|
+
const startSeq = currentCacheSeq();
|
|
661
841
|
inFlightExecutions.set(cacheKey, {
|
|
662
842
|
promise: envelopePromise,
|
|
663
843
|
registeredAt: Date.now(),
|
|
844
|
+
startSeq,
|
|
664
845
|
});
|
|
665
846
|
const clearSelf = (): void => {
|
|
666
847
|
if (inFlightExecutions.get(cacheKey)?.promise === envelopePromise) {
|
|
@@ -669,14 +850,17 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
669
850
|
};
|
|
670
851
|
|
|
671
852
|
// execute, serialize, store
|
|
853
|
+
// Record only this execution's pushes (and cached functions it calls):
|
|
854
|
+
// the handler and loaders push into the same store concurrently, and they
|
|
855
|
+
// re-run on a hit.
|
|
672
856
|
const handleStore = hasTaintedArgs ? requestCtx?._handleStore : undefined;
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
857
|
+
const execScope = createCacheExecScope();
|
|
858
|
+
const capture = handleStore
|
|
859
|
+
? startHandleCapture(handleStore, {
|
|
860
|
+
accept: () => isInCacheExecChain(execScope),
|
|
861
|
+
key: ownerKeyed ? useCacheRecordKey() : undefined,
|
|
862
|
+
})
|
|
863
|
+
: undefined;
|
|
680
864
|
|
|
681
865
|
// Stamp tainted args so ctx.set(), ctx.header(), etc. throw if called
|
|
682
866
|
// inside the cached function body (those side effects are lost on hit).
|
|
@@ -702,7 +886,7 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
702
886
|
let scoped: ReturnType<typeof runWithCacheTagScope>;
|
|
703
887
|
try {
|
|
704
888
|
scoped = runWithCacheTagScope(() =>
|
|
705
|
-
runWithCacheExecScope(() => fn.apply(this, args)),
|
|
889
|
+
runWithCacheExecScope(() => fn.apply(this, args), execScope),
|
|
706
890
|
);
|
|
707
891
|
result = await scoped.result;
|
|
708
892
|
} catch (execError) {
|
|
@@ -717,7 +901,7 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
717
901
|
unstampCacheExec(arg as object);
|
|
718
902
|
}
|
|
719
903
|
// Remove this capture token (order-independent, safe for concurrent use)
|
|
720
|
-
|
|
904
|
+
capture?.stop();
|
|
721
905
|
}
|
|
722
906
|
|
|
723
907
|
// Merge profile/DSL tags with runtime cacheTag() tags. Read scoped.tags
|
|
@@ -735,11 +919,19 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
735
919
|
const finalizeAndWrite = async (): Promise<void> => {
|
|
736
920
|
let serialized: string | null;
|
|
737
921
|
let encodedHandles: string | undefined;
|
|
922
|
+
const flightErrors: unknown[] = [];
|
|
923
|
+
const onFlightError = (error: unknown): void => {
|
|
924
|
+
flightErrors.push(error);
|
|
925
|
+
};
|
|
738
926
|
try {
|
|
739
|
-
serialized = await serializeResult(result);
|
|
740
|
-
encodedHandles = capture
|
|
741
|
-
? await encodeHandles(capture.data)
|
|
927
|
+
serialized = await serializeResult(result, onFlightError);
|
|
928
|
+
encodedHandles = capture
|
|
929
|
+
? await encodeHandles(capture.capture.data, onFlightError)
|
|
742
930
|
: undefined;
|
|
931
|
+
// Flight encodes an async component that throws, or a rejected
|
|
932
|
+
// promise in the result or a handle value, as an error row and
|
|
933
|
+
// completes normally; stored, every hit would serve that error.
|
|
934
|
+
if (flightErrors.length > 0) throw flightErrors[0];
|
|
743
935
|
} catch (buildError) {
|
|
744
936
|
// Serialize/handle-encode failed: no envelope for followers (they run
|
|
745
937
|
// fresh) and nothing to write.
|
|
@@ -762,6 +954,10 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
762
954
|
// Hand followers the envelope before the store write so a slow/failed
|
|
763
955
|
// write never stalls them.
|
|
764
956
|
resolveEnvelope({ serialized, tags: allTags, handles: encodedHandles });
|
|
957
|
+
// This request invalidated one of the value's tags after the execution
|
|
958
|
+
// started (#973): the value may predate the invalidation, and written
|
|
959
|
+
// now it would outlive it. Skip the write; the next read runs fresh.
|
|
960
|
+
if (invalidatedSince(requestCtx, allTags, startSeq)) return;
|
|
765
961
|
try {
|
|
766
962
|
await store.setItem!(cacheKey, serialized, {
|
|
767
963
|
handles: encodedHandles,
|