Abortable Promises for everyone !
yarn add @lirx/async-task
# or
npm install @lirx/async-task --save
Promises are great but they lack of cancellation
.
Usually, aborting a promise is done though an AbortSignal, but only a few APIs support it:
const controller = new AbortController();
fetch('https://example.com', {
signal: controller.signal,
})
.then((response: Response): Promise<any> => {
return response.json(); // sadly, it's not possible to abort this operation
})
.then((data: any): void => {
console.log(data);
});
setTimeout(() => {
controller.abort(new Error('Timeout'));
}, 1000);
When chaining Promises, the lack of a simple cancellation becomes frustrating, or conducts to errors or unwanted behaviours.
In the previous example, if the signal is aborted during the fetch, then the promise is cancelled as expected (rejected with an error), however, if this happens during the conversion to JSON, then it is resolved as usual which is an unwanted behaviour.
To solve this problem, we propose a new PromiseLike class called AsyncTask
, which natively supports cancellation.
const abortable = Abortable.timeout(1000);
const asyncTask = doAsyncTaskA(abortable)
.successful((data: string, abortable: Abortable):AsyncTask<string> => {
return doAsyncTaskB(data, abortable);
})
.errored((error: unknown, abortable: Abortable): AsyncTask<string> => {
return doAsyncTaskC(error, abortable);
})
.successful((data: string, abortable: Abortable): void => {
console.log(data);
});
It's possible to await
an AsyncTask
(await asyncTask;
), or to convert it into a regular Promise (await asyncTask.toPromise();
)
An AsyncTask is compatible with a Promise.
This represents a "token" able to cancel an AsyncTask
.
It replaces the classes AbortControler
and AbortSignal
as one entity.
- constructor
- properties:
- methods:
- static methods:
class Abortable {
constructor(init: (abort: IAbortFunction) => void);
}
type IAbortFunction = (reason: any) => void;
init
: a function to be executed by the constructor. It receives one function as parameter (abort
). Whenabort
is called, the Abortable is aborted.
When called via new
, the Abortable
constructor returns an abortable object.
The abortable object will become aborted with a specific reason when the functions abort
is invoked.
Creates an Abortable
aborted after 1000ms:
const abortable = new Abortable((abort: IAbortFunction): void => {
setTimeout(() => abort(new Error('Timeout'), 1000));
});
get aborted(): boolean;
Returns true if the Abortable
is aborted.
get reason(): any;
Returns the abort reason of the Abortable
.
If the Abortable
is not aborted, it returns undefined
.
This method is used to subscribe to the abort event.
onAbort(onAbort: IAbortFunction): IAbortableUnsubscribe;
type type IAbortableUnsubscribe = () => void;
onAbort
: a function to be executed when the Abortable is aborted, or immediately if the Abortable is already aborted. It receives the abortreason
.
A function to call when we want to unsubscribe of this event.
Subscribes to the abort
event:
const unsubscribe = abortable.onAbort((reason: any): void => {
console.log('aborted', reason);
});
This method is used to convert an Abortable
to an AbortSignal
.
toAbortSignal(): AbortSignal
This is useful when dealing with APIs supporting AbortSignal
.
An AbortSignal
aborted when the Abortable
is aborted.
fetch(url, {
signal: abortable.toAbortSignal(),
})
static get never(): Abortable;
Returns an Abortable
, which is never aborted.
Useful in some situations where you never want to cancel an AsyncTask
.
Creates an Abortable
from an AbortSignal
.
static fromAbortSignal(signal: AbortSignal): Abortable
signal
: theAbortSignal
to create theAbortable
from.
An Abortable
aborted when the AbortSignal
is aborted.
Creates an aborted Abortable
.
static abort(reason: any): Abortable
reason
: the reason why the operation was aborted.
An Abortable
aborted with reason
.
Creates an Abortable
aborted after a specified time.
static timeout(ms: number): Abortable
ms
: the time in milliseconds before the returnedAbortable
will abort.
An Abortable
aborted after ms
milliseconds.
A simple example showing a fetch operation that will timeout if unsuccessful after 5 seconds:
fetch(url, {
signal: Abortable.timeout(5000).toAbortSignal(),
})
static derive(...abortables: Abortable[]): IDeriveAbortableResult;
type IDeriveAbortableResult = [
abort: IAbortFunction,
aborbale: Abortable,
];
...abortables
: a list ofAbortable
to build the returnedAbortable
from. If any of theseAbortable
is aborted, the returnedAbortable
is aborted too.
A tuple composed of :
abort
: which is a function having the same type and properties than the one received as parameter from theinit
function provided in the constructor. It may be used to abort the returnedAbortable
aborbale
: anAbortable
aborted if any of the providedAbortable
is aborted, or ifabort
is called.
A simple example showing a fetch operation that will be aborted immediately:
const [abort, abortable] = Abortable.derive();
const request = fetch(url, {
signal: abortable.toAbortSignal(),
});
abort();
static merge(...abortables: Abortable[]): Abortable;
...abortables
: a list ofAbortable
to build the returnedAbortable
from. If any of theseAbortable
is aborted, the returnedAbortable
is aborted too.
An Abortable
aborted if any of the provided Abortable
is aborted.
A simple example showing a fetch operation that will be aborted after 1000ms:
const abortableA = Abortable.timeout(5000);
const abortableB = Abortable.timeout(1000);
const abortable = Abortable.merge(abortableA, abortableB);
const request = fetch(url, {
signal: abortable.toAbortSignal(),
});
The AsyncTask
object represents the eventual completion, failure, or cancellation of an asynchronous operation and its resulting value.
It's an alternative to a Promise
, supporting cancellation.
It has a similar constructor, similar methods, and similar behaviour.
It simply completes the Promise
with a native support for cancellation
.
- constructor
- methods:
- static methods:
class AsyncTask<GValue extends IAsyncTaskConstraint<GValue>> {
constructor(
init: IAsyncTaskInitFunction<GValue>,
abortable: Abortable,
);
}
type IAsyncTaskInitFunction<GValue extends IAsyncTaskConstraint<GValue>> = (
success: IAsyncTaskSuccessFunction<GValue>,
error: IAsyncTaskErrorFunction,
abortable: Abortable,
) => void;
type IAsyncTaskSuccessFunction<GValue extends IAsyncTaskConstraint<GValue>> = (value: IAsyncTaskInput<GValue>) => void;
type IAsyncTaskErrorFunction = (error: any) => void;
type IAsyncTaskInput<GValue extends IAsyncTaskConstraint<GValue>> =
| AsyncTask<GValue>
| Promise<GValue>
| GValue
;
INFO: IAsyncTaskConstraint
is just a type constrain. It ensures that GValue
cannot be a Promise
nor an AsyncTask
.
This fixes some Promise's issues with typing.
init
: a function to be executed by the constructor. It receives two functions as parameters:success
anderror
; and a third parameterabortable
. Any errors thrown in this function will cause theAsyncTask
to switch in an error state.abortable
: anAbortable
signaling theAsyncTask
to stop when aborted.
When called via new
, the AsyncTask
constructor returns an asyncTask object.
The asyncTask object will become resolving when either of the functions success
or error
are invoked; or when the
provided Abortable
is aborted.
Note that if you call success
or error
and pass another AsyncTask
or Promise
object as an argument,
it can be said to be resolving, but still not resolved.
An Asynctask
is extremely similar to a Promise in its behaviour,
but it accepts an Abortable
as input to be able to cancel the operation.
Let's break down the parameters received by the init
function:
success: (value: IAsyncTaskInput<GValue>) => void
If the value
parameter passed to the success
function is:
- another
AsyncTask
orPromise
object: the newly constructedAsyncTask
's state will be "locked in" to the value passed. When this last one resolves, theAsyncTask
is resolved with the same state (a success or an error). - another value: the newly constructed
AsyncTask
switches to a success state with this value.
error: (error: any) => void
Similar to success
, the error
parameter passed to the error
function can be another AsyncTask
or Promise
object.
In this case, the AsyncTask
's state is locked in until this "value" is resolved, at which point, it switches to an error state with the provided error
.
If the provided error
is not an AsyncTask
or Promise
, then the AsyncTask
switches to an error state with this error.
abortable: Abortable
This is the Abortable
bound to this AsyncTask
.
If this Abortable
is aborted, then the AsyncTask
is automatically aborted.
This parameter is useful to clean an async operation using for example its onAbort
method.
Creates an AsyncTask
becoming successful after a specific period of time:
function asyncTimeout(
ms: number,
abortable: Abortable,
): AsyncTask<void> {
return new AsyncTask<void>((
success: IAsyncTaskSuccessFunction<void>,
error: IAsyncTaskErrorFunction,
abortable: Abortable,
): void => {
const timer = setTimeout(success, ms);
abortable.onAbort(() => {
clearTimeout(timer);
});
}, abortable);
}
This is the main method to handle the resolved state of an AsyncTask
.
It immediately returns an equivalent AsyncTask
object, allowing you to chain calls to other asyncTask methods.
settled<GNewValue extends IAsyncTaskConstraint<GNewValue>>(
onSettled: IAsyncTaskOnSettledFunction<GValue, GNewValue>,
abortable: Abortable = this.#abortable,
): AsyncTask<GNewValue>
interface IAsyncTaskOnSettledFunction<GValue extends IAsyncTaskConstraint<GValue>, GNewValue extends IAsyncTaskConstraint<GNewValue>> {
(
state: IAsyncTaskState<GValue>,
abortable: Abortable,
): IAsyncTaskInput<GNewValue>;
}
interface IAsyncTaskSuccessState<GValue extends IAsyncTaskConstraint<GValue>> {
readonly state: 'success';
readonly value: GValue;
}
interface IAsyncTaskFinalErrorState {
readonly state: 'error';
readonly error: any;
}
interface IAsyncTaskFinalAbortState {
readonly state: 'abort';
readonly reason: any;
}
type IAsyncTaskState<GValue extends IAsyncTaskConstraint<GValue>> =
| IAsyncTaskSuccessState<GValue>
| IAsyncTaskFinalErrorState
| IAsyncTaskFinalAbortState
;
onSettled
: a function asynchronously called when theAsyncTask
is resolved. It happens when theAsyncTask
is fully resolved with a success, error, or abort state. This function receives two parameters, the state of theAsyncTask
(including its value or reason) and theAbortable
provided as input. This function may return a value, anAsyncTask
or aPromise
.abortable
: this optional parameters gives us the opportunity to create a newAsyncTask
with a differentAbortable
. If omitted, the currentAsyncTask
'sAbortable
is used instead. If the currentAsyncTask
switches to an aborted state, this parameter gives us the opportunity to create a newAsyncTask
with a differentAbortable
. If omitted, the currentAsyncTask
'sAbortable
is used instead.
Returns a new AsyncTask
immediately.
This new AsyncTask
is always pending when returned, regardless of the current AsyncTask
's status.
onSettled
will be executed to handle the current AsyncTask
's state.
The call always happens asynchronously, even when the current AsyncTask
is already resolved.
The behavior of the returned AsyncTask
(call it asyncTask
) depends on the handler's execution result, following a specific set of rules.
If the handler function:
- returns a value:
asyncTask
switches to a success state with the returned value as its value. - doesn't return anything:
asyncTask
switches to a success state withundefined
as its value. - throws an error:
asyncTask
switches to an error state with the thrown error as its value. - returns an already successful
AsyncTask
:asyncTask
switches to a success state with thatAsyncTask
's value as its value. - returns an already errored
AsyncTask
:asyncTask
switches to an error state with thatAsyncTask
's value as its value. - returns another pending
AsyncTask
:asyncTask
is pending and switches to a success/error state with thatAsyncTask
's value as its value immediately after thatAsyncTask
becomes success/error.
If an AsyncTask
is returned, it MUST have the same abortable as the one provided as second argument (abortable
).
const abortable = Abortable.never;
new AsyncTask<number>((success) => {
success(Math.random() * 1000);
}, abortable)
.settled(
(state: IAsyncTaskState<number>, abortable: Abortable): AsynTask<void> => {
switch (state.state) {
case 'success':
return asyncTimeout(value, abortable);
case 'error':
console.error(error);
throw error;
case 'aborted':
console.log('aborted');
}
},
(error: unknown, abortable: Abortable): never => {
},
);
The then()
method of an AsyncTask
object takes two arguments:
the callback functions for the success and error cases of the AsyncTask
.
It immediately returns an equivalent AsyncTask
object, allowing you to chain calls to other asyncTask methods.
then<GNewValue extends IAsyncTaskConstraint<GNewValue>>(
onSuccessful: IAsyncTaskOnSuccessfulFunction<GValue, GNewValue>,
onErrored: IAsyncTaskOnErroredFunction<GNewValue>,
): AsyncTask<GNewValue>
type IAsyncTaskOnSuccessfulFunction<GValue extends IAsyncTaskConstraint<GValue>, GNewValue extends IAsyncTaskConstraint<GNewValue>> = (
value: GValue,
abortable: Abortable,
) => IAsyncTaskInput<GNewValue>;
type IAsyncTaskOnErroredFunction<GNewValue extends IAsyncTaskConstraint<GNewValue>> = (
error: any,
abortable: Abortable,
) => IAsyncTaskInput<GNewValue>;
onSuccessful
: a function asynchronously called if theAsyncTask
is successful. This function has two parameters, the success value and theAbortable
linked to thisAsyncTask
. This function may return a value, anAsyncTask
or aPromise
.onRejected
: a function asynchronously called if theAsyncTask
is errored. This function has two parameters, the rejection reason and theAbortable
linked to thisAsyncTask
. This function may return a value, anAsyncTask
or aPromise
.
Unlike Promises, the two functions are mandatory. They can't be omitted nor null or undefined.
Returns a new AsyncTask
immediately.
This new AsyncTask
is always pending when returned, regardless of the current AsyncTask
's status.
One of the onSuccessful
and onRejected
handlers will be executed to handle the current AsyncTask
's success or error.
The call always happens asynchronously, even when the current AsyncTask
is already resolved.
The behavior of the returned AsyncTask
(call it asyncTask
) depends on the handler's execution result, following a specific set of rules.
If the handler function:
- returns a value:
asyncTask
switches to a success state with the returned value as its value. - doesn't return anything:
asyncTask
switches to a success state withundefined
as its value. - throws an error:
asyncTask
switches to an error state with the thrown error as its value. - returns an already successful
AsyncTask
:asyncTask
switches to a success state with thatAsyncTask
's value as its value. - returns an already errored
AsyncTask
:asyncTask
switches to an error state with thatAsyncTask
's value as its value. - returns another pending
AsyncTask
:asyncTask
is pending and becomes switches to a success/error state with thatAsyncTask
's value as its value immediately after thatAsyncTask
becomes success/error.
If an AsyncTask
is returned, it MUST have the same abortable as the one provided as second argument (abortable
).
This function is compatible with the Thenable object.
In consequence, it's possible to await
an AsyncTask
.
const abortable = Abortable.never;
new AsyncTask<number>((success) => {
success(Math.random() * 1000);
}, abortable)
.then(
(value: number, abortable: Abortable): AsynTask<void> => {
return asyncTimeout(value, abortable);
},
(error: unknown, abortable: Abortable): never => {
console.error(error);
throw error;
},
);
The successful()
method of an AsyncTask
is equivalent to the then()
method, but only with the onSuccessful
callback function.
successful<GNewValue extends IAsyncTaskConstraint<GNewValue>>(
onSuccessful: IAsyncTaskOnSuccessfulFunction<GValue, GNewValue>,
): AsyncTask<GNewValue>
If the current AsyncTask
switches to an error state, then the returned AsyncTask
switches to an error state too,
else the behaviour is the same as the one using the then()
method.
const abortable = Abortable.never;
new AsyncTask<number>((success) => {
success(Math.random() * 1000);
}, abortable)
.successful((value: number, abortable: Abortable): AsynTask<void> => {
return asyncTimeout(value, abortable);
})
.successful((): void => {
console.log('done !');
});
The errored()
method of an AsyncTask
is equivalent to the then()
method, but only with the onErrored
callback function.
errored<GNewValue extends IAsyncTaskConstraint<GNewValue>>(
onErrored: IAsyncTaskOnErroredFunction<GNewValue>,
): AsyncTask<GValue | GNewValue>
If the current AsyncTask
switches to a success state, then the returned AsyncTask
switches to a success state too,
else the behaviour is the same as the one using the then()
method.
const abortable = Abortable.never;
new AsyncTask<number>((success) => {
error(new Error('Error !'));
}, abortable)
.errored((error: unknown, abortable: Abortable): AsynTask<void> => {
console.log('error catched', error);
return asyncTimeout(500, abortable);
})
.successful((): void => {
console.log('done !');
});
The aborted()
method is tricky and must be used with caution:
aborted<GNewValue extends IAsyncTaskConstraint<GNewValue>>(
onAborted: IAsyncTaskOnAbortedFunction<GNewValue>,
abortable?: Abortable,
): AsyncTask<GValue | GNewValue>
type IAsyncTaskOnAbortedFunction<GNewValue extends IAsyncTaskConstraint<GNewValue>> = (
reason: any,
abortable: Abortable,
) => IAsyncTaskInput<GNewValue>;
onAborted
: a function asynchronously called if theAsyncTask
is aborted. This function has two parameters, the abort reason value and anAbortable
to use if anAsyncTask
is returned. This function may return a value, anAsyncTask
or aPromise
.abortable
: this optional parameters, allows us to "switch" ofAbortable
. Indeed, the currentAsyncTask
is in an aborted state, so this parameter gives us the opportunity to create a newAsyncTask
with a differentAbortable
. If omitted, the currentAsyncTask
'sAbortable
is used instead.
Returns a new AsyncTask
immediately, with abortable
as its own Abortable
.
This new AsyncTask
is always pending when returned, regardless of the current AsyncTask
's status.
The onAborted
handler will be executed to handle the current AsyncTask
's abort state.
The call always happens asynchronously, even when the current AsyncTask
is already resolved.
The behavior of the returned AsyncTask
is similar to the then()
method.
const abortable = Abortable.never;
new AsyncTask<number>((success) => {
success(Math.random() * 1000);
}, abortable)
.successful((ms: number, abortable: Abortable): never => {
// let's create an abortable which "races" between the received abortable ('abortable') and a timeout of 500ms
const sharedAbortable = Abortable.merge([
abortable,
Abortable.timeout(500),
]);
// in 50% of the time, it will abort before the following AsyncTask resolves
// we cannot return an AsyncTask with a different Abortable, because only one controller must exists
return asyncTimeout(ms, sharedAbortable)
// so we use the 'aborted()' method, with the original Abortable
.aborted((reason: unknown): never => {
// in which we throw an error
throw new Error(`Oops child AsyncTask aborted with: ${reason}`);
}, abortable);
});
switchAbortable(abortable: Abortable): AsyncTask<GValue>
This is equivalent to:
return this.aborted<GValue>((reason: any): never => {
throw reason;
}, abortable);
const abortable = Abortable.never;
new AsyncTask<number>((success) => {
success(Math.random() * 1000);
}, abortable)
.successful((ms: number, abortable: Abortable): never => {
const sharedAbortable = Abortable.merge([
abortable,
Abortable.timeout(500),
]);
return asyncTimeout(ms, sharedAbortable)
.switchAbortable(abortable);
});
finally(
onFinally: IAsyncTaskOnFinallyFunction<GValue>,
): AsyncTask<GValue>
type IAsyncTaskOnFinallyFunction<GValue extends IAsyncTaskConstraint<GValue>> = (
state: IAsyncTaskState<GValue>,
abortable: Abortable,
) => IAsyncTaskInput<void>
onFinally
: a function asynchronously called if theAsyncTask
is resolved (successful/errored/aborted). This function has two parameters, the state of theAsyncTask
and anAbortable
to use if anAsyncTask
is returned. This function may return a value, anAsyncTask
or aPromise
.
Returns a new AsyncTask
immediately.
The onFinally
handler will be executed when the current AsyncTask
is resolved.
The call always happens asynchronously, even when the current AsyncTask
is already resolved.
If an error is thrown in the onFinally
, a rejected Promised is returned or an errored AsyncTask
is returned,
then the newly created AsyncTask
will error too with this error.
Else, the newly created AsyncTask
will success, error or abort according to the current AsyncTask
state.
const readAll = (
reader: ReadableStreamDefaultReader<string>,
abortable: Abortable,
): AsyncTask<string> => {
return AsyncTask.fromFactory(() => reader.read(), abortable)
.successful((result: ReadableStreamReadResult<string>, abortable: Abortable) => {
if (result.done) {
return '';
} else {
return readAll(reader, abortable)
.successful((output: string): string => {
return result.value + output;
});
}
});
};
const decoder = new TextDecoderStream();
const reader = encoder.readable.getReader();
const writer = encoder.writable.getWriter();
const abortable = Abortable.never;
readAll(reader, abortable)
.successful((output: string): void => {
console.log('decoder', output);
})
.finally((): void => {
reader.releaseLock();
});
writer.write(new TextEncoder().encode('Hello world !'));
writer.close();
Creates a Promise from an AsyncTask
.
toPromise(): Promise<GValue>
Returns a Promise.
If the AsyncTask
resolves with the state:
- success: fulfill the promise with the result value.
- error: reject the promise with the result error.
- abort: reject the promise with an "Abort" error.
const abortable = Abortable.never;
asyncTimeout(1000, abortable)
.toPromise()
.then(() => {
console.log('done !');
});
static fromFactory<GValue extends IAsyncTaskConstraint<GValue>>(
factory: IAsyncTaskFactory<GValue>,
abortable: Abortable,
): AsyncTask<GValue>
type IAsyncTaskFactory<GValue extends IAsyncTaskConstraint<GValue>> = (abortable: Abortable) => IAsyncTaskInput<GValue>;
factory
: a function returning a value, anAsyncTask
or aPromise
. It receives anAbortable
.abortable
: theAbortable
linked to the returnedAsyncTask
.
- if
abortable
is aborted, returns an abortedAsyncTask
- else, calls
factory
:- if an error is thrown from the factory, returns an error
AsyncTask
- if an
AsyncTask
is returned, returns thisAsyncTask
- else, returns an
AsyncTask
resolved with this the return of thefactory
.
- if an error is thrown from the factory, returns an error
AsyncTask.fromFactory<number>(() => 45, Abortable.never); // resolved with 45
AsyncTask.fromFactory<void>(() => console.log('never happend'), Abortable.abort('a')); // the factory function is never called
AsyncTask.fromFactory<number>(() => Promise.resolve(45), Abortable.never); // resolved with 45
AsyncTask.fromFactory<number>(() => Promise.reject('error !'), Abortable.never); // rejected with 'error !'
AsyncTask.fromFactory<number>(() => AsyncTask.success(45), Abortable.never); // returns the AsyncTask generated by the factory
static retry<GValue extends IAsyncTaskConstraint<GValue>>(
factory: IAsyncTaskFactory<GValue>,
count: number,
abortable: Abortable,
): AsyncTask<GValue>
Tries count
times to create an AsyncTask from an IAsyncTaskFactory:
- if
count
is zero or less, throw an error - else call factory:
- in case of success return the result
- else decrease count by one:
- if zero or less, throw the received error
- else repeat (1) with the new
count
value
AsyncTask.retry(() => fetch('https://example.com'), 5, Abortable.never);
static success<GValue extends IAsyncTaskConstraint<GValue>>(
value: IAsyncTaskInput<GValue>,
abortable: Abortable,
): AsyncTask<GValue>
value
: a value, anAsyncTask
or aPromise
abortable
: theAbortable
linked to the returnedAsyncTask
.
Returns an AsyncTask
resolved with value
.
This is equivalent to:
return new AsyncTask<GValue>((success: IAsyncTaskSuccessFunction<GValue>): void => {
success(input);
}, abortable);
AsyncTask.success<number>(45, Abortable.never);
static error<GValue extends IAsyncTaskConstraint<GValue> = unknown>(
error: any,
abortable: Abortable,
): AsyncTask<GValue>
error
: a value, anAsyncTask
or aPromise
abortable
: theAbortable
linked to the returnedAsyncTask
.
Returns an AsyncTask
rejected with error
.
This is equivalent to:
return new AsyncTask<GValue>((
success: IAsyncTaskSuccessFunction<GValue>,
_error: IAsyncTaskErrorFunction,
): void => {
_error(error);
}, abortable)
AsyncTask.error(new Error('Errored !'), Abortable.never);
static never<GValue extends IAsyncTaskConstraint<GValue> = unknown>(
abortable: Abortable,
): AsyncTask<GValue>
abortable
: theAbortable
linked to the returnedAsyncTask
.
Returns an AsyncTask
which never resolves (may only be aborted).
This is equivalent to:
return new AsyncTask<GValue>(() => {}, abortable);
static void(
abortable: Abortable,
): AsyncTask<void>
abortable
: theAbortable
linked to the returnedAsyncTask
.
Returns an AsyncTask
resolved with undefined
.
This is equivalent to:
return this.success<void>(void 0, abortable);
AsyncTask.void(Abortable.never);
static all<GFactories extends IGenericAsyncTaskFactoriesList>(
factories: GFactories,
abortable: Abortable,
): AsyncTask<IAsyncTaskAllValuesListReturn<GFactories>>
factories
: an iterable ofIAsyncTaskFactory
.abortable
: theAbortable
linked to the returnedAsyncTask
.
Returns an AsyncTask
resolved with all the values returned by the factories.
Calls all the factories with an Abortable
derived from the provided abortable
called factoriesAbortable
.
Awaits on all the results to be resolved, and stores their returning values in an array called values
.
If all the results are in a success state OR the provided iterable is empty,
then the returned AsyncTask
is resolved with values
.
If any of the result is rejected, then the returned AsyncTask
is rejected too.
Moreover, factoriesAbortable
is aborted, meaning that other factories MUST be aborted.
This is extremely similar to Promise.all
, but works with factories instead.
If one of the factories rejects, then the other factories are cancelled, optimizing resources.
const abortable = Abortable.never;
const asyncTask = AsyncTask.all([
(abortableA: Abortable) => asyncTimeout(1000, abortableA),
(abortableB: Abortable) => AsyncTask.error(new Error('Error !'), abortableB),
], abortable);
In this example, all the factories are called.
The second one returns an errored AsyncTask
, so asyncTask
switches to an error state,
and abortableA
is aborted, effectively cleaning the pending timeout.
static race<GFactories extends IGenericAsyncTaskFactoriesList>(
factories: GFactories,
abortable: Abortable,
): AsyncTask<IAsyncTaskRaceValueReturn<GFactories>>
factories
: an iterable ofIAsyncTaskFactory
.abortable
: theAbortable
linked to the returnedAsyncTask
.
Returns an AsyncTask
resolved with the first value or error returned by the factories.
Calls all the factories with an Abortable
derived from the provided abortable
called factoriesAbortable
.
Awaits on the first result to be resolved:
- if the result is in a success state, then the returned
AsyncTask
is resolved with this value. - if the result is in an error state, then the returned
AsyncTask
is rejected with this error.
Moreover, factoriesAbortable
is aborted, meaning that other factories MUST be aborted.
If the provided iterable is empty, the AsyncTask
never resolves.
This is extremely similar to Promise.race
, but works with factories instead.
If one of the factories succeeds or rejects, then the other factories are cancelled, optimizing resources.
const abortable = Abortable.never;
const asyncTask = AsyncTask.race([
(abortableA: Abortable) => asyncTimeout(1000, abortableA),
(abortableB: Abortable) => asyncTimeout(2000, abortableA),
], abortable);
In this example, all the factories are called.
The return of the first one finishes first with undefined
, so asyncTask
switches to a success state with undefined
as value.
abortableB
is aborted, effectively cleaning the pending second timeout.
function asyncTimeout(
ms: number,
abortable: Abortable,
): AsyncTask<void>
ms
: the number of milliseconds to wait until the AsyncTask resolves.abortable
: theAbortable
linked to the returnedAsyncTask
.
Returns an AsyncTask
resolved after a specific amount of time.
const abortable = Abortable.never;
const asyncTask = AsyncTask.asyncTimeout(1000, abortable);
function asyncFetch(
input: RequestInfo | URL,
init: IAsyncFetchRequestInit,
abortable: Abortable,
): AsyncTask<Response>
Similar to the fetch()
function, but works with AsyncTask
instead.
const abortable = Abortable.timeout(2000);
asyncFetch('https://example.com', void 0, abortable)
.successful((response: Response, abortable: Abortable): AsyncTask<string> => {
if (response.ok) {
return AsyncTask.fromFactory<GData>(() => response.json(), abortable);
} else {
throw new Error(`Failed to fetch '${response.url}': ${response.status}`);
}
})
.successful((result: string) => {
console.log(result);
});
Why not using AbortController and AbortSignal ?
Because I wasn't totally satisfied of these classes.
I wanted an object able to do both but keeping the principle of controller/worker.
Moreover, I wanted more static methods, helping developers to rapidly construct such an object.
This is where the idea of Abortable
came from.
Just like a Promise, after created, only the "creator" of the Abortable
can abort it.
And the same if true for an AsyncTask
. It simply cannot cancel itself, only the controller can do it.
This prevents bad patterns and undefined behaviours, even if it wasn't clear from the start.
Why not using Promise with AbortSignal ?
Because chaining promises with a common AbortSignal
is a nightmare and is prone to errors.
Using AsyncTask
gives you a robust framework to work with cancellable async operations.