@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.c873df95

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 (64) 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 +50 -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 +26 -9
  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-runtime.ts +15 -11
  22. package/src/cache/cache-scope.ts +2 -2
  23. package/src/cache/cf/cf-cache-store.ts +453 -11
  24. package/src/cache/cf/index.ts +5 -1
  25. package/src/cache/document-cache.ts +17 -7
  26. package/src/cache/index.ts +1 -0
  27. package/src/debug.ts +2 -2
  28. package/src/route-definition/dsl-helpers.ts +32 -7
  29. package/src/route-definition/redirect.ts +2 -2
  30. package/src/route-map-builder.ts +7 -1
  31. package/src/router/find-match.ts +4 -2
  32. package/src/router/intercept-resolution.ts +2 -0
  33. package/src/router/lazy-includes.ts +4 -1
  34. package/src/router/logging.ts +5 -2
  35. package/src/router/manifest.ts +9 -3
  36. package/src/router/match-middleware/background-revalidation.ts +18 -1
  37. package/src/router/match-middleware/cache-lookup.ts +59 -9
  38. package/src/router/match-middleware/cache-store.ts +32 -6
  39. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  40. package/src/router/match-middleware/segment-resolution.ts +8 -5
  41. package/src/router/match-result.ts +25 -15
  42. package/src/router/metrics.ts +6 -1
  43. package/src/router/middleware.ts +2 -1
  44. package/src/router/router-context.ts +5 -1
  45. package/src/router/segment-resolution/fresh.ts +122 -15
  46. package/src/router/segment-resolution/loader-cache.ts +1 -0
  47. package/src/router/segment-resolution/revalidation.ts +347 -290
  48. package/src/router/segment-wrappers.ts +2 -0
  49. package/src/router.ts +5 -1
  50. package/src/segment-system.tsx +140 -4
  51. package/src/server/context.ts +90 -13
  52. package/src/server/request-context.ts +10 -4
  53. package/src/ssr/index.tsx +1 -0
  54. package/src/types/handler-context.ts +103 -17
  55. package/src/types/route-entry.ts +7 -0
  56. package/src/types/segments.ts +2 -0
  57. package/src/urls/path-helper.ts +1 -1
  58. package/src/vite/discovery/state.ts +0 -2
  59. package/src/vite/plugin-types.ts +0 -83
  60. package/src/vite/plugins/expose-action-id.ts +1 -3
  61. package/src/vite/plugins/version-plugin.ts +13 -1
  62. package/src/vite/rango.ts +144 -209
  63. package/src/vite/router-discovery.ts +0 -8
  64. 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}". ` +
@@ -199,7 +199,13 @@ export function registerRouterManifestLoader(
199
199
  }
200
200
 
201
201
  export async function ensureRouterManifest(routerId: string): Promise<void> {
202
- if (perRouterManifestMap.has(routerId)) return;
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();
@@ -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
- // Prefer per-router trie (isolated) over global trie (merged).
56
- const routeTrie = getRouterTrie(deps.routerId) ?? getRouteTrie();
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) ?? 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,
@@ -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
 
@@ -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),
@@ -70,9 +70,11 @@
70
70
  * - No segments yielded from this middleware
71
71
  *
72
72
  * Loaders:
73
- * - NEVER cached by design
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:cache-lookup",
256
- duration: performance.now() - pipelineStart,
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-lookup",
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-lookup",
471
+ label: "pipeline:cache-miss",
461
472
  duration: performance.now() - pipelineStart,
462
473
  startTime: pipelineStart - ms.requestStart,
463
474
  });
@@ -509,7 +520,34 @@ 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 search params change — the handler reads
526
+ // ctx.searchParams so different ?page= values produce different content.
527
+ const searchChanged = ctx.prevUrl.search !== ctx.url.search;
528
+ const shouldDefaultRevalidate =
529
+ searchChanged &&
530
+ (segment.type === "route" ||
531
+ (segment.belongsToRoute &&
532
+ (segment.type === "layout" || segment.type === "parallel")));
533
+
512
534
  if (!entryInfo || entryInfo.revalidate.length === 0) {
535
+ if (shouldDefaultRevalidate) {
536
+ // Search params changed — must re-render even without custom rules
537
+ if (isTraceActive()) {
538
+ pushRevalidationTraceEntry({
539
+ segmentId: segment.id,
540
+ segmentType: segment.type,
541
+ belongsToRoute: segment.belongsToRoute ?? false,
542
+ source: "cache-hit",
543
+ defaultShouldRevalidate: true,
544
+ finalShouldRevalidate: true,
545
+ reason: "cached-search-changed",
546
+ });
547
+ }
548
+ yield segment;
549
+ continue;
550
+ }
513
551
  // No revalidation rules, use default behavior (skip if client has)
514
552
  if (isTraceActive()) {
515
553
  pushRevalidationTraceEntry({
@@ -573,6 +611,7 @@ export function withCacheLookup<TEnv>(
573
611
  // Resolve loaders fresh (loaders are NOT cached by default)
574
612
  // This ensures fresh data even on cache hit
575
613
  const Store = ctx.Store;
614
+ const loaderStart = performance.now();
576
615
 
577
616
  if (ctx.isFullMatch) {
578
617
  // Full match (document request) - simple loader resolution without revalidation
@@ -605,7 +644,11 @@ export function withCacheLookup<TEnv>(
605
644
  ctx.url,
606
645
  ctx.routeKey,
607
646
  ctx.actionContext,
608
- cacheResult.shouldRevalidate || undefined,
647
+ // Loaders are never cached in the segment cache, so segment
648
+ // staleness (cacheResult.shouldRevalidate) must not propagate.
649
+ // But browser-sent staleness (ctx.stale) — indicating an action
650
+ // happened in this or another tab — must still reach loaders.
651
+ ctx.stale || undefined,
609
652
  ),
610
653
  );
611
654
 
@@ -624,9 +667,16 @@ export function withCacheLookup<TEnv>(
624
667
  }
625
668
  }
626
669
  if (ms) {
670
+ const loaderEnd = performance.now();
671
+ ms.metrics.push({
672
+ label: "pipeline:loader-resolve",
673
+ duration: loaderEnd - loaderStart,
674
+ startTime: loaderStart - ms.requestStart,
675
+ depth: 1,
676
+ });
627
677
  ms.metrics.push({
628
- label: "pipeline:cache-lookup",
629
- duration: performance.now() - pipelineStart,
678
+ label: "pipeline:cache-hit",
679
+ duration: loaderEnd - pipelineStart,
630
680
  startTime: pipelineStart - ms.requestStart,
631
681
  });
632
682
  }
@@ -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;
@@ -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() - pipelineStart,
188
- startTime: pipelineStart - ms.requestStart,
190
+ duration: performance.now() - ownStart,
191
+ startTime: ownStart - ms.requestStart,
189
192
  });
190
193
  }
191
194
  };