Skip to content

Commit

Permalink
chore: tweaks
Browse files Browse the repository at this point in the history
  • Loading branch information
jxom committed May 31, 2024
1 parent 02d44a0 commit a52d887
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 19 deletions.
54 changes: 54 additions & 0 deletions src/instance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,28 @@ test('behavior: start', async () => {
)
})

test('behavior: start (error)', async () => {
const foo = defineInstance(() => {
return {
name: 'foo',
host: 'localhost',
port: 3000,
async start() {
throw new Error('oh no')
},
async stop() {},
}
})

const instance = foo()

expect(instance.status).toEqual('idle')
await expect(instance.start()).rejects.toThrowErrorMatchingInlineSnapshot(
'[Error: oh no]',
)
expect(instance.status).toEqual('idle')
})

test('behavior: stop', async () => {
let count = 0
const foo = defineInstance(() => {
Expand Down Expand Up @@ -127,6 +149,30 @@ test('behavior: stop', async () => {
)
})

test('behavior: stop (error)', async () => {
const foo = defineInstance(() => {
return {
name: 'foo',
host: 'localhost',
port: 3000,
async start() {},
async stop() {
throw new Error('oh no')
},
}
})

const instance = foo()

await instance.start()
expect(instance.status).toEqual('started')

await expect(instance.stop()).rejects.toThrowErrorMatchingInlineSnapshot(
'[Error: oh no]',
)
expect(instance.status).toEqual('started')
})

test('behavior: events', async () => {
const foo = defineInstance(() => {
let count = 0
Expand All @@ -136,27 +182,34 @@ test('behavior: events', async () => {
port: 3000,
async start({ emitter }) {
emitter.emit('message', count.toString())
emitter.emit('listening')
if (count > 0) emitter.emit('stderr', 'stderr')
else emitter.emit('stdout', 'stdout')
count++
},
async stop({ emitter }) {
emitter.emit('exit', 0, 'SIGTERM')
emitter.emit('message', 'goodbye')
},
}
})

const listening = Promise.withResolvers<void>()
const message_1 = Promise.withResolvers<string>()
const stdout = Promise.withResolvers<string>()
const stderr = Promise.withResolvers<string>()
const exit = Promise.withResolvers<unknown>()

const instance = foo()
instance.once('listening', listening.resolve)
instance.once('message', message_1.resolve)
instance.once('stdout', stdout.resolve)
instance.once('stderr', stderr.resolve)
instance.once('exit', exit.resolve)

await instance.start()

await listening.promise
expect(await message_1.promise).toEqual('0')
expect(await stdout.promise).toEqual('stdout')

Expand All @@ -166,6 +219,7 @@ test('behavior: events', async () => {
await instance.stop()

expect(await message_2.promise).toEqual('goodbye')
await exit.promise

const message_3 = Promise.withResolvers()
instance.once('message', message_3.resolve)
Expand Down
65 changes: 49 additions & 16 deletions src/instance.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { EventEmitter } from 'eventemitter3'

type EventTypes = {
exit: [code: number | null, signal: NodeJS.Signals | null]
listening: []
message: [message: string]
stderr: [message: string]
stdout: [message: string]
}

type InstanceStartOptions_internal = { emitter: EventEmitter<EventTypes> }
type InstanceStopOptions_internal = { emitter: EventEmitter<EventTypes> }
type InstanceStartOptions_internal = {
emitter: EventEmitter<EventTypes>
status: Instance['status']
}
type InstanceStopOptions_internal = {
emitter: EventEmitter<EventTypes>
status: Instance['status']
}

export type InstanceStartOptions = {
/**
Expand All @@ -16,17 +24,20 @@ export type InstanceStartOptions = {
port?: number | undefined
}

export type DefineInstanceFn<parameters> = (parameters: parameters) => Pick<
Instance,
'host' | 'name' | 'port'
> & {
export type DefineInstanceFn<
parameters,
_internal extends object | undefined = object | undefined,
> = (parameters: parameters) => Pick<Instance, 'host' | 'name' | 'port'> & {
_internal?: _internal | undefined
start(
options: InstanceStartOptions & InstanceStartOptions_internal,
): Promise<void>
stop(options: InstanceStopOptions_internal): Promise<void>
}

export type Instance = Pick<
export type Instance<
_internal extends object | undefined = object | undefined,
> = Pick<
EventEmitter<EventTypes>,
| 'addListener'
| 'off'
Expand All @@ -35,6 +46,7 @@ export type Instance = Pick<
| 'removeAllListeners'
| 'removeListener'
> & {
_internal: _internal
/**
* Name of the instance.
*
Expand Down Expand Up @@ -109,18 +121,19 @@ export type InstanceOptions = {
* })
* ```
*/
export function defineInstance<parameters = undefined>(
fn: DefineInstanceFn<parameters>,
) {
export function defineInstance<
_internal extends object | undefined,
parameters = undefined,
>(fn: DefineInstanceFn<parameters, _internal>) {
return (
...[parametersOrOptions, options_]: parameters extends undefined
? [options?: InstanceOptions]
: [parameters: parameters, options?: InstanceOptions]
): Instance => {
): Instance<_internal> => {
const parameters = parametersOrOptions as parameters
const options = options_ || parametersOrOptions || {}

const { host, name, port, start, stop } = fn(parameters)
const { _internal, host, name, port, start, stop } = fn(parameters)
const { messageBuffer = 20, timeout = 10_000 } = options

let startResolver = Promise.withResolvers<() => void>()
Expand All @@ -135,8 +148,15 @@ export function defineInstance<parameters = undefined>(
messages.push(message)
if (messages.length > messageBuffer) messages.shift()
}
function onListening() {
status = 'started'
}
function onExit() {
status = 'stopped'
}

return {
_internal: _internal as _internal,
host,
messages: {
clear() {
Expand Down Expand Up @@ -168,9 +188,15 @@ export function defineInstance<parameters = undefined>(
}

emitter.on('message', onMessage)
emitter.on('listening', onListening)
emitter.on('exit', onExit)

status = 'starting'
start({ emitter, port })
start({
emitter,
port,
status: this.status,
})
.then(() => {
status = 'started'

Expand Down Expand Up @@ -203,17 +229,24 @@ export function defineInstance<parameters = undefined>(
}

status = 'stopping'
stop({ emitter })
stop({
emitter,
status: this.status,
})
.then((...args) => {
status = 'stopped'
this.messages.clear()

emitter.off('message', onMessage)
emitter.off('listening', onListening)
emitter.off('exit', onExit)

startResolver = Promise.withResolvers<() => void>()
stopResolver.resolve(...args)
})
.catch(() => {
.catch((error) => {
status = 'started'
stopResolver.reject()
stopResolver.reject(error)
})

return stopResolver.promise
Expand Down
31 changes: 31 additions & 0 deletions src/instances/ethereum/anvil.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,34 @@ test('behavior: starts anvil with custom options', async () => {

await instance.start()
})

test('behavior: exit', async () => {
const instance = defineInstance({ timestamp })

let exitCode: number | null | undefined = undefined
instance.on('exit', (code) => {
exitCode = code
})

await instance.start()
expect(instance.status).toEqual('started')

instance._internal.process.kill()

await new Promise<void>((res) => setTimeout(res, 100))
expect(instance.status).toEqual('stopped')
expect(exitCode).toEqual(0)
})

test('behavior: exit when status is starting', async () => {
const instance = defineInstance({ timestamp })

const promise = instance.start()
expect(instance.status).toEqual('starting')

instance._internal.process.kill()

await expect(promise).rejects.toThrowErrorMatchingInlineSnapshot(
'[Error: Anvil exited.]',
)
})
22 changes: 20 additions & 2 deletions src/instances/ethereum/anvil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,15 @@ export const anvil = defineInstance((parameters: AnvilParameters) => {
}

return {
_internal: {
get process() {
return process
},
},
name: 'anvil',
host: args.host ?? '127.0.0.1',
port: args.port ?? 8545,
async start({ emitter, port = args.port }) {
async start({ emitter, port = args.port, status }) {
const { promise, resolve, reject } = Promise.withResolvers<void>()

process = execa(binary, toArgs({ ...args, port }), {
Expand All @@ -283,7 +288,10 @@ export const anvil = defineInstance((parameters: AnvilParameters) => {
const message = stripColors(data.toString())
emitter.emit('message', message)
emitter.emit('stdout', message)
if (message.includes('Listening on')) resolve()
if (message.includes('Listening on')) {
emitter.emit('listening')
resolve()
}
})
process.stderr.on('data', async (data) => {
const message = stripColors(data.toString())
Expand All @@ -292,10 +300,20 @@ export const anvil = defineInstance((parameters: AnvilParameters) => {
await stop()
reject(new Error(`Failed to start anvil: ${data.toString()}`))
})
process.on('close', () => process.removeAllListeners())
process.on('exit', (code, signal) => {
emitter.emit('exit', code, signal)

if (!code) {
process.removeAllListeners()
if (status === 'starting') reject(new Error('Anvil exited.'))
}
})

return promise
},
async stop() {
process.removeAllListeners()
await stop()
},
}
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
// Language and environment
"moduleResolution": "NodeNext",
"module": "NodeNext",
"target": "ES2019",
"target": "ESNext",
"lib": ["ESNext"],
"types": ["@types/node"],

Expand Down

0 comments on commit a52d887

Please sign in to comment.