JavaScript

AbortablePromise

2020.10.261분 읽기4

api요청의 취소가 가능한 AbortablePromise를 구현해보았다.

export interface ExecutorFunction<T> {
  (resolve: (value?: PromiseLike<T> | T) => void, reject: (reason?: any) => void): void;
}
 
export interface AbortableExecutorFunction<T> {
  (resolve: (value?: PromiseLike<T> | T) => void, reject: (reason?: any) => void, abortSignal: AbortSignal): void;
}
 
export class AbortablePromise<T> extends Promise<T> {
  private _abortController: AbortController;
  private _onfinally: Function[];
 
  constructor(executor: AbortableExecutorFunction<T>, abortController?: AbortController) {
    if (!abortController) {
      abortController = new AbortController();
    }
    const abortSignal = abortController.signal;
 
    const normalExecutor: ExecutorFunction<T> = (resolve, reject) => {
      abortSignal.addEventListener('abort', () => {
        reject(new AbortError());
        this._onfinally.forEach(onfinally => onfinally());
      });
 
      executor(resolve, reject, abortSignal);
    };
 
    super(normalExecutor);
    Object.setPrototypeOf(this, new.target.prototype);
    this._abortController = abortController;
    this._onfinally = [];
  }
 
  then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): AbortablePromise<TResult1 | TResult2> {
    return new AbortablePromise((resolve, reject, signal) => {
      const onSettled = (
        status: 'resolved' | 'rejected',
        value: any,
        callback: ((v: any) => TResult1 | PromiseLike<TResult1> | TResult2 | PromiseLike<TResult2>) | undefined | null
      ) => {
        if ("function" === typeof callback) {
          value = callback(value);
          if (value instanceof AbortablePromise) {
            Object.assign(signal, value._abortController.signal);
          }
          return resolve(value);
        }
        "resolved" === status && resolve(value);
        "rejected" === status && reject(value);
      };
 
      super.then(
        value => onSettled("resolved", value, onfulfilled),
        reason => onSettled("rejected", reason, onrejected)
      );
    }, this._abortController);
  }
 
  catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): AbortablePromise<T | TResult> {
    return new AbortablePromise((resolve, reject, signal) => {
      const onSettled = (
        value: any,
        callback: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null
      ) => {
        if ("function" === typeof callback) {
          value = callback(value);
          if (value instanceof AbortablePromise) {
            Object.assign(signal, value._abortController.signal);
          }
          return reject(value);
        }
        reject(value);
      };
 
      super.then(
        value => onSettled(value, onrejected)
      );
    }, this._abortController);
  }
 
  finally(onfinally?: (() => void) | undefined | null): AbortablePromise<T> {
    if (onfinally) {
      this._onfinally.push(onfinally);
    }
    super.finally(onfinally);
    return this;
  }
 
  abort() {
    this._abortController.abort();
  }
}
 
export class AbortError extends PayPortCustomError {
  constructor(message: string = 'Aborted') {
    super(message);
    this.name = 'AbortError';
  }
}

참조 및 출처


관련 포스트

Giscus 댓글 영역 (GitHub Discussions 연동 예정)