diff --git a/packages/fabric/core/result/async-result.ts b/packages/fabric/core/result/async-result.ts index 6bd6e58..d9a1bfd 100644 --- a/packages/fabric/core/result/async-result.ts +++ b/packages/fabric/core/result/async-result.ts @@ -1,6 +1,6 @@ -// deno-lint-ignore-file no-namespace no-explicit-any +// deno-lint-ignore-file no-namespace no-explicit-any no-async-promise-executor +import { UnexpectedError } from "@fabric/core"; import type { TaggedError } from "../error/tagged-error.ts"; -import { UnexpectedError } from "../error/unexpected-error.ts"; import type { MaybePromise } from "../types/maybe-promise.ts"; import { Result } from "./result.ts"; @@ -8,24 +8,123 @@ import { Result } from "./result.ts"; * An AsyncResult represents the result of an asynchronous operation that can * resolve to a value of type `TValue` or an error of type `TError`. */ -export type AsyncResult< +export class AsyncResult< TValue = any, TError extends TaggedError = never, -> = Promise>; - -export namespace AsyncResult { - export async function tryFrom( +> { + static tryFrom( fn: () => MaybePromise, errorMapper: (error: any) => TError, ): AsyncResult { - try { - return Result.succeedWith(await fn()); - } catch (error) { - return Result.failWith(errorMapper(error)); - } + return new AsyncResult( + new Promise>(async (resolve) => { + try { + const value = await fn(); + resolve(Result.ok(value)); + } catch (error) { + resolve(Result.failWith(errorMapper(error))); + } + }), + ); } - export function from(fn: () => MaybePromise): AsyncResult { - return tryFrom(fn, (error) => new UnexpectedError(error) as never); + static from(fn: () => MaybePromise): AsyncResult { + return AsyncResult.tryFrom( + fn, + (error) => new UnexpectedError(error) as never, + ); + } + + static ok(value: T): AsyncResult { + return new AsyncResult(Promise.resolve(Result.ok(value))); + } + + static succeedWith = AsyncResult.ok; + + static failWith( + error: TError, + ): AsyncResult { + return new AsyncResult(Promise.resolve(Result.failWith(error))); + } + + private constructor(private r: Promise>) { + } + + promise(): Promise> { + return this.r; + } + + async unwrapOrThrow(): Promise { + return (await this.r).unwrapOrThrow(); + } + + async orThrow(): Promise { + return (await this.r).orThrow(); + } + + async unwrapErrorOrThrow(): Promise { + return (await this.r).unwrapErrorOrThrow(); + } + + /** + * Map a function over the value of the result. + */ + map( + fn: (value: TValue) => TMappedValue, + ): AsyncResult { + return new AsyncResult( + this.r.then((result) => result.map(fn)), + ); + } + + /** + * Maps a function over the value of the result and flattens the result. + */ + flatMap( + fn: (value: TValue) => AsyncResult, + ): AsyncResult { + return new AsyncResult( + this.r.then((result) => { + if (result.isError()) { + return result as any; + } + + return (fn(result.unwrapOrThrow())).promise(); + }), + ); + } + + /** + * Try to map a function over the value of the result. + * If the function throws an error, the result will be a failure. + */ + tryMap( + fn: (value: TValue) => TMappedValue, + errMapper: (error: any) => TError, + ): AsyncResult { + return new AsyncResult( + this.r.then((result) => result.tryMap(fn, errMapper)), + ); + } + + /** + * Map a function over the error of the result. + */ + mapError( + fn: (error: TError) => TMappedError, + ): AsyncResult { + return new AsyncResult( + this.r.then((result) => result.mapError(fn)), + ); + } + + /** + * Taps a function if the result is a success. + * This is useful for side effects that do not modify the result. + */ + tap(fn: (value: TValue) => void): AsyncResult { + return new AsyncResult( + this.r.then((result) => result.tap(fn)), + ); } } diff --git a/packages/fabric/core/run/run.test.ts b/packages/fabric/core/run/run.test.ts index 36b4357..9ede254 100644 --- a/packages/fabric/core/run/run.test.ts +++ b/packages/fabric/core/run/run.test.ts @@ -1,27 +1,26 @@ -// deno-lint-ignore-file require-await +import { AsyncResult } from "@fabric/core"; import { describe, expect, test } from "@fabric/testing"; import { UnexpectedError } from "../error/unexpected-error.ts"; -import { Result } from "../result/result.ts"; import { Run } from "./run.ts"; describe("Run", () => { describe("In Sequence", () => { test("should pipe the results of multiple async functions", async () => { - const result = await Run.seq( - async () => Result.succeedWith(1), - async (x) => Result.succeedWith(x + 1), - async (x) => Result.succeedWith(x * 2), + const result = Run.seq( + () => AsyncResult.succeedWith(1), + (x) => AsyncResult.succeedWith(x + 1), + (x) => AsyncResult.succeedWith(x * 2), ); - expect(result.unwrapOrThrow()).toEqual(4); + expect(await result.unwrapOrThrow()).toEqual(4); }); test("should return the first error if one of the functions fails", async () => { const result = await Run.seq( - async () => Result.succeedWith(1), - async () => Result.failWith(new UnexpectedError()), - async (x) => Result.succeedWith(x * 2), - ); + () => AsyncResult.succeedWith(1), + () => AsyncResult.failWith(new UnexpectedError()), + (x) => AsyncResult.succeedWith(x * 2), + ).promise(); expect(result.isError()).toBe(true); }); diff --git a/packages/fabric/core/run/run.ts b/packages/fabric/core/run/run.ts index 133e836..dfca83c 100644 --- a/packages/fabric/core/run/run.ts +++ b/packages/fabric/core/run/run.ts @@ -4,7 +4,7 @@ import type { AsyncResult } from "../result/async-result.ts"; export namespace Run { // prettier-ignore - export async function seq< + export function seq< T1, TE1 extends TaggedError, T2, @@ -14,7 +14,7 @@ export namespace Run { fn2: (value: T1) => AsyncResult, ): AsyncResult; // prettier-ignore - export async function seq< + export function seq< T1, TE1 extends TaggedError, T2, @@ -27,7 +27,7 @@ export namespace Run { fn3: (value: T2) => AsyncResult, ): AsyncResult; // prettier-ignore - export async function seq< + export function seq< T1, TE1 extends TaggedError, T2, @@ -42,24 +42,20 @@ export namespace Run { fn3: (value: T2) => AsyncResult, fn4: (value: T3) => AsyncResult, ): AsyncResult; - export async function seq( + export function seq( ...fns: ((...args: any[]) => AsyncResult)[] ): AsyncResult { - let result = await fns[0]!(); + let result = fns[0]!(); for (let i = 1; i < fns.length; i++) { - if (result.isError()) { - return result; - } - - result = await fns[i]!(result.unwrapOrThrow()); + result = result.flatMap((value) => fns[i]!(value)); } return result; } // prettier-ignore - export async function seqUNSAFE< + export function seqOrThrow< T1, TE1 extends TaggedError, T2, @@ -69,7 +65,7 @@ export namespace Run { fn2: (value: T1) => AsyncResult, ): Promise; // prettier-ignore - export async function seqUNSAFE< + export function seqOrThrow< T1, TE1 extends TaggedError, T2, @@ -81,21 +77,11 @@ export namespace Run { fn2: (value: T1) => AsyncResult, fn3: (value: T2) => AsyncResult, ): Promise; - export async function seqUNSAFE( + export function seqOrThrow( ...fns: ((...args: any[]) => AsyncResult)[] ): Promise { - const result = await (seq as any)(...fns); - - if (result.isError()) { - throw result.unwrapOrThrow(); - } + const result = (seq as any)(...fns); return result.unwrapOrThrow(); } - - export async function UNSAFE( - fn: () => AsyncResult, - ): Promise { - return (await fn()).unwrapOrThrow(); - } }