Files
scriptc/tests/corpus/class-abort-controller-fields.ts
T
Malte UblandClaude Opus 5.5 684a0abd59 Store AbortController and AbortSignal handles in class fields
A class field typed AbortController or AbortSignal (plain, optional,
nullable, or in an array) maps to checked-dynamic storage, and the
class-field rule refused it (SC1090) although the same handles already
live in locals, Map values, and closures. They now take the checked-dynamic
slot, like the fetch Headers, Request, and Response handles.

controller?.abort() on a nullable controller was refused as a method call
(also for locals): the static abort lowering ran only without an optional
chain. It now runs again once the chain has narrowed the receiver.

Found by pi's TUI Editor (autocompleteAbort: AbortController | undefined).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:50:42 -07:00

53 lines
1.7 KiB
TypeScript

// Class fields typed AbortController and AbortSignal (plain, optional,
// `| undefined`, `| null`, in arrays) hold the live handles: aborting
// through the field is seen through every other reference, and optional
// calls on a nullable field skip an empty slot.
class Editor {
private abort: AbortController | undefined;
private signal: AbortSignal | null = null;
private readonly lifetime = new AbortController();
history: AbortController[] = [];
last?: AbortSignal;
start(): AbortSignal {
this.abort?.abort();
this.abort = new AbortController();
this.history.push(this.abort);
this.signal = this.abort.signal;
this.last = this.signal;
return this.signal;
}
cancel(reason?: string): boolean {
this.abort?.abort(reason);
return this.signal?.aborted ?? false;
}
reason(): unknown {
return this.signal?.reason;
}
dispose(): boolean {
this.lifetime.abort();
return this.lifetime.signal.aborted;
}
}
const idle = new Editor();
console.log(idle.cancel(), idle.reason());
const e = new Editor();
const first = e.start();
let events = 0;
first.addEventListener("abort", () => {
events++;
});
const second = e.start();
console.log(first.aborted, second.aborted, events, e.history.length);
console.log(e.cancel("stop"), e.reason(), second.aborted, second.reason);
console.log(e.history[0]!.signal === first, e.last === second, e.dispose());
// A nullable local takes the same optional call.
let maybe: AbortController | undefined = e.history.length > 5 ? new AbortController() : undefined;
maybe?.abort();
console.log(maybe?.signal.aborted);
maybe = new AbortController();
maybe?.abort("late");
console.log(maybe?.signal.aborted, maybe?.signal.reason);