Skip to content

Commit 00a127c

Browse files
feat: support lazily loading routes/handlers
We can improve cold start times by only loading route modules when the particular pattern matches. This PR introduces the concept of lazy middlewares, routes and handlers to do that.
1 parent e9f0a4b commit 00a127c

File tree

17 files changed

+380
-100
lines changed

17 files changed

+380
-100
lines changed

deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"imports": {
4646
"@deno/doc": "jsr:@deno/doc@^0.172.0",
4747
"@deno/esbuild-plugin": "jsr:@deno/esbuild-plugin@^1.1.1",
48+
"@deno/loader": "jsr:@deno/loader@^0.2.1",
4849
"@std/cli": "jsr:@std/cli@^1.0.19",
4950
"@std/collections": "jsr:@std/collections@^1.0.11",
5051
"@std/http": "jsr:@std/http@^1.0.15",

deno.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { mergePath, type Method, UrlPatternRouter } from "./router.ts";
88
import type { FreshConfig, ResolvedFreshConfig } from "./config.ts";
99
import type { BuildCache } from "./build_cache.ts";
1010
import { HttpError } from "./error.ts";
11-
import type { LayoutConfig, Route } from "./types.ts";
11+
import type { LayoutConfig, MaybeLazy, Route, RouteConfig } from "./types.ts";
1212
import type { RouteComponent } from "./segments.ts";
1313
import {
1414
applyCommands,
@@ -182,14 +182,14 @@ export class App<State> {
182182
/**
183183
* Add one or more middlewares at the top or the specified path.
184184
*/
185-
use(...middleware: MiddlewareFn<State>[]): this;
186-
use(path: string, ...middleware: MiddlewareFn<State>[]): this;
185+
use(...middleware: MaybeLazy<MiddlewareFn<State>>[]): this;
186+
use(path: string, ...middleware: MaybeLazy<MiddlewareFn<State>>[]): this;
187187
use(
188-
pathOrMiddleware: string | MiddlewareFn<State>,
189-
...middlewares: MiddlewareFn<State>[]
188+
pathOrMiddleware: string | MaybeLazy<MiddlewareFn<State>>,
189+
...middlewares: MaybeLazy<MiddlewareFn<State>>[]
190190
): this {
191191
let pattern: string;
192-
let fns: MiddlewareFn<State>[];
192+
let fns: Array<MiddlewareFn<State> | MaybeLazy<MiddlewareFn<State>>>;
193193
if (typeof pathOrMiddleware === "string") {
194194
pattern = pathOrMiddleware;
195195
fns = middlewares!;
@@ -234,58 +234,62 @@ export class App<State> {
234234
return this;
235235
}
236236

237-
route(path: string, route: Route<State>): this {
238-
this.#commands.push(newRouteCmd(path, route, false));
237+
route(
238+
path: string,
239+
route: MaybeLazy<Route<State>>,
240+
config?: RouteConfig,
241+
): this {
242+
this.#commands.push(newRouteCmd(path, route, config, false));
239243
return this;
240244
}
241245

242246
/**
243247
* Add middlewares for GET requests at the specified path.
244248
*/
245-
get(path: string, ...middlewares: MiddlewareFn<State>[]): this {
249+
get(path: string, ...middlewares: MaybeLazy<MiddlewareFn<State>>[]): this {
246250
this.#commands.push(newHandlerCmd("GET", path, middlewares, false));
247251
return this;
248252
}
249253
/**
250254
* Add middlewares for POST requests at the specified path.
251255
*/
252-
post(path: string, ...middlewares: MiddlewareFn<State>[]): this {
256+
post(path: string, ...middlewares: MaybeLazy<MiddlewareFn<State>>[]): this {
253257
this.#commands.push(newHandlerCmd("POST", path, middlewares, false));
254258
return this;
255259
}
256260
/**
257261
* Add middlewares for PATCH requests at the specified path.
258262
*/
259-
patch(path: string, ...middlewares: MiddlewareFn<State>[]): this {
263+
patch(path: string, ...middlewares: MaybeLazy<MiddlewareFn<State>>[]): this {
260264
this.#commands.push(newHandlerCmd("PATCH", path, middlewares, false));
261265
return this;
262266
}
263267
/**
264268
* Add middlewares for PUT requests at the specified path.
265269
*/
266-
put(path: string, ...middlewares: MiddlewareFn<State>[]): this {
270+
put(path: string, ...middlewares: MaybeLazy<MiddlewareFn<State>>[]): this {
267271
this.#commands.push(newHandlerCmd("PUT", path, middlewares, false));
268272
return this;
269273
}
270274
/**
271275
* Add middlewares for DELETE requests at the specified path.
272276
*/
273-
delete(path: string, ...middlewares: MiddlewareFn<State>[]): this {
277+
delete(path: string, ...middlewares: MaybeLazy<MiddlewareFn<State>>[]): this {
274278
this.#commands.push(newHandlerCmd("DELETE", path, middlewares, false));
275279
return this;
276280
}
277281
/**
278282
* Add middlewares for HEAD requests at the specified path.
279283
*/
280-
head(path: string, ...middlewares: MiddlewareFn<State>[]): this {
284+
head(path: string, ...middlewares: MaybeLazy<MiddlewareFn<State>>[]): this {
281285
this.#commands.push(newHandlerCmd("HEAD", path, middlewares, false));
282286
return this;
283287
}
284288

285289
/**
286290
* Add middlewares for all HTTP verbs at the specified path.
287291
*/
288-
all(path: string, ...middlewares: MiddlewareFn<State>[]): this {
292+
all(path: string, ...middlewares: MaybeLazy<MiddlewareFn<State>>[]): this {
289293
this.#commands.push(newHandlerCmd("ALL", path, middlewares, false));
290294
return this;
291295
}
@@ -359,7 +363,7 @@ export class App<State> {
359363
}
360364
}
361365

362-
const router = new UrlPatternRouter<MiddlewareFn<State>>();
366+
const router = new UrlPatternRouter<MaybeLazy<MiddlewareFn<State>>>();
363367

364368
const { rootMiddlewares } = applyCommands(
365369
router,

src/app_test.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,3 +616,65 @@ Deno.test("App - .route() with basePath", async () => {
616616
expect(await res.text()).toEqual("ok");
617617
expect(res.status).toEqual(200);
618618
});
619+
620+
Deno.test("App - .use() - lazy", async () => {
621+
const app = new App<{ text: string }>()
622+
// deno-lint-ignore require-await
623+
.use(async () => {
624+
// FIXME: Type error
625+
return (ctx) => {
626+
ctx.state.text = "ok";
627+
return ctx.next();
628+
};
629+
})
630+
.get("/", (ctx) => new Response(ctx.state.text));
631+
632+
const server = new FakeServer(app.handler());
633+
634+
const res = await server.get("/");
635+
expect(await res.text()).toEqual("ok");
636+
});
637+
638+
Deno.test("App - .route() - lazy", async () => {
639+
const app = new App()
640+
// deno-lint-ignore require-await
641+
.route("/", async () => {
642+
return { handler: () => new Response("ok") };
643+
});
644+
645+
const server = new FakeServer(app.handler());
646+
647+
const res = await server.get("/");
648+
expect(await res.text()).toEqual("ok");
649+
});
650+
651+
Deno.test("App - .get/post/patch/put/delete/head/all() - lazy", async () => {
652+
const app = new App()
653+
.get("/", () => Promise.resolve(() => new Response("ok")))
654+
.post("/", () => Promise.resolve(() => new Response("ok")))
655+
.patch("/", () => Promise.resolve(() => new Response("ok")))
656+
.delete("/", () => Promise.resolve(() => new Response("ok")))
657+
.put("/", () => Promise.resolve(() => new Response("ok")))
658+
.head("/", () => Promise.resolve(() => new Response("ok")))
659+
.all("/", () => Promise.resolve(() => new Response("ok")));
660+
661+
const server = new FakeServer(app.handler());
662+
663+
let res = await server.get("/");
664+
expect(await res.text()).toEqual("ok");
665+
666+
res = await server.post("/");
667+
expect(await res.text()).toEqual("ok");
668+
669+
res = await server.put("/");
670+
expect(await res.text()).toEqual("ok");
671+
672+
res = await server.patch("/");
673+
expect(await res.text()).toEqual("ok");
674+
675+
res = await server.delete("/");
676+
expect(await res.text()).toEqual("ok");
677+
678+
res = await server.head("/");
679+
expect(await res.text()).toEqual("ok");
680+
});

src/commands.ts

Lines changed: 80 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
type Segment,
1111
segmentToMiddlewares,
1212
} from "./segments.ts";
13-
import type { LayoutConfig, Route } from "./types.ts";
13+
import type { LayoutConfig, MaybeLazy, Route, RouteConfig } from "./types.ts";
14+
import { isLazy } from "./utils.ts";
1415

1516
export const DEFAULT_NOT_FOUND = (): Promise<Response> => {
1617
throw new HttpError(404);
@@ -100,12 +101,12 @@ export function newLayoutCmd<State>(
100101
export interface MiddlewareCmd<State> {
101102
type: CommandType.Middleware;
102103
pattern: string;
103-
fns: MiddlewareFn<State>[];
104+
fns: MaybeLazy<MiddlewareFn<State>>[];
104105
includeLastSegment: boolean;
105106
}
106107
export function newMiddlewareCmd<State>(
107108
pattern: string,
108-
fns: MiddlewareFn<State>[],
109+
fns: MaybeLazy<MiddlewareFn<State>>[],
109110
includeLastSegment: boolean,
110111
): MiddlewareCmd<State> {
111112
return { type: CommandType.Middleware, pattern, fns, includeLastSegment };
@@ -129,29 +130,48 @@ export function newNotFoundCmd<State>(
129130
export interface RouteCommand<State> {
130131
type: CommandType.Route;
131132
pattern: string;
132-
route: Route<State>;
133+
route: MaybeLazy<Route<State>>;
134+
config: RouteConfig | undefined;
133135
includeLastSegment: boolean;
134136
}
135137
export function newRouteCmd<State>(
136138
pattern: string,
137-
route: Route<State>,
139+
route: MaybeLazy<Route<State>>,
140+
config: RouteConfig | undefined,
138141
includeLastSegment: boolean,
139142
): RouteCommand<State> {
140-
ensureHandler(route);
141-
return { type: CommandType.Route, pattern, route, includeLastSegment };
143+
let normalized;
144+
if (isLazy(route)) {
145+
normalized = async () => {
146+
const result = await route();
147+
ensureHandler(result);
148+
return result;
149+
};
150+
} else {
151+
ensureHandler(route);
152+
normalized = route;
153+
}
154+
155+
return {
156+
type: CommandType.Route,
157+
pattern,
158+
route: normalized,
159+
config,
160+
includeLastSegment,
161+
};
142162
}
143163

144164
export interface HandlerCommand<State> {
145165
type: CommandType.Handler;
146166
pattern: string;
147167
method: Method | "ALL";
148-
fns: MiddlewareFn<State>[];
168+
fns: MaybeLazy<MiddlewareFn<State>>[];
149169
includeLastSegment: boolean;
150170
}
151171
export function newHandlerCmd<State>(
152172
method: Method | "ALL",
153173
pattern: string,
154-
fns: MiddlewareFn<State>[],
174+
fns: MaybeLazy<MiddlewareFn<State>>[],
155175
includeLastSegment: boolean,
156176
): HandlerCommand<State> {
157177
return {
@@ -181,10 +201,10 @@ export type Command<State> =
181201
| FsRouteCommand<State>;
182202

183203
export function applyCommands<State>(
184-
router: Router<MiddlewareFn<State>>,
204+
router: Router<MaybeLazy<MiddlewareFn<State>>>,
185205
commands: Command<State>[],
186206
basePath: string,
187-
): { rootMiddlewares: MiddlewareFn<State>[] } {
207+
): { rootMiddlewares: MaybeLazy<MiddlewareFn<State>>[] } {
188208
const root = newSegment<State>("", null);
189209

190210
applyCommandsInner(root, router, commands, basePath);
@@ -194,7 +214,7 @@ export function applyCommands<State>(
194214

195215
function applyCommandsInner<State>(
196216
root: Segment<State>,
197-
router: Router<MiddlewareFn<State>>,
217+
router: Router<MaybeLazy<MiddlewareFn<State>>>,
198218
commands: Command<State>[],
199219
basePath: string,
200220
) {
@@ -241,32 +261,63 @@ function applyCommandsInner<State>(
241261
break;
242262
}
243263
case CommandType.Route: {
244-
const { pattern, route } = cmd;
264+
const { pattern, route, config } = cmd;
245265
const segment = getOrCreateSegment(
246266
root,
247267
pattern,
248268
cmd.includeLastSegment,
249269
);
250270
const fns = segmentToMiddlewares(segment);
251271

252-
fns.push((ctx) => renderRoute(ctx, route));
272+
if (isLazy(route)) {
273+
const routePath = mergePath(
274+
basePath,
275+
config?.routeOverride ?? pattern,
276+
);
253277

254-
const routePath = mergePath(
255-
basePath,
256-
route.config?.routeOverride ?? pattern,
257-
);
278+
let def: Route<State>;
279+
fns.push(async (ctx) => {
280+
if (def === undefined) {
281+
def = await route();
282+
}
283+
284+
return renderRoute(ctx, def);
285+
});
286+
287+
if (config === undefined || config.methods === "ALL") {
288+
router.add("GET", routePath, fns);
289+
router.add("DELETE", routePath, fns);
290+
router.add("HEAD", routePath, fns);
291+
router.add("OPTIONS", routePath, fns);
292+
router.add("PATCH", routePath, fns);
293+
router.add("POST", routePath, fns);
294+
router.add("PUT", routePath, fns);
295+
} else if (Array.isArray(config.methods)) {
296+
for (let i = 0; i < config.methods.length; i++) {
297+
const method = config.methods[i];
298+
router.add(method, routePath, fns);
299+
}
300+
}
301+
} else {
302+
fns.push((ctx) => renderRoute(ctx, route));
303+
304+
const routePath = mergePath(
305+
basePath,
306+
route.config?.routeOverride ?? pattern,
307+
);
258308

259-
if (typeof route.handler === "function") {
260-
router.add("GET", routePath, fns);
261-
router.add("DELETE", routePath, fns);
262-
router.add("HEAD", routePath, fns);
263-
router.add("OPTIONS", routePath, fns);
264-
router.add("PATCH", routePath, fns);
265-
router.add("POST", routePath, fns);
266-
router.add("PUT", routePath, fns);
267-
} else if (isHandlerByMethod(route.handler!)) {
268-
for (const method of Object.keys(route.handler)) {
269-
router.add(method as Method, routePath, fns);
309+
if (typeof route.handler === "function") {
310+
router.add("GET", routePath, fns);
311+
router.add("DELETE", routePath, fns);
312+
router.add("HEAD", routePath, fns);
313+
router.add("OPTIONS", routePath, fns);
314+
router.add("PATCH", routePath, fns);
315+
router.add("POST", routePath, fns);
316+
router.add("PUT", routePath, fns);
317+
} else if (isHandlerByMethod(route.handler!)) {
318+
for (const method of Object.keys(route.handler)) {
319+
router.add(method as Method, routePath, fns);
320+
}
270321
}
271322
}
272323
break;

0 commit comments

Comments
 (0)