/**
* This file contains the Promise algebraic data type. Promise is the javascript
* built in data structure for asynchronous computation.
*
* @module Promise
* @since 2.0.0
*/
import type { Kind, Out } from "./kind.ts";
import type { Applicable } from "./applicable.ts";
import type { Combinable } from "./combinable.ts";
import type { Either } from "./either.ts";
import type { Initializable } from "./initializable.ts";
import type { BindTo, Mappable } from "./mappable.ts";
import type { Bind, Flatmappable, Tap } from "./flatmappable.ts";
import type { Wrappable } from "./wrappable.ts";
import * as E from "./either.ts";
import { flow, handleThrow, pipe } from "./fn.ts";
import { createBind, createTap } from "./flatmappable.ts";
import { createBindTo } from "./mappable.ts";
/**
* A type for Promise over any, useful as an extension target for
* functions that take any Promise and do not need to
* unwrap the type.
* @since 2.0.0
*/
// deno-lint-ignore no-explicit-any
export type AnyPromise = Promise;
/**
* A type level unwrapor, used to pull the inner type from a Promise.
*
* @since 2.0.0
*/
export type TypeOf = T extends Promise ? A : never;
/**
* Specifies Promise as a Higher Kinded Type, with
* covariant parameter A corresponding to the 0th
* index of any Substitutions.
*
* @since 2.0.0
*/
export interface KindPromise extends Kind {
readonly kind: Promise>;
}
/**
* A Promise with the inner resolve function
* hoisted and attached to itself.
*
* @since 2.0.0
*/
export type Deferred = Promise & {
readonly resolve: (a: A | PromiseLike) => void;
};
/**
* Create a Deferred from a type.
*
* @example
* ```ts
* import { deferred } from "./promise.ts";
*
* const promise = deferred();
*
* // Logs 1 after a second
* promise.then(console.log);
* setTimeout(() => promise.resolve(1), 1000);
* ```
*
* @since 2.0.0
*/
export function deferred(): Deferred {
let method;
const promise = new Promise((res) => {
method = { resolve: async (a: A | PromiseLike) => res(await a) };
});
return Object.assign(promise, method);
}
/**
* Make an existing Promise somewhat abortable. While
* the returned promise does resolve when the abort signal
* occurs, the existing promise continues running in the
* background. For this reason it is important to
* catch any errors associated with the original promise and
* to not implement side effects in the aborted promise.
*
* @example
* ```ts
* import { abortable, wait } from "./promise.ts";
* import { pipe } from "./fn.ts";
*
* const controller = new AbortController();
* const slow = wait(1000).then(() => 1);
* const wrapped = pipe(
* slow,
* abortable(controller.signal, msg => msg),
* );
*
* setTimeout(() => controller.abort("Hi"), 500);
*
* // After 500ms result contains the following
* // { tag: "Left", left: "Hi" }
* const result = await wrapped;
* ```
*
* @since 2.0.0
*/
export function abortable(
signal: AbortSignal,
onAbort: (reason: unknown) => B,
): (ua: Promise) => Promise> {
return (ua: Promise): Promise> => {
if (signal.aborted) {
return resolve(E.left(onAbort(signal.reason)));
}
// Create abort handler
const _deferred = deferred>();
const abort = () => _deferred.resolve(E.left(onAbort(signal.reason)));
signal.addEventListener("abort", abort, { once: true });
return Promise.race([
_deferred,
ua.then(E.right).finally(() => {
// Remove abort handler
signal.removeEventListener("abort", abort);
}),
]);
};
}
/**
* Create a Promise that resolve after ms milliseconds that can also be
* disposed early.
*
* @example
* ```ts
* import { wait, map } from "./promise.ts";
* import { pipe } from "./fn.ts";
*
* const delayed = pipe(
* wait(1000),
* map(() => "Hello World"),
* );
*
* // After 1 second
* const result = await delayed; // "Hello World"
* ```
*
* @since 2.0.0
*/
export function wait(ms: number): Promise & Disposable {
const disposable = {} as unknown as Disposable;
const result = new Promise((res) => {
let open = true;
const resolve = () => {
if (open) {
open = false;
res(ms);
}
};
const handle = setTimeout(resolve, ms);
disposable[Symbol.dispose] = () => {
if (open) {
clearTimeout(handle);
resolve();
}
};
});
return Object.assign(result, disposable);
}
/**
* Delay the resolution of an existing Promise. This does not
* affect the original promise directly, it only waits for
* a ms milliseconds before flatmaping into the original promise.
*
* @example
* ```ts
* import { wrap, delay } from "./promise.ts";
* import { pipe } from "./fn.ts";
*
* // Waits 250 milliseconds before returning
* const result = await pipe(
* wrap(1),
* delay(250),
* ); // 1
* ```
*
* @since 2.0.0
*/
export function delay(ms: number): (ua: Promise) => Promise {
return (ua) => pipe(wait(ms), then(() => ua));
}
/**
* An alias for Promise.resolve.
*
* @example
* ```ts
* import { resolve } from "./promise.ts";
*
* const result = await resolve(1); // 1
* ```
*
* @since 2.0.0
*/
export function resolve(a: A | PromiseLike): Promise {
return Promise.resolve(a);
}
/**
* An alias for Promise.reject.
*
* @example
* ```ts
* import { reject } from "./promise.ts";
*
* const result = await reject(1).catch(x => x); // 1
* ```
*
* @since 2.0.0
*/
export function reject(
rejection: unknown,
): Promise {
return Promise.reject(rejection);
}
/**
* An alias for Promise.then
*
* @example
* ```ts
* import { wrap, then } from "./promise.ts";
* import { pipe } from "./fn.ts";
*
* const result = await pipe(
* wrap(1),
* then(n => n + 1),
* ); // 2
* ```
*
* @since 2.0.0
*/
export function then(
fai: (a: A) => I | Promise,
): (ua: Promise) => Promise {
return (ua) => ua.then(fai);
}
/**
* An alias for Promise.catch
*
* @example
* ```ts
* import { reject, catchError } from "./promise.ts";
* import { pipe } from "./fn.ts";
*
* const result = await pipe(
* reject(1),
* catchError(() => "Uhoh"),
* ); // "UhOh"
* ```
*
* @since 2.0.0
*/
export function catchError(
fua: (u: unknown) => A,
): (ta: Promise) => Promise {
return (ta) => ta.catch(fua);
}
/**
* An alias for Promise.all
*
* @example
* ```ts
* import { all, wrap } from "./promise.ts";
*
* const result = await all(wrap(1), wrap("Hello")); // [1, "Hello"]
* ```
*
* @since 2.0.0
*/
export function all(
...ua: T
): Promise<{ [K in keyof T]: Awaited }> {
return Promise.all(ua);
}
/**
* An alias for Promise.race. Note that Promise.race leaks
* async operations in most runtimes. This means that the
* slower promise does not stop when the faster promise
* resolves/rejects. In effect Promise.race does not
* handle cancellation.
*
* @example
* ```ts
* import { wait, map, race, wrap } from "./promise.ts";
* import { pipe } from "./fn.ts";
*
* const one = pipe(wait(200), map(() => "one"));
* const two = pipe(wait(300), map(() => "two"));
*
* // After 200 milliseconds resolves from one
* const result = await race(one, two); // "one"
* ```
*
* @since 2.0.0
*/
export function race(
...ua: T
): Promise> {
return Promise.race(ua);
}
/**
* Create a Promise from a value A or another PromiseLike. This
* is essentially an alias of Promise.resolve.
*
* @example
* ```ts
* import { wrap } from "./promise.ts";
*
* const result = await wrap(1); // 1
* ```
*
* @since 2.0.0
*/
export function wrap(a: A | PromiseLike): Promise {
return resolve(a);
}
/**
* Create a new Promise from a Promise<(a: A) => I> and
* a Promise. Although Promises encapsulate
* asynchrony, there is no way defer a Promise once created,
* thus this ap function always evaluates both input Promises
* in parallel.
*
* @example
* ```ts
* import { wrap, apply } from "./promise.ts";
* import { pipe } from "./fn.ts";
*
* type Person = { name: string, age: number };
*
* const person = (name: string) => (age: number): Person => ({ name, age });
*
* const result = pipe(
* wrap(person),
* apply(wrap("Brandon")),
* apply(wrap(37)),
* ); // Promise
* ```
*
* @since 2.0.0
*/
export function apply(
ua: Promise,
): (ufai: Promise<(a: A) => I>) => Promise {
return (ufai) =>
pipe(
all(ufai, ua),
then(([fai, a]) => fai(a)),
);
}
/**
* Create a new Promise by mapping over the result of an existing Promise.
* This is effectively Promise.then, but narrowed to non-promise returning
* functions. If the mapping function returns a Promise then the type
* for this function will be incorrect, as there is no way to create a
* Promise>.
*
* @example
* ```ts
* import { wrap, map } from "./promise.ts";
* import { pipe } from "./fn.ts";
*
* const result = await pipe(
* wrap(1),
* map(n => n + 1),
* ); // 2
* ```
*
* @since 2.0.0
*/
export function map(fai: (a: A) => I): (ua: Promise) => Promise {
return then(fai);
}
/**
* Create a new Promise by flatmaping over the result of an existing Promise.
* This is effectively Promise.then.
*
* @example
* ```ts
* import { wrap, flatmap } from "./promise.ts";
* import { pipe } from "./fn.ts";
*
* const result = await pipe(
* wrap(1),
* flatmap(n => wrap(n + 1)),
* ); // 2
* ```
*
* @since 2.0.0
*/
export function flatmap(
faui: (a: A) => Promise,
): (ua: Promise) => Promise {
return then(faui);
}
/**
* @since 2.0.0
*/
export function fail(b: unknown): Promise {
return reject(b);
}
/**
* Wrap a function that potentially throws in a try/catch block,
* handling any thrown errors and returning the result inside
* of a Promise.
*
* @example
* ```ts
* import { tryCatch, reject, wrap } from "./promise.ts";
* import { pipe, todo } from "./fn.ts";
* // Note that todo will always throw synchronously.
*
* const add = (n: number) => n + 1;
* const throwSync = (_: number): number => todo();
* const throwAsync = (_: number): Promise => reject("Ha!");
*
* const catchAdd = tryCatch(add, () => -1);
* const catchSync = tryCatch(throwSync, () => -1);
* const catchAsync = tryCatch(throwAsync, () => -1);
*
* const resultAdd = await catchAdd(1); // 2
* const resultSync = await catchSync(1); // -1
* const resultAsync = await catchAsync(1); // -1
* ```
* @since 2.0.0
*/
export function tryCatch(
handle: (...args: D) => A | PromiseLike,
onThrow: (error: unknown, args: D) => A,
): (...args: D) => Promise {
return handleThrow(
handle,
(a, args) => wrap(a).catch((err) => onThrow(err, args)),
flow(onThrow, wrap),
);
}
/**
* @since 2.0.0
*/
export function getCombinablePromise(
{ combine }: Combinable,
): Combinable> {
return {
combine: (second) => async (first) => combine(await second)(await first),
};
}
/**
* @since 2.0.0
*/
export function getInitializablePromise(
I: Initializable,
): Initializable> {
return {
init: () => resolve(I.init()),
...getCombinablePromise(I),
};
}
/**
* The canonical implementation of Applicable for Promise. It contains
* the methods wrap, apply, and map.
*
* @since 2.0.0
*/
export const ApplicablePromise: Applicable = { wrap, map, apply };
/**
* The canonical implementation of Mappable for Promise. It contains
* the method map.
*
* @since 2.0.0
*/
export const MappablePromise: Mappable = { map };
/**
* The canonical implementation of Flatmappable for Promise. It contains
* the methods wrap, apply, map, and flatmap.
*
* @since 2.0.0
*/
export const FlatmappablePromise: Flatmappable = {
apply,
flatmap,
map,
wrap,
};
/**
* @since 2.0.0
*/
export const WrappablePromise: Wrappable = { wrap };
/**
* @since 2.0.0
*/
export const tap: Tap = createTap(FlatmappablePromise);
/**
* @since 2.0.0
*/
export const bind: Bind = createBind(FlatmappablePromise);
/**
* @since 2.0.0
*/
export const bindTo: BindTo = createBindTo(MappablePromise);