@rangojs/router 0.0.0-experimental.46 → 0.0.0-experimental.4ffb98de

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.
@@ -1745,7 +1745,7 @@ import { resolve } from "node:path";
1745
1745
  // package.json
1746
1746
  var package_default = {
1747
1747
  name: "@rangojs/router",
1748
- version: "0.0.0-experimental.46",
1748
+ version: "0.0.0-experimental.4ffb98de",
1749
1749
  description: "Django-inspired RSC router with composable URL patterns",
1750
1750
  keywords: [
1751
1751
  "react",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.46",
3
+ "version": "0.0.0-experimental.4ffb98de",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -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
 
@@ -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
  }
@@ -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) => a.startTime - b.startTime);
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 {
@@ -19,6 +19,8 @@ import type {
19
19
  } from "../../types";
20
20
  import type { SegmentResolutionDeps } from "../types.js";
21
21
  import { resolveLoaderData } from "./loader-cache.js";
22
+ import { _getRequestContext } from "../../server/request-context.js";
23
+ import { appendMetric } from "../metrics.js";
22
24
  import {
23
25
  handleHandlerResult,
24
26
  tryStaticHandler,
@@ -94,8 +96,12 @@ export async function resolveLoaders<TEnv>(
94
96
  const shortCode = shortCodeOverride ?? entry.shortCode;
95
97
  const hasLoading = "loading" in entry && entry.loading !== undefined;
96
98
  const loadingDisabled = hasLoading && entry.loading === false;
99
+ const ms = _getRequestContext()?._metricsStore;
97
100
 
98
101
  if (!loadingDisabled) {
102
+ // Streaming loaders: promises kick off now, settle during RSC serialization.
103
+ // No per-loader timing here — settlement happens asynchronously during
104
+ // RSC/SSR stream consumption, after the perf timeline is logged.
99
105
  return loaderEntries.map((loaderEntry, i) => {
100
106
  const { loader } = loaderEntry;
101
107
  const segmentId = `${shortCode}D${i}.${loader.$$id}`;
@@ -120,14 +126,31 @@ export async function resolveLoaders<TEnv>(
120
126
 
121
127
  // Loading disabled: still start all loaders in parallel, but only emit
122
128
  // settled promises so handlers don't stream loading placeholders.
123
- const pendingLoaderData = loaderEntries.map((loaderEntry) =>
124
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
125
- );
126
- await Promise.all(pendingLoaderData);
129
+ // We can measure actual execution time here since we await all loaders.
130
+ const pendingLoaderData = loaderEntries.map((loaderEntry) => {
131
+ const start = performance.now();
132
+ const promise = resolveLoaderData(loaderEntry, ctx, ctx.pathname);
133
+ return { promise, start, loaderId: loaderEntry.loader.$$id };
134
+ });
135
+ await Promise.all(pendingLoaderData.map((p) => p.promise));
127
136
 
128
137
  return loaderEntries.map((loaderEntry, i) => {
129
138
  const { loader } = loaderEntry;
130
139
  const segmentId = `${shortCode}D${i}.${loader.$$id}`;
140
+ const pending = pendingLoaderData[i]!;
141
+ if (ms && !ms.metrics.some((m) => m.label === `loader:${loader.$$id}`)) {
142
+ // All loaders ran in parallel via Promise.all — each span covers
143
+ // from its own kickoff to the batch settlement, giving a ceiling
144
+ // on that loader's contribution to the overall wait.
145
+ const batchEnd = performance.now();
146
+ appendMetric(
147
+ ms,
148
+ `loader:${loader.$$id}`,
149
+ pending.start,
150
+ batchEnd - pending.start,
151
+ 2,
152
+ );
153
+ }
131
154
  return {
132
155
  id: segmentId,
133
156
  namespace: entry.id,
@@ -137,7 +160,7 @@ export async function resolveLoaders<TEnv>(
137
160
  params: ctx.params,
138
161
  loaderId: loader.$$id,
139
162
  loaderData: deps.wrapLoaderPromise(
140
- pendingLoaderData[i]!,
163
+ pending.promise,
141
164
  entry,
142
165
  segmentId,
143
166
  ctx.pathname,
@@ -147,6 +147,7 @@ export function resolveLoaderData<TEnv>(
147
147
  }
148
148
 
149
149
  const loaderId = loaderEntry.loader.$$id;
150
+
150
151
  const ttl = resolveTtl(options.ttl, store.defaults, DEFAULT_ROUTE_TTL);
151
152
  const swrWindow = resolveSwrWindow(options.swr, store.defaults);
152
153
  const swr = swrWindow || undefined;
@@ -490,6 +490,7 @@ export function createRSCHandler<
490
490
  // has completed so :post spans are captured in the timeline.
491
491
  // Handler timing parts are always emitted (even without debug metrics)
492
492
  // so non-debug requests still get bootstrap Server-Timing entries.
493
+ const finalizeStart = performance.now();
493
494
  const handlerTimingArr: string[] = variables.__handlerTiming || [];
494
495
  // Preserve any existing Server-Timing set by response routes or middleware
495
496
  const existingTiming = response.headers.get("Server-Timing");
@@ -506,6 +507,14 @@ export function createRSCHandler<
506
507
  const totalStart = earlyMetricsStore
507
508
  ? handlerStart
508
509
  : metricsStore.requestStart;
510
+ // response-finalize measures the gap between render completion and
511
+ // handler return: header assembly, onResponse callbacks, etc.
512
+ appendMetric(
513
+ metricsStore,
514
+ "response-finalize",
515
+ finalizeStart,
516
+ performance.now() - finalizeStart,
517
+ );
509
518
  appendMetric(
510
519
  metricsStore,
511
520
  "handler:total",