Skip to content

Commit e42432e

Browse files
logaretmclaude
andcommitted
refactor: replace wrapFindRouteWithTracing with wrapHandlerWithTracing
Export a simpler `wrapHandlerWithTracing` that wraps a single EventHandler at init/codegen time instead of wrapping the entire ~findRoute function at runtime. This eliminates per-request overhead — tracing is applied once during build, not on every route lookup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6b86b99 commit e42432e

2 files changed

Lines changed: 43 additions & 137 deletions

File tree

src/tracing.ts

Lines changed: 19 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import type { H3Event } from "./event.ts";
22
import type { H3, H3Core } from "./h3.ts";
3-
import {
4-
type H3Plugin,
5-
type H3Route,
6-
type MatchedRoute,
7-
type MiddlewareOptions,
8-
} from "./types/h3.ts";
3+
import { type H3Plugin, type H3Route, type MiddlewareOptions } from "./types/h3.ts";
94
import type { EventHandler, Middleware } from "./types/handler.ts";
105

116
/**
@@ -148,67 +143,30 @@ export function tracingPlugin(traceOpts?: TracingPluginOptions): H3Plugin {
148143
};
149144
}
150145

151-
type FindRouteFunction = (event: H3Event) => MatchedRoute<H3Route> | void;
152-
153146
/**
154-
* Wraps a `~findRoute` function so that returned route handlers and middleware
155-
* are traced via the `h3.request` diagnostics channel. Intended for frameworks
156-
* (e.g. nitro) that resolve routes at request time without pushing them into
157-
* `h3["~routes"]`.
147+
* Wraps an event handler so its execution is traced via the `h3.request`
148+
* diagnostics channel with `type: "route"`. Intended to be called once per
149+
* handler at initialization time (e.g. during codegen or module load), not
150+
* per request.
158151
*
159-
* Returns the original function unchanged when `diagnostics_channel` is not
160-
* available.
152+
* Returns the handler unchanged when `diagnostics_channel` is unavailable
153+
* or the handler is already traced.
161154
*/
162-
export function wrapFindRouteWithTracing(
163-
findRoute: FindRouteFunction,
164-
traceOpts?: TracingPluginOptions,
165-
): FindRouteFunction {
155+
export function wrapHandlerWithTracing(handler: EventHandler): EventHandler {
166156
const { tracingChannel } = globalThis.process?.getBuiltinModule?.("diagnostics_channel") ?? {};
167-
168157
if (!tracingChannel) {
169-
return findRoute;
170-
}
171-
172-
const channel = tracingChannel("h3.request");
173-
174-
function wrapHandler(handler: MaybeTracedEventHandler): EventHandler {
175-
if (handler.__traced__ || traceOpts?.traceRoutes === false) {
176-
return handler;
177-
}
178-
const wrapped: MaybeTracedEventHandler = (...args) => {
179-
return channel.tracePromise(async () => handler(...args), {
180-
event: args[0],
181-
type: "route",
182-
} satisfies TracingRequestEvent);
183-
};
184-
wrapped.__traced__ = true;
185-
return wrapped;
158+
return handler;
186159
}
187-
188-
function wrapMiddleware(middleware: MaybeTracedMiddleware): Middleware {
189-
if (middleware.__traced__ || traceOpts?.traceMiddleware === false) {
190-
return middleware;
191-
}
192-
const wrapped: MaybeTracedMiddleware = (...args) => {
193-
return channel.tracePromise(async () => middleware(...args), {
194-
event: args[0],
195-
type: "middleware",
196-
} satisfies TracingRequestEvent);
197-
};
198-
wrapped.__traced__ = true;
199-
return wrapped;
160+
if ((handler as MaybeTracedEventHandler).__traced__) {
161+
return handler;
200162
}
201-
202-
return (event: H3Event) => {
203-
const route = findRoute(event);
204-
if (route?.data.handler) {
205-
route.data.handler = wrapHandler(route.data.handler);
206-
}
207-
if (route?.data.middleware) {
208-
for (let i = 0; i < route.data.middleware.length; i++) {
209-
route.data.middleware[i] = wrapMiddleware(route.data.middleware[i]);
210-
}
211-
}
212-
return route;
163+
const channel = tracingChannel("h3.request");
164+
const wrapped: MaybeTracedEventHandler = (...args) => {
165+
return channel.tracePromise(async () => handler(...args), {
166+
event: args[0],
167+
type: "route",
168+
} satisfies TracingRequestEvent);
213169
};
170+
wrapped.__traced__ = true;
171+
return wrapped;
214172
}

test/tracing.test.ts

Lines changed: 24 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,125 +1158,73 @@ describe("tracing channels for H3Core instances", () => {
11581158
});
11591159
});
11601160

1161-
describe("wrapFindRouteWithTracing", () => {
1162-
it("wraps route handlers returned by findRoute", async () => {
1161+
describe("wrapHandlerWithTracing", () => {
1162+
it("wraps a handler so route traces are emitted", async () => {
11631163
const listener = createTracingListener();
11641164
const { H3Core } = await import("../src/h3.ts");
1165-
const { wrapFindRouteWithTracing } = await import("../src/tracing.ts");
1165+
const { wrapHandlerWithTracing } = await import("../src/tracing.ts");
11661166
const { H3Event } = await import("../src/event.ts");
11671167

11681168
try {
11691169
const app = new H3Core();
1170-
const routeHandler = () => "file-based response";
1170+
const routeHandler = wrapHandlerWithTracing(() => "traced response");
11711171

1172-
const baseFindRoute = (event: any) => {
1173-
if (event.url.pathname === "/file-route" && event.req.method === "GET") {
1174-
return {
1175-
data: { method: "GET", route: "/file-route", handler: routeHandler },
1176-
params: {},
1177-
};
1172+
app["~findRoute"] = (event: any) => {
1173+
if (event.url.pathname === "/test" && event.req.method === "GET") {
1174+
return { data: { method: "GET", route: "/test", handler: routeHandler }, params: {} };
11781175
}
11791176
return undefined;
11801177
};
11811178

1182-
app["~findRoute"] = wrapFindRouteWithTracing(baseFindRoute as any);
1183-
1184-
const request = new Request("http://localhost/file-route", { method: "GET" });
1179+
const request = new Request("http://localhost/test", { method: "GET" });
11851180
const event = new H3Event(request, undefined, app as any);
11861181

11871182
await app.handler(event);
11881183
await new Promise((resolve) => setTimeout(resolve, 10));
11891184

11901185
const routeEvents = listener.events.filter((e) => e.asyncStart?.data.type === "route");
1191-
expect(routeEvents.some((e) => e.asyncStart?.data.event.url.pathname === "/file-route")).toBe(
1192-
true,
1193-
);
1186+
expect(routeEvents.some((e) => e.asyncStart?.data.event.url.pathname === "/test")).toBe(true);
11941187
} finally {
11951188
listener.cleanup();
11961189
}
11971190
});
11981191

1199-
it("does not double-wrap handlers across repeated calls", async () => {
1200-
const listener = createTracingListener();
1201-
const { H3Core } = await import("../src/h3.ts");
1202-
const { wrapFindRouteWithTracing } = await import("../src/tracing.ts");
1203-
const { H3Event } = await import("../src/event.ts");
1204-
1205-
try {
1206-
const app = new H3Core();
1207-
const routeHandler = () => "ok";
1208-
1209-
const cachedRoute = {
1210-
data: { method: "GET" as const, route: "/cached", handler: routeHandler },
1211-
params: {},
1212-
};
1213-
1214-
app["~findRoute"] = wrapFindRouteWithTracing((() => cachedRoute) as any);
1215-
1216-
const run = async () => {
1217-
const request = new Request("http://localhost/cached", { method: "GET" });
1218-
const event = new H3Event(request, undefined, app as any);
1219-
await app.handler(event);
1220-
};
1221-
1222-
await run();
1223-
const wrappedHandler = cachedRoute.data.handler;
1224-
expect((wrappedHandler as any).__traced__).toBe(true);
1225-
1226-
await run();
1227-
await run();
1192+
it("is idempotent — wrapping twice returns the same wrapper", async () => {
1193+
const { wrapHandlerWithTracing } = await import("../src/tracing.ts");
12281194

1229-
expect(cachedRoute.data.handler).toBe(wrappedHandler);
1195+
const handler = () => "ok";
1196+
const wrapped = wrapHandlerWithTracing(handler);
1197+
const doubleWrapped = wrapHandlerWithTracing(wrapped);
12301198

1231-
await new Promise((resolve) => setTimeout(resolve, 10));
1232-
1233-
const routeAsyncStarts = listener.events.filter((e) => e.asyncStart?.data.type === "route");
1234-
expect(routeAsyncStarts.length).toBe(3);
1235-
} finally {
1236-
listener.cleanup();
1237-
}
1199+
expect(doubleWrapped).toBe(wrapped);
1200+
expect((wrapped as any).__traced__).toBe(true);
12381201
});
12391202

1240-
it("mutates cached route.data.middleware in place without reallocating", async () => {
1203+
it("emits exactly one trace per request on a pre-wrapped handler", async () => {
12411204
const listener = createTracingListener();
12421205
const { H3Core } = await import("../src/h3.ts");
1243-
const { wrapFindRouteWithTracing } = await import("../src/tracing.ts");
1206+
const { wrapHandlerWithTracing } = await import("../src/tracing.ts");
12441207
const { H3Event } = await import("../src/event.ts");
12451208

12461209
try {
12471210
const app = new H3Core();
1248-
const mw = (event: any) => {
1249-
event.context.mw = true;
1250-
};
1251-
const cachedRoute = {
1252-
data: {
1253-
method: "GET" as const,
1254-
route: "/cached",
1255-
handler: () => "ok",
1256-
middleware: [mw],
1257-
},
1258-
params: {},
1259-
};
1211+
const routeHandler = wrapHandlerWithTracing(() => "ok");
12601212

1261-
app["~findRoute"] = wrapFindRouteWithTracing((() => cachedRoute) as any);
1213+
app["~findRoute"] = () => ({
1214+
data: { method: "GET" as const, route: "/test", handler: routeHandler },
1215+
params: {},
1216+
});
12621217

12631218
const run = async () => {
1264-
const request = new Request("http://localhost/cached", { method: "GET" });
1219+
const request = new Request("http://localhost/test", { method: "GET" });
12651220
const event = new H3Event(request, undefined, app as any);
12661221
await app.handler(event);
12671222
};
12681223

12691224
await run();
1270-
const middlewareArray = cachedRoute.data.middleware;
1271-
const wrappedMw = middlewareArray[0];
1272-
expect((wrappedMw as any).__traced__).toBe(true);
1273-
12741225
await run();
12751226
await run();
12761227

1277-
expect(cachedRoute.data.middleware).toBe(middlewareArray);
1278-
expect(cachedRoute.data.middleware[0]).toBe(wrappedMw);
1279-
12801228
await new Promise((resolve) => setTimeout(resolve, 10));
12811229

12821230
const routeAsyncStarts = listener.events.filter((e) => e.asyncStart?.data.type === "route");

0 commit comments

Comments
 (0)