-
Notifications
You must be signed in to change notification settings - Fork 4
/
app.ts
100 lines (84 loc) · 2.91 KB
/
app.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { serve, ServeInit } from "https://deno.land/[email protected]/http/server.ts";
import { Context } from "./context.ts";
import decode from "./decode.ts";
import { Handler, ServerError } from "./types.ts";
import { Router } from "./router.ts";
const notFound = {
status: 404,
message: "The requested resource doesn't exist.",
};
interface Match {
handler: Handler;
params?: Record<string, string | undefined>;
}
export interface WebApp {
get(path: string, handler: Handler): WebApp;
post(path: string, handler: Handler): WebApp;
put(path: string, handler: Handler): WebApp;
patch(path: string, handler: Handler): WebApp;
delete(path: string, handler: Handler): WebApp;
options(path: string, handler: Handler): WebApp;
head(path: string, handler: Handler): WebApp;
}
export class WebApp {
#routes: Map<string, Handler>;
#patterns: URLPattern[];
#cache: Record<string, Match | null>;
constructor() {
this.#routes = new Map();
this.#patterns = [];
this.#cache = {};
// Define methods
// deno-fmt-ignore-line
const methods = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head'] as const;
for (const method of methods) {
this[method] = (path, handler) =>
this.#add(path, method.toUpperCase(), handler);
}
}
#add(pathname: string, method: string, handler: Handler) {
const id = method + pathname;
this.#routes.set(id, handler);
const pattern = new URLPattern({ pathname });
const has = this.#patterns.find((p) => p.pathname === pathname);
if (!has) this.#patterns.push(pattern);
return this;
}
#match(path: string, method: string) {
const cid = method + path;
const hit = this.#cache[cid];
if (hit) return hit;
const pattern = this.#patterns.find((p) => p.test(path));
if (!pattern) return this.#cache[cid] = null;
const { pathname } = pattern;
const id = method + pathname;
const handler = this.#routes.get(id);
if (!handler) return this.#cache[cid] = null;
if (pathname.includes(":")) {
const exec = pattern.exec(path);
const params = exec?.pathname.groups;
return this.#cache[cid] = { handler, params };
} else return this.#cache[cid] = { handler };
}
handle = async (request: Request) => {
try {
const match = this.#match(request.url, request.method);
const ctx: Context = new Context({ request });
ctx.assert(match, notFound);
ctx.params = match.params ?? {};
const res = await match.handler(ctx);
return decode(res, ctx.status);
} catch (error) {
const err = ServerError.from(error);
const res = err.serialize();
return decode(res, err.status);
}
};
use = (path: string, router: Router) => {
for (const [id, handler] of router.routes) {
const [method, pathname] = id.split(",");
this.#add(path + pathname, method, handler);
}
};
serve = (opts?: ServeInit) => serve(this.handle, opts);
}