@rangojs/router 0.0.0-experimental.cb54cbba → 0.0.0-experimental.debug-cache-2383ca26
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/AGENTS.md +4 -0
- package/dist/bin/rango.js +8 -3
- package/dist/vite/index.js +139 -200
- package/package.json +15 -14
- package/skills/caching/SKILL.md +37 -4
- package/skills/parallel/SKILL.md +126 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/navigation-bridge.ts +1 -3
- package/src/browser/navigation-client.ts +60 -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 +53 -13
- package/src/browser/react/Link.tsx +9 -1
- package/src/browser/react/NavigationProvider.tsx +27 -0
- 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/types.ts +9 -0
- package/src/build/route-types/router-processing.ts +12 -2
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +43 -3
- 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/debug.ts +2 -2
- package/src/route-definition/dsl-helpers.ts +32 -7
- 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/intercept-resolution.ts +2 -0
- package/src/router/lazy-includes.ts +4 -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.ts +2 -1
- package/src/router/router-context.ts +6 -1
- package/src/router/segment-resolution/fresh.ts +122 -15
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +347 -290
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router.ts +5 -1
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +90 -13
- package/src/server/request-context.ts +10 -4
- package/src/ssr/index.tsx +1 -0
- package/src/types/handler-context.ts +103 -17
- 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/version-plugin.ts +13 -1
- package/src/vite/rango.ts +144 -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/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
|
|
|
@@ -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: [],
|
|
@@ -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);
|
|
@@ -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,
|
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
|
};
|
|
@@ -70,9 +70,11 @@
|
|
|
70
70
|
* - No segments yielded from this middleware
|
|
71
71
|
*
|
|
72
72
|
* Loaders:
|
|
73
|
-
* - NEVER cached
|
|
73
|
+
* - NEVER cached in the segment cache
|
|
74
74
|
* - Always resolved fresh on every request
|
|
75
75
|
* - Ensures data freshness even with cached UI components
|
|
76
|
+
* - Segment cache staleness does NOT propagate to loader revalidation;
|
|
77
|
+
* loaders use their own revalidation rules (actionId, user-defined)
|
|
76
78
|
*
|
|
77
79
|
*
|
|
78
80
|
* REVALIDATION RULES
|
|
@@ -210,6 +212,9 @@ async function* yieldFromStore<TEnv>(
|
|
|
210
212
|
}
|
|
211
213
|
|
|
212
214
|
// Resolve loaders fresh (loaders are never pre-rendered/cached)
|
|
215
|
+
const ms = ctx.metricsStore;
|
|
216
|
+
const loaderStart = performance.now();
|
|
217
|
+
|
|
213
218
|
if (ctx.isFullMatch) {
|
|
214
219
|
if (resolveLoadersOnly) {
|
|
215
220
|
const loaderSegments = await ctx.Store.run(() =>
|
|
@@ -249,11 +254,17 @@ async function* yieldFromStore<TEnv>(
|
|
|
249
254
|
}
|
|
250
255
|
}
|
|
251
256
|
|
|
252
|
-
const ms = ctx.metricsStore;
|
|
253
257
|
if (ms) {
|
|
258
|
+
const loaderEnd = performance.now();
|
|
254
259
|
ms.metrics.push({
|
|
255
|
-
label: "pipeline:
|
|
256
|
-
duration:
|
|
260
|
+
label: "pipeline:loader-resolve",
|
|
261
|
+
duration: loaderEnd - loaderStart,
|
|
262
|
+
startTime: loaderStart - ms.requestStart,
|
|
263
|
+
depth: 1,
|
|
264
|
+
});
|
|
265
|
+
ms.metrics.push({
|
|
266
|
+
label: "pipeline:cache-hit",
|
|
267
|
+
duration: loaderEnd - pipelineStart,
|
|
257
268
|
startTime: pipelineStart - ms.requestStart,
|
|
258
269
|
});
|
|
259
270
|
}
|
|
@@ -437,7 +448,7 @@ export function withCacheLookup<TEnv>(
|
|
|
437
448
|
yield* source;
|
|
438
449
|
if (ms) {
|
|
439
450
|
ms.metrics.push({
|
|
440
|
-
label: "pipeline:cache-
|
|
451
|
+
label: "pipeline:cache-miss",
|
|
441
452
|
duration: performance.now() - pipelineStart,
|
|
442
453
|
startTime: pipelineStart - ms.requestStart,
|
|
443
454
|
});
|
|
@@ -457,7 +468,7 @@ export function withCacheLookup<TEnv>(
|
|
|
457
468
|
yield* source;
|
|
458
469
|
if (ms) {
|
|
459
470
|
ms.metrics.push({
|
|
460
|
-
label: "pipeline:cache-
|
|
471
|
+
label: "pipeline:cache-miss",
|
|
461
472
|
duration: performance.now() - pipelineStart,
|
|
462
473
|
startTime: pipelineStart - ms.requestStart,
|
|
463
474
|
});
|
|
@@ -509,7 +520,41 @@ export function withCacheLookup<TEnv>(
|
|
|
509
520
|
|
|
510
521
|
// Look up revalidation rules for this segment
|
|
511
522
|
const entryInfo = entryRevalidateMap?.get(segment.id);
|
|
523
|
+
|
|
524
|
+
// Even without explicit revalidation rules, route segments and their
|
|
525
|
+
// children must re-render when params or search params change — the
|
|
526
|
+
// handler reads ctx.params/ctx.searchParams so different values produce
|
|
527
|
+
// different content. Matches evaluateRevalidation's default logic.
|
|
528
|
+
const searchChanged = ctx.prevUrl.search !== ctx.url.search;
|
|
529
|
+
const routeParamsChanged = !paramsEqual(
|
|
530
|
+
ctx.matched.params,
|
|
531
|
+
ctx.prevParams,
|
|
532
|
+
);
|
|
533
|
+
const shouldDefaultRevalidate =
|
|
534
|
+
(searchChanged || routeParamsChanged) &&
|
|
535
|
+
(segment.type === "route" ||
|
|
536
|
+
(segment.belongsToRoute &&
|
|
537
|
+
(segment.type === "layout" || segment.type === "parallel")));
|
|
538
|
+
|
|
512
539
|
if (!entryInfo || entryInfo.revalidate.length === 0) {
|
|
540
|
+
if (shouldDefaultRevalidate) {
|
|
541
|
+
// Params or search params changed — must re-render even without custom rules
|
|
542
|
+
if (isTraceActive()) {
|
|
543
|
+
pushRevalidationTraceEntry({
|
|
544
|
+
segmentId: segment.id,
|
|
545
|
+
segmentType: segment.type,
|
|
546
|
+
belongsToRoute: segment.belongsToRoute ?? false,
|
|
547
|
+
source: "cache-hit",
|
|
548
|
+
defaultShouldRevalidate: true,
|
|
549
|
+
finalShouldRevalidate: true,
|
|
550
|
+
reason: routeParamsChanged
|
|
551
|
+
? "cached-params-changed"
|
|
552
|
+
: "cached-search-changed",
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
yield segment;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
513
558
|
// No revalidation rules, use default behavior (skip if client has)
|
|
514
559
|
if (isTraceActive()) {
|
|
515
560
|
pushRevalidationTraceEntry({
|
|
@@ -573,6 +618,7 @@ export function withCacheLookup<TEnv>(
|
|
|
573
618
|
// Resolve loaders fresh (loaders are NOT cached by default)
|
|
574
619
|
// This ensures fresh data even on cache hit
|
|
575
620
|
const Store = ctx.Store;
|
|
621
|
+
const loaderStart = performance.now();
|
|
576
622
|
|
|
577
623
|
if (ctx.isFullMatch) {
|
|
578
624
|
// Full match (document request) - simple loader resolution without revalidation
|
|
@@ -605,7 +651,11 @@ export function withCacheLookup<TEnv>(
|
|
|
605
651
|
ctx.url,
|
|
606
652
|
ctx.routeKey,
|
|
607
653
|
ctx.actionContext,
|
|
608
|
-
|
|
654
|
+
// Loaders are never cached in the segment cache, so segment
|
|
655
|
+
// staleness (cacheResult.shouldRevalidate) must not propagate.
|
|
656
|
+
// But browser-sent staleness (ctx.stale) — indicating an action
|
|
657
|
+
// happened in this or another tab — must still reach loaders.
|
|
658
|
+
ctx.stale || undefined,
|
|
609
659
|
),
|
|
610
660
|
);
|
|
611
661
|
|
|
@@ -624,9 +674,16 @@ export function withCacheLookup<TEnv>(
|
|
|
624
674
|
}
|
|
625
675
|
}
|
|
626
676
|
if (ms) {
|
|
677
|
+
const loaderEnd = performance.now();
|
|
678
|
+
ms.metrics.push({
|
|
679
|
+
label: "pipeline:loader-resolve",
|
|
680
|
+
duration: loaderEnd - loaderStart,
|
|
681
|
+
startTime: loaderStart - ms.requestStart,
|
|
682
|
+
depth: 1,
|
|
683
|
+
});
|
|
627
684
|
ms.metrics.push({
|
|
628
|
-
label: "pipeline:cache-
|
|
629
|
-
duration:
|
|
685
|
+
label: "pipeline:cache-hit",
|
|
686
|
+
duration: loaderEnd - pipelineStart,
|
|
630
687
|
startTime: pipelineStart - ms.requestStart,
|
|
631
688
|
});
|
|
632
689
|
}
|
|
@@ -104,7 +104,8 @@ import type { ResolvedSegment } from "../../types.js";
|
|
|
104
104
|
import { getRequestContext } from "../../server/request-context.js";
|
|
105
105
|
import type { MatchContext, MatchPipelineState } from "../match-context.js";
|
|
106
106
|
import { getRouterContext } from "../router-context.js";
|
|
107
|
-
import { debugLog, debugWarn } from "../logging.js";
|
|
107
|
+
import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
|
|
108
|
+
import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
|
|
108
109
|
import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
109
110
|
|
|
110
111
|
/**
|
|
@@ -120,7 +121,6 @@ export function withCacheStore<TEnv>(
|
|
|
120
121
|
return async function* (
|
|
121
122
|
source: AsyncGenerator<ResolvedSegment>,
|
|
122
123
|
): AsyncGenerator<ResolvedSegment> {
|
|
123
|
-
const pipelineStart = performance.now();
|
|
124
124
|
const ms = ctx.metricsStore;
|
|
125
125
|
|
|
126
126
|
// Collect all segments while passing them through
|
|
@@ -130,6 +130,9 @@ export function withCacheStore<TEnv>(
|
|
|
130
130
|
yield segment;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// Measure own work only (after source iteration completes)
|
|
134
|
+
const ownStart = performance.now();
|
|
135
|
+
|
|
133
136
|
// Skip caching if:
|
|
134
137
|
// 1. Cache miss but cache scope is disabled
|
|
135
138
|
// 2. This is an action (actions don't cache)
|
|
@@ -144,8 +147,8 @@ export function withCacheStore<TEnv>(
|
|
|
144
147
|
if (ms) {
|
|
145
148
|
ms.metrics.push({
|
|
146
149
|
label: "pipeline:cache-store",
|
|
147
|
-
duration: performance.now() -
|
|
148
|
-
startTime:
|
|
150
|
+
duration: performance.now() - ownStart,
|
|
151
|
+
startTime: ownStart - ms.requestStart,
|
|
149
152
|
});
|
|
150
153
|
}
|
|
151
154
|
return;
|
|
@@ -162,16 +165,23 @@ export function withCacheStore<TEnv>(
|
|
|
162
165
|
// Combine main segments with intercept segments
|
|
163
166
|
const allSegmentsToCache = [...allSegments, ...state.interceptSegments];
|
|
164
167
|
|
|
165
|
-
// Check if any non-loader segments have null components
|
|
166
|
-
//
|
|
168
|
+
// Check if any non-loader segments have null components from revalidation
|
|
169
|
+
// skip (client already had them). Segments where the handler intentionally
|
|
170
|
+
// returned null are not revalidation skips — re-rendering them will still
|
|
171
|
+
// produce null, so proactive caching would be wasted work.
|
|
172
|
+
const clientIdSet = new Set(ctx.clientSegmentIds);
|
|
167
173
|
const hasNullComponents = allSegmentsToCache.some(
|
|
168
|
-
(s) =>
|
|
174
|
+
(s) =>
|
|
175
|
+
s.component === null && s.type !== "loader" && clientIdSet.has(s.id),
|
|
169
176
|
);
|
|
170
177
|
|
|
171
178
|
const requestCtx = getRequestContext();
|
|
172
179
|
if (!requestCtx) return;
|
|
173
180
|
|
|
174
181
|
const cacheScope = ctx.cacheScope;
|
|
182
|
+
const reqId = INTERNAL_RANGO_DEBUG
|
|
183
|
+
? getOrCreateRequestId(ctx.request)
|
|
184
|
+
: undefined;
|
|
175
185
|
|
|
176
186
|
// Register onResponse callback to skip caching for non-200 responses
|
|
177
187
|
// Note: error/notFound status codes are set elsewhere (not caching-specific)
|
|
@@ -189,6 +199,11 @@ export function withCacheStore<TEnv>(
|
|
|
189
199
|
// Proactive caching: render all segments fresh in background
|
|
190
200
|
// This ensures cache has complete components for future requests
|
|
191
201
|
requestCtx.waitUntil(async () => {
|
|
202
|
+
// Prevent background metrics from polluting foreground timeline.
|
|
203
|
+
const savedMetrics = ctx.Store.metrics;
|
|
204
|
+
ctx.Store.metrics = undefined;
|
|
205
|
+
|
|
206
|
+
const start = performance.now();
|
|
192
207
|
debugLog("cacheStore", "proactive caching started", {
|
|
193
208
|
pathname: ctx.pathname,
|
|
194
209
|
});
|
|
@@ -218,7 +233,9 @@ export function withCacheStore<TEnv>(
|
|
|
218
233
|
// Use normal loader access so handle data is captured
|
|
219
234
|
setupLoaderAccess(proactiveHandlerContext, proactiveLoaderPromises);
|
|
220
235
|
|
|
221
|
-
// Re-resolve ALL segments without revalidation
|
|
236
|
+
// Re-resolve ALL segments without revalidation.
|
|
237
|
+
// Skip DSL loaders — they are never cached (cacheRoute filters them)
|
|
238
|
+
// and are always resolved fresh on each request.
|
|
222
239
|
const Store = ctx.Store;
|
|
223
240
|
const freshSegments = await Store.run(() =>
|
|
224
241
|
resolveAllSegments(
|
|
@@ -227,6 +244,7 @@ export function withCacheStore<TEnv>(
|
|
|
227
244
|
ctx.matched.params,
|
|
228
245
|
proactiveHandlerContext,
|
|
229
246
|
proactiveLoaderPromises,
|
|
247
|
+
{ skipLoaders: true },
|
|
230
248
|
),
|
|
231
249
|
);
|
|
232
250
|
|
|
@@ -256,28 +274,53 @@ export function withCacheStore<TEnv>(
|
|
|
256
274
|
completeSegments,
|
|
257
275
|
ctx.isIntercept,
|
|
258
276
|
);
|
|
277
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
278
|
+
const dur = performance.now() - start;
|
|
279
|
+
console.log(
|
|
280
|
+
`[RSC Background][req:${reqId}] Proactive cache ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${completeSegments.length}`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
259
283
|
debugLog("cacheStore", "proactive caching complete", {
|
|
260
284
|
pathname: ctx.pathname,
|
|
261
285
|
});
|
|
262
286
|
} catch (error) {
|
|
287
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
288
|
+
const dur = performance.now() - start;
|
|
289
|
+
console.log(
|
|
290
|
+
`[RSC Background][req:${reqId}] Proactive cache ${ctx.pathname} FAILED (${dur.toFixed(2)}ms) error=${String(error)}`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
263
293
|
debugWarn("cacheStore", "proactive caching failed", {
|
|
264
294
|
pathname: ctx.pathname,
|
|
265
295
|
error: String(error),
|
|
266
296
|
});
|
|
267
297
|
} finally {
|
|
268
298
|
requestCtx._handleStore = originalHandleStore;
|
|
299
|
+
ctx.Store.metrics = savedMetrics;
|
|
269
300
|
}
|
|
270
301
|
});
|
|
271
302
|
} else {
|
|
272
303
|
// All segments have components - cache directly
|
|
273
304
|
// Schedule caching in waitUntil since cacheRoute is now async (key resolution)
|
|
305
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
306
|
+
console.log(
|
|
307
|
+
`[RSC CacheStore][req:${reqId}] Direct cache path: scheduling cacheRoute for ${ctx.pathname} (${allSegmentsToCache.length} segments, hasNullComponents=${hasNullComponents})`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
274
310
|
requestCtx.waitUntil(async () => {
|
|
311
|
+
const start = performance.now();
|
|
275
312
|
await cacheScope.cacheRoute(
|
|
276
313
|
ctx.pathname,
|
|
277
314
|
ctx.matched.params,
|
|
278
315
|
allSegmentsToCache,
|
|
279
316
|
ctx.isIntercept,
|
|
280
317
|
);
|
|
318
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
319
|
+
const dur = performance.now() - start;
|
|
320
|
+
console.log(
|
|
321
|
+
`[RSC Background][req:${reqId}] Cache store ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${allSegmentsToCache.length}`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
281
324
|
});
|
|
282
325
|
}
|
|
283
326
|
|
|
@@ -287,8 +330,8 @@ export function withCacheStore<TEnv>(
|
|
|
287
330
|
if (ms) {
|
|
288
331
|
ms.metrics.push({
|
|
289
332
|
label: "pipeline:cache-store",
|
|
290
|
-
duration: performance.now() -
|
|
291
|
-
startTime:
|
|
333
|
+
duration: performance.now() - ownStart,
|
|
334
|
+
startTime: ownStart - ms.requestStart,
|
|
292
335
|
});
|
|
293
336
|
}
|
|
294
337
|
};
|