diff --git a/src/either.ts b/src/either.ts index c6977eb..ce1f28e 100644 --- a/src/either.ts +++ b/src/either.ts @@ -176,3 +176,137 @@ export class DoEither, E> { return new DoEither(right({})) } } + +/** + * A class for working with Either in a do-notation style with asynchronous computations + * @typeParam T - The type of the accumulated scope + * @typeParam E - The error type + */ +export class AsyncDoEither, E> { + /** + * @param promise - The current Promise of an Either value + */ + constructor(private readonly promise: Promise>) {} + + /** + * Binds a new value to the scope synchronously + * @typeParam K - The key to bind to + * @typeParam A - The type of the value to bind + * @param key - The property name to bind to + * @param fa - The Either value to bind + * @returns A new AsyncDoEither instance with the extended scope + */ + bind( + key: K, + fa: Either, + ): AsyncDoEither, E> { + return this.bindL(key, () => fa) + } + + /** + * Binds a new value to the scope asynchronously + * @typeParam K - The key to bind to + * @typeParam A - The type of the value to bind + * @param key - The property name to bind to + * @param f - Function returning Either or Promise of Either + * @returns A new AsyncDoEither instance with the extended scope + */ + bindL( + key: K, + f: (scope: T) => Either | Promise>, + ): AsyncDoEither, E> { + const newPromise = this.promise.then(async (currentEither) => { + if (isLeft(currentEither)) { + return currentEither as unknown as Either, E> + } + + const scope = currentEither.right + try { + const nextEither = await f(scope) + + if (isLeft(nextEither)) { + return nextEither as unknown as Either, E> + } + + return right({ + ...scope, + [key]: nextEither.right, + }) as Either, E> + } catch (err) { + return left(err as E) as Either, E> + } + }) + + return new AsyncDoEither(newPromise) + } + + /** + * Returns the final result from the scope + * @typeParam B - The return type + * @param f - The function to transform the scope + * @returns A Promise of Either containing the result + */ + return(f: (scope: T) => B): Promise> { + return this.promise.then(either => { + if (isLeft(either)) { + return either as unknown as Either + } + return right(f(either.right)) + }) + } + + /** + * Returns the current scope + * @returns A Promise of the current Either scope + */ + done(): Promise> { + return this.promise + } + + /** + * Executes an effect without modifying the scope + * @param fa - The Either effect to execute + * @returns A new AsyncDoEither instance with the same scope + */ + do(fa: Either): AsyncDoEither { + return this.doL(() => fa) + } + + /** + * Executes an effect that depends on the current scope + * @param f - Function returning Either or Promise of Either + * @returns A new AsyncDoEither instance with the same scope + */ + doL(f: + (scope: T) => + | Either + | Promise>): AsyncDoEither + { + const newPromise = this.promise.then(async (currentEither) => { + if (isLeft(currentEither)) { + return currentEither + } + + const scope = currentEither.right + try { + const effectEither = await f(scope) + if (isLeft(effectEither)) { + return effectEither as unknown as Either + } + return currentEither + } catch (err) { + return left(err as E) + } + }) + + return new AsyncDoEither(newPromise) + } + + /** + * Starts a new AsyncDoEither chain with an empty scope + * @returns A new AsyncDoEither instance with an empty scope + */ + static start(): AsyncDoEither<{}, E> { + return new AsyncDoEither(Promise.resolve(right({}))) + } +}