Skip to content

Commit 1d04b58

Browse files
bartlomiejuclaude
andcommitted
feat: compile middleware chains at build time for faster request handling
Pre-compiles middleware arrays into single handler functions during app initialization instead of rebuilding the chain on every request. Router now stores compiled handlers (T | null) instead of arrays (T[]). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2fce869 commit 1d04b58

6 files changed

Lines changed: 182 additions & 156 deletions

File tree

packages/fresh/src/app.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@ import { trace } from "@opentelemetry/api";
22

33
import { DENO_DEPLOYMENT_ID } from "@fresh/build-id";
44
import * as colors from "@std/fmt/colors";
5-
import {
6-
type MaybeLazyMiddleware,
7-
type Middleware,
8-
runMiddlewares,
9-
} from "./middlewares/mod.ts";
5+
import type { MaybeLazyMiddleware, Middleware } from "./middlewares/mod.ts";
106
import { Context } from "./context.ts";
117
import { mergePath, type Method, UrlPatternRouter } from "./router.ts";
128
import type { FreshConfig, ResolvedFreshConfig } from "./config.ts";
@@ -387,12 +383,13 @@ export class App<State> {
387383
}
388384
}
389385

390-
const router = new UrlPatternRouter<MaybeLazyMiddleware<State>>();
386+
const router = new UrlPatternRouter<Middleware<State>>();
391387

392-
const { rootMiddlewares } = applyCommands(
388+
const { rootHandler } = applyCommands(
393389
router,
394390
this.#commands,
395391
this.config.basePath,
392+
this.#onError,
396393
);
397394

398395
return async (
@@ -405,7 +402,7 @@ export class App<State> {
405402

406403
const method = req.method.toUpperCase() as Method;
407404
const matched = router.match(method, url);
408-
let { params, pattern, handlers, methodMatch } = matched;
405+
let { params, pattern, item: handler, methodMatch } = matched;
409406

410407
const span = trace.getActiveSpan();
411408
if (span && pattern) {
@@ -416,7 +413,7 @@ export class App<State> {
416413
let next: () => Promise<Response>;
417414

418415
if (pattern === null || !methodMatch) {
419-
handlers = rootMiddlewares;
416+
handler = rootHandler;
420417
}
421418

422419
if (matched.pattern !== null && !methodMatch) {
@@ -442,9 +439,7 @@ export class App<State> {
442439
);
443440

444441
try {
445-
if (handlers.length === 0) return await next();
446-
447-
const result = await runMiddlewares(handlers, ctx, this.#onError);
442+
const result = await (handler !== null ? handler(ctx) : next());
448443
if (!(result instanceof Response)) {
449444
throw new Error(
450445
`Expected a "Response" instance to be returned, but got: ${result}`,

packages/fresh/src/commands.ts

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { setAdditionalStyles } from "./context.ts";
22
import { HttpError } from "./error.ts";
33
import { isHandlerByMethod, type PageResponse } from "./handlers.ts";
4-
import type { MaybeLazyMiddleware, Middleware } from "./middlewares/mod.ts";
4+
import {
5+
compileMiddlewares,
6+
type MaybeLazyMiddleware,
7+
type Middleware,
8+
} from "./middlewares/mod.ts";
59
import { mergePath, type Method, type Router, toRoutePath } from "./router.ts";
610
import {
711
getOrCreateSegment,
@@ -202,22 +206,25 @@ export type Command<State> =
202206
| FsRouteCommand<State>;
203207

204208
export function applyCommands<State>(
205-
router: Router<MaybeLazyMiddleware<State>>,
209+
router: Router<Middleware<State>>,
206210
commands: Command<State>[],
207211
basePath: string,
208-
): { rootMiddlewares: MaybeLazyMiddleware<State>[] } {
212+
onError?: (err: unknown) => void,
213+
): { rootHandler: Middleware<State> } {
209214
const root = newSegment<State>("", null);
210215

211-
applyCommandsInner(root, router, commands, basePath);
216+
applyCommandsInner(root, router, commands, basePath, onError);
212217

213-
return { rootMiddlewares: segmentToMiddlewares(root) };
218+
const rootMiddlewares = segmentToMiddlewares(root);
219+
return { rootHandler: compileMiddlewares(rootMiddlewares, onError) };
214220
}
215221

216222
function applyCommandsInner<State>(
217223
root: Segment<State>,
218-
router: Router<MaybeLazyMiddleware<State>>,
224+
router: Router<Middleware<State>>,
219225
commands: Command<State>[],
220226
basePath: string,
227+
onError?: (err: unknown) => void,
221228
) {
222229
for (let i = 0; i < commands.length; i++) {
223230
const cmd = commands[i];
@@ -290,18 +297,19 @@ function applyCommandsInner<State>(
290297
return renderRoute(ctx, def);
291298
});
292299

300+
const compiled = compileMiddlewares(fns, onError);
293301
if (config === undefined || config.methods === "ALL") {
294-
router.add("GET", routePath, fns);
295-
router.add("DELETE", routePath, fns);
296-
router.add("HEAD", routePath, fns);
297-
router.add("OPTIONS", routePath, fns);
298-
router.add("PATCH", routePath, fns);
299-
router.add("POST", routePath, fns);
300-
router.add("PUT", routePath, fns);
302+
router.add("GET", routePath, compiled);
303+
router.add("DELETE", routePath, compiled);
304+
router.add("HEAD", routePath, compiled);
305+
router.add("OPTIONS", routePath, compiled);
306+
router.add("PATCH", routePath, compiled);
307+
router.add("POST", routePath, compiled);
308+
router.add("PUT", routePath, compiled);
301309
} else if (Array.isArray(config.methods)) {
302310
for (let i = 0; i < config.methods.length; i++) {
303311
const method = config.methods[i];
304-
router.add(method, routePath, fns);
312+
router.add(method, routePath, compiled);
305313
}
306314
}
307315
} else {
@@ -313,17 +321,18 @@ function applyCommandsInner<State>(
313321
false,
314322
));
315323

324+
const compiled = compileMiddlewares(fns, onError);
316325
if (typeof route.handler === "function") {
317-
router.add("GET", routePath, fns);
318-
router.add("DELETE", routePath, fns);
319-
router.add("HEAD", routePath, fns);
320-
router.add("OPTIONS", routePath, fns);
321-
router.add("PATCH", routePath, fns);
322-
router.add("POST", routePath, fns);
323-
router.add("PUT", routePath, fns);
326+
router.add("GET", routePath, compiled);
327+
router.add("DELETE", routePath, compiled);
328+
router.add("HEAD", routePath, compiled);
329+
router.add("OPTIONS", routePath, compiled);
330+
router.add("PATCH", routePath, compiled);
331+
router.add("POST", routePath, compiled);
332+
router.add("PUT", routePath, compiled);
324333
} else if (isHandlerByMethod(route.handler!)) {
325334
for (const method of Object.keys(route.handler)) {
326-
router.add(method as Method, routePath, fns);
335+
router.add(method as Method, routePath, compiled);
327336
}
328337
}
329338
}
@@ -340,25 +349,26 @@ function applyCommandsInner<State>(
340349

341350
result.push(...fns);
342351

352+
const compiled = compileMiddlewares(result, onError);
343353
const resPath = toRoutePath(mergePath(basePath, pattern, false));
344354
if (method === "ALL") {
345-
router.add("GET", resPath, result);
346-
router.add("DELETE", resPath, result);
347-
router.add("HEAD", resPath, result);
348-
router.add("OPTIONS", resPath, result);
349-
router.add("PATCH", resPath, result);
350-
router.add("POST", resPath, result);
351-
router.add("PUT", resPath, result);
355+
router.add("GET", resPath, compiled);
356+
router.add("DELETE", resPath, compiled);
357+
router.add("HEAD", resPath, compiled);
358+
router.add("OPTIONS", resPath, compiled);
359+
router.add("PATCH", resPath, compiled);
360+
router.add("POST", resPath, compiled);
361+
router.add("PUT", resPath, compiled);
352362
} else {
353-
router.add(method, resPath, result);
363+
router.add(method, resPath, compiled);
354364
}
355365

356366
break;
357367
}
358368
case CommandType.FsRoute: {
359369
const items = cmd.getItems();
360370
const base = mergePath(basePath, cmd.pattern, true);
361-
applyCommandsInner(root, router, items, base);
371+
applyCommandsInner(root, router, items, base, onError);
362372
break;
363373
}
364374
default:

packages/fresh/src/middlewares/mod.ts

Lines changed: 58 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -88,56 +88,69 @@ export type MaybeLazyMiddleware<State> = (
8888
ctx: Context<State>,
8989
) => Response | Promise<Response | Middleware<State>>;
9090

91-
export async function runMiddlewares<State>(
91+
export function compileMiddlewares<State>(
9292
middlewares: MaybeLazyMiddleware<State>[],
93-
ctx: Context<State>,
9493
onError?: (err: unknown) => void,
95-
): Promise<Response> {
96-
return await tracer.startActiveSpan("middlewares", {
97-
attributes: { "fresh.middleware.count": middlewares.length },
98-
}, async (span) => {
99-
try {
100-
let fn = ctx.next;
101-
let i = middlewares.length;
102-
while (i--) {
103-
const local = fn;
104-
let next = middlewares[i];
105-
const idx = i;
106-
fn = async () => {
107-
const internals = getInternals(ctx);
108-
const { app: prevApp, layouts: prevLayouts } = internals;
94+
): Middleware<State> {
95+
if (middlewares.length === 0) return (ctx) => ctx.next();
96+
97+
// Each step is a function that takes (ctx, tail) where tail is the
98+
// original ctx.next captured at invocation time. This avoids the
99+
// infinite recursion bug where the compiled tail reads an already-
100+
// overwritten ctx.next, and is safe under concurrent requests.
101+
type ChainFn = (
102+
ctx: Context<State>,
103+
tail: () => Promise<Response>,
104+
) => Response | Promise<Response>;
105+
106+
let chain: ChainFn = (_ctx, tail) => tail();
109107

110-
ctx.next = local;
111-
try {
112-
const result = await next(ctx);
113-
if (typeof result === "function") {
114-
middlewares[idx] = result;
115-
next = result;
116-
return await result(ctx);
117-
}
108+
for (let i = middlewares.length - 1; i >= 0; i--) {
109+
const nextChain = chain;
110+
let middleware = middlewares[i];
111+
chain = async (ctx, tail) => {
112+
const internals = getInternals(ctx);
113+
const { app: prevApp, layouts: prevLayouts } = internals;
118114

119-
return result;
120-
} catch (err) {
121-
if (ctx.error !== err) {
122-
ctx.error = err;
115+
ctx.next = () => Promise.resolve(nextChain(ctx, tail));
116+
try {
117+
const result = await middleware(ctx);
118+
if (typeof result === "function") {
119+
middleware = result;
120+
return await result(ctx);
121+
}
123122

124-
if (onError !== undefined) {
125-
onError(err);
126-
}
127-
}
128-
throw err;
129-
} finally {
130-
internals.app = prevApp;
131-
internals.layouts = prevLayouts;
123+
return result;
124+
} catch (err) {
125+
if (ctx.error !== err) {
126+
ctx.error = err;
127+
128+
if (onError !== undefined) {
129+
onError(err);
132130
}
133-
};
131+
}
132+
throw err;
133+
} finally {
134+
internals.app = prevApp;
135+
internals.layouts = prevLayouts;
136+
}
137+
};
138+
}
139+
140+
const count = middlewares.length;
141+
return (ctx) => {
142+
const tail = ctx.next;
143+
return tracer.startActiveSpan("middlewares", {
144+
attributes: { "fresh.middleware.count": count },
145+
}, async (span) => {
146+
try {
147+
return await chain(ctx, tail);
148+
} catch (err) {
149+
recordSpanError(span, err);
150+
throw err;
151+
} finally {
152+
span.end();
134153
}
135-
return await fn();
136-
} catch (err) {
137-
recordSpanError(span, err);
138-
throw err;
139-
} finally {
140-
span.end();
141-
}
142-
});
154+
});
155+
};
143156
}

0 commit comments

Comments
 (0)