@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
package/src/server/context.ts
CHANGED
|
@@ -12,6 +12,11 @@ import type {
|
|
|
12
12
|
} from "../types";
|
|
13
13
|
import { invariant, DslContextError } from "../errors";
|
|
14
14
|
import type { DefaultRouteName } from "../types/global-namespace.js";
|
|
15
|
+
import type { ContextVar } from "../context-var.js";
|
|
16
|
+
import {
|
|
17
|
+
getCacheExecScope,
|
|
18
|
+
type CacheExecScope,
|
|
19
|
+
} from "../cache/cache-exec-scope.js";
|
|
15
20
|
|
|
16
21
|
// ============================================================================
|
|
17
22
|
// Performance Metrics Types
|
|
@@ -65,6 +70,17 @@ export type EntryPropCommon = {
|
|
|
65
70
|
id: string;
|
|
66
71
|
shortCode: string; // Short identifier for network efficiency (e.g., "L0", "P1", "R2")
|
|
67
72
|
parent: EntryData | null;
|
|
73
|
+
/**
|
|
74
|
+
* Orphan siblings only (attachOrphanSibling in
|
|
75
|
+
* route-definition/dsl-helpers.ts): the entry whose `layout[]` holds this
|
|
76
|
+
* one. `parent` stays null on an orphan (matchError's matched-id stack in
|
|
77
|
+
* router/match-api.ts starts at the boundary-holding entry, possibly an
|
|
78
|
+
* orphan, and must stop there), so the boundary walkers in
|
|
79
|
+
* router/error-handling.ts follow this instead. A lookup that starts at an
|
|
80
|
+
* orphan (its loaders, an intercept it declares) then reaches the owner's
|
|
81
|
+
* boundaries and the owner's ancestors.
|
|
82
|
+
*/
|
|
83
|
+
orphanOwner?: EntryData;
|
|
68
84
|
/** Cache configuration for this entry (set by cache() DSL) */
|
|
69
85
|
cache?: EntryCacheConfig;
|
|
70
86
|
/** URL prefix from include() scope, used for MountContext on client */
|
|
@@ -187,9 +203,6 @@ export type InterceptEntry = {
|
|
|
187
203
|
routeName: string; // e.g., "card"
|
|
188
204
|
handler: ReactNode | Handler<any, any, any>;
|
|
189
205
|
middleware: MiddlewareFn<any, any>[];
|
|
190
|
-
revalidate: ShouldRevalidateFn<any, any>[];
|
|
191
|
-
errorBoundary: (ReactNode | ErrorBoundaryHandler)[];
|
|
192
|
-
notFoundBoundary: (ReactNode | NotFoundBoundaryHandler)[];
|
|
193
206
|
loader: LoaderEntry[];
|
|
194
207
|
loading?: ReactNode | false;
|
|
195
208
|
transition?: TransitionConfig;
|
|
@@ -839,16 +852,19 @@ const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
|
|
|
839
852
|
// does NOT affect isInsideLoaderScope(), so rendered()/barrier/deadlock
|
|
840
853
|
// gating (which must distinguish DSL from handler-invoked loaders) is
|
|
841
854
|
// unchanged.
|
|
842
|
-
|
|
843
|
-
const loaderBodyScopeALS: AsyncLocalStorage<{
|
|
855
|
+
interface LoaderBodyScope {
|
|
844
856
|
active: true;
|
|
845
857
|
loaderId?: string;
|
|
846
858
|
handlerInvoked?: boolean;
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
}
|
|
859
|
+
/** The body scope this one was entered from (a ctx.use(Loader) chain). */
|
|
860
|
+
parent?: LoaderBodyScope;
|
|
861
|
+
/** The "use cache" scope the body was entered in (assertNonCacheableReadAllowed). */
|
|
862
|
+
execScope?: CacheExecScope;
|
|
863
|
+
}
|
|
864
|
+
const LOADER_BODY_SCOPE_KEY = Symbol.for("rangojs-router:loader-body-scope");
|
|
865
|
+
const loaderBodyScopeALS: AsyncLocalStorage<LoaderBodyScope> = ((
|
|
866
|
+
globalThis as any
|
|
867
|
+
)[LOADER_BODY_SCOPE_KEY] ??= new AsyncLocalStorage<LoaderBodyScope>());
|
|
852
868
|
|
|
853
869
|
/**
|
|
854
870
|
* Check if the current execution is inside a cache() DSL boundary.
|
|
@@ -869,6 +885,45 @@ export function isInsideCacheScope(): boolean {
|
|
|
869
885
|
return true;
|
|
870
886
|
}
|
|
871
887
|
|
|
888
|
+
/**
|
|
889
|
+
* Read guard for a non-cacheable variable (`createVar({ cache: false })` or a
|
|
890
|
+
* `ctx.set(..., { cache: false })` write). Callers check isNonCacheable() first
|
|
891
|
+
* so ordinary reads never reach the scope lookups.
|
|
892
|
+
*
|
|
893
|
+
* Throws inside a "use cache" body: the key does not include the value, so the
|
|
894
|
+
* first caller's value would be stored and served to later callers (#925). A
|
|
895
|
+
* loader body entered INSIDE the cached function is exempt, the same baked-copy
|
|
896
|
+
* trade isInsideCacheScope() makes for cache(). A cached function called FROM a
|
|
897
|
+
* loader is not: the loader re-runs, the cached body does not. The body scope
|
|
898
|
+
* records the exec scope it was entered in, so the exemption holds only while
|
|
899
|
+
* that is still the innermost one.
|
|
900
|
+
*/
|
|
901
|
+
export function assertNonCacheableReadAllowed(
|
|
902
|
+
keyOrVar: string | ContextVar<unknown>,
|
|
903
|
+
): void {
|
|
904
|
+
const execScope = getCacheExecScope();
|
|
905
|
+
if (
|
|
906
|
+
execScope !== undefined &&
|
|
907
|
+
loaderBodyScopeALS.getStore()?.execScope !== execScope
|
|
908
|
+
) {
|
|
909
|
+
const name = typeof keyOrVar === "string" ? ` "${keyOrVar}"` : "";
|
|
910
|
+
throw new Error(
|
|
911
|
+
`ctx.get() for a non-cacheable variable${name} cannot be called inside a "use cache" function. ` +
|
|
912
|
+
`The variable was created with { cache: false } or set with { cache: false }, ` +
|
|
913
|
+
`and the cache key does not include its value, so the first caller's value ` +
|
|
914
|
+
`would be served to later callers. Read it before calling the cached function ` +
|
|
915
|
+
`and pass the value in as an argument so it becomes part of the cache key.`,
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
if (isInsideCacheScope()) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
`ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
|
|
921
|
+
`The variable was created with { cache: false } or set with { cache: false }, ` +
|
|
922
|
+
`and its value would be stale on cache hit. Move the read outside the cached scope.`,
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
872
927
|
/**
|
|
873
928
|
* Check if the current execution is inside a DSL loader scope
|
|
874
929
|
* (wrapped by runInsideLoaderScope). Used by rendered() barrier
|
|
@@ -1052,7 +1107,16 @@ export function runInsideLoaderBodyScope<T>(
|
|
|
1052
1107
|
loaderId?: string,
|
|
1053
1108
|
handlerInvoked?: boolean,
|
|
1054
1109
|
): T {
|
|
1055
|
-
return loaderBodyScopeALS.run(
|
|
1110
|
+
return loaderBodyScopeALS.run(
|
|
1111
|
+
{
|
|
1112
|
+
active: true,
|
|
1113
|
+
loaderId,
|
|
1114
|
+
handlerInvoked,
|
|
1115
|
+
parent: loaderBodyScopeALS.getStore(),
|
|
1116
|
+
execScope: getCacheExecScope(),
|
|
1117
|
+
},
|
|
1118
|
+
fn,
|
|
1119
|
+
);
|
|
1056
1120
|
}
|
|
1057
1121
|
|
|
1058
1122
|
/**
|
|
@@ -1067,6 +1131,19 @@ export function getCurrentLoaderBodyId(): string | undefined {
|
|
|
1067
1131
|
return loaderBodyScopeALS.getStore()?.loaderId;
|
|
1068
1132
|
}
|
|
1069
1133
|
|
|
1134
|
+
/**
|
|
1135
|
+
* True while `loaderId`'s body, or a loader it awaits via ctx.use at any
|
|
1136
|
+
* depth, is executing. The loader-level cache records these pushes
|
|
1137
|
+
* (loader-cache.ts): a HIT skips the cached body, so a dep that no other
|
|
1138
|
+
* reader runs in that request only reaches the page through the replay.
|
|
1139
|
+
*/
|
|
1140
|
+
export function isInsideLoaderBody(loaderId: string): boolean {
|
|
1141
|
+
for (let s = loaderBodyScopeALS.getStore(); s; s = s.parent) {
|
|
1142
|
+
if (s.loaderId === loaderId) return true;
|
|
1143
|
+
}
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1070
1147
|
/**
|
|
1071
1148
|
* True while a HANDLER-invoked loader body (`await ctx.use(Loader)` from a
|
|
1072
1149
|
* handler, not the DSL segment funnel) is executing. The consumption-lane
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
isInsideHandlerInvokedLoaderBody,
|
|
16
16
|
} from "./context.js";
|
|
17
17
|
import { isInsideCacheExecScope } from "../cache/cache-exec-scope.js";
|
|
18
|
+
import { PPR_LANE_HINT } from "../rsc/shell-capture-constants.js";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* A single cookie entry returned by get() and getAll().
|
|
@@ -193,12 +194,12 @@ function assertNotInsideShellCapture(ctx: unknown, fnName: string): void {
|
|
|
193
194
|
)._shellCaptureGuardTrippedLoaderId = getCurrentLoaderBodyId();
|
|
194
195
|
throw new Error(
|
|
195
196
|
`${fnName}() cannot be called while capturing a shared shell ` +
|
|
196
|
-
`(shell
|
|
197
|
+
`(ppr shell capture). The captured shell is served to every user ` +
|
|
197
198
|
`of this URL, so request-scoped data read here would leak one user's ` +
|
|
198
199
|
`${fnName === "cookies" ? "cookies" : "headers"} to others. Read it ` +
|
|
199
|
-
`inside a loader
|
|
200
|
-
`
|
|
201
|
-
|
|
200
|
+
`inside a loader without ssr: false and consume it with useLoader, e.g. ` +
|
|
201
|
+
`createLoader(async () => getUser(cookies().get("session")?.value)). ` +
|
|
202
|
+
PPR_LANE_HINT,
|
|
202
203
|
);
|
|
203
204
|
}
|
|
204
205
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getCurrentLoaderBodyId, isInsideLoaderScope } from "./context.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Handle data structure: handleName -> segmentId -> entries[]
|
|
3
5
|
*
|
|
@@ -45,6 +47,24 @@ function createLateHandlePushError(
|
|
|
45
47
|
return error;
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Per-slot flags, parallel to one handle/segment values array. Allocated on
|
|
52
|
+
* the array's first flagged slot; positions past its length are unflagged.
|
|
53
|
+
*/
|
|
54
|
+
interface SlotTag {
|
|
55
|
+
/** Pushed inside a DSL loader scope; captureHandles drops it. */
|
|
56
|
+
loader?: boolean;
|
|
57
|
+
/** A loader-cache replay of this loader's recorded push (pushReplayed). */
|
|
58
|
+
replayOf?: string;
|
|
59
|
+
/** Pushed by this loader's live run after it replaced its replayed slots. */
|
|
60
|
+
liveOf?: string;
|
|
61
|
+
/** Pushed by this loader's body and kept in records (push `owner`). */
|
|
62
|
+
owner?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Shared by every plain DSL-loader push; tags are never mutated in place.
|
|
66
|
+
const LOADER_SLOT: SlotTag = Object.freeze({ loader: true });
|
|
67
|
+
|
|
48
68
|
function cloneHandleData(data: HandleData): HandleData {
|
|
49
69
|
const clone: HandleData = {};
|
|
50
70
|
for (const handleName in data) {
|
|
@@ -116,8 +136,35 @@ export interface HandleStore {
|
|
|
116
136
|
* Push handle data for a specific handle and segment.
|
|
117
137
|
* Multiple pushes to the same handle/segment accumulate in an array.
|
|
118
138
|
* Each push triggers an emission on the stream.
|
|
139
|
+
*
|
|
140
|
+
* `loaderPush` overrides the push-time DSL-loader tag
|
|
141
|
+
* (isInsideLoaderScope()) that getDataForSegment(id, true) excludes. The
|
|
142
|
+
* PPR shell capture passes false for the bake-lane pushes its record keeps
|
|
143
|
+
* (shell-capture.ts deriveShellCaptureContext), with `owner` set to the
|
|
144
|
+
* pushing loader's id so the record can restore them through pushReplayed
|
|
145
|
+
* (getRecordOwners).
|
|
119
146
|
*/
|
|
120
|
-
push(
|
|
147
|
+
push(
|
|
148
|
+
handleName: string,
|
|
149
|
+
segmentId: string,
|
|
150
|
+
data: unknown,
|
|
151
|
+
loaderPush?: boolean,
|
|
152
|
+
owner?: string,
|
|
153
|
+
): void;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* push() a loader-cache HIT's replayed value, recorded from `loaderId`'s
|
|
157
|
+
* body (loader-cache.ts replayLoaderHandles). If that loader then runs live
|
|
158
|
+
* in this request, its first push removes every value replayed for it and
|
|
159
|
+
* takes the first one's position (later live pushes follow it), so the
|
|
160
|
+
* handle output stays live without a duplicate.
|
|
161
|
+
*/
|
|
162
|
+
pushReplayed(
|
|
163
|
+
handleName: string,
|
|
164
|
+
segmentId: string,
|
|
165
|
+
data: unknown,
|
|
166
|
+
loaderId: string,
|
|
167
|
+
): void;
|
|
121
168
|
|
|
122
169
|
/**
|
|
123
170
|
* Get all collected handle data after all handlers have settled.
|
|
@@ -151,8 +198,23 @@ export interface HandleStore {
|
|
|
151
198
|
/**
|
|
152
199
|
* Get handle data for a specific segment (for caching).
|
|
153
200
|
* Returns data in format: { handleName: [values...] }
|
|
201
|
+
*
|
|
202
|
+
* `excludeLoaderPushes` drops values tagged as DSL-loader pushes at push
|
|
203
|
+
* time (see push); see captureHandles.
|
|
204
|
+
*/
|
|
205
|
+
getDataForSegment(
|
|
206
|
+
segmentId: string,
|
|
207
|
+
excludeLoaderPushes?: boolean,
|
|
208
|
+
): Record<string, unknown[]>;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The push `owner` of each value in getDataForSegment(segmentId, true),
|
|
212
|
+
* index-aligned per handle (null: no owner). Handles without an owned
|
|
213
|
+
* value are omitted; undefined when none has one.
|
|
154
214
|
*/
|
|
155
|
-
|
|
215
|
+
getRecordOwners(
|
|
216
|
+
segmentId: string,
|
|
217
|
+
): Record<string, (string | null)[]> | undefined;
|
|
156
218
|
|
|
157
219
|
/**
|
|
158
220
|
* Replay cached handle data back into the store (for cache hits).
|
|
@@ -186,6 +248,88 @@ export interface HandleStore {
|
|
|
186
248
|
*/
|
|
187
249
|
export function createHandleStore(): HandleStore {
|
|
188
250
|
const data: HandleData = {};
|
|
251
|
+
// SlotTag arrays, keyed by the per-handle/segment values array. Positional
|
|
252
|
+
// (not value identity) so primitive values are covered too.
|
|
253
|
+
// replaySegmentData installs fresh arrays, so restored values are untagged.
|
|
254
|
+
const slotTags = new WeakMap<unknown[], (SlotTag | undefined)[]>();
|
|
255
|
+
// Replayed loaders that have not pushed live yet: loaderId -> the arrays
|
|
256
|
+
// holding their replayed slots.
|
|
257
|
+
const pendingReplays = new Map<string, Set<unknown[]>>();
|
|
258
|
+
// Replayed loaders whose live run already replaced those slots.
|
|
259
|
+
const replacedReplays = new Set<string>();
|
|
260
|
+
|
|
261
|
+
// Aligned to `values` so a mid-array insert or removal keeps positions.
|
|
262
|
+
function tagsFor(values: unknown[]): (SlotTag | undefined)[] {
|
|
263
|
+
let tags = slotTags.get(values);
|
|
264
|
+
if (!tags) slotTags.set(values, (tags = []));
|
|
265
|
+
if (tags.length < values.length) tags.length = values.length;
|
|
266
|
+
return tags;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function insertSlot(
|
|
270
|
+
values: unknown[],
|
|
271
|
+
at: number | undefined,
|
|
272
|
+
value: unknown,
|
|
273
|
+
tag: SlotTag | undefined,
|
|
274
|
+
): void {
|
|
275
|
+
if (at === undefined || at >= values.length) {
|
|
276
|
+
values.push(value);
|
|
277
|
+
if (tag) tagsFor(values)[values.length - 1] = tag;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const tags = tagsFor(values);
|
|
281
|
+
values.splice(at, 0, value);
|
|
282
|
+
tags.splice(at, 0, tag);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Remove `loaderId`'s replayed slots; returns the first one's position.
|
|
286
|
+
function removeReplayedSlots(
|
|
287
|
+
values: unknown[],
|
|
288
|
+
loaderId: string,
|
|
289
|
+
): number | undefined {
|
|
290
|
+
const tags = slotTags.get(values);
|
|
291
|
+
if (!tags) return undefined;
|
|
292
|
+
let first: number | undefined;
|
|
293
|
+
let kept = 0;
|
|
294
|
+
for (let i = 0; i < values.length; i++) {
|
|
295
|
+
if (tags[i]?.replayOf === loaderId) {
|
|
296
|
+
first ??= kept;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
values[kept] = values[i];
|
|
300
|
+
tags[kept] = tags[i];
|
|
301
|
+
kept++;
|
|
302
|
+
}
|
|
303
|
+
values.length = kept;
|
|
304
|
+
tags.length = kept;
|
|
305
|
+
return first;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Where a live push by a replayed loader lands in `values`: its first push
|
|
309
|
+
// removes all of its replayed slots and takes the first one's position in
|
|
310
|
+
// this array; later pushes follow its previous live push. undefined appends.
|
|
311
|
+
function liveReplacementIndex(
|
|
312
|
+
loaderId: string,
|
|
313
|
+
values: unknown[],
|
|
314
|
+
): number | undefined {
|
|
315
|
+
const pending = pendingReplays.get(loaderId);
|
|
316
|
+
if (pending) {
|
|
317
|
+
pendingReplays.delete(loaderId);
|
|
318
|
+
replacedReplays.add(loaderId);
|
|
319
|
+
let at: number | undefined;
|
|
320
|
+
for (const replayed of pending) {
|
|
321
|
+
const first = removeReplayedSlots(replayed, loaderId);
|
|
322
|
+
if (replayed === values) at = first;
|
|
323
|
+
}
|
|
324
|
+
return at;
|
|
325
|
+
}
|
|
326
|
+
const tags = slotTags.get(values);
|
|
327
|
+
if (!tags) return undefined;
|
|
328
|
+
for (let i = tags.length - 1; i >= 0; i--) {
|
|
329
|
+
if (tags[i]?.liveOf === loaderId) return i + 1;
|
|
330
|
+
}
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
189
333
|
|
|
190
334
|
// Settlement barriers: `settled` (handler lane) resolves when sealed AND
|
|
191
335
|
// handler inflight === 0. `fullySettled` additionally waits for the
|
|
@@ -320,7 +464,13 @@ export function createHandleStore(): HandleStore {
|
|
|
320
464
|
});
|
|
321
465
|
},
|
|
322
466
|
|
|
323
|
-
push(
|
|
467
|
+
push(
|
|
468
|
+
handleName: string,
|
|
469
|
+
segmentId: string,
|
|
470
|
+
value: unknown,
|
|
471
|
+
loaderPush?: boolean,
|
|
472
|
+
owner?: string,
|
|
473
|
+
): void {
|
|
324
474
|
if (completed) {
|
|
325
475
|
const error = createLateHandlePushError(handleName, segmentId);
|
|
326
476
|
if (this.onError) this.onError(error);
|
|
@@ -333,13 +483,58 @@ export function createHandleStore(): HandleStore {
|
|
|
333
483
|
if (!data[handleName][segmentId]) {
|
|
334
484
|
data[handleName][segmentId] = [];
|
|
335
485
|
}
|
|
336
|
-
data[handleName][segmentId]
|
|
486
|
+
const values = data[handleName][segmentId];
|
|
487
|
+
const loader = (loaderPush ?? isInsideLoaderScope()) || undefined;
|
|
488
|
+
let at: number | undefined;
|
|
489
|
+
let liveOf: string | undefined;
|
|
490
|
+
if (pendingReplays.size > 0 || replacedReplays.size > 0) {
|
|
491
|
+
const bodyId = getCurrentLoaderBodyId();
|
|
492
|
+
if (
|
|
493
|
+
bodyId !== undefined &&
|
|
494
|
+
(pendingReplays.has(bodyId) || replacedReplays.has(bodyId))
|
|
495
|
+
) {
|
|
496
|
+
at = liveReplacementIndex(bodyId, values);
|
|
497
|
+
liveOf = bodyId;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
insertSlot(
|
|
501
|
+
values,
|
|
502
|
+
at,
|
|
503
|
+
value,
|
|
504
|
+
liveOf || owner
|
|
505
|
+
? { loader, liveOf, owner }
|
|
506
|
+
: loader
|
|
507
|
+
? LOADER_SLOT
|
|
508
|
+
: undefined,
|
|
509
|
+
);
|
|
337
510
|
|
|
338
511
|
// Bump the version; each consumer's cursor decides when to clone+yield.
|
|
339
512
|
version++;
|
|
340
513
|
signalEmission();
|
|
341
514
|
},
|
|
342
515
|
|
|
516
|
+
pushReplayed(
|
|
517
|
+
handleName: string,
|
|
518
|
+
segmentId: string,
|
|
519
|
+
value: unknown,
|
|
520
|
+
loaderId: string,
|
|
521
|
+
): void {
|
|
522
|
+
const before = data[handleName]?.[segmentId]?.length ?? 0;
|
|
523
|
+
// Through this.push: active captures see the replay like a live push.
|
|
524
|
+
this.push(handleName, segmentId, value);
|
|
525
|
+
const values = data[handleName]?.[segmentId];
|
|
526
|
+
// A diverting capture kept it out of the store.
|
|
527
|
+
if (!values || values.length !== before + 1) return;
|
|
528
|
+
const tags = tagsFor(values);
|
|
529
|
+
tags[values.length - 1] = {
|
|
530
|
+
...tags[values.length - 1],
|
|
531
|
+
replayOf: loaderId,
|
|
532
|
+
};
|
|
533
|
+
let arrays = pendingReplays.get(loaderId);
|
|
534
|
+
if (!arrays) pendingReplays.set(loaderId, (arrays = new Set()));
|
|
535
|
+
arrays.add(values);
|
|
536
|
+
},
|
|
537
|
+
|
|
343
538
|
getData(): Promise<HandleData> {
|
|
344
539
|
sealInternal();
|
|
345
540
|
return this.settled.then(() => cloneHandleData(data));
|
|
@@ -417,12 +612,42 @@ export function createHandleStore(): HandleStore {
|
|
|
417
612
|
}
|
|
418
613
|
},
|
|
419
614
|
|
|
420
|
-
getDataForSegment(
|
|
615
|
+
getDataForSegment(
|
|
616
|
+
segmentId: string,
|
|
617
|
+
excludeLoaderPushes?: boolean,
|
|
618
|
+
): Record<string, unknown[]> {
|
|
421
619
|
const result: Record<string, unknown[]> = {};
|
|
422
620
|
for (const handleName in data) {
|
|
423
|
-
|
|
424
|
-
|
|
621
|
+
const values = data[handleName][segmentId];
|
|
622
|
+
if (!values) continue;
|
|
623
|
+
const tags = excludeLoaderPushes ? slotTags.get(values) : undefined;
|
|
624
|
+
if (!tags) {
|
|
625
|
+
result[handleName] = [...values];
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const kept = values.filter((_, i) => !tags[i]?.loader);
|
|
629
|
+
if (kept.length > 0) result[handleName] = kept;
|
|
630
|
+
}
|
|
631
|
+
return result;
|
|
632
|
+
},
|
|
633
|
+
|
|
634
|
+
getRecordOwners(
|
|
635
|
+
segmentId: string,
|
|
636
|
+
): Record<string, (string | null)[]> | undefined {
|
|
637
|
+
let result: Record<string, (string | null)[]> | undefined;
|
|
638
|
+
for (const handleName in data) {
|
|
639
|
+
const values = data[handleName][segmentId];
|
|
640
|
+
const tags = values && slotTags.get(values);
|
|
641
|
+
if (!tags) continue;
|
|
642
|
+
const owners: (string | null)[] = [];
|
|
643
|
+
let owned = false;
|
|
644
|
+
for (let i = 0; i < values.length; i++) {
|
|
645
|
+
if (tags[i]?.loader) continue;
|
|
646
|
+
const owner = tags[i]?.owner ?? null;
|
|
647
|
+
if (owner) owned = true;
|
|
648
|
+
owners.push(owner);
|
|
425
649
|
}
|
|
650
|
+
if (owned) (result ??= {})[handleName] = owners;
|
|
426
651
|
}
|
|
427
652
|
return result;
|
|
428
653
|
},
|
|
@@ -61,8 +61,8 @@ import type { RoutineTrace } from "../rsc/routine-plan.js";
|
|
|
61
61
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
62
62
|
import {
|
|
63
63
|
assertCachedHeaderWriteAllowed,
|
|
64
|
+
assertNonCacheableReadAllowed,
|
|
64
65
|
clearPprHeaderScope,
|
|
65
|
-
isInsideCacheScope,
|
|
66
66
|
} from "./context.js";
|
|
67
67
|
import {
|
|
68
68
|
createReverseFunction,
|
|
@@ -197,8 +197,13 @@ export interface RequestContext<
|
|
|
197
197
|
/** HTTP method (GET, POST, PUT, PATCH, DELETE, etc.) */
|
|
198
198
|
method: string;
|
|
199
199
|
|
|
200
|
-
/**
|
|
201
|
-
|
|
200
|
+
/**
|
|
201
|
+
* @internal Handle store for tracking handle data across segments.
|
|
202
|
+
* Readonly: a background render that needs its own store derives a context
|
|
203
|
+
* (rerenderAndCacheRoute, shell-capture.ts) instead of swapping this field,
|
|
204
|
+
* which the foreground still reads while it produces the page.
|
|
205
|
+
*/
|
|
206
|
+
readonly _handleStore: HandleStore;
|
|
202
207
|
|
|
203
208
|
/**
|
|
204
209
|
* @internal transition({ when }) predicates for segments matched this request,
|
|
@@ -250,13 +255,12 @@ export interface RequestContext<
|
|
|
250
255
|
/**
|
|
251
256
|
* @internal Bake-lane loader containers collected DURING a shell capture:
|
|
252
257
|
* segment-key -> the loader's (pre-wrap) result promise. Populated by
|
|
253
|
-
* resolveLoaderData for loaders
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* shared shell). Own property of the capture's derived context only.
|
|
258
|
+
* resolveLoaderData for bake-lane loaders (lane rule: see its JSDoc in
|
|
259
|
+
* loader-cache.ts). Drained by captureAndStoreShell after the shell
|
|
260
|
+
* quiesces: settled containers are promise-elided, Flight-serialized, and
|
|
261
|
+
* pinned into the snapshot's loader family; a REJECTED container refuses
|
|
262
|
+
* the capture (error UI must never bake into the shared shell). Own
|
|
263
|
+
* property of the capture's derived context only.
|
|
260
264
|
*/
|
|
261
265
|
_shellCaptureLoaderRecords?: Map<string, Promise<unknown>>;
|
|
262
266
|
|
|
@@ -377,18 +381,6 @@ export interface RequestContext<
|
|
|
377
381
|
handlerInvokedLoader: boolean;
|
|
378
382
|
};
|
|
379
383
|
|
|
380
|
-
/**
|
|
381
|
-
* @internal Handle values pushed from a DSL loader scope DURING a shell
|
|
382
|
-
* capture (identity set; populated by the capture push wrapper in
|
|
383
|
-
* shell-capture.ts). cacheRoute threads it into captureHandles so those
|
|
384
|
-
* values stay out of cache-write handle records — loaders re-run fresh on
|
|
385
|
-
* every HIT, so replaying their captured (masked) values would duplicate
|
|
386
|
-
* the fresh push and stall the Flight handle encode. Own property of the
|
|
387
|
-
* capture's derived context only; render-time handle consumers are
|
|
388
|
-
* unaffected (the exclusion applies only at the captureHandles call site).
|
|
389
|
-
*/
|
|
390
|
-
_shellCaptureLoaderHandleValues?: WeakSet<object>;
|
|
391
|
-
|
|
392
384
|
/**
|
|
393
385
|
* @internal Set (to the offending fn name) by the cookies()/headers()
|
|
394
386
|
* capture guard when it throws DURING a capture render. Load-bearing for the
|
|
@@ -650,6 +642,17 @@ export interface RequestContext<
|
|
|
650
642
|
/** @internal Per-request error dedup set for onError reporting */
|
|
651
643
|
_reportedErrors: WeakSet<object>;
|
|
652
644
|
|
|
645
|
+
/**
|
|
646
|
+
* @internal Errors this response's Flight or Fizz render reported through
|
|
647
|
+
* onError: a component that threw does not fail the stream, which completes
|
|
648
|
+
* with an error row or an errored Suspense boundary. Whole-response captures
|
|
649
|
+
* (document-cache.ts, captureAndStoreShell) store nothing while it is
|
|
650
|
+
* non-empty (issue #915). An array so a render under a derived context (the
|
|
651
|
+
* shell HIT tail) pushes into the request's own list through the prototype;
|
|
652
|
+
* deriveShellCaptureContext gives the capture its own.
|
|
653
|
+
*/
|
|
654
|
+
_renderErrors?: unknown[];
|
|
655
|
+
|
|
653
656
|
/**
|
|
654
657
|
* @internal Report a non-fatal background error through the router's
|
|
655
658
|
* onError callback. Wired by the RSC handler / router during request
|
|
@@ -749,7 +752,6 @@ export type PublicRequestContext<
|
|
|
749
752
|
| "_shellLoaderSeed"
|
|
750
753
|
| "_shellCaptureLoaderRecords"
|
|
751
754
|
| "_shellCaptureHandleLiveness"
|
|
752
|
-
| "_shellCaptureLoaderHandleValues"
|
|
753
755
|
| "_shellFragmentPayload"
|
|
754
756
|
| "_shellCaptureGuardTripped"
|
|
755
757
|
| "_shellCaptureGuardTrippedLoaderId"
|
|
@@ -770,6 +772,7 @@ export type PublicRequestContext<
|
|
|
770
772
|
| "_gateFormData"
|
|
771
773
|
| "_inActionRevalidation"
|
|
772
774
|
| "_reportedErrors"
|
|
775
|
+
| "_renderErrors"
|
|
773
776
|
| "_renderBarrier"
|
|
774
777
|
| "_resolveRenderBarrier"
|
|
775
778
|
| "_renderBarrierSegmentOrder"
|
|
@@ -1140,12 +1143,8 @@ export function createRequestContext<TEnv>(
|
|
|
1140
1143
|
},
|
|
1141
1144
|
_dynamic: false,
|
|
1142
1145
|
get: ((keyOrVar: any) => {
|
|
1143
|
-
if (isNonCacheable(variables, keyOrVar)
|
|
1144
|
-
|
|
1145
|
-
`ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
|
|
1146
|
-
`The variable was created with { cache: false } or set with { cache: false }, ` +
|
|
1147
|
-
`and its value would be stale on cache hit. Move the read outside the cached scope.`,
|
|
1148
|
-
);
|
|
1146
|
+
if (isNonCacheable(variables, keyOrVar)) {
|
|
1147
|
+
assertNonCacheableReadAllowed(keyOrVar);
|
|
1149
1148
|
}
|
|
1150
1149
|
return contextGet(variables, keyOrVar);
|
|
1151
1150
|
}) as RequestContext<TEnv>["get"],
|
|
@@ -1338,6 +1337,7 @@ export function createRequestContext<TEnv>(
|
|
|
1338
1337
|
_locationState: undefined,
|
|
1339
1338
|
|
|
1340
1339
|
_reportedErrors: new WeakSet<object>(),
|
|
1340
|
+
_renderErrors: [],
|
|
1341
1341
|
_metricsStore: undefined,
|
|
1342
1342
|
_renderForeground: undefined,
|
|
1343
1343
|
_activeRoutine: undefined,
|
|
@@ -1627,7 +1627,9 @@ export function createUseFunction<TEnv>(
|
|
|
1627
1627
|
? parseSearchParams(ctx.searchParams, searchSchema)
|
|
1628
1628
|
: {};
|
|
1629
1629
|
|
|
1630
|
-
const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv>
|
|
1630
|
+
const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> & {
|
|
1631
|
+
_routeName?: string;
|
|
1632
|
+
} = {
|
|
1631
1633
|
params: ctx.params,
|
|
1632
1634
|
routeParams: (ctx.params ?? {}) as Record<string, string>,
|
|
1633
1635
|
request: ctx.request,
|
|
@@ -1666,6 +1668,7 @@ export function createUseFunction<TEnv>(
|
|
|
1666
1668
|
? isRouteRootScoped(ctx._routeName, ctx._routerId)
|
|
1667
1669
|
: undefined,
|
|
1668
1670
|
),
|
|
1671
|
+
_routeName: ctx._routeName,
|
|
1669
1672
|
rendered: () => {
|
|
1670
1673
|
throw new Error(
|
|
1671
1674
|
`ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
|
|
@@ -1673,6 +1676,8 @@ export function createUseFunction<TEnv>(
|
|
|
1673
1676
|
);
|
|
1674
1677
|
},
|
|
1675
1678
|
};
|
|
1679
|
+
// "use cache" keys it by route, like the request ctx it reads (#940).
|
|
1680
|
+
(loaderCtx as any)[NOCACHE_SYMBOL] = true;
|
|
1676
1681
|
|
|
1677
1682
|
// Meter through the same unified phase API as the loader-resolution funnel
|
|
1678
1683
|
// (observePhase), so a loader resolved via this base request-context ctx.use
|