Skip to content

Commit

Permalink
feat: restart
Browse files Browse the repository at this point in the history
  • Loading branch information
jxom committed Jun 3, 2024
1 parent fe89060 commit c208423
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 113 deletions.
6 changes: 6 additions & 0 deletions .changeset/slimy-dolphins-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"prool": patch
---

Added `/restart` endpoint to the Pool Server.
Added `restart` method to pool instances.
1 change: 1 addition & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"noNonNullAssertion": "off"
},
"suspicious": {
"noAssignInExpressions": "off",
"noExplicitAny": "off"
}
}
Expand Down
32 changes: 32 additions & 0 deletions src/instance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,38 @@ test('behavior: stop (error)', async () => {
expect(instance.status).toEqual('started')
})

test('behavior: restart', async () => {
let count = 0
const foo = defineInstance(() => {
return {
name: 'foo',
host: 'localhost',
port: 3000,
async start() {
count++
},
async stop() {},
}
})

const instance = foo()
await instance.start()

expect(instance.status).toEqual('started')
const promise_1 = instance.restart()
expect(instance.status).toEqual('restarting')
const promise_2 = instance.restart()
expect(instance.status).toEqual('restarting')

expect(promise_1).toStrictEqual(promise_2)

await promise_1
await promise_2

expect(instance.status).toEqual('started')
expect(count).toEqual(2)
})

test('behavior: events', async () => {
const foo = defineInstance(() => {
let count = 0
Expand Down
34 changes: 31 additions & 3 deletions src/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,25 @@ export type Instance<
* @example ["Listening on http://127.0.0.1", "Started successfully."]
*/
messages: { clear(): void; get(): string[] }
/**
* Retarts the instance.
*/
restart(): Promise<void>
/**
* Status of the instance.
*
* @default "idle"
*/
status: 'idle' | 'stopped' | 'starting' | 'started' | 'stopping'
status:
| 'idle'
| 'restarting'
| 'stopped'
| 'starting'
| 'started'
| 'stopping'
/**
* Starts the instance.
*
* @param options - Options for starting the instance.
* @returns A function to stop the instance.
*/
start(): Promise<() => void>
Expand Down Expand Up @@ -147,15 +156,17 @@ export function defineInstance<
...fn(parameters),
...createParameters,
}
const { messageBuffer = 20, timeout = 10_000 } = options
const { messageBuffer = 20, timeout } = options

let restartResolver = 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'
let restarting = false

function onExit() {
status = 'stopped'
Expand All @@ -182,6 +193,7 @@ export function defineInstance<
name,
port,
get status() {
if (restarting) return 'restarting'
return status
},
async start() {
Expand Down Expand Up @@ -262,6 +274,22 @@ export function defineInstance<

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

restarting = true

this.stop()
.then(() => this.start.bind(this)())
.then(() => restartResolver.resolve())
.catch(restartResolver.reject)
.finally(() => {
restartResolver = Promise.withResolvers<void>()
restarting = false
})

return restartResolver.promise
},

addListener: emitter.addListener.bind(emitter),
off: emitter.off.bind(emitter),
Expand Down
41 changes: 41 additions & 0 deletions src/pool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,26 @@ describe.each([{ instance: anvil() }])(
expect(pool.size).toEqual(0)
})

test('restart', async () => {
pool = definePool({
instance,
})

const instance_1 = await pool.start(1)
const instance_2 = await pool.start(2)
const instance_3 = await pool.start(3)

expect(instance_1.status).toBe('started')
expect(instance_2.status).toBe('started')
expect(instance_3.status).toBe('started')
expect(pool.size).toEqual(3)

const promise_1 = pool.restart(1)
expect(instance_1.status).toBe('restarting')
await promise_1
expect(instance_1.status).toBe('started')
})

test('start > stop > start', async () => {
pool = definePool({
instance,
Expand Down Expand Up @@ -160,6 +180,27 @@ describe.each([{ instance: anvil() }])(
await promise_2
})

test('behavior: restart more than once', async () => {
pool = definePool({
instance,
})

const instance_1 = await pool.start(1)
expect(instance_1.status).toBe('started')

const promise_1 = pool.restart(1)
expect(instance_1.status).toBe('restarting')
const promise_2 = pool.restart(1)
expect(instance_1.status).toBe('restarting')

expect(promise_1).toStrictEqual(promise_2)

await promise_1
expect(instance_1.status).toBe('started')
await promise_2
expect(instance_1.status).toBe('started')
})

test('behavior: stop more than once', async () => {
pool = definePool({
instance,
Expand Down
124 changes: 66 additions & 58 deletions src/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type Pool<key = number> = Pick<
}
destroy(key: key): Promise<void>
destroyAll(): Promise<void>
restart(key: key): Promise<void>
start(key: key, options?: { port?: number }): Promise<Instance_>
stop(key: key): Promise<void>
stopAll(): Promise<void>
Expand Down Expand Up @@ -56,6 +57,7 @@ export function definePool<key = number>(
const promises = {
destroy: new Map<key, Promise<void>>(),
destroyAll: undefined as Promise<void> | undefined,
restart: new Map<key, Promise<void>>(),
start: new Map<key, Promise<Instance_>>(),
stop: new Map<key, Promise<void>>(),
stopAll: undefined as Promise<void> | undefined,
Expand All @@ -71,16 +73,14 @@ export function definePool<key = number>(

const resolver = Promise.withResolvers<void>()

try {
promises.destroy.set(key, resolver.promise)
promises.destroy.set(key, resolver.promise)

await this.stop(key)
instances.delete(key)

resolver.resolve()
} catch (error) {
resolver.reject(error)
}
this.stop(key)
.then(() => {
instances.delete(key)
resolver.resolve()
})
.catch(resolver.reject)

return resolver.promise
},
Expand All @@ -89,85 +89,93 @@ export function definePool<key = number>(

const resolver = Promise.withResolvers<void>()

try {
promises.destroyAll = resolver.promise
promises.destroyAll = resolver.promise

await Promise.all([...instances.keys()].map((key) => this.destroy(key)))
Promise.all([...instances.keys()].map((key) => this.destroy(key)))
.then(() => {
promises.destroyAll = undefined
resolver.resolve()
})
.catch(resolver.reject)

promises.destroyAll = undefined
return resolver.promise
},
async restart(key) {
const restartPromise = promises.restart.get(key)
if (restartPromise) return restartPromise

resolver.resolve()
} catch (error) {
resolver.reject(error)
}
const resolver = Promise.withResolvers<void>()

const instance_ = instances.get(key)
if (!instance_) return

promises.restart.set(key, resolver.promise)

instance_
.restart()
.then(resolver.resolve)
.catch(resolver.reject)
.finally(() => promises.restart.delete(key))

return resolver.promise
},
async start(key, options = {}) {
const startPromise = promises.start.get(key)
if (startPromise) return startPromise

const resolver = Promise.withResolvers<Instance_>()

try {
promises.start.set(key, resolver.promise)
if (limit && instances.size >= limit)
throw new Error(`Instance limit of ${limit} reached.`)

if (limit && instances.size >= limit)
throw new Error(`Instance limit of ${limit} reached.`)
promises.start.set(key, resolver.promise)

const { port = await getPort() } = options

const instance_ = instances.get(key) || instance.create({ port })
await instance_.start()

instances.set(key, instance_)
resolver.resolve(instance_)
} catch (error) {
resolver.reject(error)
} finally {
promises.start.delete(key)
}
const { port = await getPort() } = options
const instance_ = instances.get(key) || instance.create({ port })
instance_
.start()
.then(() => {
instances.set(key, instance_)
resolver.resolve(instance_)
})
.catch(resolver.reject)
.finally(() => promises.start.delete(key))

return resolver.promise
},
async stop(key) {
const stopPromise = promises.stop.get(key)
if (stopPromise) return stopPromise

const resolver = Promise.withResolvers<void>()
const instance_ = instances.get(key)
if (!instance_) return

try {
promises.stop.set(key, resolver.promise)

const instance_ = instances.get(key)
if (!instance_) {
resolver.resolve()
return
}
const resolver = Promise.withResolvers<void>()

await instance_.stop()
promises.stop.set(key, resolver.promise)
instance_
.stop()
.then(resolver.resolve)
.catch(resolver.reject)
.finally(() => promises.stop.delete(key))

resolver.resolve()
} catch (error) {
resolver.reject(error)
} finally {
promises.stop.delete(key)
}
return resolver.promise
},
async stopAll() {
if (promises.stopAll) return promises.stopAll

const resolver = Promise.withResolvers<void>()

try {
promises.stopAll = resolver.promise

await Promise.all([...instances.keys()].map((key) => this.stop(key)))
promises.stopAll = resolver.promise

promises.stopAll = undefined
Promise.all([...instances.keys()].map((key) => this.stop(key)))
.then(() => {
promises.stopAll = undefined
resolver.resolve()
})
.catch(resolver.reject)

resolver.resolve()
} catch (error) {
resolver.reject(error)
}
return resolver.promise
},

get size() {
Expand Down
Loading

0 comments on commit c208423

Please sign in to comment.