@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
package/src/route-types.ts
CHANGED
|
@@ -106,6 +106,8 @@ export type CacheItem = {
|
|
|
106
106
|
export type TransitionItem = {
|
|
107
107
|
name: string;
|
|
108
108
|
type: "transition";
|
|
109
|
+
/** Wrapper form only: the children it wraps (see hasRoutesInItem). */
|
|
110
|
+
uses?: AllUseItems[];
|
|
109
111
|
};
|
|
110
112
|
|
|
111
113
|
/**
|
|
@@ -220,11 +222,8 @@ export type ParallelUseItem =
|
|
|
220
222
|
| TransitionItem;
|
|
221
223
|
export type InterceptUseItem =
|
|
222
224
|
| MiddlewareItem
|
|
223
|
-
| RevalidateItem
|
|
224
225
|
| LoaderItem
|
|
225
226
|
| LoadingItem
|
|
226
|
-
| ErrorBoundaryItem
|
|
227
|
-
| NotFoundBoundaryItem
|
|
228
227
|
| LayoutItem
|
|
229
228
|
| RouteItem
|
|
230
229
|
| TransitionItem;
|
|
@@ -5,8 +5,9 @@ import MapRootLayout from "../server/root-layout.js";
|
|
|
5
5
|
import type { RouteEntry, TrailingSlashMode } from "../types";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Build a serialized manifest from
|
|
9
|
-
* Used by the `debugManifest()` method on the router instance.
|
|
8
|
+
* Build a serialized manifest from the eager route entries for debug
|
|
9
|
+
* inspection. Used by the `debugManifest()` method on the router instance.
|
|
10
|
+
* include() children are not expanded (lazy placeholders are skipped).
|
|
10
11
|
*/
|
|
11
12
|
export async function buildDebugManifest<TEnv = any>(
|
|
12
13
|
routesEntries: RouteEntry<TEnv>[],
|
|
@@ -14,6 +15,13 @@ export async function buildDebugManifest<TEnv = any>(
|
|
|
14
15
|
const manifest = new Map<string, EntryData>();
|
|
15
16
|
|
|
16
17
|
for (const entry of routesEntries) {
|
|
18
|
+
// Lazy include placeholders are not mounts: a top-level one carries the
|
|
19
|
+
// parent urls() handler (router.ts routes()), so re-running it re-registers
|
|
20
|
+
// the parent's routes into the shared manifest (path() duplicate-name
|
|
21
|
+
// invariant). A nested one (lazy-includes.ts runExpansion()) is only valid
|
|
22
|
+
// under its captured include context.
|
|
23
|
+
if (entry.lazy) continue;
|
|
24
|
+
|
|
17
25
|
const Store = {
|
|
18
26
|
manifest,
|
|
19
27
|
namespace: `debug.M${entry.mountIndex}`,
|
|
@@ -105,31 +105,67 @@ export function invokeOnError<TEnv = any>(
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* First non-undefined `pick(orphan)` over the routeless entries in
|
|
110
|
+
* `entry.layout` and, recursively, in theirs: depth-first in render order
|
|
111
|
+
* (resolveOrphanLayout in segment-resolution/fresh.ts). Since #922 a routeless
|
|
112
|
+
* entry nested in another renders (a layout() after a bare cache() marker, a
|
|
113
|
+
* layout() in a routeless wrapper); a one-level scan missed the boundaries and
|
|
114
|
+
* intercepts it declares (#926).
|
|
115
|
+
*
|
|
116
|
+
* `skip` is the entry an upward walk came from. Only a bare cache() marker is
|
|
117
|
+
* both in its layout's layout[] and in the chain, and its subtree was already
|
|
118
|
+
* scanned at its chain position.
|
|
119
|
+
*/
|
|
120
|
+
export function findInOrphans<T>(
|
|
121
|
+
entry: EntryData,
|
|
122
|
+
pick: (orphan: EntryData) => T | undefined,
|
|
123
|
+
skip?: EntryData | null,
|
|
124
|
+
): T | undefined {
|
|
125
|
+
for (const orphan of entry.layout ?? []) {
|
|
126
|
+
if (orphan === skip) continue;
|
|
127
|
+
const found = pick(orphan) ?? findInOrphans(orphan, pick);
|
|
128
|
+
if (found !== undefined) return found;
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** `findInOrphans` pick: the orphan when it declares an errorBoundary(). */
|
|
134
|
+
export const withErrorBoundary = (entry: EntryData): EntryData | undefined =>
|
|
135
|
+
entry.errorBoundary && entry.errorBoundary.length > 0 ? entry : undefined;
|
|
136
|
+
|
|
137
|
+
const withNotFoundBoundary = (entry: EntryData): EntryData | undefined =>
|
|
138
|
+
entry.notFoundBoundary && entry.notFoundBoundary.length > 0
|
|
139
|
+
? entry
|
|
140
|
+
: undefined;
|
|
141
|
+
|
|
108
142
|
/**
|
|
109
143
|
* Find the nearest error boundary by walking up the entry chain
|
|
110
144
|
* Also checks sibling layouts (orphan layouts) for error boundaries
|
|
111
145
|
* Returns the first fallback found, or the default error boundary if configured
|
|
146
|
+
*
|
|
147
|
+
* An orphan's parent is null, so from an orphan the walk continues at its
|
|
148
|
+
* orphanOwner (EntryData). Without that step, errors from an intercept declared
|
|
149
|
+
* in a routeless layout, or from that layout's loaders, stopped at the orphan
|
|
150
|
+
* and skipped every ancestor boundary (#898).
|
|
112
151
|
*/
|
|
113
152
|
export function findNearestErrorBoundary(
|
|
114
153
|
entry: EntryData | null,
|
|
115
154
|
defaultErrorBoundary?: ReactNode | ErrorBoundaryHandler,
|
|
116
155
|
): ReactNode | ErrorBoundaryHandler | null {
|
|
117
156
|
let current: EntryData | null = entry;
|
|
157
|
+
let prev: EntryData | null = null;
|
|
118
158
|
|
|
119
159
|
while (current) {
|
|
120
160
|
if (current.errorBoundary && current.errorBoundary.length > 0) {
|
|
121
161
|
return current.errorBoundary[current.errorBoundary.length - 1];
|
|
122
162
|
}
|
|
123
163
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (orphan.errorBoundary && orphan.errorBoundary.length > 0) {
|
|
127
|
-
return orphan.errorBoundary[orphan.errorBoundary.length - 1];
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
164
|
+
const orphan = findInOrphans(current, withErrorBoundary, prev);
|
|
165
|
+
if (orphan) return orphan.errorBoundary[orphan.errorBoundary.length - 1];
|
|
131
166
|
|
|
132
|
-
|
|
167
|
+
prev = current;
|
|
168
|
+
current = current.parent ?? current.orphanOwner ?? null;
|
|
133
169
|
}
|
|
134
170
|
|
|
135
171
|
// Return default error boundary if configured
|
|
@@ -145,6 +181,7 @@ export function findNearestNotFoundBoundary(
|
|
|
145
181
|
defaultNotFoundBoundary?: ReactNode | NotFoundBoundaryHandler,
|
|
146
182
|
): ReactNode | NotFoundBoundaryHandler | null {
|
|
147
183
|
let current: EntryData | null = entry;
|
|
184
|
+
let prev: EntryData | null = null;
|
|
148
185
|
|
|
149
186
|
while (current) {
|
|
150
187
|
if (current.notFoundBoundary && current.notFoundBoundary.length > 0) {
|
|
@@ -154,15 +191,14 @@ export function findNearestNotFoundBoundary(
|
|
|
154
191
|
// Check orphan layouts mirroring findNearestErrorBoundary: notFoundBoundary
|
|
155
192
|
// attaches identically (onto parent.notFoundBoundary), and an orphan layout
|
|
156
193
|
// (parent=null) is reachable only via this scan. First sibling is "outer".
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return orphan.notFoundBoundary[orphan.notFoundBoundary.length - 1];
|
|
161
|
-
}
|
|
162
|
-
}
|
|
194
|
+
const orphan = findInOrphans(current, withNotFoundBoundary, prev);
|
|
195
|
+
if (orphan) {
|
|
196
|
+
return orphan.notFoundBoundary[orphan.notFoundBoundary.length - 1];
|
|
163
197
|
}
|
|
164
198
|
|
|
165
|
-
|
|
199
|
+
// Orphan: continue at its owner, as findNearestErrorBoundary does.
|
|
200
|
+
prev = current;
|
|
201
|
+
current = current.parent ?? current.orphanOwner ?? null;
|
|
166
202
|
}
|
|
167
203
|
|
|
168
204
|
// Return default notFound boundary if configured
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "../context-var.js";
|
|
17
17
|
import {
|
|
18
18
|
assertCachedHeaderWriteAllowed,
|
|
19
|
-
|
|
19
|
+
assertNonCacheableReadAllowed,
|
|
20
20
|
} from "../server/context.js";
|
|
21
21
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
22
22
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
@@ -29,6 +29,32 @@ import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
|
29
29
|
// once at module load instead of per createHandlerContext (per-request) call.
|
|
30
30
|
const MUTATING_HEADERS_METHODS = new Set(["set", "append", "delete"]);
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* `headers` whose mutating methods run `assertWrite(method)` first. Shared by
|
|
34
|
+
* the handler ctx (createHandlerContext) and the middleware ctx
|
|
35
|
+
* (createMiddlewareContext).
|
|
36
|
+
*/
|
|
37
|
+
export function guardHeaderWrites(
|
|
38
|
+
headers: Headers,
|
|
39
|
+
assertWrite: (method: string | symbol) => void,
|
|
40
|
+
): Headers {
|
|
41
|
+
return new Proxy(headers, {
|
|
42
|
+
get(target, prop, receiver) {
|
|
43
|
+
const value = Reflect.get(target, prop, receiver);
|
|
44
|
+
if (typeof value === "function") {
|
|
45
|
+
if (MUTATING_HEADERS_METHODS.has(prop as string)) {
|
|
46
|
+
return (...args: any[]) => {
|
|
47
|
+
assertWrite(prop);
|
|
48
|
+
return value.apply(target, args);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return value.bind(target);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
32
58
|
/**
|
|
33
59
|
* Strip internal _rsc* query params from a URL.
|
|
34
60
|
* Returns a new URL with only user-facing params.
|
|
@@ -224,23 +250,10 @@ export function createHandlerContext<TEnv>(
|
|
|
224
250
|
// assertCachedHeaderWriteAllowed, the unified #713 guard).
|
|
225
251
|
// Uses lazy `ctx` reference (assigned below) — only the specific handler ctx
|
|
226
252
|
// is stamped by cache-runtime, not the shared request context.
|
|
227
|
-
// MUTATING_HEADERS_METHODS is hoisted to module scope (constant, read-only).
|
|
228
253
|
let ctx: InternalHandlerContext<any, TEnv>;
|
|
229
|
-
const guardedHeaders =
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (typeof value === "function") {
|
|
233
|
-
if (MUTATING_HEADERS_METHODS.has(prop as string)) {
|
|
234
|
-
return (...args: any[]) => {
|
|
235
|
-
assertNotInsideCacheExec(ctx, "headers");
|
|
236
|
-
assertCachedHeaderWriteAllowed("ctx.headers", prop);
|
|
237
|
-
return value.apply(target, args);
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
return value.bind(target);
|
|
241
|
-
}
|
|
242
|
-
return value;
|
|
243
|
-
},
|
|
254
|
+
const guardedHeaders = guardHeaderWrites(stubResponse.headers, (prop) => {
|
|
255
|
+
assertNotInsideCacheExec(ctx, "headers");
|
|
256
|
+
assertCachedHeaderWriteAllowed("ctx.headers", prop);
|
|
244
257
|
});
|
|
245
258
|
|
|
246
259
|
ctx = {
|
|
@@ -263,14 +276,10 @@ export function createHandlerContext<TEnv>(
|
|
|
263
276
|
executionContext: requestContext?.executionContext,
|
|
264
277
|
_variables: variables,
|
|
265
278
|
get: ((keyOrVar: any) => {
|
|
266
|
-
// Read-time guard: non-cacheable var inside cache() →
|
|
267
|
-
// Works for both ContextVar tokens and string keys.
|
|
268
|
-
if (isNonCacheable(variables, keyOrVar)
|
|
269
|
-
|
|
270
|
-
`ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
|
|
271
|
-
`The variable was created with { cache: false } or set with { cache: false }, ` +
|
|
272
|
-
`and its value would be stale on cache hit. Move the read outside the cached scope.`,
|
|
273
|
-
);
|
|
279
|
+
// Read-time guard: non-cacheable var inside "use cache" or cache() →
|
|
280
|
+
// throw. Works for both ContextVar tokens and string keys.
|
|
281
|
+
if (isNonCacheable(variables, keyOrVar)) {
|
|
282
|
+
assertNonCacheableReadAllowed(keyOrVar);
|
|
274
283
|
}
|
|
275
284
|
return contextGet(variables, keyOrVar);
|
|
276
285
|
}) as HandlerContext<any, TEnv>["get"],
|
|
@@ -26,8 +26,12 @@ import {
|
|
|
26
26
|
warnOnStreamedResponse,
|
|
27
27
|
buildLoaderErrorContext,
|
|
28
28
|
} from "./segment-resolution.js";
|
|
29
|
+
import { catchSegmentError } from "./segment-resolution/helpers.js";
|
|
30
|
+
import { findInOrphans } from "./error-handling.js";
|
|
31
|
+
import { resolveLoaderData } from "./segment-resolution/loader-cache.js";
|
|
29
32
|
import type { SegmentResolutionDeps } from "./types.js";
|
|
30
33
|
import { debugLog } from "./logging.js";
|
|
34
|
+
import { getRouterContext } from "./router-context.js";
|
|
31
35
|
import {
|
|
32
36
|
RangoContext,
|
|
33
37
|
latchPprHeaderScopeForEntries,
|
|
@@ -101,15 +105,20 @@ export function collectInterceptTargetNames(
|
|
|
101
105
|
fromEntry: EntryData | null,
|
|
102
106
|
): string[] {
|
|
103
107
|
const names = new Set<string>();
|
|
108
|
+
// Tolerate partial entries (unit-test mocks omit the arrays); real
|
|
109
|
+
// registration always populates layout/intercept.
|
|
110
|
+
const add = (source: EntryData): undefined => {
|
|
111
|
+
for (const intercept of source.intercept ?? []) {
|
|
112
|
+
names.add(intercept.routeName);
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
};
|
|
104
116
|
let current: EntryData | null = fromEntry;
|
|
117
|
+
let prev: EntryData | null = null;
|
|
105
118
|
while (current) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
for (const intercept of source.intercept ?? []) {
|
|
110
|
-
names.add(intercept.routeName);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
119
|
+
add(current);
|
|
120
|
+
findInOrphans(current, add, prev);
|
|
121
|
+
prev = current;
|
|
113
122
|
current = current.parent;
|
|
114
123
|
}
|
|
115
124
|
return [...names];
|
|
@@ -121,27 +130,62 @@ export function findInterceptForRoute(
|
|
|
121
130
|
selectorContext: InterceptSelectorContext | null = null,
|
|
122
131
|
isAction: boolean = false,
|
|
123
132
|
): { intercept: InterceptEntry; entry: EntryData } | null {
|
|
133
|
+
const match = (source: EntryData) => {
|
|
134
|
+
for (const intercept of source.intercept) {
|
|
135
|
+
if (
|
|
136
|
+
intercept.routeName === targetRouteKey &&
|
|
137
|
+
evaluateInterceptWhen(intercept, selectorContext, isAction)
|
|
138
|
+
) {
|
|
139
|
+
return { intercept, entry: source };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
};
|
|
124
144
|
let current: EntryData | null = fromEntry;
|
|
145
|
+
let prev: EntryData | null = null;
|
|
125
146
|
|
|
126
147
|
while (current) {
|
|
127
|
-
// current first, then its
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
intercept.routeName === targetRouteKey &&
|
|
132
|
-
evaluateInterceptWhen(intercept, selectorContext, isAction)
|
|
133
|
-
) {
|
|
134
|
-
return { intercept, entry: source };
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
148
|
+
// current first, then its orphans (nested ones included) in render order.
|
|
149
|
+
const found = match(current) ?? findInOrphans(current, match, prev);
|
|
150
|
+
if (found) return found;
|
|
138
151
|
|
|
152
|
+
prev = current;
|
|
139
153
|
current = current.parent;
|
|
140
154
|
}
|
|
141
155
|
|
|
142
156
|
return null;
|
|
143
157
|
}
|
|
144
158
|
|
|
159
|
+
/**
|
|
160
|
+
* An intercept handler's throw or notFound() renders the declaring entry's
|
|
161
|
+
* boundary as the slot component (catchSegmentError, as for route handlers on
|
|
162
|
+
* partial navigations: 500/404 status, onError "handler"). Responses and every
|
|
163
|
+
* error on the background re-render (skipMiddleware) rethrow, so a failed
|
|
164
|
+
* re-render aborts its cache write instead of storing the fallback.
|
|
165
|
+
*/
|
|
166
|
+
function renderInterceptHandlerError<TEnv>(
|
|
167
|
+
error: unknown,
|
|
168
|
+
parentEntry: EntryData,
|
|
169
|
+
params: Record<string, string>,
|
|
170
|
+
context: HandlerContext<any, TEnv>,
|
|
171
|
+
deps: SegmentResolutionDeps<TEnv>,
|
|
172
|
+
skipMiddleware: boolean | undefined,
|
|
173
|
+
): ReactNode {
|
|
174
|
+
if (error instanceof Response || skipMiddleware) throw error;
|
|
175
|
+
return catchSegmentError(
|
|
176
|
+
error,
|
|
177
|
+
parentEntry,
|
|
178
|
+
params,
|
|
179
|
+
deps,
|
|
180
|
+
{
|
|
181
|
+
...buildLoaderErrorContext(context),
|
|
182
|
+
isPartial: true,
|
|
183
|
+
telemetry: getRouterContext()?.telemetry,
|
|
184
|
+
},
|
|
185
|
+
context.pathname,
|
|
186
|
+
).component;
|
|
187
|
+
}
|
|
188
|
+
|
|
145
189
|
/**
|
|
146
190
|
* Resolve an intercept entry and emit segment with the slot name.
|
|
147
191
|
*/
|
|
@@ -210,8 +254,8 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
210
254
|
const loaderIds: string[] = [];
|
|
211
255
|
|
|
212
256
|
for (let i = 0; i < interceptEntry.loader.length; i++) {
|
|
213
|
-
const
|
|
214
|
-
|
|
257
|
+
const loaderEntry = interceptEntry.loader[i];
|
|
258
|
+
const { loader, revalidate: loaderRevalidateFns } = loaderEntry;
|
|
215
259
|
const segmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}D${i}.${loader.$$id}`;
|
|
216
260
|
|
|
217
261
|
if (revalidationContext) {
|
|
@@ -273,7 +317,11 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
273
317
|
loaderIds.push(loader.$$id);
|
|
274
318
|
loaderPromises.push(
|
|
275
319
|
deps.wrapLoaderPromise(
|
|
276
|
-
|
|
320
|
+
// The DSL binding's funnel, as for route loaders: a loader's own
|
|
321
|
+
// cache() applies here too.
|
|
322
|
+
runInsideLoaderScope(() =>
|
|
323
|
+
resolveLoaderData(loaderEntry, context, context.pathname),
|
|
324
|
+
),
|
|
277
325
|
parentEntry,
|
|
278
326
|
segmentId,
|
|
279
327
|
context.pathname,
|
|
@@ -284,10 +332,26 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
284
332
|
);
|
|
285
333
|
}
|
|
286
334
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
335
|
+
let handlerResult: ReactNode;
|
|
336
|
+
try {
|
|
337
|
+
handlerResult =
|
|
338
|
+
typeof interceptEntry.handler === "function"
|
|
339
|
+
? handleHandlerResult(interceptEntry.handler(context))
|
|
340
|
+
: interceptEntry.handler;
|
|
341
|
+
} catch (error) {
|
|
342
|
+
handlerResult = renderInterceptHandlerError(
|
|
343
|
+
error,
|
|
344
|
+
parentEntry,
|
|
345
|
+
params,
|
|
346
|
+
context,
|
|
347
|
+
deps,
|
|
348
|
+
options?.skipMiddleware,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
// Consumed only after the layout and loader awaits below; a rejection in that
|
|
352
|
+
// window would be an unhandledRejection (process crash on Node's default
|
|
353
|
+
// mode). The later await / trackHandler still observes it.
|
|
354
|
+
if (handlerResult instanceof Promise) handlerResult.catch(() => {});
|
|
291
355
|
|
|
292
356
|
let layoutElement: ReactNode | undefined;
|
|
293
357
|
if (interceptEntry.layout) {
|
|
@@ -304,6 +368,14 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
304
368
|
|
|
305
369
|
let component: ReactNode;
|
|
306
370
|
let loaderDataPromise: Promise<any[]> | any[] | undefined;
|
|
371
|
+
// A streamed (loading()) handler resolves after the 200 has started, so its
|
|
372
|
+
// rejection stays with the client; track it like route handlers do
|
|
373
|
+
// (onError report, handle-store completion).
|
|
374
|
+
const trackStreamedHandler = (promise: Promise<unknown>): ReactNode =>
|
|
375
|
+
deps.trackHandler(promise, {
|
|
376
|
+
segmentId: `${parentEntry.shortCode}.${interceptEntry.slotName}`,
|
|
377
|
+
segmentType: "parallel",
|
|
378
|
+
}) as ReactNode;
|
|
307
379
|
|
|
308
380
|
if (interceptEntry.loading && loaderPromises.length > 0) {
|
|
309
381
|
if (handlerResult instanceof Promise) {
|
|
@@ -314,20 +386,30 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
314
386
|
}
|
|
315
387
|
component =
|
|
316
388
|
handlerResult instanceof Promise
|
|
317
|
-
? handlerResult
|
|
389
|
+
? trackStreamedHandler(handlerResult)
|
|
318
390
|
: (Promise.resolve(handlerResult) as ReactNode);
|
|
319
391
|
loaderDataPromise = Promise.all(loaderPromises);
|
|
320
|
-
} else if (
|
|
321
|
-
|
|
322
|
-
component =
|
|
323
|
-
handlerResult instanceof Promise ? await handlerResult : handlerResult;
|
|
392
|
+
} else if (interceptEntry.loading && handlerResult instanceof Promise) {
|
|
393
|
+
component = trackStreamedHandler(handlerResult);
|
|
324
394
|
} else {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
395
|
+
if (loaderPromises.length > 0) {
|
|
396
|
+
loaderDataPromise = await Promise.all(loaderPromises);
|
|
397
|
+
}
|
|
398
|
+
component = handlerResult;
|
|
399
|
+
if (handlerResult instanceof Promise) {
|
|
400
|
+
try {
|
|
401
|
+
component = await handlerResult;
|
|
402
|
+
} catch (error) {
|
|
403
|
+
component = renderInterceptHandlerError(
|
|
404
|
+
error,
|
|
405
|
+
parentEntry,
|
|
406
|
+
params,
|
|
407
|
+
context,
|
|
408
|
+
deps,
|
|
409
|
+
options?.skipMiddleware,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
331
413
|
}
|
|
332
414
|
|
|
333
415
|
const interceptSegment = {
|
|
@@ -401,8 +483,8 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
401
483
|
} = revalidationContext;
|
|
402
484
|
|
|
403
485
|
for (let i = 0; i < interceptEntry.loader.length; i++) {
|
|
404
|
-
const
|
|
405
|
-
|
|
486
|
+
const loaderEntry = interceptEntry.loader[i];
|
|
487
|
+
const { loader, revalidate: loaderRevalidateFns } = loaderEntry;
|
|
406
488
|
const segmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}D${i}.${loader.$$id}`;
|
|
407
489
|
|
|
408
490
|
const interceptSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
|
|
@@ -451,7 +533,11 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
451
533
|
loaderIds.push(loader.$$id);
|
|
452
534
|
loaderPromises.push(
|
|
453
535
|
deps.wrapLoaderPromise(
|
|
454
|
-
|
|
536
|
+
// The DSL binding's funnel, as for route loaders: a loader's own
|
|
537
|
+
// cache() applies here too.
|
|
538
|
+
runInsideLoaderScope(() =>
|
|
539
|
+
resolveLoaderData(loaderEntry, context, context.pathname),
|
|
540
|
+
),
|
|
455
541
|
parentEntry,
|
|
456
542
|
segmentId,
|
|
457
543
|
context.pathname,
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
import type { ReactNode } from "react";
|
|
8
8
|
import type { EntryData } from "../server/context";
|
|
9
9
|
import { observePhase, PHASES } from "./instrument.js";
|
|
10
|
-
import { contextGet } from "../context-var.js";
|
|
10
|
+
import { contextGet, isNonCacheable } from "../context-var.js";
|
|
11
|
+
import { NOCACHE_SYMBOL } from "../cache/taint.js";
|
|
11
12
|
import type {
|
|
12
13
|
ResolvedSegment,
|
|
13
14
|
HandlerContext,
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
import { getFetchableLoader } from "../server/fetchable-loader-store.js";
|
|
44
45
|
import { _getRequestContext } from "../server/request-context.js";
|
|
45
46
|
import {
|
|
47
|
+
assertNonCacheableReadAllowed,
|
|
46
48
|
isInsideLoaderScope,
|
|
47
49
|
runInsideLoaderBodyScope,
|
|
48
50
|
isInsidePushCallbackScope,
|
|
@@ -394,6 +396,7 @@ function createLoaderExecutor<TEnv>(
|
|
|
394
396
|
// Capture RequestContext eagerly for cookie access (ALS protection on Cloudflare)
|
|
395
397
|
const reqCtxRef = _getRequestContext();
|
|
396
398
|
const silentHandles = executorOptions?.silentHandles === true;
|
|
399
|
+
const internal = ctx as InternalHandlerContext<any, TEnv>;
|
|
397
400
|
|
|
398
401
|
// Dependency graph: loaderId -> set of loader IDs it directly depends on.
|
|
399
402
|
const dependsOn = new Map<string, Set<string>>();
|
|
@@ -429,7 +432,12 @@ function createLoaderExecutor<TEnv>(
|
|
|
429
432
|
deps.add(loader.$$id);
|
|
430
433
|
}
|
|
431
434
|
|
|
432
|
-
//
|
|
435
|
+
// Every loader execution funnels here (handler and loader-to-loader
|
|
436
|
+
// reads, uncached DSL bindings, cache misses): memoized per request, so a
|
|
437
|
+
// loader runs at most once however many reads, with no store cache. A
|
|
438
|
+
// loader's cache() read-through lives in loader-cache.ts
|
|
439
|
+
// (executeLoaderData), whose ctx.use override serves a later handler read
|
|
440
|
+
// the binding's result.
|
|
433
441
|
if (loaderPromises.has(loader.$$id)) {
|
|
434
442
|
return loaderPromises.get(loader.$$id)!;
|
|
435
443
|
}
|
|
@@ -452,7 +460,7 @@ function createLoaderExecutor<TEnv>(
|
|
|
452
460
|
pendingLoaders.add(loader.$$id);
|
|
453
461
|
|
|
454
462
|
const currentLoaderId = loader.$$id;
|
|
455
|
-
const variables =
|
|
463
|
+
const variables = internal._variables;
|
|
456
464
|
|
|
457
465
|
// Capture whether this loader is being started from a DSL loader scope
|
|
458
466
|
// (runInsideLoaderScope in fresh.ts). Handler-invoked loaders are NOT
|
|
@@ -466,18 +474,26 @@ function createLoaderExecutor<TEnv>(
|
|
|
466
474
|
// stale. Attributing to the owning route/layout segment gives handle
|
|
467
475
|
// pushes the same granularity handler pushes have (segmentOrder-correct
|
|
468
476
|
// for breadcrumb accumulation).
|
|
469
|
-
const owningSegmentId =
|
|
470
|
-
._currentSegmentId;
|
|
477
|
+
const owningSegmentId = internal._currentSegmentId;
|
|
471
478
|
|
|
472
479
|
let renderedResolved = false;
|
|
473
480
|
let renderedPromise: Promise<void> | null = null;
|
|
474
481
|
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
// (
|
|
480
|
-
|
|
482
|
+
// Request-scoped like the handler ctx it derives from (issue #940): the
|
|
483
|
+
// NOCACHE_SYMBOL brand below makes a "use cache" function taking it key by
|
|
484
|
+
// the route fields (_routeName, _responseType) and replay its handle
|
|
485
|
+
// pushes into the owning segment, claimed per loader like a handler ctx
|
|
486
|
+
// (_claimLoaderPushes, _runLoaderIsolated). Non-cacheable ctx.get reads
|
|
487
|
+
// run the shared guard; the body itself stays exempt (loader body scope).
|
|
488
|
+
const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> &
|
|
489
|
+
Pick<
|
|
490
|
+
InternalHandlerContext<any, TEnv>,
|
|
491
|
+
| "_routeName"
|
|
492
|
+
| "_responseType"
|
|
493
|
+
| "_currentSegmentId"
|
|
494
|
+
| "_claimLoaderPushes"
|
|
495
|
+
| "_runLoaderIsolated"
|
|
496
|
+
> = {
|
|
481
497
|
params: ctx.params,
|
|
482
498
|
routeParams: (ctx.params ?? {}) as Record<string, string>,
|
|
483
499
|
request: ctx.request,
|
|
@@ -517,6 +533,9 @@ function createLoaderExecutor<TEnv>(
|
|
|
517
533
|
buildHandleSnapshot(reqCtx._handleStore, segmentOrder);
|
|
518
534
|
return collectHandleData(keyOrVar, snapshot, segmentOrder);
|
|
519
535
|
}
|
|
536
|
+
if (isNonCacheable(variables, keyOrVar)) {
|
|
537
|
+
assertNonCacheableReadAllowed(keyOrVar);
|
|
538
|
+
}
|
|
520
539
|
return contextGet(variables, keyOrVar);
|
|
521
540
|
}) as typeof ctx.get,
|
|
522
541
|
use: ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
@@ -552,6 +571,11 @@ function createLoaderExecutor<TEnv>(
|
|
|
552
571
|
method: "GET",
|
|
553
572
|
body: undefined,
|
|
554
573
|
reverse: ctx.reverse as LoaderContext["reverse"],
|
|
574
|
+
_routeName: internal._routeName,
|
|
575
|
+
_responseType: internal._responseType,
|
|
576
|
+
_currentSegmentId: owningSegmentId,
|
|
577
|
+
_claimLoaderPushes: internal._claimLoaderPushes,
|
|
578
|
+
_runLoaderIsolated: internal._runLoaderIsolated,
|
|
555
579
|
rendered: (): Promise<void> => {
|
|
556
580
|
// Guard: only DSL loaders may use rendered()
|
|
557
581
|
if (!isDslLoader) {
|
|
@@ -639,6 +663,7 @@ function createLoaderExecutor<TEnv>(
|
|
|
639
663
|
return renderedPromise;
|
|
640
664
|
},
|
|
641
665
|
};
|
|
666
|
+
(loaderCtx as any)[NOCACHE_SYMBOL] = true;
|
|
642
667
|
|
|
643
668
|
// Meter this loader once via observePhase (loader:<id> perf metric +
|
|
644
669
|
// rango.loader span); loaderFn runs inside the span callback so its KV/D1/
|
|
@@ -736,6 +761,26 @@ export function setupLoaderAccess<TEnv>(
|
|
|
736
761
|
|
|
737
762
|
const useLoader = createLoaderExecutor(ctx, loaderPromises);
|
|
738
763
|
|
|
764
|
+
// Loader-cache HIT replay (loader-cache.ts replayLoaderHandles): a cached
|
|
765
|
+
// entry records its ctx.use dependencies' pushes too, and a dependency is
|
|
766
|
+
// memoized here and shared with live readers. A loader that already ran
|
|
767
|
+
// here keeps its live pushes; otherwise one replay delivers its recorded
|
|
768
|
+
// pushes, and a later run here replaces them (HandleStore.pushReplayed).
|
|
769
|
+
const claimed = new Set<string>();
|
|
770
|
+
const internal = ctx as InternalHandlerContext<any, TEnv>;
|
|
771
|
+
internal._claimLoaderPushes = (loaderId) => {
|
|
772
|
+
if (loaderPromises.has(loaderId) || claimed.has(loaderId)) return false;
|
|
773
|
+
claimed.add(loaderId);
|
|
774
|
+
return true;
|
|
775
|
+
};
|
|
776
|
+
// A refresh's pushes are diverted, so its loader ctxs never claim
|
|
777
|
+
// (cache-runtime.ts refreshView).
|
|
778
|
+
internal._runLoaderIsolated = (loader) =>
|
|
779
|
+
createLoaderExecutor(
|
|
780
|
+
Object.create(ctx, { _claimLoaderPushes: { value: undefined } }),
|
|
781
|
+
new Map(),
|
|
782
|
+
)(loader, null);
|
|
783
|
+
|
|
739
784
|
ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
740
785
|
if (isHandle(item)) {
|
|
741
786
|
const handle = item;
|