Skip to content

Commit 49f5cff

Browse files
feat: speed up middleware matching
This increases raw server performance on fresh.deno.dev by 20%
1 parent c279efa commit 49f5cff

5 files changed

Lines changed: 122 additions & 129 deletions

File tree

src/app.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { trace } from "@opentelemetry/api";
44

55
import { DENO_DEPLOYMENT_ID } from "./runtime/build_id.ts";
66
import * as colors from "@std/fmt/colors";
7-
import { type MiddlewareFn, runMiddlewares } from "./middlewares/mod.ts";
7+
import { compileMiddlewares, type MiddlewareFn } from "./middlewares/mod.ts";
88
import { Context, type ServerIslandRegistry } from "./context.ts";
99
import {
1010
mergePath,
@@ -46,6 +46,10 @@ const DEFAULT_RENDER = <State>(): Promise<PageResponse<State>> =>
4646
// deno-lint-ignore no-explicit-any
4747
Promise.resolve({ data: {} as any });
4848

49+
// deno-lint-ignore no-explicit-any
50+
const PASS_THROUGH: MiddlewareFn<any> = (ctx) => {
51+
return ctx.next();
52+
};
4953
const DEFAULT_NOT_FOUND = (): Promise<Response> => {
5054
throw new HttpError(404);
5155
};
@@ -418,10 +422,25 @@ export class App<State> {
418422

419423
for (let i = 0; i < this.#routeDefs.length; i++) {
420424
const route = this.#routeDefs[i];
421-
this.#router.add(route.method, route.pattern, route.fns, route.unshift);
425+
426+
const compiled = compileMiddlewares(route.fns, DEFAULT_NOT_FOUND);
427+
if (route.method === "ALL") {
428+
this.#router.add("GET", route.pattern, compiled);
429+
this.#router.add("DELETE", route.pattern, compiled);
430+
this.#router.add("HEAD", route.pattern, compiled);
431+
this.#router.add("OPTIONS", route.pattern, compiled);
432+
this.#router.add("PATCH", route.pattern, compiled);
433+
this.#router.add("POST", route.pattern, compiled);
434+
this.#router.add("PUT", route.pattern, compiled);
435+
} else {
436+
this.#router.add(route.method, route.pattern, compiled);
437+
}
422438
}
423439

424-
const rootMiddlewares = this.#root.middlewares;
440+
const rootHandler = compileMiddlewares(
441+
this.#root.middlewares,
442+
PASS_THROUGH,
443+
);
425444

426445
return async (
427446
req: Request,
@@ -433,7 +452,7 @@ export class App<State> {
433452

434453
const method = req.method.toUpperCase() as Method;
435454
const matched = this.#router.match(method, url);
436-
let { params, pattern, handlers, methodMatch } = matched;
455+
let { params, pattern, item: handlers, methodMatch } = matched;
437456

438457
const span = trace.getActiveSpan();
439458
if (span && pattern) {
@@ -444,12 +463,12 @@ export class App<State> {
444463
let next: () => Promise<Response>;
445464

446465
if (pattern === null || !methodMatch) {
447-
handlers = rootMiddlewares;
466+
handlers = rootHandler;
448467
}
449468

450-
if (matched.pattern !== null && !methodMatch) {
469+
if (pattern !== null && !methodMatch) {
451470
if (method === "OPTIONS") {
452-
const allowed = this.#router.getAllowedMethods(matched.pattern);
471+
const allowed = this.#router.getAllowedMethods(pattern);
453472
next = defaultOptionsHandler(allowed);
454473
} else {
455474
next = DEFAULT_NOT_ALLOWED_METHOD;
@@ -458,6 +477,10 @@ export class App<State> {
458477
next = DEFAULT_NOT_FOUND;
459478
}
460479

480+
if (handlers === null) {
481+
handlers = next;
482+
}
483+
461484
const ctx = new Context<State>(
462485
req,
463486
url,
@@ -470,9 +493,7 @@ export class App<State> {
470493
);
471494

472495
try {
473-
if (handlers.length === 0) return await next();
474-
475-
const result = await runMiddlewares(handlers, ctx);
496+
const result = await handlers(ctx);
476497
if (!(result instanceof Response)) {
477498
throw new Error(
478499
`Expected a "Response" instance to be returned, but got: ${result}`,

src/middlewares/mod.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -81,22 +81,23 @@ export type MiddlewareFn<State> = (
8181
*/
8282
export type Middleware<State> = MiddlewareFn<State> | MiddlewareFn<State>[];
8383

84-
export function runMiddlewares<State>(
84+
export function compileMiddlewares<State>(
8585
middlewares: MiddlewareFn<State>[],
86-
ctx: Context<State>,
87-
): Promise<Response> {
88-
let fn = ctx.next;
89-
let i = middlewares.length;
90-
while (i--) {
91-
const local = fn;
92-
const next = middlewares[i];
93-
fn = async () => {
86+
init: MiddlewareFn<State>,
87+
): MiddlewareFn<State> {
88+
let fn: MiddlewareFn<State> = init;
89+
90+
for (let i = middlewares.length - 1; i >= 0; i--) {
91+
const local = middlewares[i];
92+
const next = fn;
93+
94+
fn = async (ctx) => {
9495
const internals = getInternals(ctx);
9596
const { app: prevApp, layouts: prevLayouts } = internals;
9697

97-
ctx.next = local;
98+
ctx.next = async () => await next(ctx);
9899
try {
99-
return await next(ctx);
100+
return await local(ctx);
100101
} catch (err) {
101102
ctx.error = err;
102103
throw err;
@@ -106,5 +107,6 @@ export function runMiddlewares<State>(
106107
}
107108
};
108109
}
109-
return fn();
110+
111+
return fn;
110112
}

src/middlewares/mod_test.ts

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
import { runMiddlewares } from "./mod.ts";
1+
import { compileMiddlewares } from "./mod.ts";
22
import { expect } from "@std/expect";
33
import { serveMiddleware } from "../test_utils.ts";
44
import type { MiddlewareFn } from "./mod.ts";
55

6-
Deno.test("runMiddleware", async () => {
6+
const THROWER = () => {
7+
throw new Error("fail");
8+
};
9+
10+
Deno.test("compileMiddlewares", async () => {
711
const middlewares: MiddlewareFn<{ text: string }>[] = [
812
(ctx) => {
913
ctx.state.text = "A";
@@ -23,16 +27,17 @@ Deno.test("runMiddleware", async () => {
2327
},
2428
];
2529

26-
const server = serveMiddleware<{ text: string }>((ctx) =>
27-
runMiddlewares(middlewares, ctx)
30+
const server = serveMiddleware<{ text: string }>(
31+
compileMiddlewares(middlewares, THROWER),
2832
);
2933

3034
const res = await server.get("/");
3135
expect(await res.text()).toEqual("AB");
3236
});
3337

34-
Deno.test("runMiddleware - middlewares should only be called once", async () => {
35-
const A: MiddlewareFn<{ count: number }> = (ctx) => {
38+
Deno.test("compileMiddlewares - middlewares should only be called once", async () => {
39+
type State = { count: number };
40+
const A: MiddlewareFn<State> = (ctx) => {
3641
if (ctx.state.count === undefined) {
3742
ctx.state.count = 0;
3843
} else {
@@ -41,11 +46,11 @@ Deno.test("runMiddleware - middlewares should only be called once", async () =>
4146
return ctx.next();
4247
};
4348

44-
const server = serveMiddleware<{ count: number }>((ctx) =>
45-
runMiddlewares(
46-
[A, (ctx) => new Response(String(ctx.state.count))],
47-
ctx,
48-
)
49+
const final: MiddlewareFn<State> = (ctx) =>
50+
new Response(String(ctx.state.count));
51+
52+
const server = serveMiddleware<{ count: number }>(
53+
compileMiddlewares([A], final),
4954
);
5055

5156
const res = await server.get("/");
@@ -55,7 +60,7 @@ Deno.test("runMiddleware - middlewares should only be called once", async () =>
5560
Deno.test("runMiddleware - runs multiple stacks", async () => {
5661
type State = { text: string };
5762
const A: MiddlewareFn<State> = (ctx) => {
58-
ctx.state.text += "A";
63+
ctx.state.text = "A";
5964
return ctx.next();
6065
};
6166
const B: MiddlewareFn<State> = (ctx) => {
@@ -71,19 +76,13 @@ Deno.test("runMiddleware - runs multiple stacks", async () => {
7176
return ctx.next();
7277
};
7378

74-
const server = serveMiddleware<State>((ctx) => {
75-
ctx.state.text = "";
76-
return runMiddlewares(
77-
[
78-
A,
79-
B,
80-
C,
81-
D,
82-
(ctx) => new Response(String(ctx.state.text)),
83-
],
84-
ctx,
85-
);
86-
});
79+
const final: MiddlewareFn<State> = (ctx) =>
80+
new Response(String(ctx.state.text));
81+
82+
const server = serveMiddleware<State>(compileMiddlewares(
83+
[A, B, C, D],
84+
final,
85+
));
8786

8887
const res = await server.get("/");
8988
expect(await res.text()).toEqual("ABCD");
@@ -119,13 +118,14 @@ Deno.test("runMiddleware - throws errors", async () => {
119118
throw err;
120119
}
121120
},
122-
() => {
123-
throw new Error("fail");
124-
},
125121
];
126122

127-
const server = serveMiddleware<{ text: string }>((ctx) =>
128-
runMiddlewares(middlewares, ctx)
123+
const final = () => {
124+
throw new Error("fail");
125+
};
126+
127+
const server = serveMiddleware<{ text: string }>(
128+
compileMiddlewares(middlewares, final),
129129
);
130130

131131
try {

0 commit comments

Comments
 (0)