Skip to content

Instantly share code, notes, and snippets.

@Raiondesu
Last active December 2, 2023 13:20
Show Gist options
  • Save Raiondesu/19ff98e31ae5be9367328dc3f48abdaa to your computer and use it in GitHub Desktop.
Save Raiondesu/19ff98e31ae5be9367328dc3f48abdaa to your computer and use it in GitHub Desktop.
A simple and usable abortable web (fetch, promise) realization in TypeScript

Abortable Web Extension for Fetch and Promise

What all this basically does is tries to turn fetch's AbortController mess with rejecting via "AbortError" to something more consistent, abstract and usable.

API and differences from standards

There are only 2 API differences from your usual fetch and promises:

  1. Promise now has 2 more fields: method abort() and a signal field, which is the AbortController.signal (can be used to manipulate abort events).
  2. Promise constructor now accepts a different argument: (resolve, reject, abort) => void. Basically, it's a standard promise executor but with an additional function argument. AbortablePromise constructor can also accept any standard promise and convert it into an Abortable one.

Abortin the promise doesn't cancel the request. It just guarantees that the response will be ignored no matter what.

Example:

const a = new Abortable.Promise((resolve, reject, abort) => setTimeout(() => {
  const randomNumber = Math.random();

  if (randomNumber > 0.5) resolve('foo');
  else abort('bar' /* abort reason here */);
}, 1000));

// 1 second later in console...
// => foo
// or
// => Promise aborted: bar
// it's a 50/50 chance, basically.

or you can just Abortable.Promise.abort(anyPromiseYouLike);.

Usage Example

Copy-paste the code somewhere (let's say to a folder "abortable", for example). Import.

Simple function that gets JSON from server and returns an abortable promise now looks like this:

import Abortable from './abortable'

request(url: string, config: RequestInit, timeout?: number) {
  return new Abortable.Promise((resolve, reject, abort) => {
    Abortable.fetch(url, config).then(resp => resp.json().then(resolve).catch(reject)).catch(reject);

    // Let's say we have our custom timeout:
    if (timeout !== undefined)
      setTimeout(() => abort('TimeOut'), timeout);
  });
}

Replacing your average Promise and fetch

Following actions are NOT SAFE. Proceed at your own risk.

Just do this:

originalPromise = { ...window.Promise };
originalFetch = window.fetch;
window.Promise = Abortable.Promise;
window.fetch = Abortable.fetch;
import AbortablePromise from './abortablePromise';
export default function abortableFetch(input?: string | Request, init?: RequestInit) {
const signal = init ? init.signal : input ? input['signal'] : undefined;
const abortController = signal ? undefined : new AbortController();
const promise = new AbortablePromise<Response>(fetch.apply(arguments), abortController);
if (signal) {
promise.signal = signal;
promise.abort = () => signal!.dispatchEvent(new Event('abort'));
}
return promise;
}
type AbortablePromiseArg<T> = (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void, abort: (reason?: any) => void) => void;
export default class AbortablePromise<T = any> implements AbortController, Promise<T> {
[Symbol.toStringTag]: 'Promise';
/// TODO:
static all<T>() { return new AbortablePromise<T[]>(Promise.all<T>(arguments[0])); };
static race<T>() { return new AbortablePromise<T>(Promise.race<T>(arguments[0])); };
static reject<T>() { return new AbortablePromise<T>(Promise.reject<T>(arguments[0])); };
static resolve<T>() { return new AbortablePromise<T>(Promise.resolve<T>(arguments[0])); };
static abort<T = never>(value: T | PromiseLike<T>, reason?: any) {
if (typeof value['then'] === 'function') {
const promise = new AbortablePromise<T>(value as PromiseLike<T>);
promise.abort(reason);
return promise;
} else if (value instanceof AbortablePromise) {
value.abort(reason);
return value;
} else {
return new AbortablePromise<T>((_resolve, _reject, abort) => {
abort(reason);
});
}
}
constructor(
executor: Promise<T> | PromiseLike<T> | AbortablePromiseArg<T>,
abortController?: AbortController
) {
this['[[AbortController]]'] = abortController || new AbortController();
this.signal = this['[[AbortController]]'].signal;
this.abort = reason => {
this['[[AbortController]]'].abort.bind(this['[[AbortController]]']);
if ((reason && (reason.valueOf && (reason.valueOf() || true))) && typeof console !== 'undefined' && console) {
console.warn('Promise Abort:', reason);
}
};
if (typeof executor === 'function') {
this['[[Promise]]'] = new Promise<T>((resolve, reject) => executor(resolve, reject, this.abort));
this._isPromiseWrapper = true;
} else {
this['[[Promise]]'] = executor as Promise<T>;
}
const wrap = (stuffing: string, ignoreCondition: boolean = false) => {
if ((!this.aborted || ignoreCondition) && this['[[Promise]]'][stuffing])
return function (this: AbortablePromise<T>) {
return this[stuffing].bind(this)(arguments);
}.bind(this['[[Promise]]']);
else if (this['[[Promise]]'][stuffing])
return function () {
console.warn('Promise Aborted!');
}.bind(this['[[Promise]]']);
};
this.then = wrap('then');
this.catch = wrap('catch');
this.finally = wrap('finally', true);
}
protected _isPromiseWrapper: boolean = false;
protected '[[Promise]]': Promise<T>;
protected '[[AbortController]]': AbortController;
get aborted() { return this.signal.aborted; }
then: <TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined) => Promise<TResult1 | TResult2>;
catch: <TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null | undefined) => Promise<T | TResult>;
finally: (onfinally?: (() => void) | null) => Promise<T> = () => Promise.reject();
abort: (reason?: any) => void;
signal: AbortSignal;
}
import AbortablePromise from './abortablePromise';
import abortableFetch from './abortableFetch';
export default Object.freeze({
Promise: AbortablePromise,
fetch: abortableFetch
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment