/
index.ts
276 lines (255 loc) · 10.3 KB
/
index.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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
/// <reference lib="webworker" />
import type { RequestListener, ServerResponse } from 'node:http';
import {
isFetchEvent,
isNodeRequest,
isRequestInit,
isServerResponse,
NodeRequest,
normalizeNodeRequest,
sendNodeResponse,
} from './utils';
import { Request as PonyfillRequestCtor } from '@whatwg-node/fetch';
export interface ServerAdapterBaseObject<
TServerContext,
THandleRequest extends ServerAdapterRequestHandler<TServerContext> = ServerAdapterRequestHandler<TServerContext>
> {
/**
* An async function that takes `Request` and the server context and returns a `Response`.
* If you use `requestListener`, the server context is `{ req: IncomingMessage, res: ServerResponse }`.
*/
handle: THandleRequest;
}
export interface ServerAdapterObject<
TServerContext,
TBaseObject extends ServerAdapterBaseObject<TServerContext, ServerAdapterRequestHandler<TServerContext>>
> extends EventListenerObject {
/**
* A basic request listener that takes a `Request` with the server context and returns a `Response`.
*/
handleRequest: TBaseObject['handle'];
/**
* WHATWG Fetch spec compliant `fetch` function that can be used for testing purposes.
*/
fetch(request: Request, ctx: TServerContext): Promise<Response> | Response;
fetch(request: Request, ...ctx: Partial<TServerContext>[]): Promise<Response> | Response;
fetch(urlStr: string, ctx: TServerContext): Promise<Response> | Response;
fetch(urlStr: string, ...ctx: Partial<TServerContext>[]): Promise<Response> | Response;
fetch(urlStr: string, init: RequestInit, ctx: TServerContext): Promise<Response> | Response;
fetch(urlStr: string, init: RequestInit, ...ctx: Partial<TServerContext>[]): Promise<Response> | Response;
fetch(url: URL, ctx: TServerContext): Promise<Response> | Response;
fetch(url: URL, ...ctx: Partial<TServerContext>[]): Promise<Response> | Response;
fetch(url: URL, init: RequestInit, ctx: TServerContext): Promise<Response> | Response;
fetch(url: URL, init: RequestInit, ...ctx: Partial<TServerContext>[]): Promise<Response> | Response;
/**
* This function takes Node's request object and returns a WHATWG Fetch spec compliant `Response` object.
**/
handleNodeRequest(nodeRequest: NodeRequest, ...ctx: Partial<TServerContext>[]): Promise<Response> | Response;
/**
* A request listener function that can be used with any Node server variation.
*/
requestListener: RequestListener;
handle(req: NodeRequest, res: ServerResponse, ...ctx: Partial<TServerContext>[]): Promise<void>;
handle(request: Request, ...ctx: Partial<TServerContext>[]): Promise<Response> | Response;
handle(fetchEvent: FetchEvent & Partial<TServerContext>, ...ctx: Partial<TServerContext>[]): void;
handle(
container: { request: Request } & Partial<TServerContext>,
...ctx: Partial<TServerContext>[]
): Promise<Response> | Response;
}
export type ServerAdapter<TServerContext, TBaseObject extends ServerAdapterBaseObject<TServerContext>> = TBaseObject &
ServerAdapterObject<TServerContext, TBaseObject>['handle'] &
ServerAdapterObject<TServerContext, TBaseObject>;
async function handleWaitUntils(waitUntilPromises: Promise<unknown>[]) {
const waitUntils = await Promise.allSettled(waitUntilPromises);
waitUntils.forEach(waitUntil => {
if (waitUntil.status === 'rejected') {
console.error(waitUntil.reason);
}
});
}
export type ServerAdapterRequestHandler<TServerContext> = (
request: Request,
ctx: TServerContext
) => Promise<Response> | Response;
export type DefaultServerAdapterContext = {
req: NodeRequest;
res: ServerResponse;
waitUntil(promise: Promise<unknown>): void;
};
function createServerAdapter<
TServerContext = DefaultServerAdapterContext,
THandleRequest extends ServerAdapterRequestHandler<TServerContext> = ServerAdapterRequestHandler<TServerContext>
>(
serverAdapterRequestHandler: THandleRequest,
RequestCtor?: typeof Request
): ServerAdapter<TServerContext, ServerAdapterBaseObject<TServerContext, THandleRequest>>;
function createServerAdapter<TServerContext, TBaseObject extends ServerAdapterBaseObject<TServerContext>>(
serverAdapterBaseObject: TBaseObject,
RequestCtor?: typeof Request
): ServerAdapter<TServerContext, TBaseObject>;
function createServerAdapter<
TServerContext = DefaultServerAdapterContext,
THandleRequest extends ServerAdapterRequestHandler<TServerContext> = ServerAdapterRequestHandler<TServerContext>,
TBaseObject extends ServerAdapterBaseObject<TServerContext, THandleRequest> = ServerAdapterBaseObject<
TServerContext,
THandleRequest
>
>(
serverAdapterBaseObject: TBaseObject | THandleRequest,
/**
* WHATWG Fetch spec compliant `Request` constructor.
*/
RequestCtor = PonyfillRequestCtor
): ServerAdapter<TServerContext, TBaseObject> {
const handleRequest =
typeof serverAdapterBaseObject === 'function' ? serverAdapterBaseObject : serverAdapterBaseObject.handle;
function handleNodeRequest(nodeRequest: NodeRequest, ...ctx: Partial<TServerContext>[]) {
const serverContext = ctx.length > 1 ? Object.assign({}, ...ctx) : ctx[0];
const request = normalizeNodeRequest(nodeRequest, RequestCtor);
return handleRequest(request, serverContext);
}
async function requestListener(
nodeRequest: NodeRequest,
serverResponse: ServerResponse,
...ctx: Partial<TServerContext>[]
) {
const waitUntilPromises: Promise<unknown>[] = [];
const defaultServerContext = {
req: nodeRequest,
res: serverResponse,
waitUntil(p: Promise<unknown>) {
waitUntilPromises.push(p);
},
};
const response = await handleNodeRequest(nodeRequest, defaultServerContext as any, ...ctx);
if (response) {
await sendNodeResponse(response, serverResponse);
} else {
await new Promise(resolve => {
serverResponse.statusCode = 404;
serverResponse.end(resolve);
});
}
if (waitUntilPromises.length > 0) {
await handleWaitUntils(waitUntilPromises);
}
}
function handleEvent(event: FetchEvent, ...ctx: Partial<TServerContext>[]): void {
if (!event.respondWith || !event.request) {
throw new TypeError(`Expected FetchEvent, got ${event}`);
}
const serverContext = ctx.length > 0 ? Object.assign({}, event, ...ctx) : event;
const response$ = handleRequest(event.request, serverContext);
event.respondWith(response$);
}
function handleRequestWithWaitUntil(request: Request, ...ctx: Partial<TServerContext>[]) {
const serverContext: TServerContext = ctx.length > 1 ? Object.assign({}, ...ctx) : ctx[0] || {};
if ('process' in globalThis && process.versions?.['bun'] != null) {
// This is required for bun
request.text();
}
if (!('waitUntil' in serverContext)) {
const waitUntilPromises: Promise<unknown>[] = [];
const response$ = handleRequest(request, {
...serverContext,
waitUntil(p: Promise<unknown>) {
waitUntilPromises.push(p);
},
});
if (waitUntilPromises.length > 0) {
return handleWaitUntils(waitUntilPromises).then(() => response$);
}
return response$;
}
return handleRequest(request, serverContext);
}
const fetchFn: ServerAdapterObject<TServerContext, TBaseObject>['fetch'] = (
input,
...maybeCtx: Partial<TServerContext>[]
) => {
if (typeof input === 'string' || input instanceof URL) {
const [initOrCtx, ...restOfCtx] = maybeCtx;
if (isRequestInit(initOrCtx)) {
return handleRequestWithWaitUntil(new RequestCtor(input, initOrCtx), ...restOfCtx);
}
return handleRequestWithWaitUntil(new RequestCtor(input), ...maybeCtx);
}
return handleRequestWithWaitUntil(input, ...maybeCtx);
};
const genericRequestHandler = (
input: Request | FetchEvent | NodeRequest | ({ request: Request } & Partial<TServerContext>),
...maybeCtx: Partial<TServerContext>[]
): Promise<Response> | Response | Promise<void> | void => {
// If it is a Node request
const [initOrCtxOrRes, ...restOfCtx] = maybeCtx;
if (isNodeRequest(input)) {
if (!isServerResponse(initOrCtxOrRes)) {
throw new TypeError(`Expected ServerResponse, got ${initOrCtxOrRes}`);
}
return requestListener(input, initOrCtxOrRes, ...restOfCtx);
}
if (isServerResponse(initOrCtxOrRes)) {
throw new TypeError('Got Node response without Node request');
}
// Is input a container object over Request?
if (typeof input === 'object' && 'request' in input) {
// Is it FetchEvent?
if (isFetchEvent(input)) {
return handleEvent(input, ...maybeCtx);
}
// In this input is also the context
return handleRequestWithWaitUntil(input.request, input, ...maybeCtx);
}
// Or is it Request itself?
// Then ctx is present and it is the context
return fetchFn(input, ...maybeCtx);
};
const adapterObj: ServerAdapterObject<TServerContext, TBaseObject> = {
handleRequest,
fetch: fetchFn,
handleNodeRequest,
requestListener,
handleEvent,
handle: genericRequestHandler as ServerAdapterObject<TServerContext, TBaseObject>['handle'],
};
return new Proxy(genericRequestHandler, {
// It should have all the attributes of the handler function and the server instance
has: (_, prop) => {
return (
prop in adapterObj ||
prop in genericRequestHandler ||
(serverAdapterBaseObject && prop in serverAdapterBaseObject)
);
},
get: (_, prop) => {
const adapterProp = adapterObj[prop];
if (adapterProp) {
if (adapterProp.bind) {
return adapterProp.bind(adapterObj);
}
return adapterProp;
}
const handleProp = genericRequestHandler[prop];
if (handleProp) {
if (handleProp.bind) {
return handleProp.bind(genericRequestHandler);
}
return handleProp;
}
if (serverAdapterBaseObject) {
const serverAdapterBaseObjectProp = serverAdapterBaseObject[prop];
if (serverAdapterBaseObjectProp) {
if (serverAdapterBaseObjectProp.bind) {
return serverAdapterBaseObjectProp.bind(serverAdapterBaseObject);
}
return serverAdapterBaseObjectProp;
}
}
},
apply(_, __, args: Parameters<ServerAdapterObject<TServerContext, TBaseObject>['handle']>) {
return genericRequestHandler(...args);
},
}) as any; // 😡
}
export { createServerAdapter };