@rangojs/router 0.0.0-experimental.f2337aef → 0.0.0-experimental.fa8a383a

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.
Files changed (57) hide show
  1. package/dist/bin/rango.js +8 -3
  2. package/dist/vite/index.js +139 -200
  3. package/package.json +1 -1
  4. package/skills/caching/SKILL.md +37 -4
  5. package/skills/parallel/SKILL.md +59 -0
  6. package/src/browser/event-controller.ts +5 -0
  7. package/src/browser/navigation-bridge.ts +1 -3
  8. package/src/browser/navigation-client.ts +60 -27
  9. package/src/browser/navigation-transaction.ts +11 -9
  10. package/src/browser/partial-update.ts +39 -9
  11. package/src/browser/prefetch/cache.ts +57 -5
  12. package/src/browser/prefetch/fetch.ts +30 -21
  13. package/src/browser/prefetch/queue.ts +53 -13
  14. package/src/browser/react/Link.tsx +9 -1
  15. package/src/browser/react/NavigationProvider.tsx +27 -0
  16. package/src/browser/rsc-router.tsx +109 -57
  17. package/src/browser/scroll-restoration.ts +20 -7
  18. package/src/browser/segment-reconciler.ts +6 -1
  19. package/src/browser/types.ts +9 -0
  20. package/src/build/route-types/router-processing.ts +12 -2
  21. package/src/cache/cache-scope.ts +2 -2
  22. package/src/cache/cf/cf-cache-store.ts +453 -11
  23. package/src/cache/cf/index.ts +5 -1
  24. package/src/cache/document-cache.ts +17 -7
  25. package/src/cache/index.ts +1 -0
  26. package/src/debug.ts +2 -2
  27. package/src/route-definition/dsl-helpers.ts +32 -7
  28. package/src/route-definition/redirect.ts +2 -2
  29. package/src/router/lazy-includes.ts +4 -1
  30. package/src/router/logging.ts +1 -1
  31. package/src/router/manifest.ts +9 -3
  32. package/src/router/match-middleware/background-revalidation.ts +18 -1
  33. package/src/router/match-middleware/cache-lookup.ts +20 -3
  34. package/src/router/match-middleware/cache-store.ts +32 -6
  35. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  36. package/src/router/match-middleware/segment-resolution.ts +7 -5
  37. package/src/router/match-result.ts +11 -1
  38. package/src/router/middleware.ts +2 -1
  39. package/src/router/segment-resolution/fresh.ts +104 -14
  40. package/src/router/segment-resolution/loader-cache.ts +1 -0
  41. package/src/router/segment-resolution/revalidation.ts +307 -272
  42. package/src/router.ts +5 -1
  43. package/src/rsc/handler.ts +9 -0
  44. package/src/segment-system.tsx +140 -4
  45. package/src/server/context.ts +90 -13
  46. package/src/server/request-context.ts +10 -4
  47. package/src/ssr/index.tsx +1 -0
  48. package/src/types/route-entry.ts +7 -0
  49. package/src/types/segments.ts +2 -0
  50. package/src/urls/path-helper.ts +1 -1
  51. package/src/vite/discovery/state.ts +0 -2
  52. package/src/vite/plugin-types.ts +0 -83
  53. package/src/vite/plugins/expose-action-id.ts +1 -3
  54. package/src/vite/plugins/version-plugin.ts +13 -1
  55. package/src/vite/rango.ts +144 -209
  56. package/src/vite/router-discovery.ts +0 -8
  57. package/src/vite/utils/banner.ts +3 -3
@@ -29,6 +29,7 @@ export { MemorySegmentCacheStore } from "./memory-segment-store.js";
29
29
  export {
30
30
  CFCacheStore,
31
31
  type CFCacheStoreOptions,
32
+ type KVNamespace,
32
33
  CACHE_STALE_AT_HEADER,
33
34
  CACHE_STATUS_HEADER,
34
35
  } from "./cf/index.js";
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 { EntryData } from "./server/context";
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?.length ?? 0,
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
- ctx.parent.parallel.push(entry);
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.url.searchParams.has("_rsc_partial") &&
74
+ !reqCtx.originalUrl.searchParams.has("_rsc_partial") &&
75
75
  !reqCtx.request.headers.has("rsc-action") &&
76
- !reqCtx.url.searchParams.has("_rsc_action")
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}". ` +
@@ -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) ?? 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,
@@ -74,7 +74,7 @@ function getHeaderRequestId(request: Request): string | null {
74
74
  return trimmed.length > 0 ? trimmed : null;
75
75
  }
76
76
 
77
- function getOrCreateRequestId(request: Request): string {
77
+ export function getOrCreateRequestId(request: Request): string {
78
78
  const existing = requestIds.get(request);
79
79
  if (existing) return existing;
80
80
 
@@ -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
- const cacheKey = `${VERSION}:${mountIndex ?? ""}:${routeKey}:${isSSR ? 1 : 0}`;
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
- (lazyContext?.parent as EntryData | null) ?? Store.parent;
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,12 @@ 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
+ const start = performance.now();
148
153
  debugLog("backgroundRevalidation", "revalidating stale route", {
149
154
  pathname: ctx.pathname,
150
155
  fullMatch: ctx.isFullMatch,
@@ -207,10 +212,22 @@ export function withBackgroundRevalidation<TEnv>(
207
212
  completeSegments,
208
213
  ctx.isIntercept,
209
214
  );
215
+ if (INTERNAL_RANGO_DEBUG) {
216
+ const dur = performance.now() - start;
217
+ console.log(
218
+ `[RSC Background][req:${reqId}] SWR revalidation ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${completeSegments.length}`,
219
+ );
220
+ }
210
221
  debugLog("backgroundRevalidation", "revalidation complete", {
211
222
  pathname: ctx.pathname,
212
223
  });
213
224
  } catch (error) {
225
+ if (INTERNAL_RANGO_DEBUG) {
226
+ const dur = performance.now() - start;
227
+ console.log(
228
+ `[RSC Background][req:${reqId}] SWR revalidation ${ctx.pathname} FAILED (${dur.toFixed(2)}ms) error=${String(error)}`,
229
+ );
230
+ }
214
231
  debugWarn("backgroundRevalidation", "revalidation failed", {
215
232
  pathname: ctx.pathname,
216
233
  error: String(error),
@@ -210,6 +210,9 @@ async function* yieldFromStore<TEnv>(
210
210
  }
211
211
 
212
212
  // Resolve loaders fresh (loaders are never pre-rendered/cached)
213
+ const ms = ctx.metricsStore;
214
+ const loaderStart = performance.now();
215
+
213
216
  if (ctx.isFullMatch) {
214
217
  if (resolveLoadersOnly) {
215
218
  const loaderSegments = await ctx.Store.run(() =>
@@ -249,11 +252,17 @@ async function* yieldFromStore<TEnv>(
249
252
  }
250
253
  }
251
254
 
252
- const ms = ctx.metricsStore;
253
255
  if (ms) {
256
+ const loaderEnd = performance.now();
257
+ ms.metrics.push({
258
+ label: "pipeline:loader-resolve",
259
+ duration: loaderEnd - loaderStart,
260
+ startTime: loaderStart - ms.requestStart,
261
+ depth: 1,
262
+ });
254
263
  ms.metrics.push({
255
264
  label: "pipeline:cache-lookup",
256
- duration: performance.now() - pipelineStart,
265
+ duration: loaderEnd - pipelineStart,
257
266
  startTime: pipelineStart - ms.requestStart,
258
267
  });
259
268
  }
@@ -573,6 +582,7 @@ export function withCacheLookup<TEnv>(
573
582
  // Resolve loaders fresh (loaders are NOT cached by default)
574
583
  // This ensures fresh data even on cache hit
575
584
  const Store = ctx.Store;
585
+ const loaderStart = performance.now();
576
586
 
577
587
  if (ctx.isFullMatch) {
578
588
  // Full match (document request) - simple loader resolution without revalidation
@@ -624,9 +634,16 @@ export function withCacheLookup<TEnv>(
624
634
  }
625
635
  }
626
636
  if (ms) {
637
+ const loaderEnd = performance.now();
638
+ ms.metrics.push({
639
+ label: "pipeline:loader-resolve",
640
+ duration: loaderEnd - loaderStart,
641
+ startTime: loaderStart - ms.requestStart,
642
+ depth: 1,
643
+ });
627
644
  ms.metrics.push({
628
645
  label: "pipeline:cache-lookup",
629
- duration: performance.now() - pipelineStart,
646
+ duration: loaderEnd - pipelineStart,
630
647
  startTime: pipelineStart - ms.requestStart,
631
648
  });
632
649
  }
@@ -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() - pipelineStart,
148
- startTime: pipelineStart - ms.requestStart,
150
+ duration: performance.now() - ownStart,
151
+ startTime: ownStart - ms.requestStart,
149
152
  });
150
153
  }
151
154
  return;
@@ -172,6 +175,9 @@ export function withCacheStore<TEnv>(
172
175
  if (!requestCtx) return;
173
176
 
174
177
  const cacheScope = ctx.cacheScope;
178
+ const reqId = INTERNAL_RANGO_DEBUG
179
+ ? getOrCreateRequestId(ctx.request)
180
+ : undefined;
175
181
 
176
182
  // Register onResponse callback to skip caching for non-200 responses
177
183
  // Note: error/notFound status codes are set elsewhere (not caching-specific)
@@ -189,6 +195,7 @@ export function withCacheStore<TEnv>(
189
195
  // Proactive caching: render all segments fresh in background
190
196
  // This ensures cache has complete components for future requests
191
197
  requestCtx.waitUntil(async () => {
198
+ const start = performance.now();
192
199
  debugLog("cacheStore", "proactive caching started", {
193
200
  pathname: ctx.pathname,
194
201
  });
@@ -256,10 +263,22 @@ export function withCacheStore<TEnv>(
256
263
  completeSegments,
257
264
  ctx.isIntercept,
258
265
  );
266
+ if (INTERNAL_RANGO_DEBUG) {
267
+ const dur = performance.now() - start;
268
+ console.log(
269
+ `[RSC Background][req:${reqId}] Proactive cache ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${completeSegments.length}`,
270
+ );
271
+ }
259
272
  debugLog("cacheStore", "proactive caching complete", {
260
273
  pathname: ctx.pathname,
261
274
  });
262
275
  } catch (error) {
276
+ if (INTERNAL_RANGO_DEBUG) {
277
+ const dur = performance.now() - start;
278
+ console.log(
279
+ `[RSC Background][req:${reqId}] Proactive cache ${ctx.pathname} FAILED (${dur.toFixed(2)}ms) error=${String(error)}`,
280
+ );
281
+ }
263
282
  debugWarn("cacheStore", "proactive caching failed", {
264
283
  pathname: ctx.pathname,
265
284
  error: String(error),
@@ -272,12 +291,19 @@ export function withCacheStore<TEnv>(
272
291
  // All segments have components - cache directly
273
292
  // Schedule caching in waitUntil since cacheRoute is now async (key resolution)
274
293
  requestCtx.waitUntil(async () => {
294
+ const start = performance.now();
275
295
  await cacheScope.cacheRoute(
276
296
  ctx.pathname,
277
297
  ctx.matched.params,
278
298
  allSegmentsToCache,
279
299
  ctx.isIntercept,
280
300
  );
301
+ if (INTERNAL_RANGO_DEBUG) {
302
+ const dur = performance.now() - start;
303
+ console.log(
304
+ `[RSC Background][req:${reqId}] Cache store ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${allSegmentsToCache.length}`,
305
+ );
306
+ }
281
307
  });
282
308
  }
283
309
 
@@ -287,8 +313,8 @@ export function withCacheStore<TEnv>(
287
313
  if (ms) {
288
314
  ms.metrics.push({
289
315
  label: "pipeline:cache-store",
290
- duration: performance.now() - pipelineStart,
291
- startTime: pipelineStart - ms.requestStart,
316
+ duration: performance.now() - ownStart,
317
+ startTime: ownStart - ms.requestStart,
292
318
  });
293
319
  }
294
320
  };
@@ -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() - pipelineStart,
142
- startTime: pipelineStart - ms.requestStart,
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() - pipelineStart,
167
- startTime: pipelineStart - ms.requestStart,
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() - pipelineStart,
220
- startTime: pipelineStart - ms.requestStart,
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() - pipelineStart,
122
- startTime: pipelineStart - ms.requestStart,
123
+ duration: performance.now() - ownStart,
124
+ startTime: ownStart - ms.requestStart,
123
125
  });
124
126
  }
125
127
  return;
@@ -185,8 +187,8 @@ export function withSegmentResolution<TEnv>(
185
187
  if (ms) {
186
188
  ms.metrics.push({
187
189
  label: "pipeline:segment-resolve",
188
- duration: performance.now() - pipelineStart,
189
- startTime: pipelineStart - ms.requestStart,
190
+ duration: performance.now() - ownStart,
191
+ startTime: ownStart - ms.requestStart,
190
192
  });
191
193
  }
192
194
  };
@@ -109,6 +109,7 @@
109
109
  import type { MatchResult, ResolvedSegment } from "../types.js";
110
110
  import type { MatchContext, MatchPipelineState } from "./match-context.js";
111
111
  import { debugLog } from "./logging.js";
112
+ import { appendMetric } from "./metrics.js";
112
113
 
113
114
  /**
114
115
  * Collect all segments from an async generator
@@ -210,10 +211,19 @@ export async function collectMatchResult<TEnv>(
210
211
  ): Promise<MatchResult> {
211
212
  const allSegments = await collectSegments(pipeline);
212
213
 
214
+ const buildStart = performance.now();
215
+
213
216
  // Update state with collected segments if not already set
214
217
  if (state.segments.length === 0) {
215
218
  state.segments = allSegments;
216
219
  }
217
220
 
218
- return buildMatchResult(allSegments, ctx, state);
221
+ const result = buildMatchResult(allSegments, ctx, state);
222
+ appendMetric(
223
+ ctx.metricsStore,
224
+ "collect-result",
225
+ buildStart,
226
+ performance.now() - buildStart,
227
+ );
228
+ return result;
219
229
  }
@@ -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).