@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
|
@@ -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
|
};
|
|
@@ -123,7 +123,6 @@ export function withInterceptResolution<TEnv>(
|
|
|
123
123
|
return async function* (
|
|
124
124
|
source: AsyncGenerator<ResolvedSegment>,
|
|
125
125
|
): AsyncGenerator<ResolvedSegment> {
|
|
126
|
-
const pipelineStart = performance.now();
|
|
127
126
|
const ms = ctx.metricsStore;
|
|
128
127
|
|
|
129
128
|
// First, yield all segments from the source (main segment resolution or cache)
|
|
@@ -133,13 +132,16 @@ export function withInterceptResolution<TEnv>(
|
|
|
133
132
|
yield segment;
|
|
134
133
|
}
|
|
135
134
|
|
|
135
|
+
// Measure own work only (after source iteration completes)
|
|
136
|
+
const ownStart = performance.now();
|
|
137
|
+
|
|
136
138
|
// Skip intercept resolution for full match (document requests don't have intercepts)
|
|
137
139
|
if (ctx.isFullMatch) {
|
|
138
140
|
if (ms) {
|
|
139
141
|
ms.metrics.push({
|
|
140
142
|
label: "pipeline:intercept",
|
|
141
|
-
duration: performance.now() -
|
|
142
|
-
startTime:
|
|
143
|
+
duration: performance.now() - ownStart,
|
|
144
|
+
startTime: ownStart - ms.requestStart,
|
|
143
145
|
});
|
|
144
146
|
}
|
|
145
147
|
return;
|
|
@@ -163,8 +165,8 @@ export function withInterceptResolution<TEnv>(
|
|
|
163
165
|
if (ms) {
|
|
164
166
|
ms.metrics.push({
|
|
165
167
|
label: "pipeline:intercept",
|
|
166
|
-
duration: performance.now() -
|
|
167
|
-
startTime:
|
|
168
|
+
duration: performance.now() - ownStart,
|
|
169
|
+
startTime: ownStart - ms.requestStart,
|
|
168
170
|
});
|
|
169
171
|
}
|
|
170
172
|
return;
|
|
@@ -216,8 +218,8 @@ export function withInterceptResolution<TEnv>(
|
|
|
216
218
|
if (ms) {
|
|
217
219
|
ms.metrics.push({
|
|
218
220
|
label: "pipeline:intercept",
|
|
219
|
-
duration: performance.now() -
|
|
220
|
-
startTime:
|
|
221
|
+
duration: performance.now() - ownStart,
|
|
222
|
+
startTime: ownStart - ms.requestStart,
|
|
221
223
|
});
|
|
222
224
|
}
|
|
223
225
|
};
|
|
@@ -104,7 +104,6 @@ export function withSegmentResolution<TEnv>(
|
|
|
104
104
|
return async function* (
|
|
105
105
|
source: AsyncGenerator<ResolvedSegment>,
|
|
106
106
|
): AsyncGenerator<ResolvedSegment> {
|
|
107
|
-
const pipelineStart = performance.now();
|
|
108
107
|
const ms = ctx.metricsStore;
|
|
109
108
|
|
|
110
109
|
// IMPORTANT: Always iterate source first to give cache-lookup a chance
|
|
@@ -113,13 +112,16 @@ export function withSegmentResolution<TEnv>(
|
|
|
113
112
|
yield segment;
|
|
114
113
|
}
|
|
115
114
|
|
|
115
|
+
// Measure own work only (after source iteration completes)
|
|
116
|
+
const ownStart = performance.now();
|
|
117
|
+
|
|
116
118
|
// If cache hit, segments were already yielded by cache lookup
|
|
117
119
|
if (state.cacheHit) {
|
|
118
120
|
if (ms) {
|
|
119
121
|
ms.metrics.push({
|
|
120
122
|
label: "pipeline:segment-resolve",
|
|
121
|
-
duration: performance.now() -
|
|
122
|
-
startTime:
|
|
123
|
+
duration: performance.now() - ownStart,
|
|
124
|
+
startTime: ownStart - ms.requestStart,
|
|
123
125
|
});
|
|
124
126
|
}
|
|
125
127
|
return;
|
|
@@ -168,6 +170,7 @@ export function withSegmentResolution<TEnv>(
|
|
|
168
170
|
ctx.interceptResult,
|
|
169
171
|
ctx.localRouteName,
|
|
170
172
|
ctx.pathname,
|
|
173
|
+
ctx.stale,
|
|
171
174
|
),
|
|
172
175
|
);
|
|
173
176
|
|
|
@@ -184,8 +187,8 @@ export function withSegmentResolution<TEnv>(
|
|
|
184
187
|
if (ms) {
|
|
185
188
|
ms.metrics.push({
|
|
186
189
|
label: "pipeline:segment-resolve",
|
|
187
|
-
duration: performance.now() -
|
|
188
|
-
startTime:
|
|
190
|
+
duration: performance.now() - ownStart,
|
|
191
|
+
startTime: ownStart - ms.requestStart,
|
|
189
192
|
});
|
|
190
193
|
}
|
|
191
194
|
};
|
|
@@ -67,10 +67,11 @@
|
|
|
67
67
|
* Keep if:
|
|
68
68
|
* - component !== null (needs rendering)
|
|
69
69
|
* - type === "loader" (carries data even with null component)
|
|
70
|
+
* - client doesn't have the segment (structurally required parent node)
|
|
70
71
|
*
|
|
71
72
|
* Skip if:
|
|
72
|
-
* - component === null AND type !== "loader"
|
|
73
|
-
* - (
|
|
73
|
+
* - component === null AND type !== "loader" AND client has it cached
|
|
74
|
+
* - (Revalidation skip — client already has this segment's UI)
|
|
74
75
|
*
|
|
75
76
|
*
|
|
76
77
|
* INTERCEPT HANDLING
|
|
@@ -109,6 +110,7 @@
|
|
|
109
110
|
import type { MatchResult, ResolvedSegment } from "../types.js";
|
|
110
111
|
import type { MatchContext, MatchPipelineState } from "./match-context.js";
|
|
111
112
|
import { debugLog } from "./logging.js";
|
|
113
|
+
import { appendMetric } from "./metrics.js";
|
|
112
114
|
|
|
113
115
|
/**
|
|
114
116
|
* Collect all segments from an async generator
|
|
@@ -167,10 +169,15 @@ export function buildMatchResult<TEnv>(
|
|
|
167
169
|
// Deduplicate allIds (defense-in-depth for partial match path)
|
|
168
170
|
allIds = [...new Set(allIds)];
|
|
169
171
|
|
|
170
|
-
// Filter out segments
|
|
171
|
-
//
|
|
172
|
+
// Filter out null-component segments only when the client already has
|
|
173
|
+
// them cached (revalidation skip). If the client doesn't have the segment,
|
|
174
|
+
// it must be included even with null component — it's structurally required
|
|
175
|
+
// as a parent node for child layouts/parallels to reconcile against.
|
|
176
|
+
// Loader segments are always included as they carry data.
|
|
177
|
+
const clientIdSet = new Set(ctx.clientSegmentIds);
|
|
172
178
|
segmentsToRender = allSegments.filter(
|
|
173
|
-
(s) =>
|
|
179
|
+
(s) =>
|
|
180
|
+
s.component !== null || s.type === "loader" || !clientIdSet.has(s.id),
|
|
174
181
|
);
|
|
175
182
|
}
|
|
176
183
|
|
|
@@ -210,10 +217,19 @@ export async function collectMatchResult<TEnv>(
|
|
|
210
217
|
): Promise<MatchResult> {
|
|
211
218
|
const allSegments = await collectSegments(pipeline);
|
|
212
219
|
|
|
220
|
+
const buildStart = performance.now();
|
|
221
|
+
|
|
213
222
|
// Update state with collected segments if not already set
|
|
214
223
|
if (state.segments.length === 0) {
|
|
215
224
|
state.segments = allSegments;
|
|
216
225
|
}
|
|
217
226
|
|
|
218
|
-
|
|
227
|
+
const result = buildMatchResult(allSegments, ctx, state);
|
|
228
|
+
appendMetric(
|
|
229
|
+
ctx.metricsStore,
|
|
230
|
+
"collect-result",
|
|
231
|
+
buildStart,
|
|
232
|
+
performance.now() - buildStart,
|
|
233
|
+
);
|
|
234
|
+
return result;
|
|
219
235
|
}
|
package/src/router/metrics.ts
CHANGED
|
@@ -15,7 +15,12 @@ function formatMs(value: number): string {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function sortMetrics(metrics: PerformanceMetric[]): PerformanceMetric[] {
|
|
18
|
-
return [...metrics].sort((a, b) =>
|
|
18
|
+
return [...metrics].sort((a, b) => {
|
|
19
|
+
// handler:total always goes last (it wraps everything)
|
|
20
|
+
if (a.label === "handler:total") return 1;
|
|
21
|
+
if (b.label === "handler:total") return -1;
|
|
22
|
+
return a.startTime - b.startTime;
|
|
23
|
+
});
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
interface Span {
|
|
@@ -27,8 +27,12 @@ type GetVariableFn = {
|
|
|
27
27
|
* Set variable function type
|
|
28
28
|
*/
|
|
29
29
|
type SetVariableFn = {
|
|
30
|
-
<T>(contextVar: ContextVar<T>, value: T): void;
|
|
31
|
-
<K extends keyof DefaultVars>(
|
|
30
|
+
<T>(contextVar: ContextVar<T>, value: T, options?: { cache?: boolean }): void;
|
|
31
|
+
<K extends keyof DefaultVars>(
|
|
32
|
+
key: K,
|
|
33
|
+
value: DefaultVars[K],
|
|
34
|
+
options?: { cache?: boolean },
|
|
35
|
+
): void;
|
|
32
36
|
};
|
|
33
37
|
|
|
34
38
|
/**
|
package/src/router/middleware.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
import { _getRequestContext } from "../server/request-context.js";
|
|
22
22
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
23
23
|
import { appendMetric, createMetricsStore } from "./metrics.js";
|
|
24
|
+
import { stripInternalParams } from "./handler-context.js";
|
|
24
25
|
|
|
25
26
|
// Re-export types and cookie utilities for backward compatibility
|
|
26
27
|
export type {
|
|
@@ -147,7 +148,7 @@ export function createMiddlewareContext<TEnv>(
|
|
|
147
148
|
search?: Record<string, unknown>,
|
|
148
149
|
) => string,
|
|
149
150
|
): MiddlewareContext<TEnv> {
|
|
150
|
-
const url = new URL(request.url);
|
|
151
|
+
const url = stripInternalParams(new URL(request.url));
|
|
151
152
|
|
|
152
153
|
// Track the initial response to detect pre/post-next() phase.
|
|
153
154
|
// Before next(): responseHolder.response === initialResponse (the stub).
|
|
@@ -203,8 +204,8 @@ export function createMiddlewareContext<TEnv>(
|
|
|
203
204
|
get: ((keyOrVar: any) =>
|
|
204
205
|
contextGet(variables, keyOrVar)) as MiddlewareContext<TEnv>["get"],
|
|
205
206
|
|
|
206
|
-
set: ((keyOrVar: any, value: unknown) => {
|
|
207
|
-
contextSet(variables, keyOrVar, value);
|
|
207
|
+
set: ((keyOrVar: any, value: unknown, options?: any) => {
|
|
208
|
+
contextSet(variables, keyOrVar, value, options);
|
|
208
209
|
}) as MiddlewareContext<TEnv>["set"],
|
|
209
210
|
|
|
210
211
|
var: variables as MiddlewareContext<TEnv>["var"],
|
|
@@ -138,6 +138,7 @@ export interface RouterContext<TEnv = any> {
|
|
|
138
138
|
interceptResult: InterceptResult | null,
|
|
139
139
|
localRouteName: string,
|
|
140
140
|
pathname: string,
|
|
141
|
+
stale?: boolean,
|
|
141
142
|
) => Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }>;
|
|
142
143
|
|
|
143
144
|
// Generator-based segment resolution (for pipeline)
|
|
@@ -188,7 +189,10 @@ export interface RouterContext<TEnv = any> {
|
|
|
188
189
|
| "cache-hit"
|
|
189
190
|
| "loader"
|
|
190
191
|
| "parallel"
|
|
191
|
-
| "orphan-layout"
|
|
192
|
+
| "orphan-layout"
|
|
193
|
+
| "route-handler"
|
|
194
|
+
| "layout-handler"
|
|
195
|
+
| "intercept-loader";
|
|
192
196
|
}) => Promise<boolean>;
|
|
193
197
|
|
|
194
198
|
// Request context
|
|
@@ -206,6 +210,7 @@ export interface RouterContext<TEnv = any> {
|
|
|
206
210
|
params: Record<string, string>,
|
|
207
211
|
handlerContext: HandlerContext<any, TEnv>,
|
|
208
212
|
loaderPromises: Map<string, Promise<any>>,
|
|
213
|
+
options?: { skipLoaders?: boolean },
|
|
209
214
|
) => Promise<ResolvedSegment[]>;
|
|
210
215
|
|
|
211
216
|
// Generator-based simple resolution
|