Cancelable Computation
Signature
CancelableComputation e a :: ((a -> void), (e -> void)) -> (() -> void)
Leaving the failure type to be implicitly an Error the signature can be shortened:
CancelableComputation a :: ((a -> void), (* -> void)) -> (() -> void)
FantasyLand
provides FantasyLand 4.0 Monad interface.
Generator Functions
Custom Cancelable addFantasyLandInterface(computationFunction)
:: CancelableComputation a → Cancelable a
Every function taking two callbacks as arguments;
- a success callback, and
- a failure callback
which returns an abort function, and which calls one of the callbacks asynchronously can be treated as a Cancelable.
import { addFantasyLandInterface } from '@visisoft/staticland/cancelable';
let cancelableWork = (res, rej) => {
const work = beginSomeLongWork();
whenCompletedWithResult(work, result => res(result));
whenFailedWorkWithError(work, error => res(error));
return () => { abort(work); };
};
addFantasyLandInterface(cancelableWork);
In order to provide FantasyLand methods such a function needs to be wrapped with addFantasyLandInterface
.
of(value)
:: a -> Cancelable () a
reject(e)
:: e => Cancelable e ()
never()
:: () -> Cancelable () ()
Creates a Cancelable which executes nothing and never settles.
later(dt, value)
:: Number -> a -> Cancelable () a
laterReject(dt, e)
:: Number -> e -> Cancelable e ()
cancelify(f)
:: (*... → Promise e a) → *... → Cancelable e a
Takes a function f
which generates a non-abort-/non-cancel-able Promise and returns a function with the same arguments but which returns a Cancelable. The computation cannot be cancelled. Cancelling the resulting Cancelable will simply prevent the continuation callbacks from getting called once the promise settles.
See promiseToCancelable
.
cancelifyWithArityAbortable(arity, f)
:: (Number, *... → Promise e a) → *... → Cancelable e a
Like cancelify
but for functions f
which accept an AbortSignal
under the key signal
in their last argument. Thus, for functions f
which return abortable Promises.
Like with cancelify
, cancelling the resulting Cancelable will prevent the continuation callbacks from getting called, but the computation will actually be aborted.
import cp from 'node:child_process';
import { promisify } from 'node:util';
import fsp from 'node:fs/promises';
const
exec = cancelifyWithArityAbortable(2, promisify(cp.exec)),
readFile = cancelifyWithArityAbortable(2, fsp.readFile),
directoryList = exec("ls -l", {}),
packageContents = readFile('./package.json', { encoding: 'utf8' });
fromNodeCallbackWithArity(arity, sourceFn)
:: Number → ((*..., NodeCallback a) → ()) → *... → Cancelable a
NodeCallback = (error, a) → ()
Creates an auto-curried function of arity arity
from a source function that accepts a Node-style callback callback(error, data)
as last parameter. The returned function can be invoked with the leading parameters of the Node-style source function, and returns a Cancelable.
The computation cannot be cancelled. Cancelling the resulting Cancelable will simply prevent the continuation callbacks from getting called once the callback is finally invoked.
The arity argument is provided to support functions with trailing default arguments. It must be a single argument less than the maximum number of arguments to the source function.
createDeferred()
DeferredCancelable e a = {cancelable: Cancelable e a, resolve: a → (), reject: e → (), cancel: () → ()}
:: () → DeferredCancelable e a
Creates a cached/shared Cancelable along with "finalize" functions cancel
, reject
and resolve
. This can act as an entry point to a cancelable pipeline.
This inverts, who is in control.
- The computation needs to be carried out elsewhere, no longer in the Cancelable's function body.
- The outcome of the computation needs to be imperatively delivered via
reject
orresolve
. - Cancellation too is not in the hands of the consumer, but can be achieved by
- never calling
reject
orresolve
, or - explicitly invoking
cancel()
which just performs some internal cleanup
Note: After calling any function of the set cancel
, reject
, resolve
again after any other, results in undefined behaviour.
fetchResponse({url, fetchSpec})
via fetchResponseIsoModule
:: {url: (String|URL), init: {}} -> Cancelable Error Response
This example fetches the number of libraries hosted at cdnjs.com.
The result is a Cancelable of a Maybe of a Number. The
- failure continuation of the cancelable contains network errors and errors in the JSON format of the response,
- nothing path of the maybe is taken in case the combined network and parse duration exceeded the timeout,
- the just path of the Maybe in the success continuation of the Cancelable contains the result.
import {ap, pair, pipe} from 'ramda';
import {chain as chain_c, later, map as map_c, fetchResponseIsoModule}
from '@visisoft/staticland/cancelable';
import { eitherToCancelable, keyPromiseToPromiseCollection, promiseToCancelable }
from '@visisoft/staticland/transformations';
import { chain as chain_e, right, left } from '@visisoft/staticland/either';
import {nothing, just, maybe } from '@visisoft/staticland/maybe';
// assume we are running in Node.js
import fetch from 'node-fetch';
import AbortController from "abort-controller";
const
// :: {url: (String|URL), init: {}} -> Cancelable Error Response
fetchResponse = fetchResponseIsoModule({fetch, AbortController}),
// in a Browser this would be
fetchResponse = fetchResponseIsoModule(globalThis);
// :: Pair Response {k:v} -> Either {k:v}
checkFetchResponseWithPayload = ([response, payload]) =>
response.ok ?
right(payload) :
left({
message: response.statusText,
code: response.status,
payload
}),
// :: {k:v} -> Either Number
checkStatsResult = payload =>
typeof payload.libraries === "number" ?
right(payload.libaries) :
left({
message: "Unexpected API response",
payload
}),
// :: Cancelable Maybe Number
numberOfCDNJSLibraries = pipe(
() => fetchResponse({url: "https://api.cdnjs.com/stats", init: { mode: "cors" }}), // :: Cancelable Response
map_c(ap(pair, response => response.json())), // :: Cancelable Pair (Response) (Promise {k:v})
map_c(keyPromiseToPromiseCollection(1)), // :: Cancelable Promise Pair (Response) ({k:v})
chain_c(promiseToCancelable), // :: Cancelable Pair (Response) ({k:v})
map_c(checkFetchResponseWithPayload), // :: Cancelable Either {k:v}
map_c(chain_e(checkStatsResult)), // :: Cancelable Either Number
chain_c(eitherToCancelable), // :: Cancelable Number
map_c(just), // :: Cancelable Maybe Number
race(later(1000, nothing())), // :: Cancelable Maybe Number
)();
new Promise(numberOfCDNJSLibraries) // running
.then( // consuming
maybeCount => {
console.log(maybe(
() => "Info: API request did not complete in time.",
n => `Result: CDNJS hosts ${n} libraries.`,
maybeCount
));
},
error => {
console.log(`Error: fetching CNDJSAPI: "${error.message}".\nData received:"${JSON.stringify(error.payload)}"`);
}
);
Transformation
coalesce(onFailure, onSuccess, cancelable)
:: Cancelable c ⇒ (x → b) → (a → b) → c x a → c x b
Maps failure via onFailure
and success via onSuccess
to new success value.
map(f, cancelable)
:: (Cancelable x) c ⇒ (a → b) → c a → c b
biMap(fnLeft, fn, cancelable)
:: Cancelable c ⇒ (x → y) → (a → b) → c x a → c y b
pluck(key)
pluck(key)
:: Cancelable c ⇒ k → c {k: v} → c v
:: Cancelable c ⇒ number → c [v] → c v
Simply k => map(R.prop(k))
for mapping to a key value.
Deprecated use pluck
from Ramda or fantasyland/pluck
.
chain(f, cancelable)
:: (a → Cancelable b) → Cancelable a → Cancelable b
biChain(fnLeft, fnRight, cancelable)
:: Cancelable c ⇒ (* → c b) → (a → c b) → c a → c b
Side-Effects
bi_tap(onFailure, onSuccess)
:: (e -> *) -> (a -> *) -> Cancelable e a -> Cancelable e a
If either function throws an error, the present outcome is replaced by a failure given of that error.
Combinations
ap(cancelableFunc, cancelable)
:: Cancelable (a → b) → Cancelable a → Cancelable b
Parallel running version: It runs both Cancelable arguments in parallel.
Note that the textbook version ap(mf, ma) = chain(f => map(f, ma), mf)
— in contrast — does run the Cancelable Computations in parallel.
liftA2(f, ccA , ccB)
:: (a → b → c) → Cancelable a → Cancelable b → Cancelable c
liftA2(f)
returns a function which takes two Cancelables as arguments and returns a Cancelable as result.
f
must be curried or auto-curried.
Similarity with Promise.all
With pair = a => b => [a, b]
, liftA2(pair)
is the equivalent to (a, b) => Promise.all([a, b])
.
Both Cancelables run in parallel.
The equivalent to Promise.all :: [Promise a] -> Promise [a]
though is sequence(Cancelable) :: [Cancelable a] -> Cancelable [a]
.
General note on lift
from Ramda
When using lift
from Ramda instead of liftA2
, liftA3
, …, the resulting "lifted" function is auto-curried with the right arity only if f
is also auto-curried. Otherwise, the "lifted" function is not curried despite reporting the arity of 1.
liftA3(f, ccA , ccB, ccC)
:: (a → b → c → d) → Cancelable a → Cancelable b → Cancelable c → Cancelable d
liftA4(f, ccA , ccB, ccC, ccD)
:: (a → b → c → d → e) → Cancelable a → Cancelable b → Cancelable c → Cancelable d → Cancelable e
race(cancelableA, cancelableB)
:: Cancelable a → Cancelable a → Cancelable a
Like Promise.race
shortcuts (i.e. aborts the other) when any input value is settled with success or failure.
Utility
share(cancelable)
:: Cancelable e a → Cancelable e a
Caches the computational result for sharing with many consumers without the need for re-computation. See the section on Copying/Sharing Cancelables and it's role model Fluture.cache.