@rangojs/router 0.0.0-experimental.39 → 0.0.0-experimental.3b1deca8
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/dist/bin/rango.js +8 -3
- package/dist/vite/index.js +292 -204
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +45 -4
- package/skills/loader/SKILL.md +53 -43
- package/skills/parallel/SKILL.md +126 -0
- package/skills/route/SKILL.md +31 -0
- package/skills/router-setup/SKILL.md +52 -2
- package/skills/typesafety/SKILL.md +10 -0
- package/src/browser/debug-channel.ts +93 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/navigation-bridge.ts +1 -5
- package/src/browser/navigation-client.ts +84 -27
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +50 -9
- package/src/browser/prefetch/cache.ts +57 -5
- package/src/browser/prefetch/fetch.ts +30 -21
- package/src/browser/prefetch/queue.ts +92 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +9 -1
- package/src/browser/react/NavigationProvider.tsx +32 -3
- package/src/browser/rsc-router.tsx +109 -57
- package/src/browser/scroll-restoration.ts +31 -34
- package/src/browser/segment-reconciler.ts +6 -1
- package/src/browser/server-action-bridge.ts +12 -0
- package/src/browser/types.ts +17 -1
- package/src/build/route-types/router-processing.ts +12 -2
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +453 -11
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/deps/browser.ts +1 -0
- package/src/route-definition/dsl-helpers.ts +32 -7
- package/src/route-definition/helpers-types.ts +6 -5
- package/src/route-definition/redirect.ts +2 -2
- package/src/route-map-builder.ts +7 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +31 -8
- package/src/router/intercept-resolution.ts +2 -0
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/loader-resolution.ts +7 -1
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +9 -3
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +66 -9
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +8 -5
- package/src/router/match-result.ts +22 -6
- package/src/router/metrics.ts +6 -1
- package/src/router/middleware-types.ts +6 -2
- package/src/router/middleware.ts +4 -3
- package/src/router/router-context.ts +6 -1
- package/src/router/segment-resolution/fresh.ts +130 -17
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +352 -290
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/types.ts +1 -0
- package/src/router.ts +6 -1
- package/src/rsc/handler.ts +28 -2
- package/src/rsc/loader-fetch.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +4 -1
- package/src/rsc/rsc-rendering.ts +4 -1
- package/src/rsc/server-action.ts +2 -0
- package/src/rsc/types.ts +7 -1
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +102 -13
- package/src/server/request-context.ts +59 -12
- package/src/ssr/index.tsx +1 -0
- package/src/types/handler-context.ts +120 -22
- package/src/types/loader-types.ts +4 -4
- package/src/types/route-entry.ts +7 -0
- package/src/types/segments.ts +2 -0
- package/src/urls/path-helper.ts +1 -1
- package/src/vite/discovery/state.ts +0 -2
- package/src/vite/plugin-types.ts +0 -83
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/performance-tracks.ts +235 -0
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +148 -209
- package/src/vite/router-discovery.ts +0 -8
- package/src/vite/utils/banner.ts +3 -3
package/src/cache/index.ts
CHANGED
package/src/cache/taint.ts
CHANGED
|
@@ -81,6 +81,61 @@ export function assertNotInsideCacheExec(
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Symbol stamped on ctx when resolving handlers inside a cache() DSL boundary.
|
|
86
|
+
* Separate from INSIDE_CACHE_EXEC ("use cache") because cache() allows
|
|
87
|
+
* ctx.set() (children are also cached) but blocks response-level side effects
|
|
88
|
+
* (headers, cookies, status) which are lost on cache hit.
|
|
89
|
+
*/
|
|
90
|
+
export const INSIDE_CACHE_SCOPE: unique symbol = Symbol.for(
|
|
91
|
+
"rango:inside-cache-scope",
|
|
92
|
+
) as any;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Mark ctx as inside a cache() scope. Must be paired with unstampCacheScope.
|
|
96
|
+
*/
|
|
97
|
+
export function stampCacheScope(obj: object): void {
|
|
98
|
+
const current = (obj as any)[INSIDE_CACHE_SCOPE] ?? 0;
|
|
99
|
+
(obj as any)[INSIDE_CACHE_SCOPE] = current + 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Remove cache() scope mark.
|
|
104
|
+
*/
|
|
105
|
+
export function unstampCacheScope(obj: object): void {
|
|
106
|
+
const current = (obj as any)[INSIDE_CACHE_SCOPE] ?? 0;
|
|
107
|
+
if (current <= 1) {
|
|
108
|
+
delete (obj as any)[INSIDE_CACHE_SCOPE];
|
|
109
|
+
} else {
|
|
110
|
+
(obj as any)[INSIDE_CACHE_SCOPE] = current - 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Throw if ctx is inside a cache() DSL boundary.
|
|
116
|
+
* Call from response-level side effects (header, setCookie, setStatus, etc.)
|
|
117
|
+
* which are lost on cache hit because the handler body is skipped.
|
|
118
|
+
* ctx.set() is allowed inside cache() — children are also cached and can
|
|
119
|
+
* read the value.
|
|
120
|
+
*/
|
|
121
|
+
export function assertNotInsideCacheScope(
|
|
122
|
+
ctx: unknown,
|
|
123
|
+
methodName: string,
|
|
124
|
+
): void {
|
|
125
|
+
if (
|
|
126
|
+
ctx !== null &&
|
|
127
|
+
ctx !== undefined &&
|
|
128
|
+
typeof ctx === "object" &&
|
|
129
|
+
(INSIDE_CACHE_SCOPE as symbol) in (ctx as Record<symbol, unknown>)
|
|
130
|
+
) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`ctx.${methodName}() cannot be called inside a cache() boundary. ` +
|
|
133
|
+
`On cache hit the handler is skipped, so this side effect would be lost. ` +
|
|
134
|
+
`Move ctx.${methodName}() to a middleware or layout outside the cache() scope.`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
84
139
|
/**
|
|
85
140
|
* Brand symbol for functions wrapped by registerCachedFunction().
|
|
86
141
|
* Used at runtime to detect when a "use cache" function is misused
|
package/src/context-var.ts
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* interface PaginationData { current: number; total: number }
|
|
13
13
|
* export const Pagination = createVar<PaginationData>();
|
|
14
14
|
*
|
|
15
|
+
* // Non-cacheable var — throws if set/get inside cache() or "use cache"
|
|
16
|
+
* export const User = createVar<UserData>({ cache: false });
|
|
17
|
+
*
|
|
15
18
|
* // handler
|
|
16
19
|
* ctx.set(Pagination, { current: 1, total: 4 });
|
|
17
20
|
*
|
|
@@ -23,18 +26,36 @@
|
|
|
23
26
|
export interface ContextVar<T> {
|
|
24
27
|
readonly __brand: "context-var";
|
|
25
28
|
readonly key: symbol;
|
|
29
|
+
/** When false, the var is non-cacheable — throws inside cache() / "use cache" */
|
|
30
|
+
readonly cache: boolean;
|
|
26
31
|
/** Phantom field to carry the type parameter. Never set at runtime. */
|
|
27
32
|
readonly __type?: T;
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
export interface ContextVarOptions {
|
|
36
|
+
/**
|
|
37
|
+
* When false, marks this variable as non-cacheable.
|
|
38
|
+
* Setting or getting this var inside a cache() boundary or "use cache"
|
|
39
|
+
* function will throw. Use for inherently request-specific data (user
|
|
40
|
+
* sessions, auth tokens, etc.) that must never be baked into cached segments.
|
|
41
|
+
*
|
|
42
|
+
* @default true
|
|
43
|
+
*/
|
|
44
|
+
cache?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
/**
|
|
31
48
|
* Create a typed context variable token.
|
|
32
49
|
*
|
|
33
50
|
* The returned object is used with ctx.set(token, value) and ctx.get(token)
|
|
34
51
|
* for compile-time-checked data flow between handlers, layouts, and middleware.
|
|
35
52
|
*/
|
|
36
|
-
export function createVar<T>(): ContextVar<T> {
|
|
37
|
-
return {
|
|
53
|
+
export function createVar<T>(options?: ContextVarOptions): ContextVar<T> {
|
|
54
|
+
return {
|
|
55
|
+
__brand: "context-var" as const,
|
|
56
|
+
key: Symbol(),
|
|
57
|
+
cache: options?.cache !== false,
|
|
58
|
+
};
|
|
38
59
|
}
|
|
39
60
|
|
|
40
61
|
/**
|
|
@@ -49,6 +70,36 @@ export function isContextVar(value: unknown): value is ContextVar<unknown> {
|
|
|
49
70
|
);
|
|
50
71
|
}
|
|
51
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Symbol used as a Set stored on the variables object to track
|
|
75
|
+
* which keys hold non-cacheable values (from write-level { cache: false }).
|
|
76
|
+
*/
|
|
77
|
+
const NON_CACHEABLE_KEYS: unique symbol = Symbol.for(
|
|
78
|
+
"rango:non-cacheable-keys",
|
|
79
|
+
) as any;
|
|
80
|
+
|
|
81
|
+
function getNonCacheableKeys(variables: any): Set<string | symbol> {
|
|
82
|
+
if (!variables[NON_CACHEABLE_KEYS]) {
|
|
83
|
+
variables[NON_CACHEABLE_KEYS] = new Set();
|
|
84
|
+
}
|
|
85
|
+
return variables[NON_CACHEABLE_KEYS];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if a variable value is non-cacheable (either var-level or write-level).
|
|
90
|
+
*/
|
|
91
|
+
export function isNonCacheable(
|
|
92
|
+
variables: any,
|
|
93
|
+
keyOrVar: string | ContextVar<any>,
|
|
94
|
+
): boolean {
|
|
95
|
+
if (typeof keyOrVar !== "string" && !keyOrVar.cache) {
|
|
96
|
+
return true; // var-level policy
|
|
97
|
+
}
|
|
98
|
+
const key = typeof keyOrVar === "string" ? keyOrVar : keyOrVar.key;
|
|
99
|
+
const set = variables[NON_CACHEABLE_KEYS] as Set<string | symbol> | undefined;
|
|
100
|
+
return set?.has(key) ?? false; // write-level policy
|
|
101
|
+
}
|
|
102
|
+
|
|
52
103
|
/**
|
|
53
104
|
* Read a variable from the variables store.
|
|
54
105
|
* Accepts either a string key (legacy) or a ContextVar token (typed).
|
|
@@ -64,6 +115,17 @@ export function contextGet(
|
|
|
64
115
|
/** Keys that must never be used as string variable names */
|
|
65
116
|
const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
66
117
|
|
|
118
|
+
export interface ContextSetOptions {
|
|
119
|
+
/**
|
|
120
|
+
* When false, marks this specific write as non-cacheable.
|
|
121
|
+
* "Least cacheable wins" — if either the var definition or this option
|
|
122
|
+
* says cache: false, the value is non-cacheable.
|
|
123
|
+
*
|
|
124
|
+
* @default true (inherits from createVar)
|
|
125
|
+
*/
|
|
126
|
+
cache?: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
67
129
|
/**
|
|
68
130
|
* Write a variable to the variables store.
|
|
69
131
|
* Accepts either a string key (legacy) or a ContextVar token (typed).
|
|
@@ -72,6 +134,7 @@ export function contextSet(
|
|
|
72
134
|
variables: any,
|
|
73
135
|
keyOrVar: string | ContextVar<any>,
|
|
74
136
|
value: any,
|
|
137
|
+
options?: ContextSetOptions,
|
|
75
138
|
): void {
|
|
76
139
|
if (typeof keyOrVar === "string") {
|
|
77
140
|
if (FORBIDDEN_KEYS.has(keyOrVar)) {
|
|
@@ -80,7 +143,14 @@ export function contextSet(
|
|
|
80
143
|
);
|
|
81
144
|
}
|
|
82
145
|
variables[keyOrVar] = value;
|
|
146
|
+
if (options?.cache === false) {
|
|
147
|
+
getNonCacheableKeys(variables).add(keyOrVar);
|
|
148
|
+
}
|
|
83
149
|
} else {
|
|
84
150
|
variables[keyOrVar.key] = value;
|
|
151
|
+
// Track write-level non-cacheable (var-level is checked via keyOrVar.cache)
|
|
152
|
+
if (options?.cache === false) {
|
|
153
|
+
getNonCacheableKeys(variables).add(keyOrVar.key);
|
|
154
|
+
}
|
|
85
155
|
}
|
|
86
156
|
}
|
package/src/debug.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Debug utilities for manifest inspection and comparison
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type
|
|
5
|
+
import { getParallelSlotCount, type EntryData } from "./server/context";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Serialized entry for debug output
|
|
@@ -64,7 +64,7 @@ export function serializeManifest(
|
|
|
64
64
|
hasLoader: entry.loader?.length > 0,
|
|
65
65
|
hasMiddleware: entry.middleware?.length > 0,
|
|
66
66
|
hasErrorBoundary: entry.errorBoundary?.length > 0,
|
|
67
|
-
parallelCount: entry.parallel
|
|
67
|
+
parallelCount: getParallelSlotCount(entry.parallel),
|
|
68
68
|
interceptCount: entry.intercept?.length ?? 0,
|
|
69
69
|
};
|
|
70
70
|
|
package/src/deps/browser.ts
CHANGED
|
@@ -282,7 +282,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
282
282
|
errorBoundary: [],
|
|
283
283
|
notFoundBoundary: [],
|
|
284
284
|
layout: [],
|
|
285
|
-
parallel:
|
|
285
|
+
parallel: {},
|
|
286
286
|
intercept: [],
|
|
287
287
|
loader: [],
|
|
288
288
|
...(cacheUrlPrefix ? { mountPath: cacheUrlPrefix } : {}),
|
|
@@ -320,7 +320,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
320
320
|
errorBoundary: [],
|
|
321
321
|
notFoundBoundary: [],
|
|
322
322
|
layout: [],
|
|
323
|
-
parallel:
|
|
323
|
+
parallel: {},
|
|
324
324
|
intercept: [],
|
|
325
325
|
loader: [],
|
|
326
326
|
...(cacheUrlPrefix2 ? { mountPath: cacheUrlPrefix2 } : {}),
|
|
@@ -393,6 +393,8 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
393
393
|
"parallel() cannot be nested inside another parallel()",
|
|
394
394
|
);
|
|
395
395
|
|
|
396
|
+
const slotNames = Object.keys(slots as Record<string, any>) as `@${string}`[];
|
|
397
|
+
|
|
396
398
|
const namespace = `${ctx.namespace}.$${store.getNextIndex("parallel")}`;
|
|
397
399
|
|
|
398
400
|
// Unwrap any static handler definitions in parallel slots
|
|
@@ -431,7 +433,7 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
431
433
|
errorBoundary: [],
|
|
432
434
|
notFoundBoundary: [],
|
|
433
435
|
layout: [],
|
|
434
|
-
parallel:
|
|
436
|
+
parallel: {},
|
|
435
437
|
intercept: [],
|
|
436
438
|
loader: [],
|
|
437
439
|
...(parallelUrlPrefix ? { mountPath: parallelUrlPrefix } : {}),
|
|
@@ -454,7 +456,30 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
454
456
|
);
|
|
455
457
|
}
|
|
456
458
|
|
|
457
|
-
|
|
459
|
+
for (const slotName of slotNames) {
|
|
460
|
+
const slotEntry = {
|
|
461
|
+
...entry,
|
|
462
|
+
handler: { [slotName]: unwrappedSlots[slotName]! },
|
|
463
|
+
middleware: [...entry.middleware],
|
|
464
|
+
revalidate: [...entry.revalidate],
|
|
465
|
+
errorBoundary: [...entry.errorBoundary],
|
|
466
|
+
notFoundBoundary: [...entry.notFoundBoundary],
|
|
467
|
+
layout: [...entry.layout],
|
|
468
|
+
parallel: { ...entry.parallel },
|
|
469
|
+
intercept: [...entry.intercept],
|
|
470
|
+
loader: [...entry.loader],
|
|
471
|
+
...(entry.staticHandlerIds?.[slotName]
|
|
472
|
+
? {
|
|
473
|
+
isStaticPrerender: true as const,
|
|
474
|
+
staticHandlerIds: { [slotName]: entry.staticHandlerIds[slotName]! },
|
|
475
|
+
}
|
|
476
|
+
: {
|
|
477
|
+
isStaticPrerender: undefined,
|
|
478
|
+
staticHandlerIds: undefined,
|
|
479
|
+
}),
|
|
480
|
+
} satisfies EntryData;
|
|
481
|
+
ctx.parent.parallel[slotName] = slotEntry;
|
|
482
|
+
}
|
|
458
483
|
return { name: namespace, type: "parallel" } as ParallelItem;
|
|
459
484
|
};
|
|
460
485
|
|
|
@@ -687,7 +712,7 @@ const transitionFn = (
|
|
|
687
712
|
errorBoundary: [],
|
|
688
713
|
notFoundBoundary: [],
|
|
689
714
|
layout: [],
|
|
690
|
-
parallel:
|
|
715
|
+
parallel: {},
|
|
691
716
|
intercept: [],
|
|
692
717
|
loader: [],
|
|
693
718
|
} as EntryData;
|
|
@@ -734,7 +759,7 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
|
|
|
734
759
|
errorBoundary: [],
|
|
735
760
|
notFoundBoundary: [],
|
|
736
761
|
layout: [],
|
|
737
|
-
parallel:
|
|
762
|
+
parallel: {},
|
|
738
763
|
intercept: [],
|
|
739
764
|
loader: [],
|
|
740
765
|
} satisfies EntryData;
|
|
@@ -791,7 +816,7 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
791
816
|
revalidate: [],
|
|
792
817
|
errorBoundary: [],
|
|
793
818
|
notFoundBoundary: [],
|
|
794
|
-
parallel:
|
|
819
|
+
parallel: {},
|
|
795
820
|
intercept: [],
|
|
796
821
|
layout: [],
|
|
797
822
|
loader: [],
|
|
@@ -228,11 +228,12 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
228
228
|
* revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
|
|
229
229
|
* ])
|
|
230
230
|
*
|
|
231
|
-
* //
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* }
|
|
231
|
+
* // Consume in client components with useLoader()
|
|
232
|
+
* // (preferred — cache-safe, always fresh)
|
|
233
|
+
* function ProductDetails() {
|
|
234
|
+
* const { data } = useLoader(ProductLoader);
|
|
235
|
+
* return <div>{data.name}</div>;
|
|
236
|
+
* }
|
|
236
237
|
* ```
|
|
237
238
|
* @param loaderDef - Loader created with createLoader()
|
|
238
239
|
* @param use - Optional callback for loader-specific revalidation rules
|
|
@@ -71,9 +71,9 @@ export function redirect(
|
|
|
71
71
|
// actions both deliver state through Flight payloads, so suppress for those.
|
|
72
72
|
if (
|
|
73
73
|
reqCtx &&
|
|
74
|
-
!reqCtx.
|
|
74
|
+
!reqCtx.originalUrl.searchParams.has("_rsc_partial") &&
|
|
75
75
|
!reqCtx.request.headers.has("rsc-action") &&
|
|
76
|
-
!reqCtx.
|
|
76
|
+
!reqCtx.originalUrl.searchParams.has("_rsc_action")
|
|
77
77
|
) {
|
|
78
78
|
console.warn(
|
|
79
79
|
`[Router] redirect() with state during a full-page (SSR) request to "${url}". ` +
|
package/src/route-map-builder.ts
CHANGED
|
@@ -199,7 +199,13 @@ export function registerRouterManifestLoader(
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
export async function ensureRouterManifest(routerId: string): Promise<void> {
|
|
202
|
-
|
|
202
|
+
// Check both manifest AND trie. The virtual module's setRouterManifest()
|
|
203
|
+
// pre-sets the manifest at startup, but the per-router trie is only
|
|
204
|
+
// available from the lazy loader. Without this, the lazy loader never
|
|
205
|
+
// runs and findMatch falls back to the global merged trie — which
|
|
206
|
+
// contains routes from ALL routers and breaks multi-router setups.
|
|
207
|
+
if (perRouterManifestMap.has(routerId) && perRouterTrieMap.has(routerId))
|
|
208
|
+
return;
|
|
203
209
|
const loader = routerManifestLoaders.get(routerId);
|
|
204
210
|
if (loader) {
|
|
205
211
|
const mod = await loader();
|
package/src/router/find-match.ts
CHANGED
|
@@ -52,8 +52,10 @@ export function createFindMatch<TEnv = any>(
|
|
|
52
52
|
: undefined;
|
|
53
53
|
|
|
54
54
|
// Phase 1: Try trie match (O(path_length))
|
|
55
|
-
//
|
|
56
|
-
|
|
55
|
+
// Only use the per-router trie. The global trie merges routes from ALL
|
|
56
|
+
// routers and must not be used — in multi-router setups (host routing)
|
|
57
|
+
// overlapping paths like "/" would match the wrong app's route.
|
|
58
|
+
const routeTrie = getRouterTrie(deps.routerId);
|
|
57
59
|
if (routeTrie) {
|
|
58
60
|
const trieStart = performance.now();
|
|
59
61
|
const trieResult = tryTrieMatch(routeTrie, pathname);
|
|
@@ -8,7 +8,13 @@ import type { HandlerContext, InternalHandlerContext } from "../types";
|
|
|
8
8
|
import { _getRequestContext } from "../server/request-context.js";
|
|
9
9
|
import { getSearchSchema, isRouteRootScoped } from "../route-map-builder.js";
|
|
10
10
|
import { parseSearchParams, serializeSearchParams } from "../search-params.js";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
contextGet,
|
|
13
|
+
contextSet,
|
|
14
|
+
isNonCacheable,
|
|
15
|
+
type ContextSetOptions,
|
|
16
|
+
} from "../context-var.js";
|
|
17
|
+
import { isInsideCacheScope } from "../server/context.js";
|
|
12
18
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
13
19
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
14
20
|
import { PRERENDER_PASSTHROUGH } from "../prerender.js";
|
|
@@ -213,7 +219,7 @@ export function createHandlerContext<TEnv>(
|
|
|
213
219
|
const stubResponse =
|
|
214
220
|
requestContext?.res ?? new Response(null, { status: 200 });
|
|
215
221
|
|
|
216
|
-
// Guard mutating Headers methods so they throw inside "use cache"
|
|
222
|
+
// Guard mutating Headers methods so they throw inside "use cache" or cache() scope.
|
|
217
223
|
// Uses lazy `ctx` reference (assigned below) — only the specific handler ctx
|
|
218
224
|
// is stamped by cache-runtime, not the shared request context.
|
|
219
225
|
const MUTATING_HEADERS_METHODS = new Set(["set", "append", "delete"]);
|
|
@@ -225,6 +231,13 @@ export function createHandlerContext<TEnv>(
|
|
|
225
231
|
if (MUTATING_HEADERS_METHODS.has(prop as string)) {
|
|
226
232
|
return (...args: any[]) => {
|
|
227
233
|
assertNotInsideCacheExec(ctx, "headers");
|
|
234
|
+
if (isInsideCacheScope()) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`ctx.headers.${String(prop)}() cannot be called inside a cache() boundary. ` +
|
|
237
|
+
`On cache hit the handler is skipped, so this side effect would be lost. ` +
|
|
238
|
+
`Move header mutations to a middleware or layout outside the cache() scope.`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
228
241
|
return value.apply(target, args);
|
|
229
242
|
};
|
|
230
243
|
}
|
|
@@ -245,13 +258,23 @@ export function createHandlerContext<TEnv>(
|
|
|
245
258
|
originalUrl: new URL(request.url),
|
|
246
259
|
env: bindings,
|
|
247
260
|
var: variables,
|
|
248
|
-
get: ((keyOrVar: any) =>
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
261
|
+
get: ((keyOrVar: any) => {
|
|
262
|
+
// Read-time guard: non-cacheable var inside cache() → throw.
|
|
263
|
+
// Works for both ContextVar tokens and string keys.
|
|
264
|
+
if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
|
|
267
|
+
`The variable was created with { cache: false } or set with { cache: false }, ` +
|
|
268
|
+
`and its value would be stale on cache hit. Move the read outside the cached scope.`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
return contextGet(variables, keyOrVar);
|
|
272
|
+
}) as HandlerContext<any, TEnv>["get"],
|
|
273
|
+
set: ((keyOrVar: any, value: any, options?: ContextSetOptions) => {
|
|
253
274
|
assertNotInsideCacheExec(ctx, "set");
|
|
254
|
-
|
|
275
|
+
// Write is dumb: store value + non-cacheable metadata.
|
|
276
|
+
// Enforcement happens at read time via ctx.get().
|
|
277
|
+
contextSet(variables, keyOrVar, value, options);
|
|
255
278
|
}) as HandlerContext<any, TEnv>["set"],
|
|
256
279
|
res: stubResponse, // Stub response for setting headers
|
|
257
280
|
headers: guardedHeaders, // Guarded shorthand for res.headers
|
|
@@ -188,6 +188,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
188
188
|
context,
|
|
189
189
|
actionContext,
|
|
190
190
|
stale,
|
|
191
|
+
traceSource: "intercept-loader",
|
|
191
192
|
});
|
|
192
193
|
|
|
193
194
|
if (!shouldRevalidate) {
|
|
@@ -355,6 +356,7 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
355
356
|
context,
|
|
356
357
|
actionContext,
|
|
357
358
|
stale,
|
|
359
|
+
traceSource: "intercept-loader",
|
|
358
360
|
});
|
|
359
361
|
|
|
360
362
|
if (!shouldRevalidate) {
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
EntryData,
|
|
5
5
|
RSCRouterContext,
|
|
6
6
|
runWithPrefixes,
|
|
7
|
+
getIsolatedLazyParent,
|
|
7
8
|
} from "../server/context";
|
|
8
9
|
import type { UrlPatterns } from "../urls.js";
|
|
9
10
|
import type { AllUseItems, IncludeItem } from "../route-types.js";
|
|
@@ -14,6 +15,7 @@ export interface LazyEvalDeps<TEnv = any> {
|
|
|
14
15
|
mergedRouteMap: Record<string, string>;
|
|
15
16
|
nextMountIndex: () => number;
|
|
16
17
|
getPrecomputedByPrefix: () => Map<string, Record<string, string>> | null;
|
|
18
|
+
routerId?: string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
// Detect lazy includes in handler result and create placeholder entries
|
|
@@ -137,7 +139,7 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
137
139
|
patternsByPrefix,
|
|
138
140
|
trailingSlash: trailingSlashMap,
|
|
139
141
|
namespace: "lazy",
|
|
140
|
-
parent: (lazyContext?.parent as EntryData | null)
|
|
142
|
+
parent: getIsolatedLazyParent(lazyContext?.parent as EntryData | null),
|
|
141
143
|
counters: lazyCounters,
|
|
142
144
|
cacheProfiles: (lazyContext as any)?.cacheProfiles,
|
|
143
145
|
rootScoped: (lazyContext as any)?.rootScoped,
|
|
@@ -200,6 +202,7 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
200
202
|
trailingSlash: entry.trailingSlash,
|
|
201
203
|
handler: (lazyInclude.patterns as UrlPatterns<TEnv>).handler,
|
|
202
204
|
mountIndex: deps.nextMountIndex(),
|
|
205
|
+
routerId: deps.routerId,
|
|
203
206
|
// Lazy evaluation fields
|
|
204
207
|
lazy: true,
|
|
205
208
|
lazyPatterns: lazyInclude.patterns,
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { ReactNode } from "react";
|
|
8
8
|
import { track } from "../server/context";
|
|
9
9
|
import type { EntryData } from "../server/context";
|
|
10
|
+
import { contextGet } from "../context-var.js";
|
|
10
11
|
import type {
|
|
11
12
|
ResolvedSegment,
|
|
12
13
|
HandlerContext,
|
|
@@ -241,6 +242,11 @@ function createLoaderExecutor<TEnv>(
|
|
|
241
242
|
pendingLoaders.add(loader.$$id);
|
|
242
243
|
|
|
243
244
|
const currentLoaderId = loader.$$id;
|
|
245
|
+
// Loader functions are always fresh (never cached), so they get an
|
|
246
|
+
// unguarded get that bypasses non-cacheable read guards. This applies
|
|
247
|
+
// to ALL loaders — DSL and handler-called — because the loader
|
|
248
|
+
// function itself always re-executes. Also handles nested deps
|
|
249
|
+
// (loaderA → use(loaderB)) since all share this unguarded get.
|
|
244
250
|
const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
|
|
245
251
|
params: ctx.params,
|
|
246
252
|
routeParams: (ctx.params ?? {}) as Record<string, string>,
|
|
@@ -251,7 +257,7 @@ function createLoaderExecutor<TEnv>(
|
|
|
251
257
|
url: ctx.url,
|
|
252
258
|
env: ctx.env,
|
|
253
259
|
var: ctx.var,
|
|
254
|
-
get: ctx.get,
|
|
260
|
+
get: ((keyOrVar: any) => contextGet(ctx.var, keyOrVar)) as typeof ctx.get,
|
|
255
261
|
use: <TDep, TDepParams = any>(
|
|
256
262
|
dep: LoaderDefinition<TDep, TDepParams>,
|
|
257
263
|
): Promise<TDep> => {
|
package/src/router/logging.ts
CHANGED
|
@@ -12,7 +12,10 @@ export interface RevalidationTraceEntry {
|
|
|
12
12
|
| "cache-hit"
|
|
13
13
|
| "loader"
|
|
14
14
|
| "parallel"
|
|
15
|
-
| "orphan-layout"
|
|
15
|
+
| "orphan-layout"
|
|
16
|
+
| "route-handler"
|
|
17
|
+
| "layout-handler"
|
|
18
|
+
| "intercept-loader";
|
|
16
19
|
defaultShouldRevalidate: boolean;
|
|
17
20
|
finalShouldRevalidate: boolean;
|
|
18
21
|
reason: string;
|
|
@@ -71,7 +74,7 @@ function getHeaderRequestId(request: Request): string | null {
|
|
|
71
74
|
return trimmed.length > 0 ? trimmed : null;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
function getOrCreateRequestId(request: Request): string {
|
|
77
|
+
export function getOrCreateRequestId(request: Request): string {
|
|
75
78
|
const existing = requestIds.get(request);
|
|
76
79
|
if (existing) return existing;
|
|
77
80
|
|
package/src/router/manifest.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { createRouteHelpers } from "../route-definition";
|
|
|
9
9
|
import {
|
|
10
10
|
getContext,
|
|
11
11
|
runWithPrefixes,
|
|
12
|
+
getIsolatedLazyParent,
|
|
12
13
|
type EntryData,
|
|
13
14
|
type MetricsStore,
|
|
14
15
|
} from "../server/context";
|
|
@@ -65,7 +66,9 @@ export async function loadManifest(
|
|
|
65
66
|
const mountIndex = entry.mountIndex;
|
|
66
67
|
|
|
67
68
|
// Check module-level cache (persists across requests within same isolate)
|
|
68
|
-
|
|
69
|
+
// Include routerId so multi-router setups (host routing) don't share cached
|
|
70
|
+
// EntryData across routers with overlapping mountIndex + routeKey combinations.
|
|
71
|
+
const cacheKey = `${VERSION}:${entry.routerId ?? ""}:${mountIndex ?? ""}:${routeKey}:${isSSR ? 1 : 0}`;
|
|
69
72
|
const cached = manifestModuleCache.get(cacheKey);
|
|
70
73
|
if (cached) {
|
|
71
74
|
const cacheStart = performance.now();
|
|
@@ -112,8 +115,11 @@ export async function loadManifest(
|
|
|
112
115
|
// This ensures routes are registered under the correct layout hierarchy
|
|
113
116
|
const lazyContext =
|
|
114
117
|
entry.lazy && entry.lazyPatterns ? entry.lazyContext : null;
|
|
115
|
-
const parentForContext =
|
|
116
|
-
(
|
|
118
|
+
const parentForContext = lazyContext
|
|
119
|
+
? getIsolatedLazyParent(
|
|
120
|
+
(lazyContext.parent as EntryData | null) ?? Store.parent,
|
|
121
|
+
)
|
|
122
|
+
: Store.parent;
|
|
117
123
|
|
|
118
124
|
// For lazy entries, merge captured counters from include() so the
|
|
119
125
|
// handler's entries get shortCode indices after sibling entries that
|
|
@@ -103,7 +103,8 @@ import type { ResolvedSegment } from "../../types.js";
|
|
|
103
103
|
import type { MatchContext, MatchPipelineState } from "../match-context.js";
|
|
104
104
|
import { getRouterContext } from "../router-context.js";
|
|
105
105
|
import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
106
|
-
import { debugLog, debugWarn } from "../logging.js";
|
|
106
|
+
import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
|
|
107
|
+
import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
|
|
107
108
|
|
|
108
109
|
/**
|
|
109
110
|
* Creates background revalidation middleware
|
|
@@ -143,8 +144,19 @@ export function withBackgroundRevalidation<TEnv>(
|
|
|
143
144
|
|
|
144
145
|
const requestCtx = getRequestContext();
|
|
145
146
|
const cacheScope = ctx.cacheScope;
|
|
147
|
+
const reqId = INTERNAL_RANGO_DEBUG
|
|
148
|
+
? getOrCreateRequestId(ctx.request)
|
|
149
|
+
: undefined;
|
|
146
150
|
|
|
147
151
|
requestCtx?.waitUntil(async () => {
|
|
152
|
+
// Prevent background metrics from polluting foreground timeline.
|
|
153
|
+
// The foreground uses its own metricsStore reference directly (via
|
|
154
|
+
// appendMetric), so nulling Store.metrics only affects track() calls
|
|
155
|
+
// inside this background Store.run() scope.
|
|
156
|
+
const savedMetrics = ctx.Store.metrics;
|
|
157
|
+
ctx.Store.metrics = undefined;
|
|
158
|
+
|
|
159
|
+
const start = performance.now();
|
|
148
160
|
debugLog("backgroundRevalidation", "revalidating stale route", {
|
|
149
161
|
pathname: ctx.pathname,
|
|
150
162
|
fullMatch: ctx.isFullMatch,
|
|
@@ -174,7 +186,9 @@ export function withBackgroundRevalidation<TEnv>(
|
|
|
174
186
|
setupLoaderAccess(freshHandlerContext, freshLoaderPromises);
|
|
175
187
|
|
|
176
188
|
// Resolve all segments fresh (without revalidation logic)
|
|
177
|
-
// to ensure complete components for caching
|
|
189
|
+
// to ensure complete components for caching.
|
|
190
|
+
// Skip DSL loaders — they are never cached (cacheRoute filters them)
|
|
191
|
+
// and are always resolved fresh on each request.
|
|
178
192
|
const freshSegments = await ctx.Store.run(() =>
|
|
179
193
|
resolveAllSegments(
|
|
180
194
|
ctx.entries,
|
|
@@ -182,6 +196,7 @@ export function withBackgroundRevalidation<TEnv>(
|
|
|
182
196
|
ctx.matched.params,
|
|
183
197
|
freshHandlerContext,
|
|
184
198
|
freshLoaderPromises,
|
|
199
|
+
{ skipLoaders: true },
|
|
185
200
|
),
|
|
186
201
|
);
|
|
187
202
|
|
|
@@ -207,16 +222,29 @@ export function withBackgroundRevalidation<TEnv>(
|
|
|
207
222
|
completeSegments,
|
|
208
223
|
ctx.isIntercept,
|
|
209
224
|
);
|
|
225
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
226
|
+
const dur = performance.now() - start;
|
|
227
|
+
console.log(
|
|
228
|
+
`[RSC Background][req:${reqId}] SWR revalidation ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${completeSegments.length}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
210
231
|
debugLog("backgroundRevalidation", "revalidation complete", {
|
|
211
232
|
pathname: ctx.pathname,
|
|
212
233
|
});
|
|
213
234
|
} catch (error) {
|
|
235
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
236
|
+
const dur = performance.now() - start;
|
|
237
|
+
console.log(
|
|
238
|
+
`[RSC Background][req:${reqId}] SWR revalidation ${ctx.pathname} FAILED (${dur.toFixed(2)}ms) error=${String(error)}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
214
241
|
debugWarn("backgroundRevalidation", "revalidation failed", {
|
|
215
242
|
pathname: ctx.pathname,
|
|
216
243
|
error: String(error),
|
|
217
244
|
});
|
|
218
245
|
} finally {
|
|
219
246
|
requestCtx._handleStore = originalHandleStore;
|
|
247
|
+
ctx.Store.metrics = savedMetrics;
|
|
220
248
|
}
|
|
221
249
|
});
|
|
222
250
|
};
|