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

@@ -350,8 +350,9 @@ export class TaskService extends Service {
* Register a completion listener, called exactly once per terminal task
* record with its snapshot and exact lifecycle owner (or `undefined` for an
* unowned task). Effect-scoped (disposed with the calling fiber); per-listener
* containment (one throwing listener is logged, never starves the rest);
* never fires after this service is disposed.
* containment (one throwing or rejecting listener is logged, never starves
* the rest); returned promises are observed but not awaited; never fires
* after this service is disposed.
* @param listener - called with each terminal snapshot and its exact owner.
* @returns the disposer that unregisters the listener.
*/
@@ -440,7 +441,10 @@ export class TaskService extends Service {
const snapshot = this.snapshot(task)
for (const listener of this.listeners) {
try {
listener(snapshot, task.owner)
const returned = listener(snapshot, task.owner)
void Promise.resolve(returned).catch((error: unknown) => {
this.selfCtx.logger.warn(`tasks: onTaskDone listener rejected for ${task.id}: ${String(error)}`)
})
} catch (error: unknown) {
this.selfCtx.logger.warn(`tasks: onTaskDone listener threw for ${task.id}: ${String(error)}`)
}