-
Notifications
You must be signed in to change notification settings - Fork 0
/
chain.ts
156 lines (140 loc) · 4.62 KB
/
chain.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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license.
// This module is browser compatible.
import type {
Chainable,
ChainableHandler,
OptionalHandler,
Responsive,
} from "./types.ts";
/** Immutable chain builder for HTTP handlers.
*
* @example
* ```ts
* import { Chain } from "https://deno.land/x/chain_handler@$VERSION/mod.ts";
* import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
*
* const chain = new Chain()
* chain.next(async (request, next) => {
* // logger
* console.log("start")
* const response = await next()
* console.log("end")
* return response
* }).next((request, next) => {
* // request proxy
* request.headers.append("x-proxy", "chain")
* return next(request)
* }).next(async (_, next) => {
* // response proxy
* const response = await next()
* response.headers.append("server", "deno")
* return response
* }).next(() => {
* // cut off chain
* return new Response("hello")
* }).next(() => {
* // not call because cut off by previous chain.
* return new Response("goodby")
* })
* const response = await chain.respond(new Request("http://localhost"))
* assertEquals(await response.text(), "hello")
* assertEquals(response.headers.get("server"), "deno")
* ```
*/
export class Chain implements Chainable, Responsive {
#handlers: readonly ChainableHandler[] = [];
/**
* @param init Initial chainable HTTP handlers.
*/
constructor(...init: readonly ChainableHandler[]) {
this.#handlers = init ?? [];
}
/** Register chainable HTTP handlers.
* @param handlers HTTP chainable handlers.
*
* @example
* ```ts
* import { Chain } from "https://deno.land/x/chain_handler@$VERSION/mod.ts";
*
* const chain = new Chain().next((request, next) => next(), () => new Response("hello"))
* ```
*/
readonly next = (...handlers: readonly ChainableHandler[]): this => {
this.#handlers = this.#handlers.concat(handlers);
return this;
};
/** All registered handlers.
*
* @example
* ```ts
* import { Chain } from "https://deno.land/x/chain_handler@$VERSION/mod.ts";
* import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
*
* assertEquals(new Chain(() => new Response("hello")).handlers.length, 1)
* ```
*/
get handlers(): readonly ChainableHandler[] {
return this.#handlers;
}
/**
* @param request `Request` object. The `Request` is cloned and not mutate.
* @param defaultResponse The default `Response` object. Change the response when the {@link handlers} is empty.
*
* @example
* ```ts
* import { Chain } from "https://deno.land/x/chain_handler@$VERSION/mod.ts";
* import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
*
* const chain = new Chain()
* const response = await chain.respond(new Request("http://localhost"))
* assertEquals(response.status, 404)
* ```
*/
readonly respond = (
request: Request,
defaultResponse?: Response,
): Promise<Response> | Response => {
const newRequest = new Request(request);
defaultResponse ??= new Response(null, { status: 404 });
const response = chain(
newRequest.clone(),
defaultResponse.clone(),
...this.handlers,
);
return response;
};
}
/** Immutable and sequential handler calls.
* @param request The initial `Request` object.
* @param response The initial `Response` object.
* @param handlers Sequential execution HTTP handlers.
*
* @example
* ```ts
* import { chain } from "https://deno.land/x/chain_handler@$VERSION/mod.ts";
* import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
*
* const initRequest = new Request("http://localhost");
* const initResponse = new Response(null, { status: 400 })
* const response = await chain(initRequest, initResponse, (_, next) => next(), () => new Response("hello"))
* assertEquals(response.status, 200)
* assertEquals(await response.text(), "hello")
* ```
*/
export function chain(
request: Request,
response: Response,
...handlers: readonly ChainableHandler[]
): Promise<Response> | Response {
const seen = new WeakSet<readonly ChainableHandler[]>();
const run: typeof chain = async (request, response, ...handlers) => {
// Recursive safe
if (seen.has(handlers)) return response.clone();
const [first, ...rest] = handlers;
if (!first) return response.clone();
const nextHandler: OptionalHandler = async (nextRequest = request) =>
(await run(nextRequest.clone(), response.clone(), ...rest)).clone();
return (await first(request.clone(), nextHandler)).clone();
};
return run(request, response, ...handlers);
}