Github Repo: https://github.com/zzdjk6/simple-abortable-promise

Короткий рассказ

Правда: Promise нельзя прервать или отменить. Это может быть только fulfilled или rejected.

Трюк: но мы можем reject на Promise пораньше, если захотим :)

Длинная история

Посмотрим правде в глаза, у всех нас есть потребность прервать или отменить Promise иногда. Например, в одностраничном приложении может потребоваться много времени для загрузки некоторых данных или выполнения некоторой обработки, в то время как пользователь хочет прервать ее и заняться чем-то другим.

К счастью, у нас есть стандартный способ прервать сетевой запрос через Fetch API. То есть мы можем передать AbortSignal для получения и прервать его через AbortController:

// Initialize fetch call
const controller = new AbortController;
const promise = fetch(url, { signal: controller.signal })
// Abort
controller.abort()

Затем Promise, возвращенный fetch, будет отклонен с AbortError, когда вызов fetch будет прерван.

Однако этот подход не прост в использовании, потому что:

  1. Нам нужно сохранить ссылку на AbortController где-то еще, чтобы вызывать .abort() при необходимости.
  2. Каждый AbortController можно использовать только один раз, и мы должны создавать новые после прерывания. То есть единственный эффективный вызов .abort() — первый раз. После того, как мы вызвали .abort() на AbortController, AbortSignal используется и остается aborted. Если вы назначите прерванный AbortSignal новому вызову fetch и попытаетесь вызвать .abort() на его контроллере, эффекта не будет.
  3. Работает только с fetch

Как насчет того, чтобы распространить это поведение на всех Promises? Давайте попробуем.

Во-первых, давайте представим, что если AbortablePromise существует, как мы можем его использовать?

В моей иллюзии мы можем сохранить ссылку AbortablePromise и просто вызвать .abort() для этого объекта ссылки, чтобы прервать его, как в коде ниже:

// Init
const promise = new AbortablePromise((resolve, reject) => {...});
// Abort
promise.abort();

Затем, чтобы сделать его простым и совместимым с поведением Fetch API, AbortablePromise следует отклонять с помощью anAbortError при его прерывании.

Основываясь на иллюзии, мы можем быстро придумать следующую реализацию:

Основная часть этой реализации для переноса функции executor и reject на Promise при получении события abort:

const wrappedExecutor: ExecutorFunction<T> = (resolve, reject) => {
  abortSignal.addEventListener('abort', () => {
    reject(new AbortError());
  });
  executor(resolve, reject);
};

Также мы присоединяем новый метод abort к объекту Promise:

this.abort = () => {
  abortController.abort();
};

Расширенная история

Очевидно, что наша история не может закончиться на этом. Чтобы сделать AbortablePromise полезным в реальных случаях, нам нужно сделать немного больше.

В идеале, в расширенной версии AbortablePromise, я думаю, необходимы следующие возможности:

  1. При инициализации AbortablePromise я хочу получить доступ к abortSignal внутри функции executor, чтобы я мог сделать что-то еще, когда произойдет прерывание. Типичным примером является передача вызовов abortSignal в fetch внутри executor, чтобы вызовы fetch также прерывались при прерывании Promise.
  2. Когда прерываю AbortablePromise, я иногда хочу задать ему индивидуальную причину вместо использования по умолчанию.
  3. Я хочу быстро обернуть обычный Promise как AbortablePromise
  4. Я хочу, чтобы это решение работало в разных браузерах. Судя по всему, AbortController и AbortSignal не поддерживаются в IE и некоторых старых версиях браузеров.

Чтобы достичь этих особенностей, мы придумали версию 2:

Приходите и найдите больше на моем github :)