@rangojs/router 0.0.0-experimental.23 → 0.0.0-experimental.25
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/vite/index.js +1 -1
- package/package.json +1 -1
- package/src/router/match-result.ts +1 -2
- package/src/router/metrics.ts +36 -0
- package/src/router/middleware.ts +49 -1
- package/src/router/router-interfaces.ts +6 -0
- package/src/router.ts +13 -4
- package/src/rsc/handler.ts +5 -0
- package/src/rsc/rsc-rendering.ts +36 -6
- package/src/rsc/server-action.ts +21 -2
- package/src/server/request-context.ts +6 -1
package/dist/vite/index.js
CHANGED
|
@@ -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.
|
|
1748
|
+
version: "0.0.0-experimental.25",
|
|
1749
1749
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
1750
1750
|
keywords: [
|
|
1751
1751
|
"react",
|
package/package.json
CHANGED
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
*/
|
|
109
109
|
import type { MatchResult, ResolvedSegment } from "../types.js";
|
|
110
110
|
import type { MatchContext, MatchPipelineState } from "./match-context.js";
|
|
111
|
-
import { generateServerTiming
|
|
111
|
+
import { generateServerTiming } from "./metrics.js";
|
|
112
112
|
import { debugLog } from "./logging.js";
|
|
113
113
|
|
|
114
114
|
/**
|
|
@@ -189,7 +189,6 @@ export function buildMatchResult<TEnv>(
|
|
|
189
189
|
// Output metrics if enabled
|
|
190
190
|
let serverTiming: string | undefined;
|
|
191
191
|
if (ctx.metricsStore) {
|
|
192
|
-
logMetrics(ctx.request.method, ctx.pathname, ctx.metricsStore);
|
|
193
192
|
serverTiming = generateServerTiming(ctx.metricsStore);
|
|
194
193
|
}
|
|
195
194
|
|
package/src/router/metrics.ts
CHANGED
|
@@ -69,6 +69,42 @@ export function createMetricsStore(
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Append a metric to the request store using an absolute start timestamp.
|
|
74
|
+
*/
|
|
75
|
+
export function appendMetric(
|
|
76
|
+
metricsStore: MetricsStore | undefined,
|
|
77
|
+
label: string,
|
|
78
|
+
start: number,
|
|
79
|
+
duration: number,
|
|
80
|
+
depth?: number,
|
|
81
|
+
): void {
|
|
82
|
+
if (!metricsStore) return;
|
|
83
|
+
metricsStore.metrics.push({
|
|
84
|
+
label,
|
|
85
|
+
duration,
|
|
86
|
+
startTime: start - metricsStore.requestStart,
|
|
87
|
+
depth,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Log the current request metrics and return the corresponding Server-Timing value.
|
|
93
|
+
* Falls back to an existing header value when no metrics store is active.
|
|
94
|
+
*/
|
|
95
|
+
export function buildMetricsTiming(
|
|
96
|
+
method: string,
|
|
97
|
+
pathname: string,
|
|
98
|
+
metricsStore: MetricsStore | undefined,
|
|
99
|
+
fallback?: string,
|
|
100
|
+
): string | undefined {
|
|
101
|
+
if (!metricsStore) {
|
|
102
|
+
return fallback;
|
|
103
|
+
}
|
|
104
|
+
logMetrics(method, pathname, metricsStore);
|
|
105
|
+
return generateServerTiming(metricsStore) || undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
72
108
|
/**
|
|
73
109
|
* Log metrics to console in a formatted way.
|
|
74
110
|
* Uses a shared-axis timeline so overlapping work stays visible.
|
package/src/router/middleware.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
} from "./middleware-types.js";
|
|
21
21
|
import { _getRequestContext } from "../server/request-context.js";
|
|
22
22
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
23
|
+
import { appendMetric, createMetricsStore } from "./metrics.js";
|
|
23
24
|
|
|
24
25
|
// Re-export types and cookie utilities for backward compatibility
|
|
25
26
|
export type {
|
|
@@ -49,6 +50,29 @@ function warnCtxSetBeforeRedirect(handler: Function): void {
|
|
|
49
50
|
);
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
const MIDDLEWARE_METRIC_DEPTH = 1;
|
|
54
|
+
|
|
55
|
+
function getMiddlewareMetricBase<TEnv>(
|
|
56
|
+
entry: MiddlewareEntry<TEnv>,
|
|
57
|
+
ordinal: number,
|
|
58
|
+
): string {
|
|
59
|
+
const handlerName = entry.handler.name?.trim();
|
|
60
|
+
const scope = entry.pattern ?? "*";
|
|
61
|
+
|
|
62
|
+
if (handlerName) {
|
|
63
|
+
return `${handlerName}@${scope}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return `${scope}#${ordinal + 1}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getMiddlewareMetricLabel<TEnv>(
|
|
70
|
+
entry: MiddlewareEntry<TEnv>,
|
|
71
|
+
ordinal: number,
|
|
72
|
+
): string {
|
|
73
|
+
return `middleware:${getMiddlewareMetricBase(entry, ordinal)}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
52
76
|
/** Reset W5 deduplication state (for tests only). */
|
|
53
77
|
export function _resetW5Warnings(): void {
|
|
54
78
|
warnedRedirectMiddleware = new WeakSet();
|
|
@@ -232,6 +256,7 @@ export function createMiddlewareContext<TEnv>(
|
|
|
232
256
|
const reqCtx = _getRequestContext();
|
|
233
257
|
if (reqCtx) {
|
|
234
258
|
reqCtx._debugPerformance = true;
|
|
259
|
+
reqCtx._metricsStore ??= createMetricsStore(true);
|
|
235
260
|
}
|
|
236
261
|
},
|
|
237
262
|
};
|
|
@@ -346,6 +371,7 @@ export async function executeMiddleware<TEnv>(
|
|
|
346
371
|
return responseHolder.response;
|
|
347
372
|
}
|
|
348
373
|
|
|
374
|
+
const middlewareOrdinal = index;
|
|
349
375
|
const { entry, params } = middlewares[index++];
|
|
350
376
|
const ctx = createMiddlewareContext(
|
|
351
377
|
request,
|
|
@@ -355,6 +381,20 @@ export async function executeMiddleware<TEnv>(
|
|
|
355
381
|
responseHolder,
|
|
356
382
|
reverse,
|
|
357
383
|
);
|
|
384
|
+
const metricStart = performance.now();
|
|
385
|
+
let middlewareFinished = false;
|
|
386
|
+
const finishMiddleware = () => {
|
|
387
|
+
if (!middlewareFinished) {
|
|
388
|
+
middlewareFinished = true;
|
|
389
|
+
appendMetric(
|
|
390
|
+
_getRequestContext()?._metricsStore,
|
|
391
|
+
getMiddlewareMetricLabel(entry, middlewareOrdinal),
|
|
392
|
+
metricStart,
|
|
393
|
+
performance.now() - metricStart,
|
|
394
|
+
MIDDLEWARE_METRIC_DEPTH,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
};
|
|
358
398
|
|
|
359
399
|
// Track if next() was called and capture its Promise.
|
|
360
400
|
// Guard against double-calling: a second call would re-enter the
|
|
@@ -366,6 +406,7 @@ export async function executeMiddleware<TEnv>(
|
|
|
366
406
|
`[@rangojs/router] Middleware called next() more than once.`,
|
|
367
407
|
);
|
|
368
408
|
}
|
|
409
|
+
finishMiddleware();
|
|
369
410
|
nextPromise = next();
|
|
370
411
|
return nextPromise;
|
|
371
412
|
};
|
|
@@ -380,7 +421,14 @@ export async function executeMiddleware<TEnv>(
|
|
|
380
421
|
}) as typeof ctx.set;
|
|
381
422
|
}
|
|
382
423
|
|
|
383
|
-
|
|
424
|
+
let result: Response | void;
|
|
425
|
+
try {
|
|
426
|
+
result = await entry.handler(ctx, wrappedNext);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
finishMiddleware();
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
finishMiddleware();
|
|
384
432
|
|
|
385
433
|
// Explicit return takes precedence (middleware short-circuit).
|
|
386
434
|
// Merge stub headers (from ctx.header before this point) and
|
|
@@ -269,6 +269,12 @@ export interface RSCRouterInternal<
|
|
|
269
269
|
*/
|
|
270
270
|
readonly warmupEnabled: boolean;
|
|
271
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Whether router-wide performance debugging is enabled.
|
|
274
|
+
* Used by the request handler to create metrics before middleware runs.
|
|
275
|
+
*/
|
|
276
|
+
readonly debugPerformance?: boolean;
|
|
277
|
+
|
|
272
278
|
/**
|
|
273
279
|
* Whether ?__debug_manifest is allowed in production.
|
|
274
280
|
* Always enabled in development.
|
package/src/router.ts
CHANGED
|
@@ -359,10 +359,16 @@ export function createRouter<TEnv = any>(
|
|
|
359
359
|
|
|
360
360
|
// Wrapper to pass debugPerformance to external createMetricsStore.
|
|
361
361
|
// Also checks per-request flag set by ctx.debugPerformance() in middleware.
|
|
362
|
-
const getMetricsStore = () =>
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
);
|
|
362
|
+
const getMetricsStore = () => {
|
|
363
|
+
const reqCtx = _getRequestContext();
|
|
364
|
+
const enabled = debugPerformance || !!reqCtx?._debugPerformance;
|
|
365
|
+
if (!enabled) return undefined;
|
|
366
|
+
if (!reqCtx) {
|
|
367
|
+
return createMetricsStore(true);
|
|
368
|
+
}
|
|
369
|
+
reqCtx._metricsStore ??= createMetricsStore(true);
|
|
370
|
+
return reqCtx._metricsStore;
|
|
371
|
+
};
|
|
366
372
|
|
|
367
373
|
// Wrapper to pass defaults to error/notFound boundary finders
|
|
368
374
|
const findNearestErrorBoundary = (entry: EntryData | null) =>
|
|
@@ -879,6 +885,9 @@ export function createRouter<TEnv = any>(
|
|
|
879
885
|
// Expose warmup enabled flag for handler and client
|
|
880
886
|
warmupEnabled,
|
|
881
887
|
|
|
888
|
+
// Expose router-wide performance debugging for request-level metrics setup
|
|
889
|
+
debugPerformance,
|
|
890
|
+
|
|
882
891
|
// Expose debug manifest flag for handler
|
|
883
892
|
allowDebugManifest: allowDebugManifestOption,
|
|
884
893
|
|
package/src/rsc/handler.ts
CHANGED
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
createDefaultTimeoutResponse,
|
|
67
67
|
type TimeoutPhase,
|
|
68
68
|
} from "../router/timeout.js";
|
|
69
|
+
import { createMetricsStore } from "../router/metrics.js";
|
|
69
70
|
|
|
70
71
|
/**
|
|
71
72
|
* Create an RSC request handler.
|
|
@@ -381,6 +382,10 @@ export function createRSCHandler<
|
|
|
381
382
|
executionContext: executionCtx,
|
|
382
383
|
themeConfig: router.themeConfig,
|
|
383
384
|
});
|
|
385
|
+
if (router.debugPerformance) {
|
|
386
|
+
requestContext._debugPerformance = true;
|
|
387
|
+
requestContext._metricsStore ??= createMetricsStore(true);
|
|
388
|
+
}
|
|
384
389
|
// Wire background error reporting so "use cache" and other subsystems
|
|
385
390
|
// can surface non-fatal errors through the router's onError callback.
|
|
386
391
|
requestContext._reportBackgroundError = (
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getLocationState,
|
|
13
13
|
} from "../server/request-context.js";
|
|
14
14
|
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
15
|
+
import { appendMetric, buildMetricsTiming } from "../router/metrics.js";
|
|
15
16
|
import type { RscPayload } from "./types.js";
|
|
16
17
|
import {
|
|
17
18
|
createResponseWithMergedHeaders,
|
|
@@ -166,10 +167,20 @@ export async function handleRscRendering<TEnv>(
|
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
|
|
170
|
+
const metricsStore = reqCtx._metricsStore;
|
|
171
|
+
const renderStart = performance.now();
|
|
172
|
+
|
|
169
173
|
// Serialize to RSC stream
|
|
170
174
|
const rscSerializeStart = performance.now();
|
|
171
175
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
|
|
172
176
|
const rscSerializeDur = performance.now() - rscSerializeStart;
|
|
177
|
+
// This measures synchronous stream creation, not end-to-end stream consumption.
|
|
178
|
+
appendMetric(
|
|
179
|
+
metricsStore,
|
|
180
|
+
"rsc-serialize",
|
|
181
|
+
rscSerializeStart,
|
|
182
|
+
rscSerializeDur,
|
|
183
|
+
);
|
|
173
184
|
|
|
174
185
|
// Determine if this is an RSC request or HTML request.
|
|
175
186
|
// Partial requests (_rsc_partial) are always RSC -- they come from client-side
|
|
@@ -183,12 +194,19 @@ export async function handleRscRendering<TEnv>(
|
|
|
183
194
|
|
|
184
195
|
// Build complete Server-Timing: handler phases + match/manifest + RSC serialize
|
|
185
196
|
const timingParts: string[] = [...handlerTimingArr];
|
|
186
|
-
if (serverTiming) {
|
|
187
|
-
timingParts.push(serverTiming);
|
|
188
|
-
}
|
|
189
|
-
timingParts.push(`rsc-serialize;dur=${rscSerializeDur.toFixed(2)}`);
|
|
190
197
|
|
|
191
198
|
if (isRscRequest) {
|
|
199
|
+
const renderDur = performance.now() - renderStart;
|
|
200
|
+
appendMetric(metricsStore, "render:total", renderStart, renderDur);
|
|
201
|
+
const metricsTiming = buildMetricsTiming(
|
|
202
|
+
request.method,
|
|
203
|
+
url.pathname,
|
|
204
|
+
metricsStore,
|
|
205
|
+
serverTiming,
|
|
206
|
+
);
|
|
207
|
+
if (metricsTiming) {
|
|
208
|
+
timingParts.push(metricsTiming);
|
|
209
|
+
}
|
|
192
210
|
const fullTiming = timingParts.join(", ");
|
|
193
211
|
const rscHeaders: Record<string, string> = {
|
|
194
212
|
"content-type": "text/x-component;charset=utf-8",
|
|
@@ -220,7 +238,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
220
238
|
ctx.resolveStreamMode(request, env, url),
|
|
221
239
|
]);
|
|
222
240
|
const ssrSetupDur = performance.now() - ssrSetupStart;
|
|
223
|
-
|
|
241
|
+
appendMetric(metricsStore, "ssr-setup", ssrSetupStart, ssrSetupDur);
|
|
224
242
|
|
|
225
243
|
const ssrRenderStart = performance.now();
|
|
226
244
|
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
@@ -228,7 +246,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
228
246
|
streamMode,
|
|
229
247
|
});
|
|
230
248
|
const ssrRenderDur = performance.now() - ssrRenderStart;
|
|
231
|
-
|
|
249
|
+
appendMetric(metricsStore, "ssr-render-html", ssrRenderStart, ssrRenderDur);
|
|
232
250
|
|
|
233
251
|
// Add total handler duration
|
|
234
252
|
if (handlerStart) {
|
|
@@ -236,6 +254,18 @@ export async function handleRscRendering<TEnv>(
|
|
|
236
254
|
timingParts.push(`handler-total;dur=${totalHandler.toFixed(2)}`);
|
|
237
255
|
}
|
|
238
256
|
|
|
257
|
+
const renderDur = performance.now() - renderStart;
|
|
258
|
+
appendMetric(metricsStore, "render:total", renderStart, renderDur);
|
|
259
|
+
const metricsTiming = buildMetricsTiming(
|
|
260
|
+
request.method,
|
|
261
|
+
url.pathname,
|
|
262
|
+
metricsStore,
|
|
263
|
+
serverTiming,
|
|
264
|
+
);
|
|
265
|
+
if (metricsTiming) {
|
|
266
|
+
timingParts.push(metricsTiming);
|
|
267
|
+
}
|
|
268
|
+
|
|
239
269
|
const fullTiming = timingParts.join(", ");
|
|
240
270
|
const htmlHeaders: Record<string, string> = {
|
|
241
271
|
"content-type": "text/html;charset=utf-8",
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
getLocationState,
|
|
22
22
|
} from "../server/request-context.js";
|
|
23
23
|
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
24
|
+
import { appendMetric, buildMetricsTiming } from "../router/metrics.js";
|
|
24
25
|
import type { RscPayload } from "./types.js";
|
|
25
26
|
import {
|
|
26
27
|
hasBodyContent,
|
|
@@ -274,6 +275,8 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
274
275
|
): Promise<Response> {
|
|
275
276
|
const { returnValue, actionStatus, temporaryReferences, actionContext } =
|
|
276
277
|
continuation;
|
|
278
|
+
const reqCtx = requireRequestContext();
|
|
279
|
+
const metricsStore = reqCtx._metricsStore;
|
|
277
280
|
|
|
278
281
|
const matchResult = await ctx.router.matchPartial(
|
|
279
282
|
request,
|
|
@@ -326,15 +329,31 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
326
329
|
|
|
327
330
|
attachLocationState(payload);
|
|
328
331
|
|
|
332
|
+
const renderStart = performance.now();
|
|
329
333
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
330
334
|
temporaryReferences,
|
|
331
335
|
});
|
|
336
|
+
const rscSerializeDur = performance.now() - renderStart;
|
|
337
|
+
// This measures synchronous stream creation, not end-to-end stream consumption.
|
|
338
|
+
appendMetric(metricsStore, "rsc-serialize", renderStart, rscSerializeDur);
|
|
339
|
+
appendMetric(
|
|
340
|
+
metricsStore,
|
|
341
|
+
"render:total",
|
|
342
|
+
renderStart,
|
|
343
|
+
performance.now() - renderStart,
|
|
344
|
+
);
|
|
332
345
|
|
|
333
346
|
const actionHeaders: Record<string, string> = {
|
|
334
347
|
"content-type": "text/x-component;charset=utf-8",
|
|
335
348
|
};
|
|
336
|
-
|
|
337
|
-
|
|
349
|
+
const metricsTiming = buildMetricsTiming(
|
|
350
|
+
request.method,
|
|
351
|
+
url.pathname,
|
|
352
|
+
metricsStore,
|
|
353
|
+
serverTiming,
|
|
354
|
+
);
|
|
355
|
+
if (metricsTiming) {
|
|
356
|
+
actionHeaders["Server-Timing"] = metricsTiming;
|
|
338
357
|
}
|
|
339
358
|
|
|
340
359
|
return createResponseWithMergedHeaders(rscStream, {
|
|
@@ -23,7 +23,7 @@ import type { Handle } from "../handle.js";
|
|
|
23
23
|
import { type ContextVar, contextGet, contextSet } from "../context-var.js";
|
|
24
24
|
import { createHandleStore, type HandleStore } from "./handle-store.js";
|
|
25
25
|
import { isHandle } from "../handle.js";
|
|
26
|
-
import { track } from "./context.js";
|
|
26
|
+
import { track, type MetricsStore } from "./context.js";
|
|
27
27
|
import { getFetchableLoader } from "./fetchable-loader-store.js";
|
|
28
28
|
import type { SegmentCacheStore } from "../cache/types.js";
|
|
29
29
|
import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
|
|
@@ -269,6 +269,9 @@ export interface RequestContext<
|
|
|
269
269
|
|
|
270
270
|
/** @internal Per-request debug performance override (set via ctx.debugPerformance()) */
|
|
271
271
|
_debugPerformance?: boolean;
|
|
272
|
+
|
|
273
|
+
/** @internal Request-scoped performance metrics store */
|
|
274
|
+
_metricsStore?: MetricsStore;
|
|
272
275
|
}
|
|
273
276
|
|
|
274
277
|
/**
|
|
@@ -297,6 +300,7 @@ export type PublicRequestContext<
|
|
|
297
300
|
| "_reportedErrors"
|
|
298
301
|
| "_reportBackgroundError"
|
|
299
302
|
| "_debugPerformance"
|
|
303
|
+
| "_metricsStore"
|
|
300
304
|
>;
|
|
301
305
|
|
|
302
306
|
// AsyncLocalStorage instance for request context
|
|
@@ -681,6 +685,7 @@ export function createRequestContext<TEnv>(
|
|
|
681
685
|
_locationState: undefined,
|
|
682
686
|
|
|
683
687
|
_reportedErrors: new WeakSet<object>(),
|
|
688
|
+
_metricsStore: undefined,
|
|
684
689
|
|
|
685
690
|
reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
|
|
686
691
|
};
|