@rangojs/router 0.15.1 → 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 +52 -0
- 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 +36 -10
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +3 -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/thenable-status.d.ts +9 -0
- 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 +884 -768
- package/package.json +4 -4
- 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 +43 -1
- 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 +75 -80
- 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 +106 -0
- package/src/prerender/build-shell-capture.ts +8 -11
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +134 -42
- package/src/route-definition/helpers-types.ts +36 -10
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +2 -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/ssr-suspension-warning.ts +2 -13
- 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/ThemeProvider.tsx +14 -12
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/thenable-status.ts +16 -0
- 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 +24 -4
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
|
@@ -72,6 +72,13 @@ type GetScrollKeyFunction = (location: {
|
|
|
72
72
|
|
|
73
73
|
let customGetKey: GetScrollKeyFunction | null = null;
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Scroll key of the entry on screen. At popstate history.state already belongs
|
|
77
|
+
* to the destination, so this is the only way to save the page being left
|
|
78
|
+
* under its own key (see handleTraversalStart).
|
|
79
|
+
*/
|
|
80
|
+
let displayedKey: string | null = null;
|
|
81
|
+
|
|
75
82
|
/**
|
|
76
83
|
* Generate a unique key for the current history entry.
|
|
77
84
|
* Uses history.state.key if available, otherwise generates and stores a new one.
|
|
@@ -138,7 +145,7 @@ export function initScrollRestoration(options?: {
|
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
// Ensure current history entry has a key
|
|
141
|
-
|
|
148
|
+
trackDisplayedEntry();
|
|
142
149
|
|
|
143
150
|
// Save scroll positions on pagehide (before leaving/refreshing)
|
|
144
151
|
const handlePageHide = () => {
|
|
@@ -148,7 +155,14 @@ export function initScrollRestoration(options?: {
|
|
|
148
155
|
window.history.scrollRestoration = "auto";
|
|
149
156
|
};
|
|
150
157
|
|
|
158
|
+
// A bfcache restore keeps the "auto" set on pagehide; take scroll back.
|
|
159
|
+
// Only here: without <Html.ScrollRestoration> the browser owns scroll.
|
|
160
|
+
const handlePageShow = (event: PageTransitionEvent) => {
|
|
161
|
+
if (event.persisted) window.history.scrollRestoration = "manual";
|
|
162
|
+
};
|
|
163
|
+
|
|
151
164
|
window.addEventListener("pagehide", handlePageHide);
|
|
165
|
+
window.addEventListener("pageshow", handlePageShow);
|
|
152
166
|
|
|
153
167
|
if (IS_BROWSER_DEBUG) {
|
|
154
168
|
debugLog(
|
|
@@ -160,10 +174,12 @@ export function initScrollRestoration(options?: {
|
|
|
160
174
|
return () => {
|
|
161
175
|
cancelScrollRestorationPolling();
|
|
162
176
|
window.removeEventListener("pagehide", handlePageHide);
|
|
177
|
+
window.removeEventListener("pageshow", handlePageShow);
|
|
163
178
|
window.history.scrollRestoration = "auto";
|
|
164
179
|
initialized = false;
|
|
165
180
|
savedScrollPositions = {};
|
|
166
181
|
scrollKeyOrder = [];
|
|
182
|
+
displayedKey = null;
|
|
167
183
|
};
|
|
168
184
|
}
|
|
169
185
|
|
|
@@ -172,8 +188,10 @@ export function initScrollRestoration(options?: {
|
|
|
172
188
|
* Maintains bounded size by evicting oldest entries when the limit is exceeded.
|
|
173
189
|
*/
|
|
174
190
|
export function saveCurrentScrollPosition(): void {
|
|
175
|
-
|
|
191
|
+
saveScrollPosition(getScrollKey(), window.scrollY);
|
|
192
|
+
}
|
|
176
193
|
|
|
194
|
+
function saveScrollPosition(key: string, y: number): void {
|
|
177
195
|
// If this key already exists, remove it from its current position
|
|
178
196
|
// in the order array so it can be re-appended at the end (most recent).
|
|
179
197
|
const existingIndex = scrollKeyOrder.indexOf(key);
|
|
@@ -181,7 +199,7 @@ export function saveCurrentScrollPosition(): void {
|
|
|
181
199
|
scrollKeyOrder.splice(existingIndex, 1);
|
|
182
200
|
}
|
|
183
201
|
|
|
184
|
-
savedScrollPositions[key] =
|
|
202
|
+
savedScrollPositions[key] = y;
|
|
185
203
|
scrollKeyOrder.push(key);
|
|
186
204
|
|
|
187
205
|
// Evict oldest entries if we exceed the limit
|
|
@@ -354,6 +372,23 @@ export function handleNavigationStart(): void {
|
|
|
354
372
|
saveCurrentScrollPosition();
|
|
355
373
|
}
|
|
356
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Handle scroll for a back/forward traversal, at popstate before anything
|
|
377
|
+
* renders: save the page being left under ITS key (history has already moved,
|
|
378
|
+
* so saveCurrentScrollPosition would use the destination's key) and track the
|
|
379
|
+
* destination as the displayed entry.
|
|
380
|
+
*/
|
|
381
|
+
export function handleTraversalStart(): void {
|
|
382
|
+
if (!initialized) return;
|
|
383
|
+
const leftKey = displayedKey;
|
|
384
|
+
trackDisplayedEntry();
|
|
385
|
+
// A key shared with the destination (a getKey that collapses entries, e.g.
|
|
386
|
+
// by pathname) keeps the destination's saved position.
|
|
387
|
+
if (leftKey !== null && leftKey !== displayedKey) {
|
|
388
|
+
saveScrollPosition(leftKey, window.scrollY);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
357
392
|
/**
|
|
358
393
|
* Handle scroll after navigation completes.
|
|
359
394
|
* @param options.restore - If true, restore saved position (for popstate)
|
|
@@ -376,10 +411,17 @@ export function handleNavigationEnd(options: {
|
|
|
376
411
|
// But basic scroll-to-top and hash scrolling work without it — this
|
|
377
412
|
// matters during cross-app navigation where ScrollRestoration unmounts
|
|
378
413
|
// and remounts, creating a brief window where initialized is false.
|
|
379
|
-
if (restore
|
|
380
|
-
if (
|
|
414
|
+
if (restore) {
|
|
415
|
+
if (
|
|
416
|
+
initialized &&
|
|
417
|
+
restoreScrollPosition({ retryIfStreaming: true, isStreaming })
|
|
418
|
+
) {
|
|
381
419
|
return;
|
|
382
420
|
}
|
|
421
|
+
// An "auto" entry (no <Html.ScrollRestoration>, or created before it mounted)
|
|
422
|
+
// is restored by the browser. A scrollTo here races that restore, and
|
|
423
|
+
// when it runs second the page lands at the top.
|
|
424
|
+
if (window.history.scrollRestoration === "auto") return;
|
|
383
425
|
// Fall through to hash or top if no saved position
|
|
384
426
|
}
|
|
385
427
|
|
|
@@ -410,5 +452,10 @@ export function handleNavigationEnd(options: {
|
|
|
410
452
|
* Call this after changing history to ensure new entry has a key.
|
|
411
453
|
*/
|
|
412
454
|
export function ensureHistoryKey(): void {
|
|
413
|
-
|
|
455
|
+
trackDisplayedEntry();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Mints the current entry's key if missing and records it as displayed. */
|
|
459
|
+
function trackDisplayedEntry(): void {
|
|
460
|
+
displayedKey = getScrollKey();
|
|
414
461
|
}
|
|
@@ -26,19 +26,52 @@
|
|
|
26
26
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
27
27
|
import { _setCacheExecScopeProbe } from "./taint.js";
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* One "use cache" execution. Identity matters: the execution's handle capture
|
|
31
|
+
* (cache-runtime.ts) records only pushes from its own chain, including cached
|
|
32
|
+
* functions it calls (`parent` links them), not concurrent pushes into the
|
|
33
|
+
* same request store.
|
|
34
|
+
*/
|
|
35
|
+
export interface CacheExecScope {
|
|
36
|
+
readonly parent?: CacheExecScope;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const cacheExecStorage = new AsyncLocalStorage<CacheExecScope>();
|
|
30
40
|
|
|
31
41
|
/**
|
|
32
42
|
* Run fn with the "use cache" execution scope active. Continuations spawned
|
|
33
43
|
* from fn's synchronous kickoff inherit the scope; parallel chains do not.
|
|
44
|
+
* Pass `scope` (createCacheExecScope) to test membership with
|
|
45
|
+
* isInCacheExecChain.
|
|
34
46
|
*/
|
|
35
|
-
export function runWithCacheExecScope<T>(
|
|
36
|
-
|
|
47
|
+
export function runWithCacheExecScope<T>(
|
|
48
|
+
fn: () => T,
|
|
49
|
+
scope: CacheExecScope = createCacheExecScope(),
|
|
50
|
+
): T {
|
|
51
|
+
return cacheExecStorage.run(scope, fn);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A scope nested in the calling chain's current one, if any. */
|
|
55
|
+
export function createCacheExecScope(): CacheExecScope {
|
|
56
|
+
return { parent: cacheExecStorage.getStore() };
|
|
37
57
|
}
|
|
38
58
|
|
|
39
59
|
/** True when the calling async chain is inside a "use cache" body. */
|
|
40
60
|
export function isInsideCacheExecScope(): boolean {
|
|
41
|
-
return cacheExecStorage.getStore()
|
|
61
|
+
return cacheExecStorage.getStore() !== undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The calling chain's innermost "use cache" scope, if any. */
|
|
65
|
+
export function getCacheExecScope(): CacheExecScope | undefined {
|
|
66
|
+
return cacheExecStorage.getStore();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** True when the calling async chain runs inside `scope`, at any depth. */
|
|
70
|
+
export function isInCacheExecChain(scope: CacheExecScope): boolean {
|
|
71
|
+
for (let s = cacheExecStorage.getStore(); s; s = s.parent) {
|
|
72
|
+
if (s === scope) return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
42
75
|
}
|
|
43
76
|
|
|
44
77
|
// Wire the ambient ctx-method guards (assertNotInsideCacheExec in taint.ts)
|
|
@@ -20,12 +20,7 @@ import type { SearchParamsFilter } from "./search-params-filter.js";
|
|
|
20
20
|
* slot; an allowlist keeps the router's own params out of the key while leaving
|
|
21
21
|
* consumer `__` params intact.
|
|
22
22
|
*/
|
|
23
|
-
const RESERVED_SEARCH_PARAMS = new Set([
|
|
24
|
-
"__no_cache",
|
|
25
|
-
"__rsc",
|
|
26
|
-
"__html",
|
|
27
|
-
"__prerender_collect",
|
|
28
|
-
]);
|
|
23
|
+
const RESERVED_SEARCH_PARAMS = new Set(["__no_cache", "__rsc", "__html"]);
|
|
29
24
|
|
|
30
25
|
function isReservedSearchParam(key: string): boolean {
|
|
31
26
|
return key.startsWith("_rsc") || RESERVED_SEARCH_PARAMS.has(key);
|
|
@@ -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.
|