UNPKG

@effect-ts/system

Version:

Effect-TS is a zero dependency set of libraries to write highly productive, purely functional TypeScript at scale.

1,025 lines (904 loc) 25.8 kB
// ets_tracing: off /* eslint-disable prefer-const */ import * as Tp from "../Collections/Immutable/Tuple/index.js" import { _A, _E, _R, _U } from "../Effect/commons.js" import * as E from "../Either/index.js" import { pipe } from "../Function/index.js" import type { Option } from "../Option/index.js" import { Stack } from "../Stack/index.js" import type * as U from "../Utils/index.js" /** * `Async[R, E, A]` is a purely functional description of an async computation * that requires an environment `R` and may either fail with an `E` or succeed * with an `A`. */ export interface Async<R, E, A> extends U.HasUnify {} export abstract class Async<R, E, A> { readonly [_U]!: "Async"; readonly [_E]!: () => E; readonly [_A]!: () => A; readonly [_R]!: (_: R) => void } export interface UIO<A> extends Async<unknown, never, A> {} export interface RIO<R, A> extends Async<R, never, A> {} export interface IO<E, A> extends Async<unknown, E, A> {} /** * @ets_optimize identity */ function concrete<R, E, A>(_: Async<R, E, A>): Concrete<R, E, A> { return _ as any } class ISucceed<A> extends Async<unknown, never, A> { readonly _asyncTag = "Succeed" constructor(readonly a: A) { super() } } class ISuspend<R, E, A> extends Async<R, E, A> { readonly _asyncTag = "Suspend" constructor(readonly f: () => Async<R, E, A>) { super() } } class IFail<E> extends Async<unknown, E, never> { readonly _asyncTag = "Fail" constructor(readonly e: E) { super() } } class IFlatMap<R, R1, E, E1, A, B> extends Async<R & R1, E1 | E, B> { readonly _asyncTag = "FlatMap" constructor( readonly value: Async<R, E, A>, readonly cont: (a: A) => Async<R1, E1, B> ) { super() } } class IFold<R, E1, E2, A, B> extends Async<R, E2, B> { readonly _asyncTag = "Fold" constructor( readonly value: Async<R, E1, A>, readonly failure: (e: E1) => Async<R, E2, B>, readonly success: (a: A) => Async<R, E2, B> ) { super() } } class IAccess<R, E, A> extends Async<R, E, A> { readonly _asyncTag = "Access" constructor(readonly access: (r: R) => Async<R, E, A>) { super() } } class IProvide<R, E, A> extends Async<unknown, E, A> { readonly _asyncTag = "Provide" constructor(readonly r: R, readonly cont: Async<R, E, A>) { super() } } class IPromise<E, A> extends Async<unknown, E, A> { readonly _asyncTag = "Promise" constructor( readonly promise: (onInterrupt: (f: () => void) => void) => Promise<A>, readonly onError: (u: unknown) => E ) { super() } } class IDone<E, A> extends Async<unknown, E, A> { readonly _asyncTag = "Done" constructor(readonly exit: Exit<E, A>) { super() } } type Concrete<R, E, A> = | ISucceed<A> | IFail<E> | IFlatMap<R, R, E, E, unknown, A> | IFold<R, unknown, E, unknown, A> | IAccess<R, E, A> | IProvide<R, E, A> | ISuspend<R, E, A> | IPromise<E, A> | IDone<E, A> class FoldFrame { readonly _asyncTag = "FoldFrame" constructor( readonly failure: (e: any) => Async<any, any, any>, readonly apply: (e: any) => Async<any, any, any> ) {} } class ApplyFrame { readonly _asyncTag = "ApplyFrame" constructor(readonly apply: (e: any) => Async<any, any, any>) {} } type Frame = FoldFrame | ApplyFrame /** * Models the state of interruption, allows for listening to interruption events & firing interruption events */ export class InterruptionState { private isInterrupted = false readonly listeners = new Set<() => void>() // listen to an interruption event listen(f: () => void) { this.listeners.add(f) return () => { // stop listening this.listeners.delete(f) } } get interrupted() { return this.isInterrupted } interrupt() { if (!this.isInterrupted) { // set to interrupted this.isInterrupted = true // notify this.listeners.forEach((i) => { i() }) } } } export interface Failure<E> { readonly _tag: "Failure" e: E } export interface Interrupt { readonly _tag: "Interrupt" } export interface Success<A> { readonly _tag: "Success" a: A } export type Rejection<E> = Failure<E> | Interrupt export type Exit<E, A> = Rejection<E> | Success<A> export const failExit = <E>(e: E): Rejection<E> => ({ _tag: "Failure", e }) export const interruptExit = <Exit<never, never>>{ _tag: "Interrupt" } export const successExit = <A>(a: A): Exit<never, A> => ({ _tag: "Success", a }) /** * Models a cancellable promise */ class CancelablePromise<E, A> { // holds the type information of E readonly _E!: () => E // gets called with a Rejection<E>, any here is to not break covariance imposed by _E private rejection: ((e: Rejection<any>) => void) | undefined = undefined // holds the current running promise private current: Promise<A> | undefined = undefined constructor( // creates the promise readonly promiseFactory: (onInterrupt: (f: () => void) => void) => Promise<A>, // listens for interruption events readonly is: InterruptionState ) {} // creates the computation linking it to the interruption state readonly promise: () => Promise<A> = () => { if (this.current) { throw new Error("Bug: promise() have been called twice") } else if (this.is.interrupted) { throw new Error("Bug: trying to create a promise already interrupted") } else { const onInterrupt = <(() => void)[]>[] // we record the current interrupt in the interruption registry const removeListener = this.is.listen(() => { onInterrupt.forEach((f) => { f() }) this.interrupt() }) const p = new Promise<A>((res, rej) => { // set the rejection handler this.rejection = rej // creates the underlying promise this.promiseFactory((f) => { onInterrupt.push(f) }) .then((a) => { // removes the call to interrupt from the interruption registry removeListener() // if not interrupted we continue if (!this.is.interrupted) { res(a) } }) .catch((e) => { // removes the call to interrupt from the interruption registry removeListener() // if not interrupted we continue if (!this.is.interrupted) { rej(e) } }) }) // track the current running promise to avoid re-creation this.current = p // return the promise return p } } readonly interrupt = () => { // triggeres a promise rejection on the current promise with an interrupt exit this.rejection?.(interruptExit as any) } } export class Tracer { private running = new Set<Promise<any>>() constructor() { this.traced = this.traced.bind(this) this.wait = this.wait.bind(this) this.clear = this.clear.bind(this) } // tracks a lazy promise lifetime traced<A>(promise: () => Promise<A>) { return async () => { const p = promise() this.running.add(p) try { const a = await p this.running.delete(p) return Promise.resolve(a) } catch (e) { this.running.delete(p) return Promise.reject(e) } } } // awaits for all the running promises to complete async wait(): Promise<Exit<any, any>[]> { const t = await Promise.all( Array.from(this.running).map((p) => p.then((a) => successExit(a)).catch((e) => Promise.resolve(e)) ) ) return await new Promise((r) => { setTimeout(() => { r(t) }, 0) }) } // clears itself clear() { this.running.clear() } } // create the root tracing context export const tracingContext = new Tracer() /** * Runs this computation with the specified initial state, returning either a * failure or the updated state and the result */ export function runPromiseExitEnv<R, E, A>( self: Async<R, E, A>, ri: R, is: InterruptionState = new InterruptionState() ): Promise<Exit<E, A>> { return tracingContext.traced(async () => { let stack: Stack<Frame> | undefined = undefined let a = null let r = ri let failed = false let curAsync = self as Async<any, any, any> | undefined let cnt = 0 let interruptedLocal = false function isInterruted() { return interruptedLocal || is.interrupted } function pop() { const nextInstr = stack if (nextInstr) { stack = stack?.previous } return nextInstr?.value } function push(cont: Frame) { stack = new Stack(cont, stack) } function findNextErrorHandler() { let unwinding = true while (unwinding) { const nextInstr = pop() if (nextInstr == null) { unwinding = false } else { if (nextInstr._asyncTag === "FoldFrame") { unwinding = false push(new ApplyFrame(nextInstr.failure)) } } } } while (curAsync != null && !isInterruted()) { if (cnt > 10_000) { await new Promise((r) => { setTimeout(() => { r(undefined) }, 0) }) cnt = 0 } cnt += 1 const xp = concrete(curAsync) switch (xp._asyncTag) { case "FlatMap": { const nested = concrete(xp.value) const continuation = xp.cont switch (nested._asyncTag) { case "Succeed": { curAsync = continuation(nested.a) break } default: { curAsync = nested push(new ApplyFrame(continuation)) } } break } case "Suspend": { curAsync = xp.f() break } case "Succeed": { a = xp.a const nextInstr = pop() if (nextInstr) { curAsync = nextInstr.apply(a) } else { curAsync = undefined } break } case "Fail": { findNextErrorHandler() const nextInst = pop() if (nextInst) { curAsync = nextInst.apply(xp.e) } else { failed = true a = xp.e curAsync = undefined } break } case "Fold": { curAsync = xp.value push(new FoldFrame(xp.failure, xp.success)) break } case "Done": { switch (xp.exit._tag) { case "Failure": { curAsync = new IFail(xp.exit.e) break } case "Interrupt": { interruptedLocal = true curAsync = undefined break } case "Success": { curAsync = new ISucceed(xp.exit.a) break } } break } case "Access": { curAsync = xp.access(r) break } case "Provide": { r = xp.r curAsync = xp.cont break } case "Promise": { try { curAsync = new ISucceed( await new CancelablePromise( (s) => xp.promise(s).catch((e) => Promise.reject(failExit(xp.onError(e)))), is ).promise() ) } catch (e) { const e_ = <Rejection<E>>e switch (e_._tag) { case "Failure": { curAsync = new IFail(e_.e) break } case "Interrupt": { interruptedLocal = true curAsync = undefined break } } } break } } } if (is.interrupted) { return interruptExit } if (failed) { return failExit(a) } return successExit(a) })() } export function runPromiseExit<E, A>( self: Async<unknown, E, A>, is: InterruptionState = new InterruptionState() ): Promise<Exit<E, A>> { return runPromiseExitEnv(self, {}, is) } // runs as a Promise of an Exit export async function runPromise<E, A>( task: Async<unknown, E, A>, is = new InterruptionState() ): Promise<A> { return runPromiseExit(task, is).then((e) => e._tag === "Failure" ? Promise.reject(e.e) : e._tag === "Interrupt" ? Promise.reject(e) : Promise.resolve(e.a) ) } // runs as a Cancellable export function runAsync<E, A>( task: Async<unknown, E, A>, cb?: (e: Exit<E, A>) => void ) { const is = new InterruptionState() runPromiseExit(task, is).then(cb) return () => { is.interrupt() } } // runs as a Cancellable export function runAsyncEnv<R, E, A>( task: Async<R, E, A>, r: R, cb?: (e: Exit<E, A>) => void ) { const is = new InterruptionState() runPromiseExitEnv(task, r, is).then(cb) return () => { is.interrupt() } } /** * Extends this computation with another computation that depends on the * result of this computation by running the first computation, using its * result to generate a second computation, and running that computation. * * @ets_data_first chain_ */ export function chain<A, R1, E1, B>(f: (a: A) => Async<R1, E1, B>) { return <R, E>(self: Async<R, E, A>): Async<R & R1, E | E1, B> => new IFlatMap(self, f) } /** * Extends this computation with another computation that depends on the * result of this computation by running the first computation, using its * result to generate a second computation, and running that computation. */ export function chain_<R, E, A, R1, E1, B>( self: Async<R, E, A>, f: (a: A) => Async<R1, E1, B> ): Async<R & R1, E | E1, B> { return new IFlatMap(self, f) } /** * Returns a computation that effectfully "peeks" at the success of this one. * * @ets_data_first tap_ */ export function tap<A, R1, E1, X>(f: (a: A) => Async<R1, E1, X>) { return <R, E>(self: Async<R, E, A>): Async<R & R1, E | E1, A> => tap_(self, f) } /** * Returns a computation that effectfully "peeks" at the success of this one. */ export function tap_<R, E, A, R1, E1, X>( self: Async<R, E, A>, f: (a: A) => Async<R1, E1, X> ): Async<R & R1, E | E1, A> { return chain_(self, (a) => map_(f(a), () => a)) } /** * Constructs a computation that always succeeds with the specified value. */ export function succeed<A>(a: A): Async<unknown, never, A> { return new ISucceed(a) } /** * Constructs a computation that always succeeds with the specified value, * passing the state through unchanged. */ export function fail<E>(a: E): Async<unknown, E, never> { return new IFail(a) } /** * Extends this computation with another computation that depends on the * result of this computation by running the first computation, using its * result to generate a second computation, and running that computation. */ export function map_<R, E, A, B>(self: Async<R, E, A>, f: (a: A) => B) { return chain_(self, (a) => succeed(f(a))) } /** * Extends this computation with another computation that depends on the * result of this computation by running the first computation, using its * result to generate a second computation, and running that computation. * * @ets_data_first map_ */ export function map<A, B>(f: (a: A) => B) { return <R, E>(self: Async<R, E, A>) => map_(self, f) } /** * Recovers from errors by accepting one computation to execute for the case * of an error, and one computation to execute for the case of success. */ export function foldM_<R, E, A, R1, E1, B, R2, E2, C>( self: Async<R, E, A>, failure: (e: E) => Async<R1, E1, B>, success: (a: A) => Async<R2, E2, C> ): Async<R & R1 & R2, E1 | E2, B | C> { return new IFold( self as Async<R & R1 & R2, E, A>, failure as (e: E) => Async<R1 & R2, E1 | E2, B | C>, success ) } /** * Recovers from errors by accepting one computation to execute for the case * of an error, and one computation to execute for the case of success. * * @ets_data_first foldM_ */ export function foldM<E, A, R1, E1, B, R2, E2, C>( failure: (e: E) => Async<R1, E1, B>, success: (a: A) => Async<R2, E2, C> ) { return <R>(self: Async<R, E, A>) => foldM_(self, failure, success) } /** * Folds over the failed or successful results of this computation to yield * a computation that does not fail, but succeeds with the value of the left * or right function passed to `fold`. * * @ets_data_first fold_ */ export function fold<E, A, B, C>(failure: (e: E) => B, success: (a: A) => C) { return <R>(self: Async<R, E, A>) => fold_(self, failure, success) } /** * Folds over the failed or successful results of this computation to yield * a computation that does not fail, but succeeds with the value of the left * or righr function passed to `fold`. */ export function fold_<R, E, A, B, C>( self: Async<R, E, A>, failure: (e: E) => B, success: (a: A) => C ): Async<R, never, B | C> { return foldM_( self, (e) => succeed(failure(e)), (a) => succeed(success(a)) ) } /** * Recovers from all errors. * * @ets_data_first catchAll_ */ export function catchAll<E, R1, E1, B>(failure: (e: E) => Async<R1, E1, B>) { return <R, A>(self: Async<R, E, A>) => catchAll_(self, failure) } /** * Recovers from all errors. */ export function catchAll_<R, E, A, R1, E1, B>( self: Async<R, E, A>, failure: (e: E) => Async<R1, E1, B> ) { return foldM_(self, failure, (a) => succeed(a)) } /** * Returns a computation whose error and success channels have been mapped * by the specified functions, `f` and `g`. * * @ets_data_first bimap_ */ export function bimap<E, A, E1, A1>(f: (e: E) => E1, g: (a: A) => A1) { return <R>(self: Async<R, E, A>) => bimap_(self, f, g) } /** * Returns a computation whose error and success channels have been mapped * by the specified functions, `f` and `g`. */ export function bimap_<R, E, A, E1, A1>( self: Async<R, E, A>, f: (e: E) => E1, g: (a: A) => A1 ) { return foldM_( self, (e) => fail(f(e)), (a) => succeed(() => g(a)) ) } /** * Transforms the error type of this computation with the specified * function. * * @ets_data_first mapError_ */ export function mapError<E, E1>(f: (e: E) => E1) { return <R, A>(self: Async<R, E, A>) => mapError_(self, f) } /** * Transforms the error type of this computation with the specified * function. */ export function mapError_<R, E, A, E1>(self: Async<R, E, A>, f: (e: E) => E1) { return catchAll_(self, (e) => fail(f(e))) } /** * Constructs a computation that always returns the `Unit` value, passing the * state through unchanged. */ export const unit = succeed<void>(undefined) /** * Transforms the initial state of this computation` with the specified * function. */ export function provideSome<R0, R1>(f: (s: R0) => R1) { return <E, A>(self: Async<R1, E, A>) => accessM((r: R0) => provideAll(f(r))(self)) } /** * Provides this computation with its required environment. * * @ets_data_first provideAll_ */ export function provideAll<R>(r: R) { return <E, A>(self: Async<R, E, A>): Async<unknown, E, A> => new IProvide(r, self) } /** * Provides this computation with its required environment. */ export function provideAll_<R, E, A>(self: Async<R, E, A>, r: R): Async<unknown, E, A> { return new IProvide(r, self) } /** * Provides some of the environment required to run this effect, * leaving the remainder `R0` and combining it automatically using spread. */ export function provide<R = unknown>(r: R) { return <E, A, R0 = unknown>(next: Async<R & R0, E, A>): Async<R0, E, A> => provideSome((r0: R0) => ({ ...r0, ...r }))(next) } /** * Access the environment monadically */ export function accessM<R, R1, E, A>( f: (_: R) => Async<R1, E, A> ): Async<R1 & R, E, A> { return new IAccess<R1 & R, E, A>(f) } /** * Access the environment with the function f */ export function access<R, A>(f: (_: R) => A): Async<R, never, A> { return accessM((r: R) => succeed(f(r))) } /** * Access the environment */ export function environment<R>(): Async<R, never, R> { return accessM((r: R) => succeed(r)) } /** * Returns a computation whose failure and success have been lifted into an * `Either`. The resulting computation cannot fail, because the failure case * has been exposed as part of the `Either` success case. */ export function either<R, E, A>(self: Async<R, E, A>): Async<R, never, E.Either<E, A>> { return fold_(self, E.left, E.right) } /** * Executes this computation and returns its value, if it succeeds, but * otherwise executes the specified computation. * * @ets_data_first orElseEither_ */ export function orElseEither<R2, E2, A2>(that: () => Async<R2, E2, A2>) { return <R, E, A>(self: Async<R, E, A>): Async<R & R2, E2, E.Either<A, A2>> => orElseEither_(self, that) } /** * Executes this computation and returns its value, if it succeeds, but * otherwise executes the specified computation. */ export function orElseEither_<R, E, A, R2, E2, A2>( self: Async<R, E, A>, that: () => Async<R2, E2, A2> ): Async<R & R2, E2, E.Either<A, A2>> { return foldM_( self, () => map_(that(), (a) => E.right(a)), (a) => succeed(E.left(a)) ) } /** * Combines this computation with the specified computation, passing the * updated state from this computation to that computation and combining the * results of both using the specified function. * * @ets_data_first zipWith_ */ export function zipWith<R1, E1, A, B, C>(that: Async<R1, E1, B>, f: (a: A, b: B) => C) { return <R, E>(self: Async<R, E, A>): Async<R & R1, E1 | E, C> => zipWith_(self, that, f) } /** * Combines this computation with the specified computation, passing the * updated state from this computation to that computation and combining the * results of both using the specified function. */ export function zipWith_<R, E, A, R1, E1, B, C>( self: Async<R, E, A>, that: Async<R1, E1, B>, f: (a: A, b: B) => C ) { return chain_(self, (a) => map_(that, (b) => f(a, b))) } /** * Combines this computation with the specified computation, passing the * updated state from this computation to that computation and combining the * results of both into a tuple. * * @ets_data_first zip_ */ export function zip<R1, E1, B>(that: Async<R1, E1, B>) { return <R, E, A>(self: Async<R, E, A>) => zip_(self, that) } /** * Combines this computation with the specified computation, passing the * updated state from this computation to that computation and combining the * results of both into a tuple. */ export function zip_<R, E, A, R1, E1, B>(self: Async<R, E, A>, that: Async<R1, E1, B>) { return zipWith_(self, that, Tp.tuple) } /** * Suspend a computation, useful in recursion */ export function suspend<R, E, A>(f: () => Async<R, E, A>): Async<R, E, A> { return new ISuspend(f) } /** * Lift a sync (non failable) computation */ export function succeedWith<A>(f: () => A) { return suspend(() => succeed<A>(f())) } /** * Lift a sync (non failable) computation */ export function tryCatch<E, A>(f: () => A, onThrow: (u: unknown) => E) { return suspend(() => { try { return succeed<A>(f()) } catch (u) { return fail(onThrow(u)) } }) } // construct from a promise export function promise<E, A>( promise: (onInterrupt: (f: () => void) => void) => Promise<A>, onError: (u: unknown) => E ): Async<unknown, E, A> { return new IPromise(promise, onError) } // construct from a non failable promise export function unfailable<A>( promise: (onInterrupt: (f: () => void) => void) => Promise<A> ): Async<unknown, never, A> { return new IPromise(promise, () => undefined as never) } // construct a Task from an exit value export function done<E, A>(exit: Exit<E, A>): Async<unknown, E, A> { return new IDone(exit) } // like .then in Promise when the result of f is a Promise but ignores the outout of f // useful for logging or doing things that should not change the result export function tapError<EA, B, EB, R>(f: (_: EA) => Async<R, EB, B>) { return <R1, A>(self: Async<R1, EA, A>) => pipe( self, catchAll((e) => pipe( f(e), chain((_) => fail(e)) ) ) ) } // sleeps for ms milliseconds export function sleep(ms: number) { return unfailable( (onInterrupt) => new Promise((res) => { const timer = setTimeout(() => { res(undefined) }, ms) onInterrupt(() => { clearTimeout(timer) }) }) ) } // delay the computation prepending a sleep of ms milliseconds export function delay(ms: number) { return <R, E, A>(self: Async<R, E, A>) => pipe( sleep(ms), chain(() => self) ) } // list an Either export function fromEither<E, A>(e: E.Either<E, A>) { return e._tag === "Right" ? succeed(e.right) : fail(e.left) } /** * Compact the union produced by the result of f * * @ets_optimize identity */ export function unionFn<ARGS extends any[], Ret extends Async<any, any, any>>( _: (...args: ARGS) => Ret ): (...args: ARGS) => Async<U._R<Ret>, U._E<Ret>, U._A<Ret>> { return _ as any } /** * Compact the union * * @ets_optimize identity */ export function union<Ret extends Async<any, any, any>>( _: Ret ): Async<U._R<Ret>, U._E<Ret>, U._A<Ret>> { return _ as any } /** * Get the A from an option */ export default function tryCatchOption_<A, E>(ma: Option<A>, onNone: () => E) { return pipe(E.fromOption_(ma, onNone), fromEither) } /** * Get the A from an option * * @ets_data_first tryCatchOption_ */ export function tryCatchOption<A, E>(onNone: () => E) { return (ma: Option<A>) => tryCatchOption_(ma, onNone) }