Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/soft-rings-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"prool": patch
---

Cleaned up instance start timeout handling so timed-out starts reject catchably and do not leave the instance stuck in a starting state.
14 changes: 11 additions & 3 deletions src/Instance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,9 +358,17 @@ test('options: timeout', async () => {
})

const instance_1 = foo({ timeout: 100 })
await expect(() => instance_1.start()).rejects.toThrow(
'Instance "foo" failed to start in time',
)
try {
await instance_1.start()
throw new Error('Expected start to time out.')
} catch (error) {
expect(error).toMatchInlineSnapshot(
'[Error: Instance "foo" failed to start in time.]',
)
}
expect(instance_1.status).toBe('idle')
await new Promise((resolve) => setTimeout(resolve, 150))
expect(instance_1.status).toBe('idle')

const bar = Instance.define(() => {
return {
Expand Down
53 changes: 41 additions & 12 deletions src/Instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,24 @@ export function define<
`Instance "${name}" is not in an idle or stopped state. Status: ${status}`,
)

startResolver = Promise.withResolvers<() => void>()
const resolver = startResolver
let timedOut = false
let timer: NodeJS.Timeout | undefined
const clearTimer = () => {
if (timer) clearTimeout(timer)
timer = undefined
}

if (typeof timeout === 'number') {
const timer = setTimeout(() => {
clearTimeout(timer)
startResolver.reject(
timer = setTimeout(() => {
timedOut = true
status = 'idle'
this.messages.clear()
emitter.off('message', onMessage)
emitter.off('listening', onListening)
emitter.off('exit', onExit)
resolver.reject(
new Error(`Instance "${name}" failed to start in time.`),
)
}, timeout)
Expand All @@ -206,29 +220,42 @@ export function define<
},
)
.then(() => {
if (timedOut) return
clearTimer()
status = 'started'

stopResolver = Promise.withResolvers<void>()
startResolver.resolve(this.stop.bind(this))
resolver.resolve(this.stop.bind(this))
})
.catch((error) => {
if (timedOut) return
clearTimer()
status = 'idle'
this.messages.clear()
emitter.off('message', onMessage)
startResolver.reject(error)
emitter.off('listening', onListening)
emitter.off('exit', onExit)
resolver.reject(error)
})

return startResolver.promise
return resolver.promise
},
async stop() {
if (status === 'stopping') return stopResolver.promise
if (status === 'starting')
throw new Error(`Instance "${name}" is starting.`)

stopResolver = Promise.withResolvers<void>()
const resolver = stopResolver
let timer: NodeJS.Timeout | undefined
const clearTimer = () => {
if (timer) clearTimeout(timer)
timer = undefined
}

if (typeof timeout === 'number') {
const timer = setTimeout(() => {
clearTimeout(timer)
stopResolver.reject(
timer = setTimeout(() => {
resolver.reject(
new Error(`Instance "${name}" failed to stop in time.`),
)
}, timeout)
Expand All @@ -240,6 +267,7 @@ export function define<
status: this.status,
})
.then((...args) => {
clearTimer()
status = 'stopped'
this.messages.clear()

Expand All @@ -248,14 +276,15 @@ export function define<
emitter.off('exit', onExit)

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

return stopResolver.promise
return resolver.promise
},
async restart() {
if (restarting) return restartResolver.promise
Expand Down