fix(process): preserve owning operation cancellation

This commit is contained in:
morluto committed 2026-10-17 04:21:21 +08:00
1 parent bf3475867f
commit 50dbd875cf
2 files changed
+44 -3

No files matched your search

+32
View File
@@ -70,3 +70,35 @@ it("does not lose an unexpected producer rejection after its final caller cancel
reject?.(cause);
await expect(waiting).rejects.toBe(cause);
});
it.each(["caller", "owner"])(
"preserves %s interruption when owning cancellation and closure occur together",
async (first) => {
const completed = createDeferred<string>();
const controller = new AbortController();
const reason = { caller: "operation owner" };
const attempt = new SharedStartupAttempt(
() => completed.promise,
controller.signal,
);
const waiting = attempt.wait();
if (first === "caller") {
controller.abort(reason);
attempt.close();
} else {
attempt.close();
expect(attempt.signal.aborted).toBe(true);
expect(attempt.signal.reason).toBe(STARTUP_OWNER_CLOSED);
controller.abort(reason);
}
expect(attempt.signal.reason).toBe(
first === "caller" ? reason : STARTUP_OWNER_CLOSED,
);
expect(attempt.settled).toBe(false);
completed.resolve("finalized");
await expect(waiting).resolves.toEqual({
status: "completed",
value: "finalized",
});
},
);
+12 -3
View File
@@ -18,11 +18,20 @@ type StartupSettlement<Value> =
export class SharedStartupAttempt<Value> {
readonly #controller = new AbortController();
readonly promise: Promise<Value>;
readonly #signal: AbortSignal;
#waiters = 0;
#settlement: StartupSettlement<Value> = { status: "pending" };
constructor(start: (signal: AbortSignal) => Promise<Value>) {
this.promise = start(this.#controller.signal);
constructor(
start: (signal: AbortSignal) => Promise<Value>,
/** An owning operation's cancellation is independent of individual waiters. */
ownerSignal?: AbortSignal,
) {
this.#signal =
ownerSignal === undefined
? this.#controller.signal
: AbortSignal.any([ownerSignal, this.#controller.signal]);
this.promise = start(this.#signal);
void this.promise.then(
(value) => {
this.#settlement = { status: "completed", value };
@@ -35,7 +44,7 @@ export class SharedStartupAttempt<Value> {
/** Signal consumed by the startup producer, never by an individual waiter. */
get signal(): AbortSignal {
return this.#controller.signal;
return this.#signal;
}
/** Completion includes the producer's final cleanup, not just its abort. */