Skip to content

Commit

Permalink
chore: wip
Browse files Browse the repository at this point in the history
  • Loading branch information
jxom committed May 31, 2024
1 parent 8b3ecce commit e0c772a
Show file tree
Hide file tree
Showing 9 changed files with 584 additions and 123 deletions.
7 changes: 7 additions & 0 deletions src/exports/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export {
type DefineInstanceFn,
type Instance,
type InstanceOptions,
type InstanceStartOptions,
defineInstance,
} from '../instances/defineInstance.js'
1 change: 1 addition & 0 deletions src/exports/instances/ethereum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { type AnvilParameters, anvil } from '../../instances/ethereum/anvil.js'
5 changes: 0 additions & 5 deletions src/index.ts

This file was deleted.

114 changes: 94 additions & 20 deletions src/instances/defineInstance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ test('behavior: start', async () => {
expect(instance.status).toEqual('stopping')

expect(() => instance.start()).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Instance "foo" is not in an idle or stopped state.]`,
`[Error: Instance "foo" is not in an idle or stopped state. Status: stopping]`,
)
})

Expand Down Expand Up @@ -123,10 +123,102 @@ test('behavior: stop', async () => {
expect(instance.status).toEqual('starting')

expect(() => instance.stop()).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Instance "foo" has not started.]`,
`[Error: Instance "foo" has not started. Status: starting]`,
)
})

test('behavior: events', async () => {
const foo = defineInstance(() => {
let count = 0
return {
name: 'foo',
host: 'localhost',
port: 3000,
async start({ emitter }) {
emitter.emit('message', count.toString())
if (count > 0) emitter.emit('stderr', 'stderr')
else emitter.emit('stdout', 'stdout')
count++
},
async stop({ emitter }) {
emitter.emit('message', 'goodbye')
},
}
})

const message_1 = Promise.withResolvers<string>()
const stdout = Promise.withResolvers<string>()
const stderr = Promise.withResolvers<string>()

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

await instance.start()

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

const message_2 = Promise.withResolvers()
instance.once('message', message_2.resolve)

await instance.stop()

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

const message_3 = Promise.withResolvers()
instance.once('message', message_3.resolve)

await instance.start()

expect(await message_3.promise).toEqual('1')
expect(await stderr.promise).toEqual('stderr')
})

test('behavior: messages', async () => {
const foo = defineInstance(() => {
return {
name: 'foo',
host: 'localhost',
port: 3000,
async start({ emitter }) {
for (let i = 0; i < 50; i++) emitter.emit('message', i.toString())
},
async stop() {},
}
})

const instance = foo()
expect(instance.messages.get()).toEqual([])

await instance.start()
expect(instance.messages.get()).toMatchInlineSnapshot(`
[
"30",
"31",
"32",
"33",
"34",
"35",
"36",
"37",
"38",
"39",
"40",
"41",
"42",
"43",
"44",
"45",
"46",
"47",
"48",
"49",
]
`)
})

test('options: timeout', async () => {
const foo = defineInstance(() => {
return {
Expand Down Expand Up @@ -163,21 +255,3 @@ test('options: timeout', async () => {
'Instance "bar" failed to stop in time',
)
})

test('behavior: events', async () => {
const foo = defineInstance(() => {
return {
name: 'foo',
host: 'localhost',
port: 3000,
async start({ emitter }) {
emitter.emit('message', 'hello')
},
async stop() {
emitter.emit('message', 'goodbye')
},
}
})

const instance = foo()
})
58 changes: 38 additions & 20 deletions src/instances/defineInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ type InstanceStartOptions_internal = { emitter: EventEmitter<EventTypes> }
type InstanceStopOptions_internal = { emitter: EventEmitter<EventTypes> }

export type InstanceStartOptions = {
/**
* Port to start the instance on.
*/
port?: number | undefined
}

Expand Down Expand Up @@ -78,9 +81,9 @@ export type Instance = Pick<
}

export type InstanceOptions = {
/** Number of messages to store in-memory. */
/** Number of messages to store in-memory. @default 20 */
messageBuffer?: number
/** Timeout (in milliseconds) for starting and stopping the instance. */
/** Timeout (in milliseconds) for starting and stopping the instance. @default 10_000 */
timeout?: number
}

Expand All @@ -96,16 +99,21 @@ export function defineInstance<parameters = undefined>(
const options = options_ || parametersOrOptions || {}

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

const startResolver = Promise.withResolvers<() => void>()
const stopResolver = Promise.withResolvers<void>()
let startResolver = Promise.withResolvers<() => void>()
let stopResolver = Promise.withResolvers<void>()

const emitter = new EventEmitter<EventTypes>()

let messages: string[] = []
let status: Instance['status'] = 'idle'

function onMessage(message: string) {
messages.push(message)
if (messages.length > messageBuffer) messages.shift()
}

return {
host,
messages: {
Expand All @@ -125,7 +133,7 @@ export function defineInstance<parameters = undefined>(
if (status === 'starting') return startResolver.promise
if (status !== 'idle' && status !== 'stopped')
throw new Error(
`Instance "${name}" is not in an idle or stopped state.`,
`Instance "${name}" is not in an idle or stopped state. Status: ${status}`,
)

if (typeof timeout === 'number') {
Expand All @@ -137,25 +145,31 @@ export function defineInstance<parameters = undefined>(
}, timeout)
}

emitter.on('message', (message) => {
messages.push(message)
if (messages.length > messageBuffer) messages.shift()
})
emitter.on('message', onMessage)

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

stopResolver = Promise.withResolvers<void>()
startResolver.resolve(this.stop)
})
.catch(startResolver.reject)
.catch((error) => {
status = 'idle'
this.messages.clear()
emitter.off('message', onMessage)
startResolver.reject(error)
})

return startResolver.promise
},
async stop() {
if (status === 'stopping') return stopResolver.promise
if (status !== 'started')
throw new Error(`Instance "${name}" has not started.`)
throw new Error(
`Instance "${name}" has not started. Status: ${status}`,
)

if (typeof timeout === 'number') {
const timer = setTimeout(() => {
Expand All @@ -171,20 +185,24 @@ export function defineInstance<parameters = undefined>(
.then((...args) => {
status = 'stopped'
this.messages.clear()
emitter.removeAllListeners()
emitter.off('message', onMessage)
startResolver = Promise.withResolvers<() => void>()
stopResolver.resolve(...args)
})
.catch(stopResolver.reject)
.catch(() => {
status = 'started'
stopResolver.reject()
})

return stopResolver.promise
},

addListener: emitter.addListener,
off: emitter.off,
on: emitter.on,
once: emitter.once,
removeListener: emitter.removeListener,
removeAllListeners: emitter.removeAllListeners,
addListener: emitter.addListener.bind(emitter),
off: emitter.off.bind(emitter),
on: emitter.on.bind(emitter),
once: emitter.once.bind(emitter),
removeListener: emitter.removeListener.bind(emitter),
removeAllListeners: emitter.removeAllListeners.bind(emitter),
}
}
}
Loading

0 comments on commit e0c772a

Please sign in to comment.