A set of useful utilities and tools.
- Toolbelt
A fast, simple, typed way to chain together asynchronous functions - with the output of each function acting as the input to the subsequent function.
If an error is returned by a function in the chain, that effectively ends any further processing of the chain.
An await
ed chain returns an AwaitedChainController
- which can be used to communicate with the currently executing function, including for example implementing a cancel ability.
import { chain } from '...'
import type { Resolver } from '....'
// function to generate dummy asynchronous functions
const addChar =
<T extends string, C extends string>(c: C) =>
(x: T, resolver: Resolver<(result: `${C}:${T}`) => void>) =>
resolver(`${c}:${x}` as `${C}:${T}`)
// adds three asynchronous functions to the chain
const fooChain = chain(
addChar<'start', 'A'>('A'),
addChar<'A:start', 'B'>('B'),
addChar<'B:A:start', 'C'>('C'),
)
// adds a further three asynchronous functions to the chain
const fooBarChain = fooChain(
addChar<'C:B:A:start', 'A'>('A'),
addChar<'A:C:B:A:start', 'B'>('B'),
addChar<'B:A:C:B:A:start', 'C'>('C'),
)
// awaits chain of asynchronous functions
fooBarChain.await('start' as const, (result) => {
expect(result).toEqual('C:B:A:C:B:A:start')
done(undefined)
})
chain( ...asyncFns: [AsyncFn, ...AsyncFn[]] ) => ChainNode
Functions passed to the chain must have the following form:
;(
input: any, // input provided to the async function
resolver: Resolver, // a Resolver object is provided to the function - see below
) => any // ResultResolverController
The Resolver
is a function object passed to the AsyncFn
and it's used to return either a result
or an error
:
{
(result: any):any
result: (result: any)=>any
error: (error: any)=>any
}
A simple way to type a Resolver
is using the provided type Resolver<ResultFn, ErrorFn>
- note ErrorFn
is optional e.g.:
import type { Resolver } from '....'
const asyncFn = (
x: string,
resolver: Resolver<(output: string) => void, (error: Error) => void>,
) => {
resolver('...') // or
resolver.result('...') // or
resolver.error(new Error('...')) // or
}
Adding AsyncFn
s to the chain, creates a new ChainNode
. ChainNodes can have other AsyncFns added, and can be awaited, or trap upstream errors.
{
// function to add additional AsyncFns to the chain
( asyncFns: [AsyncFn, ...AsyncFn[]] ): ChainNode
// Symbol('Chain Node')
type: chainNodeType
// captures all downstream errors and prevents error bubbling to
// for example an errorCb in await.
// all errors effectively halt the execution of the rest
// of the chain.
onError(callback: (finalError: any)=>any): ChainNode
// awaits a chain
await(
input: any,
resultCb: (finalResult: any)=>any,
errorCb?: (finalError: any)=>any,
): AwaitedChainController
// converts the chain into a AsyncFn, which can then
// be spliced, or added into other chains.
readonly asyncFn: (input:any, resolver: Resolver)=>any
}
An object that contains the ResultResolverController
of the current asynchronously executing AsyncFn
. ResultResolverControllers
are mostly not used and set to void
. However they can be used to provide a mechanism to communicate with the currently executing AsyncFn
- for example one could implement cancel
or currentStatus
functionality.
{
controller: any
}
EnhancedChain
, is built on top of Chain
and provides additional functionality including:
- can be converted into a promise, via the
.input(startInput:any): Promise<any>
method - synchronous functions can be added to the chain via
.sync(...syncFunctions: (input:any)=>any[]): EnhancedChainNode
And the following options can be specified:
thrownErrorToErrorCb
- any errors thrown are routed to theerrorCb
. default istrue
enforceSingleResolution
- a resolver may be called once - any subsequent calls throw an error. default istrue
forceAsync
- allValidAsync
calls can be wrapped insetImmediate
|nextTick
|queueMicrotask
- the default isnone
resolveReturnedPromises
any promises returned via a resolver are resolved, before being passed on. default istrue
- note typing for this not yet properly implement!callbacks
- chain execution can be tracked via a range of callbacks/
import { enhancedChain } from '...'
import type { Resolver } from '...'
const eChainy = enhancedChain(
{
callbacks: { afterChainResolved: () => console.log('chain resolved') },
},
(x: 'start', resolve: Resolver<(arg: 'A') => void>) => {
console.log(x) // 'start'
resolve('A')
},
).sync(
(x: 'A') => 'B' as const,
(x: 'B') => new Promise((resolve) => resolve('C')),
)
const eChainyMore = eChainy((x: 'C', resolve: Resolver<(arg: 'done') => void>) => {
console.log(x) // 'C'
resolve('done' as const)
})
eChainy.input('start').then((result) => {
console.log(result) // 'C'
})
eChainyMore.await('start', (result) => {
console.log(result) // 'done'
done(undefined)
})
enhancedChain( options: EnhanceChainOptions, ...asyncFns: AsyncFn[] ) => EnhancedChainNode
{
thrownErrorToErrorCb?: boolean;
enforceSingleResolution?: boolean;
forceAsync?: 'setImmediate' | 'nextTick' | 'queueMicrotask' | 'none';
resolveReturnedPromises?: boolean;
callbacks?: {
beforeChainStart?: () => void
beforeChainResult?: () => void
afterChainResult?: () => void
beforeChainError?: () => void
afterChainError?: () => void
beforeChainResolved?: () => void
afterChainResolved?: () => void
beforeNodeStart?: () => void
beforeNodeResult?: () => void
afterNodeResult?: () => void
beforeNodeError?: () => void
afterNodeError?: () => void
};
}
expands ChainNode
by addition of the following methods:
{
// function to add additional synchronous functions to the chain
sync( ...syncFns: [syncFn, ...syncFn[]] ): EnhancedChainNode
// returns a Promise of the EnhancedChain
input(inputArg: any): Promise(any)
// Symbol('Enhanced Chain Node')
type: enhancedChainNodeType
...see ChainNode
}
Composes multiple functions, into a single function. Equivalent to (arg)=>fn3(fn2(fn1(arg)))
import { compose } from '...'
const fn = compose(
(a: string) => `${a}:A`,
(a: string) => `${a}:B`,
)
console.log(fn('start')) //`start:A:B`
An object to streamline function composition
import { compositor } from '...'
// chains functions together
const fn = compositor(
(input: 'a') => `${input}:b` as 'a:b',
(input: 'a:b') => `${input}:c` as 'a:b:c',
)
const fnFn = fn.call // makes a snapshot of chained functions
console.log(fnFn('a')) // 'a:b:c'
// chains are expandable
const fn2 = fn(
(input: 'a:b:c') => `${input}:d` as 'a:b:c:d',
(input: 'a:b:c:d') => `${input}:e` as 'a:b:c:d:e',
)
console.log(fn2.call('a')) // 'a:b:c:d:e'
console.log(fnFn('a')) // 'a:b:c'
// an empty compositor returns whatever it is called with
console.log(compositor().call('hello')) // hello
// effects are functions added to the composition, but
// which don't impact the composition calculation.
fn2.addEffects(
() => console.log('hello'),
() => console.log('world'),
)
// logs 'hello' 'world' and returns 'a:b:c:d:e'
console.log(fn2.call('a'))
// insertPlaceholder creates a placeholder for a function
// which can later be provided
// useful if the function changes between calls.
const setFn = fn2.insertPlaceholder(undefined as unknown as (arg: 'a:b:c:d:e') => 'a:b:c:d:e:f')
// logs 'hello' 'world' and returns 'a:b:c:d:e:f'
console.log(setFn((input: 'a:b:c:d:e') => `${input}:f` as 'a:b:c:d:e:f')('a'))
{
// add functions to the compositor
(...fns: [(input: any) => any, ...((input: any) => any)[]]): Compositor
// creates and returns the composed function
call(input:any): any
/**
* utility to add functions that take no input, and return no output
* but to run them in sequence in the chain - useful for example to fire events,
* or notifiers.
*/
addEffects(...effects: [() => void, ...(() => void)[]]): Compositor
}
Enables the coupling of two async callbacks: incomingCallback
and outgoingCallback
- which methods can be renamed as require. The callbacks may be added in any sequence and are enqueued.
Once both callbacks have been added: outgoingCallback(incomingCallback)
is called. Optionally, instead of FIFO, a manual index may be specified causing callbacks to be made in index order.
asyncCouplerWorkAround
provides a DRY'er way to specify the typings.
// default asyncCoupler has `addOutgoingCallback` and `addIncomingCallback` methods
const coupler = asyncCoupler<(result: number) => void>()
coupler.addOutgoingCallback((incomingCb) => incomingCb(1))
coupler.addIncomingCallback(
(result) => console.log(result), // 1
)
// renaming methods
const cCoupler = asyncCoupler<
(input: number) => void,
{
outgoingCallbackName: 'addA'
incomingCallbackName: 'addB'
}
>({
outgoingCallbackName: 'addA',
incomingCallbackName: 'addB',
})
cCoupler.addA((incomingCb) => incomingCb(1))
cCoupler.addB((result) => {
console.log(result) // 1
done(undefined)
})
// a workaround to reduce typing
const cCouplerA = asyncCouplerWorkAround({
outgoingCallbackName: 'addA',
incomingCallbackName: 'addB',
} as const)<(input: number) => void>()
cCouplerA.addA((incomingCb) => incomingCb(1))
cCouplerA.addB((result) => {
console.log(result) // 1
done(undefined)
})
AsyncFnsInParallel executes asynchronous functions in parallel. Similar to, but simpler than Promise.all
and more performant.
Asynchronous functions take the form:
;(input: any, resolver: AsyncFnResolver) => controller
import { asyncFnsInParallel } from '...'
import type { AsyncFnResolver } from '...'
const parallelFns = asyncFnsInParallel(
(a: 1, resolver: AsyncFnResolver<(value: 1) => void>) =>
setTimeout(() => resolver((a * 1) as 1), 100),
(a: 1, resolver: AsyncFnResolver<(value: 2) => void>) =>
setTimeout(() => resolver((a * 2) as 2), 100),
)
parallelFns.await(1, (results) => {
console.log(results) // [1,2]
})
asyncFnsInParallel( ...asyncFns: [AsyncFnAny, ...AsyncFnAny[]]) =>{
await: (
input: Input,
resultCb: (resultArray: ResultsArray) => void,
errorCb?: (errorArray: ErrorsArray, resultArray: ResultsArray) => void,
)=>AsyncFnsInParallelController
promise: (input: any)=>Promise<any>
}
{
state: 'awaited' | 'halted' | 'done' | 'error'
// all controllers that are not undefined.
controllers: Controllers[]
resultQueue: [...results:any[]][]
errorQueue: [...errorArgs:any[]][]
controllerQueue: Controllers[]
// halts the AsyncFnsInParallel and no resultCb or errorCb will be made
// useful if one wants to cancel the `AsyncFnsInParallel`.
halt():void
}
A simple way to type a Resolver
is using the provided type AsyncFnResolver<ResultFn, ErrorFn>
- note ErrorFn
is optional e.g.:
import type { Resolver } from '....'
const AsyncFn = (
x: string,
resolver: AsyncFnResolver<(output: string) => void, (error: Error) => void>,
) => {
resolver('...') // or
resolver.result('...') // or
resolver.error(new Error('...')) // or
}
Finds the set of all elements in the first array not contained in the second array (i.e. non duplicated items).
Note: typing is not battle tested and so unexpected edge cases may exist
const u1 = difference([1, 2, 3, 4] as const, [7, 6, 5, 4, 3] as const) //=> [1,2]
const u2 = difference([7, 6, 5, 4, 3] as const, [1, 2, 3, 4] as const) //=> [7,6,5]
const u3 = difference([7, 6, 5, 4, 3], [1, 2, 3, 4]) //=> [7,6,5] type: number[]
Given two arrays, intersection returns a set composed of the elements common to both arrays.
Note: typing is not battle tested and so unexpected edge cases may exist
const u1 = intersection([1, 2, 3, 4] as const, [7, 6, 5, 4, 3] as const) //=> [3,4]
const u2 = intersection([7, 6, 5, 4, 3] as const, [1, 2, 3, 4] as const) //=> [3,4]
const u3 = intersection([7, 6, 5, 4, 3] as const, [1, 2, 3, 4, 'a'] as const) //=> [3,4]
const u4 = intersection([7, 6, 5, 4, 3] as const, [1, 2, 3, 4]) //=> [3,4] type: number[]
const u5 = intersection([7, 6, 5, 4, 3] as const, [1, 2, 3, 4, 'a']) //=> [3,4] type: (string | number)[]
const u6 = intersection([7, 6, 5, 4, 3], [1, 2, 3, 4, 'a']) //=> [3,4] type: (string | number)[]
Enhances javascript's map
function
<V>(...iterable: readonly V[]) => {
/**
* adds an item and an optional `key` can be supplied,
* otherwise insertion order is used.
* @returns a function that removes the added item from the map.
*/
add(item: V, key?: number): () => boolean;
/**
* adds an array of item to the map.
* @returns a function that removes all of the added item from the map.
*/
addItems(...items: V[]): () => void;
/**
*
* @param basedOnInsertionOrder whether to shift
* based on insertion order, or key order
* @returns V|undefined
*/
shift(basedOnInsertionOrder?: boolean): V | undefined;
/**
* sets the item at `key`
*/
set(key: number, value: V): any;
/**
* count of the total number of items added to the queue
*/
readonly countOfItemsAdded: number;
reduce<U>(callbackfn: (previousValue: U, currentValue: V, currentKey: number, index: number) => U, initialValue: U, reverseOrder?: boolean): U;
map<U>(callbackfn: (value: V, key: number) => U, reverseOrder?: boolean): U[];
clear(): void;
delete(key: number): boolean;
forEach(callbackfn: (value: V, key: number) => void, thisArg?: any): void;
get(key: number): V | undefined;
has(key: number): boolean;
[Symbol.iterator](): IterableIterator<[number, V]>;
readonly [Symbol.toStringTag]: string;
readonly entries: IterableIterator<[number, V]>;
readonly keys: IterableIterator<number>;
readonly values: IterableIterator<V>;
readonly size: number;
}
A function that creates an object which provides convenient ways to route an outcome.
const exampleResultErrorGenerator = outputPins<
{ result: [result: 'RESULT']; error: [error: Error]; cancel: [cancelReason: 'CANCEL'] },
'result'
>('result', 'error', 'cancel')
type OutputError = OutputPinGetter<
{ result: [result: 'RESULT']; error: [error: Error]; cancel: [cancelReason: unknown] },
'error'
>
type OutputCancel = OutputPinGetter<
{ result: [result: 'RESULT']; error: [error: Error]; cancel: [cancelReason: unknown] },
'cancel'
>
type OutputResult = OutputPinGetter<
{ result: [result: 'RESULT']; error: [error: Error]; cancel: [cancelReason: unknown] },
'result'
>
const fn = (error: boolean) => {
const returnResult = exampleResultErrorGenerator()
// eslint-disable-next-line no-constant-condition
if (false) return returnResult.cancel('CANCEL')
return error ? returnResult.error(new Error('error')) : returnResult('RESULT')
}
const results = fn(true)
if (results.isError()) throw (results as OutputError).error
if (results.isCancel()) throw (results as OutputCancel).cancel
console.log(results()) // 'RESULT'
console.log(results.isResult())
console.log((results as OutputResult).result)
Inspired by the either
monad, this function returns a function object, that can have either a result
or an error
set.
const fn = (error: boolean) => {
const returnResult = resultError<'RESULT', Error>()
return error ? returnResult.error(new Error('error')) : returnResult('RESULT')
}
const results = fn(false)
if (results.isError()) throw (results as ResultError<'RESULT', Error, 'error'>).error
console.log(results()) // 'RESULT'
console.log((results as ResultError<'RESULT', Error, 'result'>).result) // 'RESULT'
Inspired by the maybe
monad, this function returns a function object, that can have either a result
or a none
set.
const fn = (error: boolean) => {
const returnResult = resultNone<'RESULT', null>()
return error ? returnResult.none(null) : returnResult('RESULT')
}
const results = fn(false)
if (results.isNone()) throw new Error('null')
console.log(results()) // 'RESULT'
console.log((results as ResultNone<'RESULT', null, 'result'>).result) // 'RESULT'
-
didError
const fn = (error: boolean) => { const output = didError<Error>() return error ? output.error(new Error('ERROR')) : output() } const results = fn(true) if (results.isError()) console.log((results as DidError<Error, true>).errorValue()) // Error('ERROR') expect((results as DidError<Error, true>).errorValue()).toBeInstanceOf(Error)
-
wrapTryCatchInDidError
const fn = wrapTryCatchInDidError((error: boolean) => { if (error) throw new Error('ERROR') }) const results = fn(true) if (results.isError()) console.log((results as DidError<Error, true>).errorValue()) // Error('ERROR') expect((results as DidError<Error, true>).errorValue()).toBeInstanceOf(Error)
-
times
console.log(times(50, (previousResult) => previousResult + 1, 10)) // 60
-
runFunctionsOnlyOnce
let x = 1 const once = runFunctionsOnlyOnce() const fn1 = once(() => { x += 1 console.log(x) }, 'fn1') const fn2 = once(() => { x -= 1 console.log(x) }, 'fn2') fn1() fn2() // throws cannot call 'fn2' after calling 'fn1'
-
validateFn
-
requireValue
const fn = requireValue((x: any) => x) console.log(fn('a')) fn() // throws this function requires a value
-
callbackTee
-
reverseForEach
-
createUid
-
methodOnlyOnce
-
validObjects
-
capitaliseWords, capitalise
const x = capitalise('cat') // 'Cat' const y = capitaliseWords('cat dog') // 'Cat Dog'
-
isObjectAndHasExecutableProperty
-
isGetter
-
isSetter
-
isValue
-
isFunction