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/instance.test.ts
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { expect, test } from 'vitest'
|
|
2
|
+
import { defineInstance } from './instance.js'
|
|
3
|
+
|
|
4
|
+
test('default', async () => {
|
|
5
|
+
let started = false
|
|
6
|
+
const foo = defineInstance(() => {
|
|
7
|
+
return {
|
|
8
|
+
name: 'foo',
|
|
9
|
+
host: 'localhost',
|
|
10
|
+
port: 3000,
|
|
11
|
+
async start() {
|
|
12
|
+
started = true
|
|
13
|
+
},
|
|
14
|
+
async stop() {
|
|
15
|
+
started = false
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const instance = foo()
|
|
21
|
+
|
|
22
|
+
expect(started).toEqual(false)
|
|
23
|
+
await instance.start()
|
|
24
|
+
expect(started).toEqual(true)
|
|
25
|
+
await instance.stop()
|
|
26
|
+
expect(started).toEqual(false)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('behavior: parameters', async () => {
|
|
30
|
+
let started = [false, {}]
|
|
31
|
+
const foo = defineInstance((parameters: { bar: string }) => {
|
|
32
|
+
return {
|
|
33
|
+
name: 'foo',
|
|
34
|
+
host: 'localhost',
|
|
35
|
+
port: 3000,
|
|
36
|
+
async start() {
|
|
37
|
+
started = [true, parameters]
|
|
38
|
+
},
|
|
39
|
+
async stop() {
|
|
40
|
+
started = [false, {}]
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const instance = foo({ bar: 'baz' })
|
|
46
|
+
|
|
47
|
+
expect(started).toEqual([false, {}])
|
|
48
|
+
await instance.start()
|
|
49
|
+
expect(started).toEqual([true, { bar: 'baz' }])
|
|
50
|
+
await instance.stop()
|
|
51
|
+
expect(started).toEqual([false, {}])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('behavior: start', async () => {
|
|
55
|
+
let count = 0
|
|
56
|
+
const foo = defineInstance(() => {
|
|
57
|
+
return {
|
|
58
|
+
name: 'foo',
|
|
59
|
+
host: 'localhost',
|
|
60
|
+
port: 3000,
|
|
61
|
+
async start() {
|
|
62
|
+
count++
|
|
63
|
+
},
|
|
64
|
+
async stop() {},
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const instance = foo()
|
|
69
|
+
|
|
70
|
+
expect(instance.status).toEqual('idle')
|
|
71
|
+
|
|
72
|
+
const promise_1 = instance.start()
|
|
73
|
+
expect(instance.status).toEqual('starting')
|
|
74
|
+
|
|
75
|
+
const promise_2 = instance.start()
|
|
76
|
+
expect(instance.status).toEqual('starting')
|
|
77
|
+
|
|
78
|
+
expect(promise_1).toStrictEqual(promise_2)
|
|
79
|
+
expect(count).toEqual(1)
|
|
80
|
+
|
|
81
|
+
await promise_1
|
|
82
|
+
|
|
83
|
+
expect(instance.status).toEqual('started')
|
|
84
|
+
|
|
85
|
+
instance.stop()
|
|
86
|
+
expect(instance.status).toEqual('stopping')
|
|
87
|
+
|
|
88
|
+
expect(() => instance.start()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
89
|
+
`[Error: Instance "foo" is not in an idle or stopped state. Status: stopping]`,
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('behavior: start (error)', async () => {
|
|
94
|
+
const foo = defineInstance(() => {
|
|
95
|
+
return {
|
|
96
|
+
name: 'foo',
|
|
97
|
+
host: 'localhost',
|
|
98
|
+
port: 3000,
|
|
99
|
+
async start() {
|
|
100
|
+
throw new Error('oh no')
|
|
101
|
+
},
|
|
102
|
+
async stop() {},
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const instance = foo()
|
|
107
|
+
|
|
108
|
+
expect(instance.status).toEqual('idle')
|
|
109
|
+
await expect(instance.start()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
110
|
+
'[Error: oh no]',
|
|
111
|
+
)
|
|
112
|
+
expect(instance.status).toEqual('idle')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('behavior: stop', async () => {
|
|
116
|
+
let count = 0
|
|
117
|
+
const foo = defineInstance(() => {
|
|
118
|
+
return {
|
|
119
|
+
name: 'foo',
|
|
120
|
+
host: 'localhost',
|
|
121
|
+
port: 3000,
|
|
122
|
+
async start() {},
|
|
123
|
+
async stop() {
|
|
124
|
+
count++
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const instance = foo()
|
|
130
|
+
await instance.start()
|
|
131
|
+
|
|
132
|
+
const promise_1 = instance.stop()
|
|
133
|
+
expect(instance.status).toEqual('stopping')
|
|
134
|
+
const promise_2 = instance.stop()
|
|
135
|
+
expect(instance.status).toEqual('stopping')
|
|
136
|
+
|
|
137
|
+
expect(promise_1).toStrictEqual(promise_2)
|
|
138
|
+
expect(count).toEqual(1)
|
|
139
|
+
|
|
140
|
+
await promise_1
|
|
141
|
+
|
|
142
|
+
expect(instance.status).toEqual('stopped')
|
|
143
|
+
|
|
144
|
+
instance.start()
|
|
145
|
+
expect(instance.status).toEqual('starting')
|
|
146
|
+
|
|
147
|
+
expect(() => instance.stop()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
148
|
+
`[Error: Instance "foo" is starting.]`,
|
|
149
|
+
)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('behavior: stop (error)', async () => {
|
|
153
|
+
const foo = defineInstance(() => {
|
|
154
|
+
return {
|
|
155
|
+
name: 'foo',
|
|
156
|
+
host: 'localhost',
|
|
157
|
+
port: 3000,
|
|
158
|
+
async start() {},
|
|
159
|
+
async stop() {
|
|
160
|
+
throw new Error('oh no')
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
const instance = foo()
|
|
166
|
+
|
|
167
|
+
await instance.start()
|
|
168
|
+
expect(instance.status).toEqual('started')
|
|
169
|
+
|
|
170
|
+
await expect(instance.stop()).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
171
|
+
'[Error: oh no]',
|
|
172
|
+
)
|
|
173
|
+
expect(instance.status).toEqual('started')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('behavior: events', async () => {
|
|
177
|
+
const foo = defineInstance(() => {
|
|
178
|
+
let count = 0
|
|
179
|
+
return {
|
|
180
|
+
name: 'foo',
|
|
181
|
+
host: 'localhost',
|
|
182
|
+
port: 3000,
|
|
183
|
+
async start({ emitter }) {
|
|
184
|
+
emitter.emit('message', count.toString())
|
|
185
|
+
emitter.emit('listening')
|
|
186
|
+
if (count > 0) emitter.emit('stderr', 'stderr')
|
|
187
|
+
else emitter.emit('stdout', 'stdout')
|
|
188
|
+
count++
|
|
189
|
+
},
|
|
190
|
+
async stop({ emitter }) {
|
|
191
|
+
emitter.emit('exit', 0, 'SIGTERM')
|
|
192
|
+
emitter.emit('message', 'goodbye')
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const listening = Promise.withResolvers<void>()
|
|
198
|
+
const message_1 = Promise.withResolvers<string>()
|
|
199
|
+
const stdout = Promise.withResolvers<string>()
|
|
200
|
+
const stderr = Promise.withResolvers<string>()
|
|
201
|
+
const exit = Promise.withResolvers<unknown>()
|
|
202
|
+
|
|
203
|
+
const instance = foo()
|
|
204
|
+
instance.once('listening', listening.resolve)
|
|
205
|
+
instance.once('message', message_1.resolve)
|
|
206
|
+
instance.once('stdout', stdout.resolve)
|
|
207
|
+
instance.once('stderr', stderr.resolve)
|
|
208
|
+
instance.once('exit', exit.resolve)
|
|
209
|
+
|
|
210
|
+
await instance.start()
|
|
211
|
+
|
|
212
|
+
await listening.promise
|
|
213
|
+
expect(await message_1.promise).toEqual('0')
|
|
214
|
+
expect(await stdout.promise).toEqual('stdout')
|
|
215
|
+
|
|
216
|
+
const message_2 = Promise.withResolvers()
|
|
217
|
+
instance.once('message', message_2.resolve)
|
|
218
|
+
|
|
219
|
+
await instance.stop()
|
|
220
|
+
|
|
221
|
+
expect(await message_2.promise).toEqual('goodbye')
|
|
222
|
+
await exit.promise
|
|
223
|
+
|
|
224
|
+
const message_3 = Promise.withResolvers()
|
|
225
|
+
instance.once('message', message_3.resolve)
|
|
226
|
+
|
|
227
|
+
await instance.start()
|
|
228
|
+
|
|
229
|
+
expect(await message_3.promise).toEqual('1')
|
|
230
|
+
expect(await stderr.promise).toEqual('stderr')
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test('behavior: messages', async () => {
|
|
234
|
+
const foo = defineInstance(() => {
|
|
235
|
+
return {
|
|
236
|
+
name: 'foo',
|
|
237
|
+
host: 'localhost',
|
|
238
|
+
port: 3000,
|
|
239
|
+
async start({ emitter }) {
|
|
240
|
+
for (let i = 0; i < 50; i++) emitter.emit('message', i.toString())
|
|
241
|
+
},
|
|
242
|
+
async stop() {},
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
const instance = foo()
|
|
247
|
+
expect(instance.messages.get()).toEqual([])
|
|
248
|
+
|
|
249
|
+
await instance.start()
|
|
250
|
+
expect(instance.messages.get()).toMatchInlineSnapshot(`
|
|
251
|
+
[
|
|
252
|
+
"30",
|
|
253
|
+
"31",
|
|
254
|
+
"32",
|
|
255
|
+
"33",
|
|
256
|
+
"34",
|
|
257
|
+
"35",
|
|
258
|
+
"36",
|
|
259
|
+
"37",
|
|
260
|
+
"38",
|
|
261
|
+
"39",
|
|
262
|
+
"40",
|
|
263
|
+
"41",
|
|
264
|
+
"42",
|
|
265
|
+
"43",
|
|
266
|
+
"44",
|
|
267
|
+
"45",
|
|
268
|
+
"46",
|
|
269
|
+
"47",
|
|
270
|
+
"48",
|
|
271
|
+
"49",
|
|
272
|
+
]
|
|
273
|
+
`)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
test('options: timeout', async () => {
|
|
277
|
+
const foo = defineInstance(() => {
|
|
278
|
+
return {
|
|
279
|
+
name: 'foo',
|
|
280
|
+
host: 'localhost',
|
|
281
|
+
port: 3000,
|
|
282
|
+
async start() {
|
|
283
|
+
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
284
|
+
},
|
|
285
|
+
async stop() {},
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
const instance_1 = foo({ timeout: 100 })
|
|
290
|
+
await expect(() => instance_1.start()).rejects.toThrow(
|
|
291
|
+
'Instance "foo" failed to start in time',
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
const bar = defineInstance(() => {
|
|
295
|
+
return {
|
|
296
|
+
name: 'bar',
|
|
297
|
+
host: 'localhost',
|
|
298
|
+
port: 3000,
|
|
299
|
+
async start() {},
|
|
300
|
+
async stop() {
|
|
301
|
+
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
const instance_2 = bar({ timeout: 100 })
|
|
307
|
+
await instance_2.start()
|
|
308
|
+
await expect(() => instance_2.stop()).rejects.toThrow(
|
|
309
|
+
'Instance "bar" failed to stop in time',
|
|
310
|
+
)
|
|
311
|
+
})
|
package/instance.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { EventEmitter } from 'eventemitter3'
|
|
2
|
+
|
|
3
|
+
type EventTypes = {
|
|
4
|
+
exit: [code: number | null, signal: NodeJS.Signals | null]
|
|
5
|
+
listening: []
|
|
6
|
+
message: [message: string]
|
|
7
|
+
stderr: [message: string]
|
|
8
|
+
stdout: [message: string]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type InstanceStartOptions_internal = {
|
|
12
|
+
emitter: EventEmitter<EventTypes>
|
|
13
|
+
status: Instance['status']
|
|
14
|
+
}
|
|
15
|
+
type InstanceStopOptions_internal = {
|
|
16
|
+
emitter: EventEmitter<EventTypes>
|
|
17
|
+
status: Instance['status']
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type InstanceStartOptions = {
|
|
21
|
+
/**
|
|
22
|
+
* Port to start the instance on.
|
|
23
|
+
*/
|
|
24
|
+
port?: number | undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type DefineInstanceFn<
|
|
28
|
+
parameters,
|
|
29
|
+
_internal extends object | undefined = object | undefined,
|
|
30
|
+
> = (parameters: parameters) => Pick<Instance, 'host' | 'name' | 'port'> & {
|
|
31
|
+
_internal?: _internal | undefined
|
|
32
|
+
start(
|
|
33
|
+
options: InstanceStartOptions & InstanceStartOptions_internal,
|
|
34
|
+
): Promise<void>
|
|
35
|
+
stop(options: InstanceStopOptions_internal): Promise<void>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type Instance<
|
|
39
|
+
_internal extends object | undefined = object | undefined,
|
|
40
|
+
> = Pick<
|
|
41
|
+
EventEmitter<EventTypes>,
|
|
42
|
+
| 'addListener'
|
|
43
|
+
| 'off'
|
|
44
|
+
| 'on'
|
|
45
|
+
| 'once'
|
|
46
|
+
| 'removeAllListeners'
|
|
47
|
+
| 'removeListener'
|
|
48
|
+
> & {
|
|
49
|
+
_internal: _internal
|
|
50
|
+
/**
|
|
51
|
+
* Creates an instance.
|
|
52
|
+
*/
|
|
53
|
+
create(
|
|
54
|
+
parameters?: { port?: number | undefined } | undefined,
|
|
55
|
+
): Omit<Instance<_internal>, 'create'>
|
|
56
|
+
/**
|
|
57
|
+
* Host the instance is running on.
|
|
58
|
+
*/
|
|
59
|
+
host: string
|
|
60
|
+
/**
|
|
61
|
+
* Name of the instance.
|
|
62
|
+
*
|
|
63
|
+
* @example "anvil"
|
|
64
|
+
*/
|
|
65
|
+
name: string
|
|
66
|
+
/**
|
|
67
|
+
* Port the instance is running on.
|
|
68
|
+
*/
|
|
69
|
+
port: number
|
|
70
|
+
/**
|
|
71
|
+
* Set of messages emitted from the `"message"` event stored in-memory,
|
|
72
|
+
* with length {@link InstanceOptions`messageBuffer`}.
|
|
73
|
+
* Useful for debugging.
|
|
74
|
+
*
|
|
75
|
+
* @example ["Listening on http://127.0.0.1", "Started successfully."]
|
|
76
|
+
*/
|
|
77
|
+
messages: { clear(): void; get(): string[] }
|
|
78
|
+
/**
|
|
79
|
+
* Status of the instance.
|
|
80
|
+
*
|
|
81
|
+
* @default "idle"
|
|
82
|
+
*/
|
|
83
|
+
status: 'idle' | 'stopped' | 'starting' | 'started' | 'stopping'
|
|
84
|
+
/**
|
|
85
|
+
* Starts the instance.
|
|
86
|
+
*
|
|
87
|
+
* @param options - Options for starting the instance.
|
|
88
|
+
* @returns A function to stop the instance.
|
|
89
|
+
*/
|
|
90
|
+
start(): Promise<() => void>
|
|
91
|
+
/**
|
|
92
|
+
* Stops the instance.
|
|
93
|
+
*/
|
|
94
|
+
stop(): Promise<void>
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type InstanceOptions = {
|
|
98
|
+
/** Number of messages to store in-memory. @default 20 */
|
|
99
|
+
messageBuffer?: number
|
|
100
|
+
/** Timeout (in milliseconds) for starting and stopping the instance. @default 10_000 */
|
|
101
|
+
timeout?: number
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type DefineInstanceReturnType<
|
|
105
|
+
_internal extends object | undefined = object | undefined,
|
|
106
|
+
parameters = undefined,
|
|
107
|
+
> = (
|
|
108
|
+
...parameters: parameters extends undefined
|
|
109
|
+
? [options?: InstanceOptions]
|
|
110
|
+
: [parameters: parameters, options?: InstanceOptions]
|
|
111
|
+
) => Instance<_internal>
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Creates an instance definition.
|
|
115
|
+
*
|
|
116
|
+
* @param fn - Function to define the instance.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* const foo = defineInstance((parameters: FooParameters) => {
|
|
121
|
+
* return {
|
|
122
|
+
* name: 'foo',
|
|
123
|
+
* host: 'localhost',
|
|
124
|
+
* port: 3000,
|
|
125
|
+
* async start() {
|
|
126
|
+
* // ...
|
|
127
|
+
* },
|
|
128
|
+
* async stop() {
|
|
129
|
+
* // ...
|
|
130
|
+
* },
|
|
131
|
+
* }
|
|
132
|
+
* })
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export function defineInstance<
|
|
136
|
+
_internal extends object | undefined,
|
|
137
|
+
parameters = undefined,
|
|
138
|
+
>(
|
|
139
|
+
fn: DefineInstanceFn<parameters, _internal>,
|
|
140
|
+
): DefineInstanceReturnType<_internal, parameters> {
|
|
141
|
+
return (...[parametersOrOptions, options_]) => {
|
|
142
|
+
function create(createParameters: Parameters<Instance['create']>[0] = {}) {
|
|
143
|
+
const parameters = parametersOrOptions as parameters
|
|
144
|
+
const options = options_ || parametersOrOptions || {}
|
|
145
|
+
|
|
146
|
+
const { _internal, host, name, port, start, stop } = {
|
|
147
|
+
...fn(parameters),
|
|
148
|
+
...createParameters,
|
|
149
|
+
}
|
|
150
|
+
const { messageBuffer = 20, timeout = 10_000 } = options
|
|
151
|
+
|
|
152
|
+
let startResolver = Promise.withResolvers<() => void>()
|
|
153
|
+
let stopResolver = Promise.withResolvers<void>()
|
|
154
|
+
|
|
155
|
+
const emitter = new EventEmitter<EventTypes>()
|
|
156
|
+
|
|
157
|
+
let messages: string[] = []
|
|
158
|
+
let status: Instance['status'] = 'idle'
|
|
159
|
+
|
|
160
|
+
function onExit() {
|
|
161
|
+
status = 'stopped'
|
|
162
|
+
}
|
|
163
|
+
function onListening() {
|
|
164
|
+
status = 'started'
|
|
165
|
+
}
|
|
166
|
+
function onMessage(message: string) {
|
|
167
|
+
messages.push(message)
|
|
168
|
+
if (messages.length > messageBuffer) messages.shift()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
_internal: _internal as _internal,
|
|
173
|
+
messages: {
|
|
174
|
+
clear() {
|
|
175
|
+
messages = []
|
|
176
|
+
},
|
|
177
|
+
get() {
|
|
178
|
+
return messages
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
host,
|
|
182
|
+
name,
|
|
183
|
+
port,
|
|
184
|
+
get status() {
|
|
185
|
+
return status
|
|
186
|
+
},
|
|
187
|
+
async start() {
|
|
188
|
+
if (status === 'starting') return startResolver.promise
|
|
189
|
+
if (status !== 'idle' && status !== 'stopped')
|
|
190
|
+
throw new Error(
|
|
191
|
+
`Instance "${name}" is not in an idle or stopped state. Status: ${status}`,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if (typeof timeout === 'number') {
|
|
195
|
+
const timer = setTimeout(() => {
|
|
196
|
+
clearTimeout(timer)
|
|
197
|
+
startResolver.reject(
|
|
198
|
+
new Error(`Instance "${name}" failed to start in time.`),
|
|
199
|
+
)
|
|
200
|
+
}, timeout)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
emitter.on('message', onMessage)
|
|
204
|
+
emitter.on('listening', onListening)
|
|
205
|
+
emitter.on('exit', onExit)
|
|
206
|
+
|
|
207
|
+
status = 'starting'
|
|
208
|
+
start({
|
|
209
|
+
emitter,
|
|
210
|
+
port,
|
|
211
|
+
status: this.status,
|
|
212
|
+
})
|
|
213
|
+
.then(() => {
|
|
214
|
+
status = 'started'
|
|
215
|
+
|
|
216
|
+
stopResolver = Promise.withResolvers<void>()
|
|
217
|
+
startResolver.resolve(this.stop)
|
|
218
|
+
})
|
|
219
|
+
.catch((error) => {
|
|
220
|
+
status = 'idle'
|
|
221
|
+
this.messages.clear()
|
|
222
|
+
emitter.off('message', onMessage)
|
|
223
|
+
startResolver.reject(error)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
return startResolver.promise
|
|
227
|
+
},
|
|
228
|
+
async stop() {
|
|
229
|
+
if (status === 'stopping') return stopResolver.promise
|
|
230
|
+
if (status === 'starting')
|
|
231
|
+
throw new Error(`Instance "${name}" is starting.`)
|
|
232
|
+
|
|
233
|
+
if (typeof timeout === 'number') {
|
|
234
|
+
const timer = setTimeout(() => {
|
|
235
|
+
clearTimeout(timer)
|
|
236
|
+
stopResolver.reject(
|
|
237
|
+
new Error(`Instance "${name}" failed to stop in time.`),
|
|
238
|
+
)
|
|
239
|
+
}, timeout)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
status = 'stopping'
|
|
243
|
+
stop({
|
|
244
|
+
emitter,
|
|
245
|
+
status: this.status,
|
|
246
|
+
})
|
|
247
|
+
.then((...args) => {
|
|
248
|
+
status = 'stopped'
|
|
249
|
+
this.messages.clear()
|
|
250
|
+
|
|
251
|
+
emitter.off('message', onMessage)
|
|
252
|
+
emitter.off('listening', onListening)
|
|
253
|
+
emitter.off('exit', onExit)
|
|
254
|
+
|
|
255
|
+
startResolver = Promise.withResolvers<() => void>()
|
|
256
|
+
stopResolver.resolve(...args)
|
|
257
|
+
})
|
|
258
|
+
.catch((error) => {
|
|
259
|
+
status = 'started'
|
|
260
|
+
stopResolver.reject(error)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
return stopResolver.promise
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
addListener: emitter.addListener.bind(emitter),
|
|
267
|
+
off: emitter.off.bind(emitter),
|
|
268
|
+
on: emitter.on.bind(emitter),
|
|
269
|
+
once: emitter.once.bind(emitter),
|
|
270
|
+
removeListener: emitter.removeListener.bind(emitter),
|
|
271
|
+
removeAllListeners: emitter.removeAllListeners.bind(emitter),
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return Object.assign(create(), { create })
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { afterEach, expect, test } from 'vitest'
|
|
2
|
+
import type { Instance } from '../instance.js'
|
|
3
|
+
import { type AnvilParameters, anvil } from './anvil.js'
|
|
4
|
+
|
|
5
|
+
const instances: Instance[] = []
|
|
6
|
+
const timestamp = 1717114065
|
|
7
|
+
|
|
8
|
+
const defineInstance = (parameters: AnvilParameters = {}) => {
|
|
9
|
+
const instance = anvil(parameters)
|
|
10
|
+
instances.push(instance)
|
|
11
|
+
return instance
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
for (const instance of instances) await instance.stop().catch(() => {})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('default', async () => {
|
|
19
|
+
const messages: string[] = []
|
|
20
|
+
const stdouts: string[] = []
|
|
21
|
+
|
|
22
|
+
const instance = defineInstance({ timestamp })
|
|
23
|
+
|
|
24
|
+
instance.on('message', (m) => messages.push(m))
|
|
25
|
+
instance.on('stdout', (m) => stdouts.push(m))
|
|
26
|
+
|
|
27
|
+
expect(instance.messages.get()).toMatchInlineSnapshot('[]')
|
|
28
|
+
|
|
29
|
+
await instance.start()
|
|
30
|
+
expect(instance.status).toEqual('started')
|
|
31
|
+
|
|
32
|
+
expect(messages.join('')).toBeDefined()
|
|
33
|
+
expect(stdouts.join('')).toBeDefined()
|
|
34
|
+
expect(instance.messages.get().join('')).toBeDefined()
|
|
35
|
+
|
|
36
|
+
await instance.stop()
|
|
37
|
+
expect(instance.status).toEqual('stopped')
|
|
38
|
+
|
|
39
|
+
expect(messages.join('')).toBeDefined()
|
|
40
|
+
expect(stdouts.join('')).toBeDefined()
|
|
41
|
+
expect(instance.messages.get()).toMatchInlineSnapshot('[]')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('behavior: instance errored (duplicate ports)', async () => {
|
|
45
|
+
const instance_1 = defineInstance({ timestamp, port: 8545 })
|
|
46
|
+
const instance_2 = defineInstance({ timestamp, port: 8545 })
|
|
47
|
+
|
|
48
|
+
await instance_1.start()
|
|
49
|
+
await expect(() => instance_2.start()).rejects.toThrowError(
|
|
50
|
+
'Failed to start anvil',
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('behavior: start and stop multiple times', async () => {
|
|
55
|
+
const instance = defineInstance()
|
|
56
|
+
|
|
57
|
+
await instance.start()
|
|
58
|
+
await instance.stop()
|
|
59
|
+
await instance.start()
|
|
60
|
+
await instance.stop()
|
|
61
|
+
await instance.start()
|
|
62
|
+
await instance.stop()
|
|
63
|
+
await instance.start()
|
|
64
|
+
await instance.stop()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('behavior: can subscribe to stdout', async () => {
|
|
68
|
+
const messages: string[] = []
|
|
69
|
+
const instance = defineInstance({ timestamp })
|
|
70
|
+
instance.on('stdout', (message) => messages.push(message))
|
|
71
|
+
|
|
72
|
+
await instance.start()
|
|
73
|
+
expect(messages.length).toBeGreaterThanOrEqual(1)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('behavior: can subscribe to stderr', async () => {
|
|
77
|
+
const messages: string[] = []
|
|
78
|
+
|
|
79
|
+
const instance_1 = defineInstance({ timestamp, port: 8545 })
|
|
80
|
+
const instance_2 = defineInstance({ timestamp, port: 8545 })
|
|
81
|
+
|
|
82
|
+
await instance_1.start()
|
|
83
|
+
instance_2.on('stderr', (message) => messages.push(message))
|
|
84
|
+
await expect(instance_2.start()).rejects.toThrow('Failed to start anvil')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('behavior: starts anvil with custom options', async () => {
|
|
88
|
+
const instance = defineInstance({
|
|
89
|
+
timestamp,
|
|
90
|
+
chainId: 123,
|
|
91
|
+
forkBlockNumber: 69420,
|
|
92
|
+
forkUrl: 'https://cloudflare-eth.com',
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
await instance.start()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('behavior: exit', async () => {
|
|
99
|
+
const instance = defineInstance({ timestamp })
|
|
100
|
+
|
|
101
|
+
let exitCode: number | null | undefined = undefined
|
|
102
|
+
instance.on('exit', (code) => {
|
|
103
|
+
exitCode = code
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
await instance.start()
|
|
107
|
+
expect(instance.status).toEqual('started')
|
|
108
|
+
|
|
109
|
+
instance._internal.process.kill()
|
|
110
|
+
|
|
111
|
+
await new Promise<void>((res) => setTimeout(res, 100))
|
|
112
|
+
expect(instance.status).toEqual('stopped')
|
|
113
|
+
expect(typeof exitCode !== 'undefined').toBeTruthy()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('behavior: exit when status is starting', async () => {
|
|
117
|
+
const instance = defineInstance({ timestamp })
|
|
118
|
+
|
|
119
|
+
const promise = instance.start()
|
|
120
|
+
expect(instance.status).toEqual('starting')
|
|
121
|
+
|
|
122
|
+
instance._internal.process.kill()
|
|
123
|
+
|
|
124
|
+
await expect(promise).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
125
|
+
'[Error: Failed to start anvil: exited.]',
|
|
126
|
+
)
|
|
127
|
+
})
|