Deferred Promise Tied to AbortSignal

Expose resolve and reject controls for a promise and automatically reject with AbortError when the linked signal fires.

TypescriptResilience

Code

snippet
export interface Deferred<Value> {
  readonly promise: Promise<Value>;
  readonly settled: () => boolean;
  resolve(value: Value | PromiseLike<Value>): void;
  reject(reason?: unknown): void;
}

export function createDeferred<Value>(signal?: AbortSignal): Deferred<Value> {
  if (signal !== undefined && !(signal instanceof AbortSignal)) {
    throw new TypeError("signal must be an AbortSignal");
  }
  let settle: ((value: Value | PromiseLike<Value>) => void) | undefined;
  let fail: ((reason?: unknown) => void) | undefined;
  let settled = false;
  const promise = new Promise<Value>((resolve, reject) => {
    settle = resolve;
    fail = reject;
  });
  const markSettled = (): void => {
    settled = true;
  };
  void promise.then(markSettled, markSettled);

  const onAbort = (): void => {
    if (settled || fail === undefined) return;
    settled = true;
    fail(new DOMException("The operation was aborted", "AbortError"));
  };

  if (signal?.aborted === true) {
    onAbort();
  } else if (signal !== undefined) {
    signal.addEventListener("abort", onAbort, { once: true });
    const detach = (): void => signal.removeEventListener("abort", onAbort);
    void promise.then(detach, detach);
  }

  return {
    promise,
    settled: () => settled,
    resolve(value) {
      if (settled || settle === undefined) return;
      settled = true;
      settle(value);
    },
    reject(reason) {
      if (settled || fail === undefined) return;
      settled = true;
      fail(reason);
    },
  };
}

const controller = new AbortController();
const aborted = createDeferred<number>(controller.signal);
aborted.promise.catch((error: unknown) => {
  console.log(error instanceof DOMException ? error.name : "unknown");
});
controller.abort();
console.log(aborted.settled());

const manual = createDeferred<string>();
manual.promise.then((value) => console.log(value));
manual.resolve("done");
manual.resolve("ignored");
console.log(manual.settled());