diff --git a/packages/spf/src/core/tasks/task.ts b/packages/spf/src/core/tasks/task.ts index 6fd3e8d5..653cb14d 100644 --- a/packages/spf/src/core/tasks/task.ts +++ b/packages/spf/src/core/tasks/task.ts @@ -105,24 +105,28 @@ export class Task implements TaskLike { - // Memoized: run the work once; repeated calls share the same promise (which - // resolves/rejects immediately once settled). Re-running needs a `clone()`. - this.#promise ??= (async () => { - this.#status = 'running'; - try { - const result = await this.#runFn(this.#signal); - this.#value = result; // value before status — ordering guarantee - this.#status = 'done'; - return result; - } catch (e) { - this.#error = e as TError; // error before status — ordering guarantee - this.#status = 'error'; - throw e; - } - })(); + // Memoized: run the work once, sharing the same promise across calls. The + // memoized promise IS what callers await, so it's always handled (no orphan + // `Promise.resolve/reject`); a sync-throwing `#runFn` is captured as a + // rejection rather than re-run. + this.#promise ??= this.#execute(); return this.#promise; } + async #execute(): Promise { + this.#status = 'running'; + try { + const result = await this.#runFn(this.#signal); + this.#value = result; // value before status — ordering guarantee + this.#status = 'done'; + return result; + } catch (e) { + this.#error = e as TError; // error before status — ordering guarantee + this.#status = 'error'; + throw e; + } + } + abort(): void { this.#abortController.abort(); }