src/Task/Task.ts
/* eslint-disable @typescript-eslint/no-misused-promises, @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-use-before-define */
import { constant, drop, identity, range, take, Validation } from "../util"
export type Reject<E> = (error: E) => void
export type Resolve<S> = (result: S) => void
export type Fork<E, S> = (reject: Reject<E>, resolve: Resolve<S>) => void
/**
* Creates a Task which can be resolved/rejected externally.
*/
export const external = <E, S>(): ExternalTask<E, S> => new ExternalTask()
export const emitter = <Args extends unknown[], R>(
fn: (...args: Args) => R,
): [ExternalTask<unknown, R>, (...args: Args) => void] => {
const task = external<unknown, R>()
return [
task,
(...args: Args) => {
try {
task.resolve(fn(...args))
} catch (e) {
task.reject(e)
}
},
]
}
/**
* Creates a Task which has already successfully completed with `result`.
* @alias of
* @alias ok
* @param result The value to place into the successful Task.
*/
export const succeed = <S>(result: S): Task<never, S> =>
new Task((_, resolve) => resolve(result))
export const of = succeed
/**
* Creates a Task which succeeds when forked.
* @param result The function which will produce the result.
*/
export const succeedBy = <S>(result: () => S): Task<unknown, S> =>
new Task((reject, resolve) => {
try {
resolve(result())
} catch (e) {
reject(e)
}
})
export const try_ = succeedBy
/**
* Creates a Task has an empty result.
* @alias unit
*/
export const empty = (): Task<never, undefined> => of(void 0)
/**
* Creates a Task which automatically succeeds at some time in the future with `result`.
* @param ms How many milliseconds until it succeeds.
* @param result The value to place into the successful Task.
*/
export const succeedIn = <S>(ms: number, result: S): Task<never, S> =>
new Task((_, resolve) => setTimeout(() => resolve(result), ms))
/**
* Creates a Task which has already failed with `error`.
* @alias err
* @param error The error to place into the failed Task.
*/
export const fail = <E>(error: E): Task<E, never> =>
new Task(reject => reject(error))
/**
* Creates a Task which automatically fails at some time in the future with `error`.
* @param ms How many milliseconds until it succeeds.
* @param error The error to place into the failed Task.
*/
export const failIn = <E>(ms: number, error: E): Task<E, never> =>
new Task(reject => setTimeout(() => reject(error), ms))
/**
* Creates a Task will never finish.
*/
export const never = (): Task<never, never> => new Task(() => void 0)
/**
* Execute task computation and call handlers on completion.
* @param reject Function to call on failure.
* @param resolve Function to call on success.
* @param task The task to fork.
*/
export const fork = <E, S>(
reject: Reject<E>,
resolve: Resolve<S>,
task: Task<E, S>,
): { cancel: () => void } => task.fork(reject, resolve)
/**
* Chain a task to run after a previous task has succeeded.
* @param fn Takes a successful result and returns a new task.
* @param task The task which will chain to the next one on success.
*/
export const chain = <E, S, S2, E2>(
fn: (result: S) => Task<E2, S2>,
task: Task<E, S>,
): Task<E | E2, S2> =>
new Task((reject, resolve) =>
task.fork(reject, b => fn(b).fork(reject, resolve)),
)
/**
* When forked, run a function which can check whether the task has already succeeded.
* @param fn The function which either returns a success value or undefined.
* @param task The task to run if the check fails (returns undefined).
*/
export const succeedIf = <E, S>(
fn: () => S | undefined,
task: Task<E, S>,
): Task<E, S> =>
new Task((reject, resolve) => {
const result = fn()
if (result) {
resolve(result)
return
}
task.fork(reject, resolve)
})
/**
* A task which only runs once. Caches the success or failure. Be careful.
* @alias share
* @param task The task to cache results.
*/
export const onlyOnce = <E, S>(task: Task<E, S>): Task<E, S> => {
let state: "initialized" | "pending" | "success" | "failure" = "initialized"
let cachedResult: S
let cachedError: E
let callbackId = 0
const callbacks: {
[id: string]: { reject: Reject<E>; resolve: Resolve<S> }
} = {}
const notify = (reject: Reject<E>, resolve: Resolve<S>) => {
const id = callbackId++
callbacks[id] = { reject, resolve }
}
const triggerReject = (error: E) => {
state = "failure"
cachedError = error
Object.keys(callbacks).forEach(id => {
callbacks[id].reject(error)
delete callbacks[id]
})
}
const triggerResolve = (result: S) => {
state = "success"
cachedResult = result
Object.keys(callbacks).forEach(id => {
callbacks[id].resolve(result)
delete callbacks[id]
})
}
return new Task((reject, resolve) => {
switch (state) {
case "success":
resolve(cachedResult!)
break
case "failure":
reject(cachedError!)
break
case "pending":
notify(reject, resolve)
break
case "initialized":
state = "pending"
notify(reject, resolve)
task.fork(triggerReject, triggerResolve)
}
})
}
export const share = onlyOnce
/**
* Given a promise, create a Task which relies on it.
* @param promise The promise we will gather the success from.
*/
export const fromPromise = <S>(
maybePromise: S | Promise<S>,
): Task<unknown, S> =>
maybePromise instanceof Promise
? new Task((reject, resolve) => maybePromise.then(resolve, reject))
: of(maybePromise)
/**
* Given an array of promises, create a Task which relies on it.
* @param promise The promises we will gather the success from.
*/
export const fromPromises = <S>(
promises: Array<Promise<S>>,
): Task<unknown, S[]> => all(promises.map(fromPromise))
/**
* Take a function which generates a promise and lazily execute it.
* @param getPromise The getter function
*/
export const fromLazyPromise = <S>(
getPromise: () => S | Promise<S>,
): Task<unknown, S> => succeedBy(getPromise).chain(fromPromise)
/**
* Given a function that returns a promise, return a new function that
* lazily returns a Task instead.
* @param fn A function which returns a promise
*/
export const wrapPromiseCreator =
<S, Args extends unknown[]>(fn: (...args: Args) => Promise<S>) =>
(...args: Args): Task<unknown, S> =>
fromLazyPromise(() => fn(...args))
/**
* Given a task, create a Promise which resolves when the task does.
* @param task The task we will convert to a promise.
*/
export const toPromise = <E, S>(task: Task<E, S>): Promise<S> =>
new Promise((resolve, reject) => task.fork(reject, resolve))
/**
* Given an array of tasks, return the one which finishes first.
* @alias select
* @param tasks The tasks to run in parallel.
*/
export const race = <E, S>(tasks: Array<Task<E, S>>): Task<E, S> =>
new Task<E, S>((reject, resolve) => {
let done = false
return tasks.map(task =>
task.fork(
(error: E) => {
if (done) {
return
}
done = true
reject(error)
},
(result: S) => {
if (done) {
return
}
done = true
resolve(result)
},
),
)
})
export class LoopBreak<S> {
constructor(public readonly value: S) {}
}
export class LoopContinue<S> {
constructor(public readonly value: S) {}
}
/**
* Given an initialValue, asynchronously loop until either a value is
* resolved by returning a Task<E, LoopBreak<S>>.
* @param fn A function that takes the current loop value and decides whether to continue or break.
* @param initialValue The initial value.
*/
export const loop = <E, S, T>(
fn: (currentValue: T) => Task<E, LoopBreak<S> | LoopContinue<T>>,
initialValue: T,
): Task<E, S> =>
new Task((reject, resolve) => {
const tryLoop = (currentValue: T) => {
fn(currentValue).fork(
err => {
reject(err)
},
result => {
if (result instanceof LoopBreak) {
resolve(result.value)
}
if (result instanceof LoopContinue) {
tryLoop(result.value)
}
},
)
}
tryLoop(initialValue)
})
/**
* An async reducer. Given an initial return value and an array of
* items to sequentially loop over, pass each step through a reducer
* function which returns a Task of the next reduced value.
* @param fn
* @param initialValue
* @param items
*/
export const reduce = <E, T, V>(
fn: (acc: V, currentValue: T, index: number, original: T[]) => Task<E, V>,
initialValue: V,
items: T[],
): Task<E, V> =>
loop(
({ remainingItems, currentResult }) => {
if (remainingItems.length === 0) {
return of(new LoopBreak(currentResult))
}
const [head, ...tail] = remainingItems
const index = items.length - tail.length - 1
return fn(currentResult, head, index, items).map(
nextResult =>
new LoopContinue({ remainingItems: tail, currentResult: nextResult }),
)
},
{ remainingItems: items, currentResult: initialValue },
)
/**
* Given an array of tasks, return the one which finishes successfully first.
* @param tasks The tasks to run in parallel.
*/
export const firstSuccess = <E, S>(tasks: Array<Task<E, S>>): Task<E[], S> =>
tasks.length === 0
? fail([])
: new Task<E[], S>((reject, resolve) => {
let isDone = false
let runningTasks = tasks.length
const errors: E[] = []
return tasks.map(task =>
task.fork(
(error: E) => {
if (isDone) {
return
}
runningTasks -= 1
errors.push(error)
if (runningTasks === 0) {
reject(errors)
}
},
(result: S) => {
if (isDone) {
return
}
isDone = true
resolve(result)
},
),
)
})
/**
* Given an array of task which return a result, return a new task which returns an array of results.
* @alias collect
* @param tasks The tasks to run in parallel.
*/
export const all = <E, S>(tasks: Array<Task<E, S>>): Task<E, S[]> =>
sequence(tasks, Infinity)
/**
* Given an array of task which return a result, return a new task which returns an array of successful results.
* @param tasks The tasks to run in parallel.
*/
export const allSuccesses = <E, S>(
tasks: Array<Task<E, S>>,
): Task<never, S[]> =>
tasks.length === 0
? of([])
: new Task<never, S[]>((_reject, resolve) => {
let runningTasks = tasks.length
const results: S[] = []
return tasks.map(task =>
task.fork(
() => {
runningTasks -= 1
if (runningTasks === 0) {
resolve(results)
}
},
(result: S) => {
runningTasks -= 1
results.push(result)
if (runningTasks === 0) {
resolve(results)
}
},
),
)
})
/**
* Creates a task that waits for two tasks of different types to
* resolve as a two-tuple of the results.
* @param taskA The first task.
* @param taskB The second task.
*/
export const zip = <E, E2, S, S2>(
taskA: Task<E, S>,
taskB: Task<E2, S2>,
): Task<E | E2, [S, S2]> => map2(a => b => [a, b], taskA, taskB)
/**
* Creates a task that waits for two tasks of different types to
* resolve, then passing the resulting two-tuple of results through
* a mapping function.
* @param fn
* @param taskA The first task.
* @param taskB The second task.
*/
export const zipWith = <E, E2, S, S2, V>(
fn: (resultA: S, resultB: S2) => V,
taskA: Task<E, S>,
taskB: Task<E2, S2>,
): Task<E | E2, V> => map2(a => b => fn(a, b), taskA, taskB)
/**
* Given an array of task which return a result, return a new task which results an array of results.
* @param tasks The tasks to run in sequence.
*/
export const sequence = <E, S>(
tasks: Array<Task<E, S>>,
maxConcurrent = 1,
): Task<E, S[]> =>
new Task((reject, resolve) => {
let isDone = false
type TaskPosition = [Task<E, S>, number]
let queue = tasks.map<TaskPosition>((task, i) => [task, i])
const inflight = new Set<Task<E, S>>()
const results: S[] = []
const enqueue = () => {
if (isDone) {
return
}
if (queue.length <= 0 && inflight.size <= 0) {
isDone = true
resolve(results)
return
}
const howMany = Math.min(queue.length, maxConcurrent - inflight.size)
const readyTasks = take(howMany, queue)
queue = drop(howMany, queue)
readyTasks.forEach(([task, i]) => {
inflight.add(task)
task.fork(
(error: E) => {
if (isDone) {
return
}
isDone = true
reject(error)
},
(result: S) => {
results[i] = result
inflight.delete(task)
enqueue()
},
)
})
}
enqueue()
})
/**
* Given a task, swap the error and success values.
* @param task The task to swap the results of.
*/
export const swap = <E, S, E2 extends E, S2 extends S>(
task: Task<E, S>,
): Task<S2, E2> =>
new Task<S2, E2>((reject, resolve) =>
task.fork(
e => resolve(e as E2),
s => reject(s as S2),
),
)
/**
* Given a task, map the successful value to a Task.
* @param fn A function which takes the original successful result and returns the new one.
* @param task The task to map the succcessful result.
*/
export const map = <E, S, S2>(
fn: (result: S) => S2,
task: Task<E, S>,
): Task<E, S2> =>
new Task<E, S2>((reject, resolve) =>
task.fork(reject, result => resolve(fn(result))),
)
export const map2 = <E, E2, S, S2, S3>(
fn: (a: S) => (b: S2) => S3,
taskA: Task<E, S>,
taskB: Task<E2, S2>,
): Task<E | E2, S3> => Task.of(fn).ap(taskA).ap(taskB)
export const map3 = <E, E2, E3, S, S2, S3, S4>(
fn: (a: S) => (b: S2) => (c: S3) => S4,
taskA: Task<E, S>,
taskB: Task<E2, S2>,
taskC: Task<E3, S3>,
): Task<E | E2 | E3, S4> => Task.of(fn).ap(taskA).ap(taskB).ap(taskC)
export const map4 = <E, E2, E3, E4, S, S2, S3, S4, S5>(
fn: (a: S) => (b: S2) => (c: S3) => (d: S4) => S5,
taskA: Task<E, S>,
taskB: Task<E2, S2>,
taskC: Task<E3, S3>,
taskD: Task<E4, S4>,
): Task<E | E2 | E3 | E4, S5> =>
Task.of(fn).ap(taskA).ap(taskB).ap(taskC).ap(taskD)
/**
* Run a side-effect on success. Useful for logging.
* @param fn A function will fire with the successful value.
* @param task The task to tap on succcess.
*/
export const tap = <E, S>(
fn: (result: S) => void,
task: Task<E, S>,
): Task<E, S> =>
map(result => {
fn(result)
return result
}, task)
/**
* Run an additional task on success. Useful for async side-effects.
* @alias defer
* @param fn A function will fire with the successful value.
* @param task The task to tap on succcess.
*/
export const tapChain = <E, S, S2>(
fn: (result: S) => Task<E, S2>,
task: Task<E, S>,
): Task<E, S> => chain(result => fn(result).forward(result), task)
/**
* Run a function on a successful value which can fail the task or modify the type.
* @param fn A function will return a Validation on the value.
* @param task The task to tap on succcess.
*/
export const validate = <E, S, E2, S2>(
fn: (value: S) => Validation<E2, S2>,
task: Task<E, S>,
): Task<E | E2, S2> =>
chain((value: S) => {
const result = fn(value)
return result.success ? of(result.value) : fail(result.error)
}, task)
/**
* Given a task, map the failure error to a Task.
* @alias recoverWith
* @alias rescue
* @param fn A function which takes the original failure error and returns the new one.
* @param task The task to map the failure.
*/
export const mapError = <E, S, E2>(
fn: (error: E) => E2,
task: Task<E, S>,
): Task<E2, S> =>
new Task<E2, S>((reject, resolve) =>
task.fork(error => reject(fn(error)), resolve),
)
export const validateError = <E, S, E2 extends E>(
fn: (err: E) => err is E2,
task: Task<E, S>,
): Task<E2, S> =>
mapError(err => {
if (!fn(err)) {
throw new Error(`validateError failed`)
}
return err
}, task)
export const errorUnion = <E, S, E2>(task: Task<E, S>): Task<E | E2, S> => task
/**
* Given a task, map both the failure error and the success result to a Task.
* @param handleError A function which takes the original failure error and returns the new one.
* @param handleSuccess A function which takes the original successful result and returns the new one.
* @param task The task to map the failure and succeess of.
*/
export const mapBoth = <E, S, E2, S2>(
handleError: (error: E) => E2,
handleSuccess: (success: S) => S2,
task: Task<E, S>,
): Task<E2, S2> => mapError(handleError, map(handleSuccess, task))
/**
* Given a task, map both the failure error and the success result to a Task which always succeeds.
* @param handleError A function which takes the original failure error and returns a successful result.
* @param handleSuccess A function which takes the original successful result and returns a new successful result.
* @param task The task to map failure and succeess to a success for.
*/
export const fold = <E, S, R>(
handleError: (error: E) => R,
handleSuccess: (success: S) => R,
task: Task<E, S>,
): Task<never, R> =>
new Task<never, R>((_, resolve) =>
task.fork(
error => resolve(handleError(error)),
result => resolve(handleSuccess(result)),
),
)
/**
* Given a task, if the result in a failure, attemp to generate another Task from the error.
* @param fn A function which takes the original failure error and returns a Task.
* @param task The task to try to run a recovery function on failure.
*/
export const orElse = <E, S>(
fn: (error: E) => Task<E, S>,
task: Task<E, S>,
): Task<E, S> =>
new Task<E, S>((reject, resolve) =>
task.fork(error => fn(error).fork(reject, resolve), resolve),
)
/**
* Given a task that succeeds with a map function as its result,
* run that function over the result of a second successful Task.
* @param appliedTask The task whose value will be passed to the map function.
* @param task The task who will return a map function as the success result.
*/
export const ap = <E, S, S2>(
task: Task<E, (result: S) => S2>,
appliedTask: Task<E, S>,
): Task<E, S2> =>
new Task((reject, resolve) => {
let targetResult: S
let applierFunction: ((result: S) => S2) | undefined
let hasResultLoaded = false
let isRejected = false
const handleResolve = <T>(onResolve: (result: T) => void) => {
return (x: T) => {
if (isRejected) {
return
}
onResolve(x)
if (applierFunction && hasResultLoaded) {
resolve(applierFunction(targetResult))
}
}
}
const handleReject = (x: E) => {
if (isRejected) {
return
}
isRejected = true
reject(x)
}
task.fork(
handleReject,
handleResolve((x: (result: S) => S2) => {
applierFunction = x
}),
)
appliedTask.fork(
handleReject,
handleResolve<S>((x: S) => {
hasResultLoaded = true
targetResult = x
}),
)
})
/**
* Wait some number of seconds to continue after a successful task.
* @param ms How long to wait in milliseconds.
* @param task Which task to wait to succeed with.
*/
export const wait = <E, S>(ms: number, task: Task<E, S>): Task<E, S> =>
new Task((reject, resolve) => {
setTimeout(() => task.fork(reject, resolve), ms)
})
/**
* If a task fails, retry it in the future.
* @param ms How long to wait before trying.
* @param task Which task to retry.
*/
export const retryIn = <E, S>(ms: number, task: Task<E, S>): Task<E, S> =>
task.orElse(() => task.wait(ms))
/**
* If a task fails, retry it X times, with exponential backoff.
* @param ms How long to wait before trying the first time.
* @param times How many times to attempt, each waiting 2x the previous time.
* @param task Which task to retry.
*/
export const retryWithExponentialBackoff = <E, S>(
ms: number,
times: number,
task: Task<E, S>,
): Task<E, S> => range(times).reduce((sum, i) => sum.retryIn(ms * 2 ** i), task)
/**
* Takes a nested task of tasks, which often comes from a map, and
* flattens to just the resulting chained task.
* @param task The task which resolves to an other task.
*/
export const flatten = <E, S>(task: Task<E, Task<E, S>>): Task<E, S> =>
task.chain(identity)
/**
* Given a predicate, if it returns true, error the task with a given value.
* @param pred Run this on a successful task, return true to fail the task.
* @param error If the predicate succeeded, run this function to get the error result.
*/
export const failIf = <E, S, E2>(
pred: (result: S) => boolean,
error: (result: S) => E2,
task: Task<E, S>,
): Task<E | E2, S> =>
task.chain(result => (pred(result) ? fail(error(result)) : of(result)))
/**
* Create a new task.
* @param computation A function which will be run when the task starts.
*/
export class Task<E, S> implements PromiseLike<S> {
public static fail = fail
public static succeed = succeed
public static empty = empty
public static failIn = failIn
public static succeedIn = succeedIn
public static of = succeed
public static all = all
public static allSuccesses = allSuccesses
public static sequence = sequence
public static firstSuccess = firstSuccess
public static never = never
public static fromPromise = fromPromise
public static fromPromises = fromPromises
public static fromLazyPromise = fromLazyPromise
public static wrapPromiseCreator = wrapPromiseCreator
public static race = race
public static external = external
public static emitter = emitter
public static succeedBy = succeedBy
public static ap = ap
public static map2 = map2
public static map3 = map3
public static map4 = map4
public static loop = loop
public static reduce = reduce
public static zip = zip
public static zipWith = zipWith
public static flatten = flatten
public isCanceled = false
constructor(private computation: Fork<E, S>) {}
fork(reject: Reject<E>, resolve: Resolve<S>): { cancel: () => void } {
let localCancel = this.isCanceled
const result = {
cancel: () => (localCancel = true),
}
if (localCancel) {
return result
}
this.computation(
err => {
if (!localCancel) {
reject(err)
}
},
value => {
if (!localCancel) {
resolve(value)
}
},
)
return result
}
cancel(): void {
this.isCanceled = true
}
/**
* Alias to match promise API and let async/await work.
* Mostly "private". Do not use.
*/
public then<TResult1 = S, TResult2 = never>(
onfulfilled?:
| ((value: S) => TResult1 | PromiseLike<TResult1>)
| undefined
| null,
onrejected?:
| ((reason: unknown) => TResult2 | PromiseLike<TResult2>)
| undefined
| null,
): PromiseLike<TResult1 | TResult2> {
return this.toPromise().then(onfulfilled, onrejected)
}
public chain<E2, S2>(fn: (result: S) => Task<E2, S2>): Task<E | E2, S2> {
return chain(fn, this)
}
public succeedIf(fn: () => S | undefined): Task<E, S> {
return succeedIf(fn, this)
}
public onlyOnce(): Task<E, S> {
return onlyOnce(this)
}
public toPromise(): Promise<S> {
return toPromise(this)
}
public swap<E2 extends E, S2 extends S>(): Task<S2, E2> {
return swap<E, S, E2, S2>(this)
}
public map<S2>(fn: (result: S) => S2): Task<E, S2> {
return map(fn, this)
}
public forward<S2>(value: S2): Task<E, S2> {
return map(constant(value), this)
}
public append<A, B, C, D, E>(
a: A,
b: B,
c: C,
d: D,
e: E,
): Task<E, [S, A, B, C, D, E]>
public append<A, B, C, D>(a: A, b: B, c: C, d: D): Task<E, [S, A, B, C, D]>
public append<A, B, C>(a: A, b: B, c: C): Task<E, [S, A, B, C]>
public append<A, B>(a: A, b: B): Task<E, [S, A, B]>
public append<A>(a: A): Task<E, [S, A]>
public append(...items: unknown[]): Task<E, unknown[]> {
return map<E, S, unknown[]>(a => [a, ...items], this)
}
public prepend<A, B, C, D, E>(
a: A,
b: B,
c: C,
d: D,
e: E,
): Task<E, [S, A, B, C, D, E]>
public prepend<A, B, C, D>(a: A, b: B, c: C, d: D): Task<E, [A, B, C, D, S]>
public prepend<A, B, C>(a: A, b: B, c: C): Task<E, [A, B, C, S]>
public prepend<A, B>(a: A, b: B): Task<E, [A, B, S]>
public prepend<A>(a: A): Task<E, [A, S]>
public prepend(...items: unknown[]): Task<E, unknown[]> {
return map<E, S, unknown[]>(a => [...items, a], this)
}
public tap(fn: (result: S) => void): Task<E, S> {
return tap(fn, this)
}
public tapChain<S2>(fn: (result: S) => Task<E, S2>): Task<E, S> {
return tapChain(fn, this)
}
public validate<E2, S2>(
fn: (value: S) => Validation<E2, S2>,
): Task<E | E2, S2> {
return validate(fn, this)
}
public mapError<E2>(fn: (error: E) => E2): Task<E2, S> {
return mapError(fn, this)
}
public validateError<E2 extends E>(fn: (err: E) => err is E2): Task<E2, S> {
return validateError(fn, this)
}
public errorUnion<E2>(): Task<E | E2, S> {
return errorUnion<E, S, E2>(this)
}
public mapBoth<E2, S2>(
handleError: (error: E) => E2,
handleSuccess: (success: S) => S2,
): Task<E2, S2> {
return mapBoth(handleError, handleSuccess, this)
}
public fold<R>(
handleError: (error: E) => R,
handleSuccess: (success: S) => R,
): Task<never, R> {
return fold(handleError, handleSuccess, this)
}
public orElse<S2>(fn: (error: E) => Task<E, S | S2>): Task<E, S | S2> {
return orElse(fn, this)
}
public ap<
E2,
S2,
S3 = S extends (arg: S2) => unknown ? ReturnType<S> : never,
>(task: Task<E | E2, S2>): Task<E | E2, S3> {
return ap(this as unknown as Task<E, (result: S2) => S3>, task)
}
public wait(ms: number): Task<E, S> {
return wait(ms, this)
}
public retryIn(ms: number): Task<E, S> {
return retryIn(ms, this)
}
public retryWithExponentialBackoff(ms: number, times: number): Task<E, S> {
return retryWithExponentialBackoff(ms, times, this)
}
public flatten<S2>(this: Task<E, Task<E, S2>>): Task<E, S2> {
return flatten(this)
}
public failIf<E2>(
pred: (result: S) => boolean,
error: (result: S) => E2,
): Task<E | E2, S> {
return failIf(pred, error, this)
}
}
/**
* A special form of Task which can be resolved/rejected externally.
*/
export class ExternalTask<E, S> extends Task<E, S> {
private computationReject_?: (error: E) => void
private computationResolve_?: (result: S) => void
private alreadyError_?: E
private alreadyResult_?: S
private lastState_: "pending" | "error" | "success" = "pending"
constructor() {
super((reject, resolve) => {
switch (this.lastState_) {
case "error":
reject(this.alreadyError_!)
case "success":
resolve(this.alreadyResult_!)
case "pending":
this.computationReject_ = reject
this.computationResolve_ = resolve
}
})
}
public reject(error: E): void {
this.alreadyError_ = error
this.lastState_ = "error"
if (this.computationReject_) {
this.computationReject_(error)
}
}
public resolve(result: S): void {
this.alreadyResult_ = result
this.lastState_ = "success"
if (this.computationResolve_) {
this.computationResolve_(result)
}
}
}
declare global {
interface Promise<T> {
toTask(): Task<unknown, T>
}
}
Promise.prototype.toTask = function <S>(this: Promise<S>): Task<unknown, S> {
return fromPromise(this)
}