prool 0.0.0 → 0.0.2
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 +21 -0
- package/_lib/exports/index.d.ts +4 -0
- package/_lib/exports/index.d.ts.map +1 -0
- package/_lib/exports/index.js +4 -0
- package/_lib/exports/index.js.map +1 -0
- package/_lib/exports/instances.d.ts +2 -0
- package/_lib/exports/instances.d.ts.map +1 -0
- package/_lib/exports/instances.js +2 -0
- package/_lib/exports/instances.js.map +1 -0
- package/_lib/instance.d.ts +111 -0
- package/_lib/instance.d.ts.map +1 -0
- package/_lib/instance.js +141 -0
- package/_lib/instance.js.map +1 -0
- package/_lib/instances/anvil.d.ts +459 -0
- package/_lib/instances/anvil.d.ts.map +1 -0
- package/_lib/instances/anvil.js +74 -0
- package/_lib/instances/anvil.js.map +1 -0
- package/_lib/pool.d.ts +38 -0
- package/_lib/pool.d.ts.map +1 -0
- package/_lib/pool.js +134 -0
- package/_lib/pool.js.map +1 -0
- package/_lib/server.d.ts +21 -0
- package/_lib/server.d.ts.map +1 -0
- package/_lib/server.js +93 -0
- package/_lib/server.js.map +1 -0
- package/_lib/utils.d.ts +35 -0
- package/_lib/utils.d.ts.map +1 -0
- package/_lib/utils.js +71 -0
- package/_lib/utils.js.map +1 -0
- package/exports/index.test.ts +12 -0
- package/exports/index.ts +20 -0
- package/exports/instances.test.ts +10 -0
- package/exports/instances.ts +1 -0
- package/instance.test.ts +311 -0
- package/instance.ts +277 -0
- package/instances/anvil.test.ts +127 -0
- package/instances/anvil.ts +322 -0
- package/package.json +39 -8
- package/pool.test.ts +209 -0
- package/pool.ts +183 -0
- package/server.test.ts +294 -0
- package/server.ts +135 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/utils.test.ts +44 -0
- package/utils.ts +92 -0
package/pool.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import getPort from 'get-port'
|
|
2
|
+
|
|
3
|
+
import type { Instance } from './instance.js'
|
|
4
|
+
|
|
5
|
+
type Instance_ = Omit<Instance, 'create'>
|
|
6
|
+
|
|
7
|
+
export type Pool<key = number> = Pick<
|
|
8
|
+
Map<key, Instance_>,
|
|
9
|
+
'entries' | 'keys' | 'forEach' | 'get' | 'has' | 'size' | 'values'
|
|
10
|
+
> & {
|
|
11
|
+
_internal: {
|
|
12
|
+
instance: Instance_
|
|
13
|
+
}
|
|
14
|
+
destroy(key: key): Promise<void>
|
|
15
|
+
destroyAll(): Promise<void>
|
|
16
|
+
start(key: key, options?: { port?: number }): Promise<Instance_>
|
|
17
|
+
stop(key: key): Promise<void>
|
|
18
|
+
stopAll(): Promise<void>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type DefinePoolParameters = {
|
|
22
|
+
/** Instance for the pool. */
|
|
23
|
+
instance: Instance
|
|
24
|
+
/** The maximum number of instances that can be started. */
|
|
25
|
+
limit?: number | number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type DefinePoolReturnType<key = number> = Pool<key>
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Defines an instance pool. Instances can be started, cached, and stopped against an identifier.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```
|
|
35
|
+
* const pool = definePool({
|
|
36
|
+
* instance: anvil(),
|
|
37
|
+
* })
|
|
38
|
+
*
|
|
39
|
+
* const instance_1 = await pool.start(1)
|
|
40
|
+
* const instance_2 = await pool.start(2)
|
|
41
|
+
* const instance_3 = await pool.start(3)
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function definePool<key = number>(
|
|
45
|
+
parameters: DefinePoolParameters,
|
|
46
|
+
): DefinePoolReturnType<key> {
|
|
47
|
+
const { instance, limit } = parameters
|
|
48
|
+
|
|
49
|
+
type Instance_ = Omit<Instance, 'create'>
|
|
50
|
+
const instances = new Map<key, Instance_>()
|
|
51
|
+
|
|
52
|
+
// Define promise instances for mutators to avoid race conditions, and return
|
|
53
|
+
// identical instances of the promises (instead of duplicating them).
|
|
54
|
+
// We utilize `Promise.withResolvers` instead of `new Promise((resolve, reject) => ...)`
|
|
55
|
+
// to avoid async Promise executor functions (https://biomejs.dev/linter/rules/no-async-promise-executor/).
|
|
56
|
+
const promises = {
|
|
57
|
+
destroy: new Map<key, Promise<void>>(),
|
|
58
|
+
destroyAll: undefined as Promise<void> | undefined,
|
|
59
|
+
start: new Map<key, Promise<Instance_>>(),
|
|
60
|
+
stop: new Map<key, Promise<void>>(),
|
|
61
|
+
stopAll: undefined as Promise<void> | undefined,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
_internal: {
|
|
66
|
+
instance,
|
|
67
|
+
},
|
|
68
|
+
async destroy(key) {
|
|
69
|
+
const destroyPromise = promises.destroy.get(key)
|
|
70
|
+
if (destroyPromise) return destroyPromise
|
|
71
|
+
|
|
72
|
+
const resolver = Promise.withResolvers<void>()
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
promises.destroy.set(key, resolver.promise)
|
|
76
|
+
|
|
77
|
+
await this.stop(key)
|
|
78
|
+
instances.delete(key)
|
|
79
|
+
|
|
80
|
+
resolver.resolve()
|
|
81
|
+
} catch (error) {
|
|
82
|
+
resolver.reject(error)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return resolver.promise
|
|
86
|
+
},
|
|
87
|
+
async destroyAll() {
|
|
88
|
+
if (promises.destroyAll) return promises.destroyAll
|
|
89
|
+
|
|
90
|
+
const resolver = Promise.withResolvers<void>()
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
promises.destroyAll = resolver.promise
|
|
94
|
+
|
|
95
|
+
await Promise.all([...instances.keys()].map((key) => this.destroy(key)))
|
|
96
|
+
|
|
97
|
+
promises.destroyAll = undefined
|
|
98
|
+
|
|
99
|
+
resolver.resolve()
|
|
100
|
+
} catch (error) {
|
|
101
|
+
resolver.reject(error)
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
async start(key, options = {}) {
|
|
105
|
+
const startPromise = promises.start.get(key)
|
|
106
|
+
if (startPromise) return startPromise
|
|
107
|
+
|
|
108
|
+
const resolver = Promise.withResolvers<Instance_>()
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
promises.start.set(key, resolver.promise)
|
|
112
|
+
|
|
113
|
+
if (limit && instances.size >= limit)
|
|
114
|
+
throw new Error(`Instance limit of ${limit} reached.`)
|
|
115
|
+
|
|
116
|
+
const { port = await getPort() } = options
|
|
117
|
+
|
|
118
|
+
const instance_ = instances.get(key) || instance.create({ port })
|
|
119
|
+
await instance_.start()
|
|
120
|
+
|
|
121
|
+
instances.set(key, instance_)
|
|
122
|
+
resolver.resolve(instance_)
|
|
123
|
+
} catch (error) {
|
|
124
|
+
resolver.reject(error)
|
|
125
|
+
} finally {
|
|
126
|
+
promises.start.delete(key)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return resolver.promise
|
|
130
|
+
},
|
|
131
|
+
async stop(key) {
|
|
132
|
+
const stopPromise = promises.stop.get(key)
|
|
133
|
+
if (stopPromise) return stopPromise
|
|
134
|
+
|
|
135
|
+
const resolver = Promise.withResolvers<void>()
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
promises.stop.set(key, resolver.promise)
|
|
139
|
+
|
|
140
|
+
const instance_ = instances.get(key)
|
|
141
|
+
if (!instance_) {
|
|
142
|
+
resolver.resolve()
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await instance_.stop()
|
|
147
|
+
|
|
148
|
+
resolver.resolve()
|
|
149
|
+
} catch (error) {
|
|
150
|
+
resolver.reject(error)
|
|
151
|
+
} finally {
|
|
152
|
+
promises.stop.delete(key)
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
async stopAll() {
|
|
156
|
+
if (promises.stopAll) return promises.stopAll
|
|
157
|
+
|
|
158
|
+
const resolver = Promise.withResolvers<void>()
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
promises.stopAll = resolver.promise
|
|
162
|
+
|
|
163
|
+
await Promise.all([...instances.keys()].map((key) => this.stop(key)))
|
|
164
|
+
|
|
165
|
+
promises.stopAll = undefined
|
|
166
|
+
|
|
167
|
+
resolver.resolve()
|
|
168
|
+
} catch (error) {
|
|
169
|
+
resolver.reject(error)
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
get size() {
|
|
174
|
+
return instances.size
|
|
175
|
+
},
|
|
176
|
+
entries: instances.entries.bind(instances),
|
|
177
|
+
keys: instances.keys.bind(instances),
|
|
178
|
+
forEach: instances.forEach.bind(instances),
|
|
179
|
+
get: instances.get.bind(instances).bind(instances),
|
|
180
|
+
has: instances.has.bind(instances),
|
|
181
|
+
values: instances.values.bind(instances),
|
|
182
|
+
}
|
|
183
|
+
}
|
package/server.test.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { type MessageEvent, WebSocket } from 'ws'
|
|
3
|
+
import { anvil } from './instances/anvil.js'
|
|
4
|
+
import { createServer } from './server.js'
|
|
5
|
+
|
|
6
|
+
describe.each([{ instance: anvil() }])(
|
|
7
|
+
'instance: $instance.name',
|
|
8
|
+
({ instance }) => {
|
|
9
|
+
test('default', async () => {
|
|
10
|
+
const pool = createServer({
|
|
11
|
+
instance,
|
|
12
|
+
})
|
|
13
|
+
expect(pool).toBeDefined()
|
|
14
|
+
|
|
15
|
+
await pool.start()
|
|
16
|
+
expect(pool.address()).toBeDefined()
|
|
17
|
+
|
|
18
|
+
// Stop via instance method.
|
|
19
|
+
await pool.stop()
|
|
20
|
+
expect(pool.address()).toBeNull()
|
|
21
|
+
|
|
22
|
+
const stop = await pool.start()
|
|
23
|
+
expect(pool.address()).toBeDefined()
|
|
24
|
+
|
|
25
|
+
// Stop via return value.
|
|
26
|
+
await stop()
|
|
27
|
+
expect(pool.address()).toBeNull()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('args: port', async () => {
|
|
31
|
+
const pool = createServer({
|
|
32
|
+
instance,
|
|
33
|
+
port: 3000,
|
|
34
|
+
})
|
|
35
|
+
expect(pool).toBeDefined()
|
|
36
|
+
|
|
37
|
+
const stop = await pool.start()
|
|
38
|
+
expect(pool.address()?.port).toBe(3000)
|
|
39
|
+
await stop()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('args: host', async () => {
|
|
43
|
+
const pool = createServer({
|
|
44
|
+
instance,
|
|
45
|
+
host: 'localhost',
|
|
46
|
+
port: 3000,
|
|
47
|
+
})
|
|
48
|
+
expect(pool).toBeDefined()
|
|
49
|
+
|
|
50
|
+
const stop = await pool.start()
|
|
51
|
+
expect(pool.address()?.address).toBe('::1')
|
|
52
|
+
expect(pool.address()?.port).toBe(3000)
|
|
53
|
+
await stop()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('request: /healthcheck', async () => {
|
|
57
|
+
const server = createServer({
|
|
58
|
+
instance,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const stop = await server.start()
|
|
62
|
+
const { port } = server.address()!
|
|
63
|
+
const response = await fetch(`http://localhost:${port}/healthcheck`)
|
|
64
|
+
expect(response.status).toBe(200)
|
|
65
|
+
|
|
66
|
+
await stop()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('request: /start + /stop', async () => {
|
|
70
|
+
const server = createServer({
|
|
71
|
+
instance,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const stop = await server.start()
|
|
75
|
+
const { port } = server.address()!
|
|
76
|
+
const response = await fetch(`http://localhost:${port}/1/start`)
|
|
77
|
+
expect(response.status).toBe(200)
|
|
78
|
+
|
|
79
|
+
const json = (await response.json()) as any
|
|
80
|
+
expect(json.host).toBeDefined()
|
|
81
|
+
expect(json.port).toBeDefined()
|
|
82
|
+
|
|
83
|
+
const response_err = await fetch(`http://localhost:${port}/1/start`)
|
|
84
|
+
expect(response_err.status).toBe(400)
|
|
85
|
+
expect(await response_err.json()).toEqual({
|
|
86
|
+
message: `Instance "${instance.name}" is not in an idle or stopped state. Status: started`,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const response_stop = await fetch(`http://localhost:${port}/1/stop`)
|
|
90
|
+
expect(response_stop.status).toBe(200)
|
|
91
|
+
|
|
92
|
+
const response_2 = await fetch(`http://localhost:${port}/1/start`)
|
|
93
|
+
expect(response_2.status).toBe(200)
|
|
94
|
+
|
|
95
|
+
await stop()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('ws', async () => {
|
|
99
|
+
const server = createServer({
|
|
100
|
+
instance,
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const stop = await server.start()
|
|
104
|
+
const { port } = server.address()!
|
|
105
|
+
const ws = new WebSocket(`ws://localhost:${port}/1`)
|
|
106
|
+
await new Promise((resolve) => ws.addEventListener('open', resolve))
|
|
107
|
+
ws.send('test')
|
|
108
|
+
await new Promise((resolve) => ws.addEventListener('message', resolve))
|
|
109
|
+
|
|
110
|
+
await stop()
|
|
111
|
+
})
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
describe("instance: 'anvil'", () => {
|
|
116
|
+
test('request: /{id}', async () => {
|
|
117
|
+
const server = createServer({
|
|
118
|
+
instance: anvil(),
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const stop = await server.start()
|
|
122
|
+
const { port } = server.address()!
|
|
123
|
+
const response = await fetch(`http://localhost:${port}/1`)
|
|
124
|
+
// Standard Anvil HTTP response for invalid request.
|
|
125
|
+
expect(response.status).toBe(400)
|
|
126
|
+
expect(await response.text()).toBe(
|
|
127
|
+
"Connection header did not include 'upgrade'",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
// Check block numbers
|
|
131
|
+
expect(
|
|
132
|
+
await fetch(`http://localhost:${port}/1`, {
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
method: 'eth_blockNumber',
|
|
135
|
+
id: 0,
|
|
136
|
+
jsonrpc: '2.0',
|
|
137
|
+
}),
|
|
138
|
+
headers: {
|
|
139
|
+
'Content-Type': 'application/json',
|
|
140
|
+
},
|
|
141
|
+
method: 'POST',
|
|
142
|
+
}).then((x) => x.json()),
|
|
143
|
+
).toMatchInlineSnapshot(`
|
|
144
|
+
{
|
|
145
|
+
"id": 0,
|
|
146
|
+
"jsonrpc": "2.0",
|
|
147
|
+
"result": "0x0",
|
|
148
|
+
}
|
|
149
|
+
`)
|
|
150
|
+
expect(
|
|
151
|
+
await fetch(`http://localhost:${port}/2`, {
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
method: 'eth_blockNumber',
|
|
154
|
+
id: 0,
|
|
155
|
+
jsonrpc: '2.0',
|
|
156
|
+
}),
|
|
157
|
+
headers: {
|
|
158
|
+
'Content-Type': 'application/json',
|
|
159
|
+
},
|
|
160
|
+
method: 'POST',
|
|
161
|
+
}).then((x) => x.json()),
|
|
162
|
+
).toMatchInlineSnapshot(`
|
|
163
|
+
{
|
|
164
|
+
"id": 0,
|
|
165
|
+
"jsonrpc": "2.0",
|
|
166
|
+
"result": "0x0",
|
|
167
|
+
}
|
|
168
|
+
`)
|
|
169
|
+
|
|
170
|
+
// Mine block number
|
|
171
|
+
await fetch(`http://localhost:${port}/1`, {
|
|
172
|
+
body: JSON.stringify({
|
|
173
|
+
method: 'anvil_mine',
|
|
174
|
+
params: ['0x69', '0x0'],
|
|
175
|
+
id: 0,
|
|
176
|
+
jsonrpc: '2.0',
|
|
177
|
+
}),
|
|
178
|
+
headers: {
|
|
179
|
+
'Content-Type': 'application/json',
|
|
180
|
+
},
|
|
181
|
+
method: 'POST',
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// Check block numbers
|
|
185
|
+
expect(
|
|
186
|
+
await fetch(`http://localhost:${port}/1`, {
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
method: 'eth_blockNumber',
|
|
189
|
+
id: 0,
|
|
190
|
+
jsonrpc: '2.0',
|
|
191
|
+
}),
|
|
192
|
+
headers: {
|
|
193
|
+
'Content-Type': 'application/json',
|
|
194
|
+
},
|
|
195
|
+
method: 'POST',
|
|
196
|
+
}).then((x) => x.json()),
|
|
197
|
+
).toMatchInlineSnapshot(`
|
|
198
|
+
{
|
|
199
|
+
"id": 0,
|
|
200
|
+
"jsonrpc": "2.0",
|
|
201
|
+
"result": "0x69",
|
|
202
|
+
}
|
|
203
|
+
`)
|
|
204
|
+
expect(
|
|
205
|
+
await fetch(`http://localhost:${port}/2`, {
|
|
206
|
+
body: JSON.stringify({
|
|
207
|
+
method: 'eth_blockNumber',
|
|
208
|
+
id: 0,
|
|
209
|
+
jsonrpc: '2.0',
|
|
210
|
+
}),
|
|
211
|
+
headers: {
|
|
212
|
+
'Content-Type': 'application/json',
|
|
213
|
+
},
|
|
214
|
+
method: 'POST',
|
|
215
|
+
}).then((x) => x.json()),
|
|
216
|
+
).toMatchInlineSnapshot(`
|
|
217
|
+
{
|
|
218
|
+
"id": 0,
|
|
219
|
+
"jsonrpc": "2.0",
|
|
220
|
+
"result": "0x0",
|
|
221
|
+
}
|
|
222
|
+
`)
|
|
223
|
+
|
|
224
|
+
await stop()
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('request: /messages', async () => {
|
|
228
|
+
const server = createServer({
|
|
229
|
+
instance: anvil(),
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const stop = await server.start()
|
|
233
|
+
const { port } = server.address()!
|
|
234
|
+
|
|
235
|
+
await fetch(`http://localhost:${port}/1`)
|
|
236
|
+
|
|
237
|
+
expect(
|
|
238
|
+
(
|
|
239
|
+
(await fetch(`http://localhost:${port}/1/messages`, {
|
|
240
|
+
body: JSON.stringify({
|
|
241
|
+
method: 'eth_blockNumber',
|
|
242
|
+
id: 0,
|
|
243
|
+
jsonrpc: '2.0',
|
|
244
|
+
}),
|
|
245
|
+
headers: {
|
|
246
|
+
'Content-Type': 'application/json',
|
|
247
|
+
},
|
|
248
|
+
method: 'POST',
|
|
249
|
+
}).then((x) => x.json())) as any
|
|
250
|
+
).length > 0,
|
|
251
|
+
).toBeTruthy()
|
|
252
|
+
|
|
253
|
+
await stop()
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test('ws', async () => {
|
|
257
|
+
const server = createServer({
|
|
258
|
+
instance: anvil(),
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
const stop = await server.start()
|
|
262
|
+
const { port } = server.address()!
|
|
263
|
+
const ws = new WebSocket(`ws://localhost:${port}/1`)
|
|
264
|
+
await new Promise((resolve) => ws.addEventListener('open', resolve))
|
|
265
|
+
ws.send(
|
|
266
|
+
JSON.stringify({
|
|
267
|
+
method: 'eth_blockNumber',
|
|
268
|
+
id: 0,
|
|
269
|
+
jsonrpc: '2.0',
|
|
270
|
+
}),
|
|
271
|
+
)
|
|
272
|
+
const { data } = await new Promise<MessageEvent>((resolve) =>
|
|
273
|
+
ws.addEventListener('message', resolve),
|
|
274
|
+
)
|
|
275
|
+
expect(data).toMatchInlineSnapshot(
|
|
276
|
+
`"{"jsonrpc":"2.0","id":0,"result":"0x0"}"`,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
await stop()
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
test('404', async () => {
|
|
284
|
+
const server = createServer({
|
|
285
|
+
instance: anvil(),
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
const stop = await server.start()
|
|
289
|
+
const { port } = server.address()!
|
|
290
|
+
const response = await fetch(`http://localhost:${port}/wat`)
|
|
291
|
+
expect(response.status).toBe(404)
|
|
292
|
+
|
|
293
|
+
await stop()
|
|
294
|
+
})
|
package/server.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type IncomingMessage,
|
|
3
|
+
type Server,
|
|
4
|
+
type ServerResponse,
|
|
5
|
+
createServer as createServer_,
|
|
6
|
+
} from 'node:http'
|
|
7
|
+
import type { AddressInfo } from 'node:net'
|
|
8
|
+
import { createProxyServer } from 'http-proxy'
|
|
9
|
+
|
|
10
|
+
import { type DefinePoolParameters, definePool } from './pool.js'
|
|
11
|
+
import { extractPath } from './utils.js'
|
|
12
|
+
|
|
13
|
+
export type createServerParameters = DefinePoolParameters &
|
|
14
|
+
(
|
|
15
|
+
| {
|
|
16
|
+
/** Host to run the server on. */
|
|
17
|
+
host?: string | undefined
|
|
18
|
+
/** Port to run the server on. */
|
|
19
|
+
port: number
|
|
20
|
+
}
|
|
21
|
+
| {
|
|
22
|
+
host?: undefined
|
|
23
|
+
port?: undefined
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
export type createServerReturnType = Omit<
|
|
28
|
+
Server<typeof IncomingMessage, typeof ServerResponse>,
|
|
29
|
+
'address'
|
|
30
|
+
> & {
|
|
31
|
+
address(): AddressInfo | null
|
|
32
|
+
start(): Promise<() => Promise<void>>
|
|
33
|
+
stop(): Promise<void>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createServer(
|
|
37
|
+
parameters: createServerParameters,
|
|
38
|
+
): createServerReturnType {
|
|
39
|
+
const { host = '::', instance, limit, port } = parameters
|
|
40
|
+
|
|
41
|
+
const pool = definePool({ instance, limit })
|
|
42
|
+
const proxy = createProxyServer({
|
|
43
|
+
ignorePath: true,
|
|
44
|
+
ws: true,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const server = createServer_(async (request, response) => {
|
|
48
|
+
try {
|
|
49
|
+
const url = request.url
|
|
50
|
+
if (!url) {
|
|
51
|
+
response.end()
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { id, path } = extractPath(url)
|
|
56
|
+
|
|
57
|
+
if (typeof id === 'number' && path === '/') {
|
|
58
|
+
const { host, port } = pool.get(id) || (await pool.start(id))
|
|
59
|
+
return proxy.web(request, response, {
|
|
60
|
+
target: `http://${host}:${port}`,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
if (typeof id === 'number' && path === '/start') {
|
|
64
|
+
const { host, port } = await pool.start(id)
|
|
65
|
+
response
|
|
66
|
+
.writeHead(200, { 'Content-Type': 'application/json' })
|
|
67
|
+
.end(JSON.stringify({ host, port }))
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
if (typeof id === 'number' && path === '/stop') {
|
|
71
|
+
await pool.stop(id)
|
|
72
|
+
response.writeHead(200, { 'Content-Type': 'application/json' }).end()
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
if (typeof id === 'number' && path === '/messages') {
|
|
76
|
+
const messages = pool.get(id)?.messages.get() || []
|
|
77
|
+
response
|
|
78
|
+
.writeHead(200, { 'Content-Type': 'application/json' })
|
|
79
|
+
.end(JSON.stringify(messages))
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (path === '/healthcheck') {
|
|
84
|
+
response.writeHead(200, { 'Content-Type': 'application/json' }).end()
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
response.writeHead(404, { 'Content-Type': 'application/json' }).end()
|
|
89
|
+
return
|
|
90
|
+
} catch (error) {
|
|
91
|
+
response
|
|
92
|
+
.writeHead(400, { 'Content-Type': 'application/json' })
|
|
93
|
+
.end(JSON.stringify({ message: (error as Error).message }))
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
server.on('upgrade', async (request, socket, head) => {
|
|
99
|
+
const url = request.url
|
|
100
|
+
if (!url) {
|
|
101
|
+
socket.destroy(new Error('Unsupported request'))
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { id, path } = extractPath(url)
|
|
106
|
+
|
|
107
|
+
if (typeof id === 'number' && path === '/') {
|
|
108
|
+
const { host, port } = pool.get(id) || (await pool.start(id))
|
|
109
|
+
proxy.ws(request, socket, head, {
|
|
110
|
+
target: `ws://${host}:${port}`,
|
|
111
|
+
})
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
socket.destroy(new Error('Unsupported request'))
|
|
116
|
+
return
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
return Object.assign(server as any, {
|
|
120
|
+
start() {
|
|
121
|
+
return new Promise<() => Promise<void>>((resolve) => {
|
|
122
|
+
if (port) server.listen(port, host, () => resolve(this.stop))
|
|
123
|
+
else server.listen(() => resolve(this.stop))
|
|
124
|
+
})
|
|
125
|
+
},
|
|
126
|
+
async stop() {
|
|
127
|
+
await Promise.allSettled([
|
|
128
|
+
new Promise<void>((resolve, reject) =>
|
|
129
|
+
server.close((error) => (error ? reject(error) : resolve())),
|
|
130
|
+
),
|
|
131
|
+
pool.destroyAll(),
|
|
132
|
+
])
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
}
|