@atproto/lex-server 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/LICENSE.txt +7 -0
- package/README.md +598 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +39 -0
- package/dist/errors.js.map +1 -0
- package/dist/example.d.ts +2 -0
- package/dist/example.d.ts.map +1 -0
- package/dist/example.js +36 -0
- package/dist/example.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/lex-auth-error.d.ts +15 -0
- package/dist/lex-auth-error.d.ts.map +1 -0
- package/dist/lex-auth-error.js +52 -0
- package/dist/lex-auth-error.js.map +1 -0
- package/dist/lex-server.d.ts +80 -0
- package/dist/lex-server.d.ts.map +1 -0
- package/dist/lex-server.js +285 -0
- package/dist/lex-server.js.map +1 -0
- package/dist/lib/drain-websocket.d.ts +6 -0
- package/dist/lib/drain-websocket.d.ts.map +1 -0
- package/dist/lib/drain-websocket.js +16 -0
- package/dist/lib/drain-websocket.js.map +1 -0
- package/dist/lib/sleep.d.ts +2 -0
- package/dist/lib/sleep.d.ts.map +1 -0
- package/dist/lib/sleep.js +22 -0
- package/dist/lib/sleep.js.map +1 -0
- package/dist/lib/www-authenticate.d.ts +7 -0
- package/dist/lib/www-authenticate.d.ts.map +1 -0
- package/dist/lib/www-authenticate.js +22 -0
- package/dist/lib/www-authenticate.js.map +1 -0
- package/dist/nodejs.d.ts +35 -0
- package/dist/nodejs.d.ts.map +1 -0
- package/dist/nodejs.js +236 -0
- package/dist/nodejs.js.map +1 -0
- package/dist/subscripotion.d.ts +2 -0
- package/dist/subscripotion.d.ts.map +1 -0
- package/dist/subscripotion.js +36 -0
- package/dist/subscripotion.js.map +1 -0
- package/dist/test.d.mts +2 -0
- package/dist/test.d.mts.map +1 -0
- package/dist/test.mjs +52 -0
- package/dist/test.mjs.map +1 -0
- package/nodejs.js +5 -0
- package/package.json +64 -0
- package/src/errors.ts +54 -0
- package/src/index.ts +8 -0
- package/src/lex-server.test.ts +1621 -0
- package/src/lex-server.ts +551 -0
- package/src/lib/drain-websocket.ts +23 -0
- package/src/lib/sleep.ts +25 -0
- package/src/lib/www-authenticate.ts +26 -0
- package/src/nodejs.test.ts +107 -0
- package/src/nodejs.ts +367 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +8 -0
- package/tsconfig.tests.json +9 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { AddressInfo } from 'node:net'
|
|
2
|
+
import { scheduler } from 'node:timers/promises'
|
|
3
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
4
|
+
import { Server, serve } from './nodejs.js'
|
|
5
|
+
|
|
6
|
+
describe('Node.js RequestListener', () => {
|
|
7
|
+
let server: Server
|
|
8
|
+
let address: string
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
server = await serve(async (request) => {
|
|
12
|
+
const { pathname } = new URL(request.url)
|
|
13
|
+
if (pathname === '/hello') {
|
|
14
|
+
return new Response('Hello, world!', {
|
|
15
|
+
status: 200,
|
|
16
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
17
|
+
})
|
|
18
|
+
} else if (pathname === '/throw') {
|
|
19
|
+
throw new Error('Test error')
|
|
20
|
+
} else if (pathname === '/echo') {
|
|
21
|
+
return new Response(request.body, {
|
|
22
|
+
status: 200,
|
|
23
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
return new Response('Not Found', { status: 404 })
|
|
27
|
+
})
|
|
28
|
+
const { port } = server.address() as AddressInfo
|
|
29
|
+
address = `http://localhost:${port}`
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
afterAll(async () => {
|
|
33
|
+
await server.terminate()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should respond with Hello, world! on /hello', async () => {
|
|
37
|
+
const res = await fetch(new URL(`/hello`, address))
|
|
38
|
+
const text = await res.text()
|
|
39
|
+
expect(res.status).toBe(200)
|
|
40
|
+
expect(text).toBe('Hello, world!')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should respond with Not Found on unknown path', async () => {
|
|
44
|
+
const res = await fetch(new URL(`/unknown`, address))
|
|
45
|
+
const text = await res.text()
|
|
46
|
+
expect(res.status).toBe(404)
|
|
47
|
+
expect(text).toBe('Not Found')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should handle thrown errors and respond with 500', async () => {
|
|
51
|
+
const res = await fetch(new URL(`/throw`, address))
|
|
52
|
+
const text = await res.text()
|
|
53
|
+
expect(res.status).toBe(500)
|
|
54
|
+
expect(text).toBe('Internal Server Error')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should handle streaming bodies', async () => {
|
|
58
|
+
const totalSize = 1024 * 1024
|
|
59
|
+
const consumerSize = 42 * 1024
|
|
60
|
+
|
|
61
|
+
let sentBytes = 0
|
|
62
|
+
let receivedBytes = 0
|
|
63
|
+
|
|
64
|
+
const res = await fetch(new URL(`/echo`, address), {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
// @ts-expect-error
|
|
67
|
+
duplex: 'half',
|
|
68
|
+
body: new ReadableStream({
|
|
69
|
+
async pull(controller) {
|
|
70
|
+
const chunkSize = Math.min(1024, totalSize - sentBytes)
|
|
71
|
+
controller.enqueue('A'.repeat(chunkSize))
|
|
72
|
+
sentBytes += chunkSize
|
|
73
|
+
await scheduler.wait(0) // Yield to event loop
|
|
74
|
+
if (sentBytes === totalSize) controller.close()
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const reader = res.body!.getReader()
|
|
80
|
+
|
|
81
|
+
// eslint-disable-next-line no-constant-condition
|
|
82
|
+
while (true) {
|
|
83
|
+
const result = await reader.read()
|
|
84
|
+
if (result.done) break
|
|
85
|
+
receivedBytes += Buffer.byteLength(result.value)
|
|
86
|
+
if (receivedBytes >= consumerSize) {
|
|
87
|
+
await reader.cancel()
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
expect(receivedBytes).toBeGreaterThanOrEqual(consumerSize)
|
|
93
|
+
expect(sentBytes).toBeGreaterThanOrEqual(consumerSize)
|
|
94
|
+
expect(sentBytes).toBeLessThan(totalSize)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should echo back request body on /echo', async () => {
|
|
98
|
+
const body = `Echo this back`
|
|
99
|
+
const res = await fetch(new URL(`/echo`, address), {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
body,
|
|
102
|
+
})
|
|
103
|
+
const text = await res.text()
|
|
104
|
+
expect(res.status).toBe(200)
|
|
105
|
+
expect(text).toBe(body)
|
|
106
|
+
})
|
|
107
|
+
})
|
package/src/nodejs.ts
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { once } from 'node:events'
|
|
2
|
+
import {
|
|
3
|
+
IncomingHttpHeaders,
|
|
4
|
+
IncomingMessage,
|
|
5
|
+
RequestListener,
|
|
6
|
+
Server as HttpServer,
|
|
7
|
+
ServerOptions,
|
|
8
|
+
ServerResponse,
|
|
9
|
+
createServer as createHttpServer,
|
|
10
|
+
} from 'node:http'
|
|
11
|
+
import { ListenOptions } from 'node:net'
|
|
12
|
+
import { Readable } from 'node:stream'
|
|
13
|
+
import { pipeline } from 'node:stream/promises'
|
|
14
|
+
import { createHttpTerminator } from 'http-terminator'
|
|
15
|
+
import { WebSocket as WebSocketPonyfill, WebSocketServer } from 'ws'
|
|
16
|
+
|
|
17
|
+
// @ts-expect-error
|
|
18
|
+
Symbol.asyncDispose ??= Symbol.for('Symbol.asyncDispose')
|
|
19
|
+
|
|
20
|
+
const kResponseWs = Symbol.for('@atproto/lex-server:WebSocket')
|
|
21
|
+
|
|
22
|
+
function isUpgradeRequest(request: Request, upgrade: string): boolean {
|
|
23
|
+
return (
|
|
24
|
+
request.method === 'GET' &&
|
|
25
|
+
request.headers.get('connection')?.toLowerCase() === 'upgrade' &&
|
|
26
|
+
request.headers.get('upgrade')?.toLowerCase() === upgrade
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function upgradeWebSocket(request: Request): {
|
|
31
|
+
response: Response
|
|
32
|
+
socket: WebSocket
|
|
33
|
+
} {
|
|
34
|
+
if (!isUpgradeRequest(request, 'websocket')) {
|
|
35
|
+
throw new TypeError('upgradeWebSocket() expects a WebSocket upgrade')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Placeholder response for WebSocket upgrade. The actual handling will happen
|
|
39
|
+
// through the handleWebSocketUpgrade function. Headers set on the response
|
|
40
|
+
// will be applied during the upgrade.
|
|
41
|
+
const response = new Response(null, { status: 200 })
|
|
42
|
+
|
|
43
|
+
// The Response constructor does not allow setting status 101, so we
|
|
44
|
+
// define it directly. The purpose of this response is just to signal
|
|
45
|
+
// that an upgrade is needed, and to carry any headers.
|
|
46
|
+
Object.defineProperty(response, 'status', {
|
|
47
|
+
value: 101,
|
|
48
|
+
enumerable: false,
|
|
49
|
+
configurable: false,
|
|
50
|
+
writable: false,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// @ts-expect-error
|
|
54
|
+
const socket: WebSocket = new WebSocketPonyfill(null, undefined, {
|
|
55
|
+
autoPong: true,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// Attach the WebSocket to the response for later retrieval
|
|
59
|
+
Object.defineProperty(response, kResponseWs, {
|
|
60
|
+
value: socket,
|
|
61
|
+
enumerable: false,
|
|
62
|
+
configurable: false,
|
|
63
|
+
writable: false,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
return { response, socket }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handleWebSocketUpgrade(
|
|
70
|
+
req: IncomingMessage,
|
|
71
|
+
response: Response,
|
|
72
|
+
): void {
|
|
73
|
+
const ws = (response as { [kResponseWs]?: WebSocketPonyfill })[kResponseWs]
|
|
74
|
+
if (!ws) throw new TypeError('Response not created by upgradeWebSocket()')
|
|
75
|
+
|
|
76
|
+
// Create a one time use WebSocketServer to handle the upgrade
|
|
77
|
+
const wss = new WebSocketServer({
|
|
78
|
+
autoPong: true,
|
|
79
|
+
noServer: true,
|
|
80
|
+
clientTracking: false,
|
|
81
|
+
perMessageDeflate: true,
|
|
82
|
+
// @ts-expect-error
|
|
83
|
+
WebSocket: function () {
|
|
84
|
+
// Return the websocket that was created earlier instead of a new instance
|
|
85
|
+
return ws
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// Apply headers that might have been set on the response object during
|
|
90
|
+
// handling. This will be called during wss.handleUpgrade().
|
|
91
|
+
wss.on('headers', (headers) => {
|
|
92
|
+
for (const [name, value] of response.headers) {
|
|
93
|
+
headers.push(`${name}: ${value}`)
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
wss.handleUpgrade(req, req.socket, Buffer.alloc(0), (_socket) => {
|
|
98
|
+
// @TODO find a way to properly "close" the _socket when the server is
|
|
99
|
+
// shutting down (might require replacing http-terminator with a local
|
|
100
|
+
// implementation)
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function sendResponse(
|
|
105
|
+
req: IncomingMessage,
|
|
106
|
+
res: ServerResponse,
|
|
107
|
+
response: Response,
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
// Invalid usage
|
|
110
|
+
if (res.headersSent) {
|
|
111
|
+
throw new TypeError('Response has already been sent')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (response.status === 101) {
|
|
115
|
+
return handleWebSocketUpgrade(req, response)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
res.statusCode = response.status
|
|
119
|
+
res.statusMessage = response.statusText
|
|
120
|
+
|
|
121
|
+
for (const [key, value] of response.headers) {
|
|
122
|
+
res.appendHeader(key, value)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (response.body != null && req.method !== 'HEAD') {
|
|
126
|
+
const stream = Readable.fromWeb(response.body as any)
|
|
127
|
+
await pipeline(stream, res)
|
|
128
|
+
} else {
|
|
129
|
+
await response.body?.cancel()
|
|
130
|
+
res.end()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function toRequest(req: IncomingMessage): Request {
|
|
135
|
+
const host = req.headers.host ?? req.socket?.localAddress ?? 'localhost'
|
|
136
|
+
const isEncrypted = (req.socket as any).encrypted === true
|
|
137
|
+
const protocol = isEncrypted ? 'https' : 'http'
|
|
138
|
+
const url = new URL(req.url ?? '/', `${protocol}://${host}`)
|
|
139
|
+
const headers = toHeaders(req.headers)
|
|
140
|
+
const body = toBody(req)
|
|
141
|
+
|
|
142
|
+
const abortController = new AbortController()
|
|
143
|
+
const abort = (err?: Error) => abortController.abort(err)
|
|
144
|
+
|
|
145
|
+
req.on('close', abort)
|
|
146
|
+
req.on('error', abort)
|
|
147
|
+
req.on('end', abort)
|
|
148
|
+
|
|
149
|
+
abortController.signal.addEventListener(
|
|
150
|
+
'abort',
|
|
151
|
+
() => {
|
|
152
|
+
req.off('close', abort)
|
|
153
|
+
req.off('error', abort)
|
|
154
|
+
req.off('end', abort)
|
|
155
|
+
},
|
|
156
|
+
{ once: true },
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return new Request(url, {
|
|
160
|
+
signal: abortController.signal,
|
|
161
|
+
method: req.method,
|
|
162
|
+
headers,
|
|
163
|
+
body,
|
|
164
|
+
referrer: headers.get('referrer') ?? headers.get('referer') ?? undefined,
|
|
165
|
+
redirect: 'manual',
|
|
166
|
+
// @ts-expect-error
|
|
167
|
+
duplex: body ? 'half' : undefined,
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function toHeaders(headers: IncomingHttpHeaders): Headers {
|
|
172
|
+
const result = new Headers()
|
|
173
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
174
|
+
if (value === undefined) continue
|
|
175
|
+
if (Array.isArray(value)) {
|
|
176
|
+
for (const v of value) result.append(key, v)
|
|
177
|
+
} else {
|
|
178
|
+
result.set(key, value)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return result
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function toBody(req: IncomingMessage): null | ReadableStream<Uint8Array> {
|
|
185
|
+
if (
|
|
186
|
+
req.method === 'GET' ||
|
|
187
|
+
req.method === 'HEAD' ||
|
|
188
|
+
req.method === 'OPTIONS'
|
|
189
|
+
) {
|
|
190
|
+
return null
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (
|
|
194
|
+
req.headers['content-type'] == null &&
|
|
195
|
+
req.headers['transfer-encoding'] == null &&
|
|
196
|
+
req.headers['content-length'] == null
|
|
197
|
+
) {
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return Readable.toWeb(req) as ReadableStream<Uint8Array>
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export type NetAddr = {
|
|
205
|
+
hostname: string
|
|
206
|
+
port: number
|
|
207
|
+
transport: 'tcp'
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export type NodeConnectionInfo = {
|
|
211
|
+
localAddr?: NetAddr
|
|
212
|
+
remoteAddr?: NetAddr
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface HandlerFunction {
|
|
216
|
+
(req: Request, info: NodeConnectionInfo): Response | Promise<Response>
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface HandlerObject {
|
|
220
|
+
handle: HandlerFunction
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function handleRequest(
|
|
224
|
+
req: IncomingMessage,
|
|
225
|
+
res: ServerResponse,
|
|
226
|
+
handlerFn: HandlerFunction,
|
|
227
|
+
) {
|
|
228
|
+
const request = toRequest(req)
|
|
229
|
+
const info = toConnectionInfo(req)
|
|
230
|
+
const response = await handlerFn(request, info)
|
|
231
|
+
await sendResponse(req, res, response)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function toConnectionInfo(req: IncomingMessage): NodeConnectionInfo {
|
|
235
|
+
const { socket } = req
|
|
236
|
+
return {
|
|
237
|
+
localAddr:
|
|
238
|
+
socket.localAddress != null
|
|
239
|
+
? {
|
|
240
|
+
hostname: socket.localAddress,
|
|
241
|
+
port: socket.localPort!,
|
|
242
|
+
transport: 'tcp',
|
|
243
|
+
}
|
|
244
|
+
: undefined,
|
|
245
|
+
remoteAddr:
|
|
246
|
+
socket.remoteAddress != null
|
|
247
|
+
? {
|
|
248
|
+
hostname: socket.remoteAddress,
|
|
249
|
+
port: socket.remotePort!,
|
|
250
|
+
transport: 'tcp',
|
|
251
|
+
}
|
|
252
|
+
: undefined,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function toRequestListener<
|
|
257
|
+
Request extends typeof IncomingMessage = typeof IncomingMessage,
|
|
258
|
+
Response extends typeof ServerResponse<
|
|
259
|
+
InstanceType<Request>
|
|
260
|
+
> = typeof ServerResponse,
|
|
261
|
+
>(handlerFn: HandlerFunction) {
|
|
262
|
+
return ((
|
|
263
|
+
req: InstanceType<Request>,
|
|
264
|
+
res: InstanceType<Response> & { req: InstanceType<Request> },
|
|
265
|
+
next?: (err?: unknown) => void,
|
|
266
|
+
): void => {
|
|
267
|
+
handleRequest(req, res, handlerFn).catch((err) => {
|
|
268
|
+
if (next) next(err)
|
|
269
|
+
else {
|
|
270
|
+
if (!res.headersSent) {
|
|
271
|
+
res.statusCode = 500
|
|
272
|
+
res.setHeader('content-type', 'text/plain; charset=utf-8')
|
|
273
|
+
res.end('Internal Server Error')
|
|
274
|
+
} else if (!res.writableEnded) {
|
|
275
|
+
res.destroy()
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
}) satisfies RequestListener<Request, Response>
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export type CreateServerOptions<
|
|
283
|
+
Request extends typeof IncomingMessage = typeof IncomingMessage,
|
|
284
|
+
Response extends typeof ServerResponse<
|
|
285
|
+
InstanceType<Request>
|
|
286
|
+
> = typeof ServerResponse,
|
|
287
|
+
> = ServerOptions<Request, Response> & {
|
|
288
|
+
gracefulTerminationTimeout?: number
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface Server<
|
|
292
|
+
Request extends typeof IncomingMessage = typeof IncomingMessage,
|
|
293
|
+
Response extends typeof ServerResponse<
|
|
294
|
+
InstanceType<Request>
|
|
295
|
+
> = typeof ServerResponse,
|
|
296
|
+
> extends HttpServer<Request, Response>,
|
|
297
|
+
AsyncDisposable {
|
|
298
|
+
terminate(): Promise<void>
|
|
299
|
+
[Symbol.asyncDispose](): Promise<void>
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function createServer<
|
|
303
|
+
Request extends typeof IncomingMessage = typeof IncomingMessage,
|
|
304
|
+
Response extends typeof ServerResponse<
|
|
305
|
+
InstanceType<Request>
|
|
306
|
+
> = typeof ServerResponse,
|
|
307
|
+
>(
|
|
308
|
+
handler: HandlerFunction | HandlerObject,
|
|
309
|
+
options: CreateServerOptions<Request, Response> = {},
|
|
310
|
+
): Server<Request, Response> {
|
|
311
|
+
const handlerFn =
|
|
312
|
+
typeof handler === 'function' ? handler : handler.handle.bind(handler)
|
|
313
|
+
|
|
314
|
+
const listener = toRequestListener(handlerFn)
|
|
315
|
+
const server = createHttpServer(options, listener)
|
|
316
|
+
|
|
317
|
+
const terminator = createHttpTerminator({
|
|
318
|
+
server: server as HttpServer,
|
|
319
|
+
gracefulTerminationTimeout: options?.gracefulTerminationTimeout,
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
const terminate = async function terminate(this: Server<Request, Response>) {
|
|
323
|
+
if (this !== server) {
|
|
324
|
+
throw new TypeError('Server.terminate called with incorrect context')
|
|
325
|
+
}
|
|
326
|
+
// @TODO properly close all active WebSocket connections
|
|
327
|
+
return terminator.terminate()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
Object.defineProperty(server, 'terminate', {
|
|
331
|
+
value: terminate,
|
|
332
|
+
enumerable: false,
|
|
333
|
+
configurable: false,
|
|
334
|
+
writable: false,
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
Object.defineProperty(server, Symbol.asyncDispose, {
|
|
338
|
+
value: terminate,
|
|
339
|
+
enumerable: false,
|
|
340
|
+
configurable: false,
|
|
341
|
+
writable: false,
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
return server as Server<Request, Response>
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export type StartServerOptions<
|
|
348
|
+
Request extends typeof IncomingMessage = typeof IncomingMessage,
|
|
349
|
+
Response extends typeof ServerResponse<
|
|
350
|
+
InstanceType<Request>
|
|
351
|
+
> = typeof ServerResponse,
|
|
352
|
+
> = ListenOptions & CreateServerOptions<Request, Response>
|
|
353
|
+
|
|
354
|
+
export async function serve<
|
|
355
|
+
Request extends typeof IncomingMessage = typeof IncomingMessage,
|
|
356
|
+
Response extends typeof ServerResponse<
|
|
357
|
+
InstanceType<Request>
|
|
358
|
+
> = typeof ServerResponse,
|
|
359
|
+
>(
|
|
360
|
+
handler: HandlerFunction | HandlerObject,
|
|
361
|
+
options?: StartServerOptions<Request, Response>,
|
|
362
|
+
): Promise<Server<Request, Response>> {
|
|
363
|
+
const server = createServer(handler, options)
|
|
364
|
+
server.listen(options)
|
|
365
|
+
await once(server, 'listening')
|
|
366
|
+
return server
|
|
367
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": ["../../../tsconfig/isomorphic.json"],
|
|
3
|
+
"include": ["./src"],
|
|
4
|
+
"exclude": ["**/*.test.ts"],
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"noImplicitAny": true,
|
|
7
|
+
"importHelpers": true,
|
|
8
|
+
"target": "ES2023",
|
|
9
|
+
"rootDir": "./src",
|
|
10
|
+
"outDir": "./dist"
|
|
11
|
+
}
|
|
12
|
+
}
|
package/tsconfig.json
ADDED