@rangojs/router 0.16.0 → 0.17.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 +254 -89
- package/dist/testing/vitest.js +252 -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-scope.d.ts +34 -5
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +53 -13
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +15 -3
- 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/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/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +3 -2
- 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 +37 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
- package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
- 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/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
- package/dist/types/rsc/shell-capture.d.ts +5 -4
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +10 -0
- package/dist/types/server/context.d.ts +33 -3
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +24 -21
- package/dist/types/ssr/index.d.ts +15 -0
- package/dist/types/static-handler.d.ts +4 -1
- 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/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 +14 -0
- package/dist/types/types/loader-types.d.ts +8 -6
- 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/vite/index.js +1381 -1346
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +65 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +207 -97
- package/skills/caching/SKILL.md +295 -93
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +53 -31
- package/skills/cloudflare/SKILL.md +72 -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 +39 -33
- 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 +2 -1
- package/skills/deployment-caching/SKILL.md +6 -1
- package/skills/document-cache/SKILL.md +92 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +53 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +31 -16
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +37 -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 +242 -106
- package/skills/middleware/SKILL.md +119 -23
- package/skills/migrate-nextjs/SKILL.md +100 -45
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +49 -38
- package/skills/migrate-react-router/component-migration.md +25 -6
- package/skills/migrate-react-router/data-and-actions.md +18 -5
- package/skills/migrate-react-router/route-mapping.md +12 -6
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +28 -14
- package/skills/parallel/SKILL.md +75 -39
- package/skills/ppr/SKILL.md +345 -150
- package/skills/prerender/SKILL.md +189 -100
- package/skills/rango/SKILL.md +65 -34
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +165 -60
- package/skills/router-setup/SKILL.md +418 -240
- package/skills/scripts/SKILL.md +31 -20
- package/skills/server-actions/SKILL.md +220 -59
- package/skills/shell-manifest/SKILL.md +12 -7
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +68 -32
- package/skills/testing/cache-prerender.md +55 -29
- 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 +8 -5
- 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/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 +106 -29
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +126 -47
- 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 +200 -82
- package/skills/vercel/SKILL.md +21 -11
- package/skills/view-transitions/SKILL.md +41 -20
- package/src/__augment-tests__/augmented.check.ts +26 -1
- 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 +1 -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 +1 -6
- package/src/cache/cache-runtime.ts +230 -61
- package/src/cache/cache-scope.ts +76 -12
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +132 -28
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/tag-invalidation.ts +4 -3
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +16 -3
- 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 +8 -11
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +117 -30
- 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 +34 -25
- package/src/router/intercept-resolution.ts +125 -39
- package/src/router/loader-resolution.ts +56 -11
- package/src/router/match-api.ts +12 -17
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +93 -10
- package/src/router/match-middleware/cache-store.ts +17 -90
- package/src/router/middleware-types.ts +3 -5
- package/src/router/middleware.ts +52 -9
- package/src/router/prerender-match.ts +53 -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 +44 -12
- package/src/router/segment-resolution/loader-cache.ts +205 -45
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +6 -6
- package/src/router/segment-resolution/mask-nested.ts +7 -7
- package/src/router/segment-resolution/revalidation.ts +34 -6
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +8 -45
- package/src/rsc/shell-capture-constants.ts +13 -0
- package/src/rsc/shell-capture.ts +111 -90
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +11 -0
- package/src/server/context.ts +88 -11
- package/src/server/cookie-store.ts +5 -4
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +35 -30
- package/src/ssr/index.tsx +40 -7
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -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 +5 -1
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +8 -6
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
|
@@ -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,15 @@ 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";
|
|
56
61
|
import type { CacheItemResult } from "./types.js";
|
|
62
|
+
import type { InternalHandlerContext } from "../types.js";
|
|
57
63
|
|
|
58
64
|
/**
|
|
59
65
|
* DJB2 hash returning an 8-char hex string. Deterministic across runtimes
|
|
@@ -125,10 +131,74 @@ export async function replyToCacheKey(
|
|
|
125
131
|
return encodeKV(pairs, { sort: true });
|
|
126
132
|
}
|
|
127
133
|
|
|
134
|
+
/**
|
|
135
|
+
* An unescaped temporary-reference token in an encodeReply part. A user
|
|
136
|
+
* string "$T" encodes as "$$T", a quote inside a string is escaped, and an
|
|
137
|
+
* object key "$T" is followed by ":".
|
|
138
|
+
*/
|
|
139
|
+
const TEMPORARY_REFERENCE_TOKEN = /(?<!\\)"\$T"(?!:)/g;
|
|
140
|
+
|
|
141
|
+
const REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element");
|
|
142
|
+
const CLIENT_REFERENCE_TYPE = Symbol.for("react.client.reference");
|
|
143
|
+
const SERVER_REFERENCE_TYPE = Symbol.for("react.server.reference");
|
|
144
|
+
|
|
145
|
+
function countTemporaryReferences(encoded: string | FormData): number {
|
|
146
|
+
if (typeof encoded === "string") {
|
|
147
|
+
return encoded.match(TEMPORARY_REFERENCE_TOKEN)?.length ?? 0;
|
|
148
|
+
}
|
|
149
|
+
let count = 0;
|
|
150
|
+
encoded.forEach((value) => {
|
|
151
|
+
if (typeof value === "string") {
|
|
152
|
+
count += value.match(TEMPORARY_REFERENCE_TOKEN)?.length ?? 0;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return count;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A React element, or a client or server reference function. */
|
|
159
|
+
function isRenderSlot(value: unknown): boolean {
|
|
160
|
+
if (typeof value === "object" && value !== null) {
|
|
161
|
+
return (value as { $$typeof?: unknown }).$$typeof === REACT_ELEMENT_TYPE;
|
|
162
|
+
}
|
|
163
|
+
if (typeof value === "function") {
|
|
164
|
+
const tag = (value as { $$typeof?: unknown }).$$typeof;
|
|
165
|
+
return tag === CLIENT_REFERENCE_TYPE || tag === SERVER_REFERENCE_TYPE;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Whether encodeReply wrote "$T" for a value that cannot be keyed. Given a
|
|
172
|
+
* temporary-reference set it does not throw on a value it cannot serialize:
|
|
173
|
+
* it writes "$T", so distinct arguments would share one key (issue #924).
|
|
174
|
+
* React elements and client/server references are render slots, left out of
|
|
175
|
+
* the key on purpose (the interleave route, e2e/use-cache-streaming.test.ts);
|
|
176
|
+
* a function, a symbol or a class instance is not.
|
|
177
|
+
*
|
|
178
|
+
* The set is the Map encodeReply fills: one entry per "$T" it writes, plus
|
|
179
|
+
* every object it visits. So more tokens than slot entries means a non-slot
|
|
180
|
+
* "$T", with no traversal of the arguments. Runs only on the encoder path.
|
|
181
|
+
*/
|
|
182
|
+
function hasUnkeyableReference(
|
|
183
|
+
encoded: string | FormData,
|
|
184
|
+
tempRefs: unknown,
|
|
185
|
+
): boolean {
|
|
186
|
+
const tokens = countTemporaryReferences(encoded);
|
|
187
|
+
if (tokens === 0) return false;
|
|
188
|
+
let slots = 0;
|
|
189
|
+
for (const value of (tempRefs as Map<string, unknown>).values()) {
|
|
190
|
+
if (isRenderSlot(value)) slots++;
|
|
191
|
+
}
|
|
192
|
+
return tokens > slots;
|
|
193
|
+
}
|
|
194
|
+
|
|
128
195
|
// Cached-fn ids already warned about running uncached under a test runner, so
|
|
129
196
|
// the test-ergonomics warning fires once per fn rather than once per call.
|
|
130
197
|
const warnedUncachedUnderTest = new Set<string>();
|
|
131
198
|
|
|
199
|
+
// Cached-fn ids already warned about arguments that cannot be keyed (dev only).
|
|
200
|
+
const warnedUnkeyableArgs = new Set<string>();
|
|
201
|
+
|
|
132
202
|
/**
|
|
133
203
|
* Fast-path cache-key builder for JSON-safe key args. Returns a deterministic
|
|
134
204
|
* string (object keys recursively sorted so insertion order can't change the
|
|
@@ -264,6 +334,30 @@ function raceLeader(
|
|
|
264
334
|
});
|
|
265
335
|
}
|
|
266
336
|
|
|
337
|
+
/**
|
|
338
|
+
* A stale refresh's view of a handler ctx: loader reads run on their own
|
|
339
|
+
* executor (setupLoaderAccess _runLoaderIsolated), and an inner HIT replays in
|
|
340
|
+
* full without claiming. The refresh diverts its pushes, so the request's
|
|
341
|
+
* memoized run of a loader would take that loader's live pushes off the page,
|
|
342
|
+
* and a claim would starve the page's own replay of it.
|
|
343
|
+
*/
|
|
344
|
+
function refreshView(arg: unknown): unknown {
|
|
345
|
+
const ctx = arg as InternalHandlerContext<any, any>;
|
|
346
|
+
const runIsolated = isTainted(arg) ? ctx._runLoaderIsolated : undefined;
|
|
347
|
+
if (!runIsolated) return arg;
|
|
348
|
+
const runs = new Map<string, Promise<any>>();
|
|
349
|
+
const use = (item: any) => {
|
|
350
|
+
if (isHandle(item)) return ctx.use(item);
|
|
351
|
+
let run = runs.get(item.$$id);
|
|
352
|
+
if (!run) runs.set(item.$$id, (run = runIsolated(item)));
|
|
353
|
+
return run;
|
|
354
|
+
};
|
|
355
|
+
return Object.create(ctx, {
|
|
356
|
+
use: { value: use },
|
|
357
|
+
_claimLoaderPushes: { value: undefined },
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
267
361
|
// ============================================================================
|
|
268
362
|
// Core: registerCachedFunction
|
|
269
363
|
// ============================================================================
|
|
@@ -333,17 +427,33 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
333
427
|
);
|
|
334
428
|
}
|
|
335
429
|
|
|
336
|
-
// Separate
|
|
337
|
-
// For
|
|
338
|
-
// searchParams), extract serializable values
|
|
339
|
-
// different routes, param combinations, and query
|
|
340
|
-
// distinct cache entries.
|
|
430
|
+
// Separate request-scoped args (ctx, a Request, the request's env) from
|
|
431
|
+
// key-generating args. For those that carry route context (ctx params,
|
|
432
|
+
// pathname, searchParams; a Request's URL), extract serializable values
|
|
433
|
+
// into the key so different routes, param combinations, and query
|
|
434
|
+
// variants produce distinct cache entries.
|
|
341
435
|
const keyArgs: unknown[] = [];
|
|
342
436
|
let hasTaintedArgs = false;
|
|
437
|
+
// The calling segment, read synchronously as ctx.use(Handle) does: a HIT
|
|
438
|
+
// replays into it. A route's layouts and page share one handler ctx and
|
|
439
|
+
// so one key; the recorded ids are the first caller's. Its ctx's claim
|
|
440
|
+
// keeps a replayed loader's pushes to one copy per request.
|
|
441
|
+
let callerSegmentId: string | undefined;
|
|
442
|
+
let claimLoaderPushes: ((loaderId: string) => boolean) | undefined;
|
|
443
|
+
// A fetchable loader's request body (loader-fetch.ts) is an input the
|
|
444
|
+
// route fields leave out: such a call runs uncached (#940).
|
|
445
|
+
let hasBodyCtx = false;
|
|
343
446
|
for (const arg of args) {
|
|
344
447
|
if (isTainted(arg)) {
|
|
345
448
|
hasTaintedArgs = true;
|
|
346
449
|
const ctx = arg as any;
|
|
450
|
+
if (ctx.body !== undefined || ctx.formData !== undefined) {
|
|
451
|
+
hasBodyCtx = true;
|
|
452
|
+
}
|
|
453
|
+
if (callerSegmentId === undefined) {
|
|
454
|
+
callerSegmentId = ctx._currentSegmentId;
|
|
455
|
+
claimLoaderPushes = ctx._claimLoaderPushes;
|
|
456
|
+
}
|
|
347
457
|
if (ctx.params && typeof ctx.params === "object") {
|
|
348
458
|
// Include host to prevent cross-host cache collisions (same
|
|
349
459
|
// pattern as route-level cache-scope.ts key generation).
|
|
@@ -373,6 +483,22 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
373
483
|
}
|
|
374
484
|
}
|
|
375
485
|
}
|
|
486
|
+
} else if (arg instanceof Request) {
|
|
487
|
+
// A raw Request (ctx.request) is request-scoped like ctx: fold in its
|
|
488
|
+
// URL with the same host-namespacing and search normalization.
|
|
489
|
+
const url = new URL(arg.url);
|
|
490
|
+
keyArgs.push(
|
|
491
|
+
cacheKeyBase(
|
|
492
|
+
url.host,
|
|
493
|
+
url.pathname,
|
|
494
|
+
url.searchParams,
|
|
495
|
+
undefined,
|
|
496
|
+
requestCtx?._searchParamsFilter,
|
|
497
|
+
),
|
|
498
|
+
);
|
|
499
|
+
} else if (arg != null && arg === requestCtx?.env) {
|
|
500
|
+
// The request's env is left out: constant per deployment, and its
|
|
501
|
+
// bindings are not serializable.
|
|
376
502
|
} else {
|
|
377
503
|
keyArgs.push(arg);
|
|
378
504
|
}
|
|
@@ -383,7 +509,7 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
383
509
|
// gone. Throw early rather than silently dropping handle side effects.
|
|
384
510
|
if (hasTaintedArgs && !requestCtx?._handleStore) {
|
|
385
511
|
throw new Error(
|
|
386
|
-
`[use cache] "${id}" receives a
|
|
512
|
+
`[use cache] "${id}" receives a ctx argument but the ` +
|
|
387
513
|
`HandleStore is not available. This typically happens when a "use cache" ` +
|
|
388
514
|
`function with ctx runs outside the request context (e.g., during late ` +
|
|
389
515
|
`streaming after AsyncLocalStorage context is lost). Move the "use cache" ` +
|
|
@@ -391,10 +517,16 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
391
517
|
`use the route-level cache() DSL instead.`,
|
|
392
518
|
);
|
|
393
519
|
}
|
|
520
|
+
// Records group pushes by owning loader (useCacheRecordKey) and replay
|
|
521
|
+
// into the caller's segment; a caller without one records segment ids.
|
|
522
|
+
const ownerKeyed = callerSegmentId !== undefined;
|
|
394
523
|
|
|
395
524
|
// Generate cache key
|
|
396
525
|
let cacheKey: string;
|
|
397
526
|
try {
|
|
527
|
+
if (hasBodyCtx) {
|
|
528
|
+
throw new Error("ctx carries a request body");
|
|
529
|
+
}
|
|
398
530
|
if (keyArgs.length > 0) {
|
|
399
531
|
// Fast path: when every key arg is JSON-safe, build the key with a
|
|
400
532
|
// deterministic stable-stringify and skip encodeReply (the Flight reply
|
|
@@ -410,6 +542,9 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
410
542
|
const encoded = await encodeReply(keyArgs as unknown[], {
|
|
411
543
|
temporaryReferences: tempRefs,
|
|
412
544
|
});
|
|
545
|
+
if (hasUnkeyableReference(encoded, tempRefs)) {
|
|
546
|
+
throw new Error("unserializable key argument");
|
|
547
|
+
}
|
|
413
548
|
const argsKey = await replyToCacheKey(encoded);
|
|
414
549
|
cacheKey = `use-cache:${id}:${argsKey}`;
|
|
415
550
|
}
|
|
@@ -420,6 +555,19 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
420
555
|
// Non-serializable args: run uncached (within a tag scope so cacheTag()
|
|
421
556
|
// still does not throw). Record runtime tags so the document union still
|
|
422
557
|
// sees them even though this call is not itself cached.
|
|
558
|
+
if (
|
|
559
|
+
process.env.NODE_ENV !== "production" &&
|
|
560
|
+
!warnedUnkeyableArgs.has(id)
|
|
561
|
+
) {
|
|
562
|
+
warnedUnkeyableArgs.add(id);
|
|
563
|
+
console.warn(
|
|
564
|
+
`[use cache] "${id}" ran uncached: an argument, or a value nested in ` +
|
|
565
|
+
`one, cannot be serialized into the cache key (a function, class ` +
|
|
566
|
+
`instance or symbol, or a loader ctx carrying a request body). ` +
|
|
567
|
+
`Pass serializable values; ctx, a Request, the request's env and ` +
|
|
568
|
+
`React elements are handled.`,
|
|
569
|
+
);
|
|
570
|
+
}
|
|
423
571
|
const scoped = runWithCacheTagScope(() => fn.apply(this, args));
|
|
424
572
|
const result = await scoped.result;
|
|
425
573
|
recordRequestTags(scoped.tags, requestCtx);
|
|
@@ -441,7 +589,9 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
441
589
|
const handleStore = requestCtx?._handleStore;
|
|
442
590
|
if (handleStore) {
|
|
443
591
|
const r = await decodeHandles(entry.handles);
|
|
444
|
-
if (r)
|
|
592
|
+
if (r) {
|
|
593
|
+
appendHandles(r, handleStore, callerSegmentId, claimLoaderPushes);
|
|
594
|
+
}
|
|
445
595
|
}
|
|
446
596
|
}
|
|
447
597
|
recordRequestTags(entry.tags, requestCtx);
|
|
@@ -474,34 +624,29 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
474
624
|
// Stale hit: return stale value, revalidate in background
|
|
475
625
|
try {
|
|
476
626
|
const result = await serveCached(cached);
|
|
627
|
+
const liveStore = hasTaintedArgs ? requestCtx?._handleStore : undefined;
|
|
628
|
+
const recordKey = ownerKeyed ? useCacheRecordKey() : undefined;
|
|
477
629
|
// Background revalidation — must capture handles if tainted args present.
|
|
478
630
|
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
|
-
|
|
631
|
+
// The background body's handle pushes belong to the refreshed entry
|
|
632
|
+
// only: the foreground already replayed the stale entry's. They
|
|
633
|
+
// reach the REQUEST's store (a handler ctx.use(Handle) binds it at
|
|
634
|
+
// setupLoaderAccess, so a derived context with its own store never
|
|
635
|
+
// sees them), so the capture accepts only this body's chain and
|
|
636
|
+
// diverts what it records away from the live store. The foreground
|
|
637
|
+
// is still rendering here (runBackground starts on the next
|
|
638
|
+
// microtask), and its concurrent pushes are neither recorded nor
|
|
639
|
+
// diverted (issue #684, plan 010). A root scope: the task is
|
|
640
|
+
// detached, so an outer "use cache" body that scheduled it must not
|
|
641
|
+
// record the refresh's pushes.
|
|
642
|
+
const bgScope: CacheExecScope = {};
|
|
643
|
+
const bgCapture = liveStore
|
|
644
|
+
? startHandleCapture(liveStore, {
|
|
645
|
+
accept: () => isInCacheExecChain(bgScope),
|
|
646
|
+
divert: true,
|
|
647
|
+
key: recordKey,
|
|
496
648
|
})
|
|
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
|
-
}
|
|
649
|
+
: undefined;
|
|
505
650
|
|
|
506
651
|
// Tainted args are NOT stamped here, in contrast to the foreground
|
|
507
652
|
// miss path below. The args include the live HandlerContext the
|
|
@@ -522,23 +667,25 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
522
667
|
// getRequestContext().env.ApiKey) resolves during the background
|
|
523
668
|
// revalidation instead of throwing "called outside of a request
|
|
524
669
|
// 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.
|
|
670
|
+
// synchronous kickoff; its async continuations inherit it.
|
|
528
671
|
//
|
|
529
672
|
// The span opens inside the request ALS and wraps the WHOLE task —
|
|
530
673
|
// fn kickoff through the store write — so its platform spans nest
|
|
531
674
|
// under rango.background and a slow or failing setItem is part of
|
|
532
675
|
// the traced revalidation, not an untraced tail.
|
|
533
|
-
await runWithRequestContext(
|
|
676
|
+
await runWithRequestContext(requestCtx, () =>
|
|
534
677
|
observePhase(
|
|
535
678
|
PHASES.background("use-cache-revalidation"),
|
|
536
679
|
async () => {
|
|
680
|
+
const refreshArgs = args.map(refreshView);
|
|
537
681
|
const scoped = runWithCacheTagScope(() =>
|
|
538
|
-
runWithCacheExecScope(
|
|
682
|
+
runWithCacheExecScope(
|
|
683
|
+
() => fn.apply(this, refreshArgs),
|
|
684
|
+
bgScope,
|
|
685
|
+
),
|
|
539
686
|
);
|
|
540
687
|
const freshResult = await scoped.result;
|
|
541
|
-
|
|
688
|
+
bgCapture?.stop();
|
|
542
689
|
// Merge profile/DSL tags with runtime cacheTag() tags, read
|
|
543
690
|
// after awaiting so post-await cacheTag() calls are included.
|
|
544
691
|
// Normalize (drops empty profile tags, matching the
|
|
@@ -549,11 +696,24 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
549
696
|
),
|
|
550
697
|
];
|
|
551
698
|
recordRequestTags(freshTags, requestCtx);
|
|
552
|
-
const
|
|
699
|
+
const flightErrors: unknown[] = [];
|
|
700
|
+
const onFlightError = (error: unknown): void => {
|
|
701
|
+
flightErrors.push(error);
|
|
702
|
+
};
|
|
703
|
+
const serialized = await serializeResult(
|
|
704
|
+
freshResult,
|
|
705
|
+
onFlightError,
|
|
706
|
+
);
|
|
553
707
|
if (serialized !== null) {
|
|
554
|
-
const encodedHandles = bgCapture
|
|
555
|
-
? await encodeHandles(
|
|
708
|
+
const encodedHandles = bgCapture
|
|
709
|
+
? await encodeHandles(
|
|
710
|
+
bgCapture.capture.data,
|
|
711
|
+
onFlightError,
|
|
712
|
+
)
|
|
556
713
|
: undefined;
|
|
714
|
+
// An error row would replace the stale entry; the catch
|
|
715
|
+
// below reports it and the stale entry keeps serving.
|
|
716
|
+
if (flightErrors.length > 0) throw flightErrors[0];
|
|
557
717
|
await store.setItem!(cacheKey, serialized, {
|
|
558
718
|
handles: encodedHandles,
|
|
559
719
|
ttl: profile.ttl,
|
|
@@ -565,7 +725,7 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
565
725
|
),
|
|
566
726
|
);
|
|
567
727
|
} catch (bgError) {
|
|
568
|
-
|
|
728
|
+
bgCapture?.stop();
|
|
569
729
|
// Pass requestCtx explicitly: this runs in a detached background
|
|
570
730
|
// task where the ALS context is gone, so onError can only fire if
|
|
571
731
|
// we hand it the context captured up front.
|
|
@@ -576,8 +736,6 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
576
736
|
requestCtx,
|
|
577
737
|
);
|
|
578
738
|
}
|
|
579
|
-
// No finally: nothing shared was mutated — the derived context and
|
|
580
|
-
// its handle store are garbage after the task settles.
|
|
581
739
|
});
|
|
582
740
|
return result;
|
|
583
741
|
} catch (error) {
|
|
@@ -669,14 +827,17 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
669
827
|
};
|
|
670
828
|
|
|
671
829
|
// execute, serialize, store
|
|
830
|
+
// Record only this execution's pushes (and cached functions it calls):
|
|
831
|
+
// the handler and loaders push into the same store concurrently, and they
|
|
832
|
+
// re-run on a hit.
|
|
672
833
|
const handleStore = hasTaintedArgs ? requestCtx?._handleStore : undefined;
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
834
|
+
const execScope = createCacheExecScope();
|
|
835
|
+
const capture = handleStore
|
|
836
|
+
? startHandleCapture(handleStore, {
|
|
837
|
+
accept: () => isInCacheExecChain(execScope),
|
|
838
|
+
key: ownerKeyed ? useCacheRecordKey() : undefined,
|
|
839
|
+
})
|
|
840
|
+
: undefined;
|
|
680
841
|
|
|
681
842
|
// Stamp tainted args so ctx.set(), ctx.header(), etc. throw if called
|
|
682
843
|
// inside the cached function body (those side effects are lost on hit).
|
|
@@ -702,7 +863,7 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
702
863
|
let scoped: ReturnType<typeof runWithCacheTagScope>;
|
|
703
864
|
try {
|
|
704
865
|
scoped = runWithCacheTagScope(() =>
|
|
705
|
-
runWithCacheExecScope(() => fn.apply(this, args)),
|
|
866
|
+
runWithCacheExecScope(() => fn.apply(this, args), execScope),
|
|
706
867
|
);
|
|
707
868
|
result = await scoped.result;
|
|
708
869
|
} catch (execError) {
|
|
@@ -717,7 +878,7 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
717
878
|
unstampCacheExec(arg as object);
|
|
718
879
|
}
|
|
719
880
|
// Remove this capture token (order-independent, safe for concurrent use)
|
|
720
|
-
|
|
881
|
+
capture?.stop();
|
|
721
882
|
}
|
|
722
883
|
|
|
723
884
|
// Merge profile/DSL tags with runtime cacheTag() tags. Read scoped.tags
|
|
@@ -735,11 +896,19 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
735
896
|
const finalizeAndWrite = async (): Promise<void> => {
|
|
736
897
|
let serialized: string | null;
|
|
737
898
|
let encodedHandles: string | undefined;
|
|
899
|
+
const flightErrors: unknown[] = [];
|
|
900
|
+
const onFlightError = (error: unknown): void => {
|
|
901
|
+
flightErrors.push(error);
|
|
902
|
+
};
|
|
738
903
|
try {
|
|
739
|
-
serialized = await serializeResult(result);
|
|
740
|
-
encodedHandles = capture
|
|
741
|
-
? await encodeHandles(capture.data)
|
|
904
|
+
serialized = await serializeResult(result, onFlightError);
|
|
905
|
+
encodedHandles = capture
|
|
906
|
+
? await encodeHandles(capture.capture.data, onFlightError)
|
|
742
907
|
: undefined;
|
|
908
|
+
// Flight encodes an async component that throws, or a rejected
|
|
909
|
+
// promise in the result or a handle value, as an error row and
|
|
910
|
+
// completes normally; stored, every hit would serve that error.
|
|
911
|
+
if (flightErrors.length > 0) throw flightErrors[0];
|
|
743
912
|
} catch (buildError) {
|
|
744
913
|
// Serialize/handle-encode failed: no envelope for followers (they run
|
|
745
914
|
// fresh) and nothing to write.
|
package/src/cache/cache-scope.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { reportCacheError } from "./cache-error.js";
|
|
|
28
28
|
// is unchanged: both methods are async and already awaited the codec.
|
|
29
29
|
import {
|
|
30
30
|
captureHandles,
|
|
31
|
+
captureHandleOwners,
|
|
31
32
|
restoreHandles,
|
|
32
33
|
encodeHandles,
|
|
33
34
|
decodeHandles,
|
|
@@ -149,6 +150,14 @@ export class CacheScope {
|
|
|
149
150
|
config: PartialCacheOptions | false,
|
|
150
151
|
parent: CacheScope | null = null,
|
|
151
152
|
private readonly defaultKeyPrefix?: "doc",
|
|
153
|
+
/**
|
|
154
|
+
* @internal shortCode of the outermost cache() entry of this scope's
|
|
155
|
+
* enabled chain (createCacheScope). Entries above it are live: their
|
|
156
|
+
* segments are not stored and resolve fresh on a hit (withCacheLookup).
|
|
157
|
+
* Undefined covers the whole matched chain (the implicit shell doc scope,
|
|
158
|
+
* a ppr route).
|
|
159
|
+
*/
|
|
160
|
+
readonly boundary?: string,
|
|
152
161
|
) {
|
|
153
162
|
this.config = config;
|
|
154
163
|
this.parent = parent;
|
|
@@ -156,6 +165,24 @@ export class CacheScope {
|
|
|
156
165
|
this.explicitStore = config !== false ? config.store : undefined;
|
|
157
166
|
}
|
|
158
167
|
|
|
168
|
+
/**
|
|
169
|
+
* @internal Whether a segment belongs to this scope's cache entry. ShortCodes are
|
|
170
|
+
* hierarchical (server/context.ts getShortCode: parent shortCode + include
|
|
171
|
+
* scope + type letter + counter), so the boundary's subtree is every id that
|
|
172
|
+
* extends the boundary shortCode at a non-digit (`C1` must not claim `C10`).
|
|
173
|
+
* Intercept slot segments ride the target route's entry wherever the
|
|
174
|
+
* intercept is declared.
|
|
175
|
+
*/
|
|
176
|
+
covers(id: string, namespace?: string): boolean {
|
|
177
|
+
const boundary = this.boundary;
|
|
178
|
+
if (boundary === undefined) return true;
|
|
179
|
+
if (id.startsWith(boundary)) {
|
|
180
|
+
const next = id.charCodeAt(boundary.length);
|
|
181
|
+
return !(next >= 48 && next <= 57);
|
|
182
|
+
}
|
|
183
|
+
return namespace?.startsWith("intercept:") === true;
|
|
184
|
+
}
|
|
185
|
+
|
|
159
186
|
/**
|
|
160
187
|
* Whether caching is enabled for this scope
|
|
161
188
|
*/
|
|
@@ -316,11 +343,14 @@ export class CacheScope {
|
|
|
316
343
|
* @param pathname - URL pathname for cache key generation
|
|
317
344
|
* @param params - Route params for cache key generation
|
|
318
345
|
* @param isIntercept - Whether this is an intercept navigation (uses different cache key)
|
|
346
|
+
* @param claimLoaderPushes - The handler context's _claimLoaderPushes, for
|
|
347
|
+
* restoreHandles' loader-owned values
|
|
319
348
|
*/
|
|
320
349
|
async lookupRoute(
|
|
321
350
|
pathname: string,
|
|
322
351
|
params: Record<string, string>,
|
|
323
352
|
isIntercept?: boolean,
|
|
353
|
+
claimLoaderPushes?: (loaderId: string) => boolean,
|
|
324
354
|
): Promise<{
|
|
325
355
|
segments: ResolvedSegment[];
|
|
326
356
|
shouldRevalidate: boolean;
|
|
@@ -329,6 +359,7 @@ export class CacheScope {
|
|
|
329
359
|
pathname,
|
|
330
360
|
params,
|
|
331
361
|
isIntercept,
|
|
362
|
+
claimLoaderPushes,
|
|
332
363
|
);
|
|
333
364
|
return outcome.status === "hit" ? outcome.result : null;
|
|
334
365
|
}
|
|
@@ -355,6 +386,7 @@ export class CacheScope {
|
|
|
355
386
|
pathname: string,
|
|
356
387
|
params: Record<string, string>,
|
|
357
388
|
isIntercept?: boolean,
|
|
389
|
+
claimLoaderPushes?: (loaderId: string) => boolean,
|
|
358
390
|
): Promise<CacheRouteLookupOutcome> {
|
|
359
391
|
if (!this.enabled) return { status: "bypass" };
|
|
360
392
|
if (!this.conditionAllows("read")) return { status: "bypass" };
|
|
@@ -424,6 +456,14 @@ export class CacheScope {
|
|
|
424
456
|
return { status: "miss" };
|
|
425
457
|
}
|
|
426
458
|
|
|
459
|
+
// A record from a whole-chain writer (a previous build on an unversioned
|
|
460
|
+
// store) can hold segments above the boundary. withCacheLookup resolves
|
|
461
|
+
// those fresh, so replaying them too would duplicate them and their
|
|
462
|
+
// handle pushes.
|
|
463
|
+
if (this.boundary !== undefined) {
|
|
464
|
+
segments = segments.filter((s) => this.covers(s.id, s.namespace));
|
|
465
|
+
}
|
|
466
|
+
|
|
427
467
|
// A hit serves content that was tagged at write time, so the document
|
|
428
468
|
// tag union must include this entry's tags for updateTag()/revalidateTag()
|
|
429
469
|
// to invalidate any full-page entry built on top of it. The write path
|
|
@@ -438,7 +478,18 @@ export class CacheScope {
|
|
|
438
478
|
if (handleStore && cached.handles) {
|
|
439
479
|
const handlesRecord = await decodeHandles(cached.handles);
|
|
440
480
|
if (handlesRecord) {
|
|
441
|
-
|
|
481
|
+
if (this.boundary !== undefined) {
|
|
482
|
+
const kept = new Set(segments.map((s) => s.id));
|
|
483
|
+
for (const id of Object.keys(handlesRecord)) {
|
|
484
|
+
if (!kept.has(id)) delete handlesRecord[id];
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
restoreHandles(
|
|
488
|
+
handlesRecord,
|
|
489
|
+
handleStore,
|
|
490
|
+
cached.handleOwners,
|
|
491
|
+
claimLoaderPushes,
|
|
492
|
+
);
|
|
442
493
|
}
|
|
443
494
|
}
|
|
444
495
|
|
|
@@ -515,8 +566,11 @@ export class CacheScope {
|
|
|
515
566
|
if (!handleStore || !requestCtx) return;
|
|
516
567
|
|
|
517
568
|
// Exclude loader segments - loaders are always fresh by default
|
|
518
|
-
// Loaders can opt-in to caching with their own cache() config
|
|
519
|
-
|
|
569
|
+
// Loaders can opt-in to caching with their own cache() config.
|
|
570
|
+
// Segments above the boundary are live and never stored.
|
|
571
|
+
const nonLoaderSegments = segments.filter(
|
|
572
|
+
(s) => s.type !== "loader" && this.covers(s.id, s.namespace),
|
|
573
|
+
);
|
|
520
574
|
if (nonLoaderSegments.length === 0) return;
|
|
521
575
|
|
|
522
576
|
const ttl = this.ttl;
|
|
@@ -585,11 +639,8 @@ export class CacheScope {
|
|
|
585
639
|
}
|
|
586
640
|
|
|
587
641
|
// Collect handle data for non-loader segments only
|
|
588
|
-
const handles = captureHandles(
|
|
589
|
-
|
|
590
|
-
handleStore,
|
|
591
|
-
requestCtx._shellCaptureLoaderHandleValues,
|
|
592
|
-
);
|
|
642
|
+
const handles = captureHandles(nonLoaderSegments, handleStore);
|
|
643
|
+
const handleOwners = captureHandleOwners(nonLoaderSegments, handleStore);
|
|
593
644
|
|
|
594
645
|
try {
|
|
595
646
|
if (INTERNAL_RANGO_DEBUG) {
|
|
@@ -602,10 +653,18 @@ export class CacheScope {
|
|
|
602
653
|
// through the codec (not raw into the entry) so Promise/ReactNode handle
|
|
603
654
|
// values survive a JSON-serializing store — see encodeHandles.
|
|
604
655
|
const { serializeSegments } = await import("./segment-codec.js");
|
|
656
|
+
const flightErrors: unknown[] = [];
|
|
657
|
+
const onFlightError = (error: unknown): void => {
|
|
658
|
+
flightErrors.push(error);
|
|
659
|
+
};
|
|
605
660
|
const [serializedSegments, encodedHandles] = await Promise.all([
|
|
606
|
-
serializeSegments(nonLoaderSegments),
|
|
607
|
-
encodeHandles(handles),
|
|
661
|
+
serializeSegments(nonLoaderSegments, onFlightError),
|
|
662
|
+
encodeHandles(handles, onFlightError),
|
|
608
663
|
]);
|
|
664
|
+
// Flight encodes a component that throws (an async server component in
|
|
665
|
+
// the tree) or a rejected handle value as an error row and completes
|
|
666
|
+
// normally; stored, every HIT would render that error until expiry.
|
|
667
|
+
if (flightErrors.length > 0) throw flightErrors[0];
|
|
609
668
|
|
|
610
669
|
const data: CachedEntryData = {
|
|
611
670
|
segments: serializedSegments,
|
|
@@ -613,6 +672,7 @@ export class CacheScope {
|
|
|
613
672
|
expiresAt: Date.now() + ttl * 1000,
|
|
614
673
|
tags,
|
|
615
674
|
};
|
|
675
|
+
if (handleOwners && encodedHandles) data.handleOwners = handleOwners;
|
|
616
676
|
|
|
617
677
|
if (INTERNAL_RANGO_DEBUG) {
|
|
618
678
|
debugCacheLog(`[CacheScope] waitUntil: calling store.set for ${key}`);
|
|
@@ -640,14 +700,18 @@ export class CacheScope {
|
|
|
640
700
|
}
|
|
641
701
|
|
|
642
702
|
/**
|
|
643
|
-
* Create a cache scope from entry's cache config
|
|
703
|
+
* Create a cache scope from entry's cache config. `shortCode` is the cache()
|
|
704
|
+
* entry's: a scope nested in an enabled parent keeps the parent's boundary
|
|
705
|
+
* (one entry covers both), any other opens its own at this entry.
|
|
644
706
|
*/
|
|
645
707
|
export function createCacheScope(
|
|
646
708
|
config: { options: PartialCacheOptions | false } | undefined,
|
|
647
709
|
parent: CacheScope | null = null,
|
|
710
|
+
shortCode?: string,
|
|
648
711
|
): CacheScope | null {
|
|
649
712
|
if (!config) return parent; // No config, inherit parent
|
|
650
|
-
|
|
713
|
+
const boundary = parent?.enabled ? parent.boundary : shortCode;
|
|
714
|
+
return new CacheScope(config.options, parent, undefined, boundary);
|
|
651
715
|
}
|
|
652
716
|
|
|
653
717
|
type ShellImplicitCacheMarker = NonNullable<
|