fix(tasks): contain async completion listeners

TaskService contained synchronous listener throws but discarded returned promises. An async TaskDoneListener could therefore reject as an unhandled process rejection even though the API promises per-listener containment.

Allow listeners to return PromiseLike<void> and attach a rejection handler to each result without awaiting it. This logs asynchronous failures independently while preserving the observation-only contract: later listeners, task waiters, and teardown are not delayed. Add a regression test and synchronize the public docs and generated API declaration.
This commit is contained in:
Tianyi Cui
2026-07-15 11:40:47 +08:00
parent 1a69a3debe
commit d462348556
7 changed files with 36 additions and 9 deletions

View File

@@ -155,6 +155,23 @@ describe('TaskService reads and settlement', () => {
expect(warn).toHaveBeenCalledWith(expect.stringContaining('listener boom'))
})
it('contains a rejecting onTaskDone listener without starving later listeners', async () => {
const ctx = await harness()
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
const seen: TaskId[] = []
ctx.tasks.onTaskDone(async () => { throw new Error('async listener boom') })
ctx.tasks.onTaskDone(snapshot => void seen.push(snapshot.id))
const p = producer()
const id = ctx.tasks.start(p.spec)
p.settle({ status: 'completed' })
await tick()
expect(seen).toEqual([id])
expect(warn).toHaveBeenCalledWith(expect.stringContaining('onTaskDone listener rejected'))
expect(warn).toHaveBeenCalledWith(expect.stringContaining('async listener boom'))
})
it('contains a rejecting done as a failed outcome (producer contract violation)', async () => {
const ctx = await harness()
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})