Skip to content

Commit c081d29

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 097fb6b commit c081d29

File tree

20 files changed

+391
-108
lines changed

20 files changed

+391
-108
lines changed

deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"imports": {
4747
"@deno/doc": "jsr:@deno/doc@^0.172.0",
4848
"@deno/esbuild-plugin": "jsr:@deno/esbuild-plugin@^1.1.1",
49+
"@deno/loader": "jsr:@deno/loader@^0.2.1",
4950
"@std/cli": "jsr:@std/cli@^1.0.19",
5051
"@std/collections": "jsr:@std/collections@^1.0.11",
5152
"@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: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import { trace } from "@opentelemetry/api";
22

33
import { DENO_DEPLOYMENT_ID } from "./runtime/build_id.ts";
44
import * as colors from "@std/fmt/colors";
5-
import { type MiddlewareFn, runMiddlewares } from "./middlewares/mod.ts";
5+
import {
6+
type MaybeLazyMiddleware,
7+
type MiddlewareFn,
8+
runMiddlewares,
9+
} from "./middlewares/mod.ts";
610
import { Context } from "./context.ts";
711
import { mergePath, type Method, UrlPatternRouter } from "./router.ts";
812
import type { FreshConfig, ResolvedFreshConfig } from "./config.ts";
913
import type { BuildCache } from "./build_cache.ts";
1014
import { HttpError } from "./error.ts";
11-
import type { LayoutConfig, Route } from "./types.ts";
15+
import type { LayoutConfig, MaybeLazy, Route, RouteConfig } from "./types.ts";
1216
import type { RouteComponent } from "./segments.ts";
1317
import {
1418
applyCommands,
@@ -182,14 +186,14 @@ export class App<State> {
182186
/**
183187
* Add one or more middlewares at the top or the specified path.
184188
*/
185-
use(...middleware: MiddlewareFn<State>[]): this;
186-
use(path: string, ...middleware: MiddlewareFn<State>[]): this;
189+
use(...middleware: MaybeLazyMiddleware<State>[]): this;
190+
use(path: string, ...middleware: MaybeLazyMiddleware<State>[]): this;
187191
use(
188-
pathOrMiddleware: string | MiddlewareFn<State>,
189-
...middlewares: MiddlewareFn<State>[]
192+
pathOrMiddleware: string | MaybeLazyMiddleware<State>,
193+
...middlewares: MaybeLazyMiddleware<State>[]
190194
): this {
191195
let pattern: string;
192-
let fns: MiddlewareFn<State>[];
196+
let fns: MaybeLazyMiddleware<State>[];
193197
if (typeof pathOrMiddleware === "string") {
194198
pattern = pathOrMiddleware;
195199
fns = middlewares!;
@@ -234,58 +238,62 @@ export class App<State> {
234238
return this;
235239
}
236240

237-
route(path: string, route: Route<State>): this {
238-
this.#commands.push(newRouteCmd(path, route, false));
241+
route(
242+
path: string,
243+
route: MaybeLazy<Route<State>>,
244+
config?: RouteConfig,
245+
): this {
246+
this.#commands.push(newRouteCmd(path, route, config, false));
239247
return this;
240248
}
241249

242250
/**
243251
* Add middlewares for GET requests at the specified path.
244252
*/
245-
get(path: string, ...middlewares: MiddlewareFn<State>[]): this {
253+
get(path: string, ...middlewares: MaybeLazy<MiddlewareFn<State>>[]): this {
246254
this.#commands.push(newHandlerCmd("GET", path, middlewares, false));
247255
return this;
248256
}
249257
/**
250258
* Add middlewares for POST requests at the specified path.
251259
*/
252-
post(path: string, ...middlewares: MiddlewareFn<State>[]): this {
260+
post(path: string, ...middlewares: MaybeLazy<MiddlewareFn<State>>[]): this {
253261
this.#commands.push(newHandlerCmd("POST", path, middlewares, false));
254262
return this;
255263
}
256264
/**
257265
* Add middlewares for PATCH requests at the specified path.
258266
*/
259-
patch(path: string, ...middlewares: MiddlewareFn<State>[]): this {
267+
patch(path: string, ...middlewares: MaybeLazy<MiddlewareFn<State>>[]): this {
260268
this.#commands.push(newHandlerCmd("PATCH", path, middlewares, false));
261269
return this;
262270
}
263271
/**
264272
* Add middlewares for PUT requests at the specified path.
265273
*/
266-
put(path: string, ...middlewares: MiddlewareFn<State>[]): this {
274+
put(path: string, ...middlewares: MaybeLazy<MiddlewareFn<State>>[]): this {
267275
this.#commands.push(newHandlerCmd("PUT", path, middlewares, false));
268276
return this;
269277
}
270278
/**
271279
* Add middlewares for DELETE requests at the specified path.
272280
*/
273-
delete(path: string, ...middlewares: MiddlewareFn<State>[]): this {
281+
delete(path: string, ...middlewares: MaybeLazy<MiddlewareFn<State>>[]): this {
274282
this.#commands.push(newHandlerCmd("DELETE", path, middlewares, false));
275283
return this;
276284
}
277285
/**
278286
* Add middlewares for HEAD requests at the specified path.
279287
*/
280-
head(path: string, ...middlewares: MiddlewareFn<State>[]): this {
288+
head(path: string, ...middlewares: MaybeLazy<MiddlewareFn<State>>[]): this {
281289
this.#commands.push(newHandlerCmd("HEAD", path, middlewares, false));
282290
return this;
283291
}
284292

285293
/**
286294
* Add middlewares for all HTTP verbs at the specified path.
287295
*/
288-
all(path: string, ...middlewares: MiddlewareFn<State>[]): this {
296+
all(path: string, ...middlewares: MaybeLazy<MiddlewareFn<State>>[]): this {
289297
this.#commands.push(newHandlerCmd("ALL", path, middlewares, false));
290298
return this;
291299
}
@@ -359,7 +367,7 @@ export class App<State> {
359367
}
360368
}
361369

362-
const router = new UrlPatternRouter<MiddlewareFn<State>>();
370+
const router = new UrlPatternRouter<MaybeLazyMiddleware<State>>();
363371

364372
const { rootMiddlewares } = applyCommands(
365373
router,

src/app_test.tsx

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

src/commands.ts

Lines changed: 81 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { HttpError } from "./error.ts";
22
import { isHandlerByMethod, type PageResponse } from "./handlers.ts";
3-
import type { MiddlewareFn } from "./middlewares/mod.ts";
3+
import type { MaybeLazyMiddleware, MiddlewareFn } from "./middlewares/mod.ts";
44
import { mergePath, type Method, type Router } from "./router.ts";
55
import {
66
getOrCreateSegment,
@@ -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: MaybeLazyMiddleware<State>[];
104105
includeLastSegment: boolean;
105106
}
106107
export function newMiddlewareCmd<State>(
107108
pattern: string,
108-
fns: MiddlewareFn<State>[],
109+
fns: MaybeLazyMiddleware<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<MaybeLazyMiddleware<State>>,
185205
commands: Command<State>[],
186206
basePath: string,
187-
): { rootMiddlewares: MiddlewareFn<State>[] } {
207+
): { rootMiddlewares: MaybeLazyMiddleware<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<MaybeLazyMiddleware<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)