import { wait } from "./promises"; import { deindent, nicify } from "./strings"; export type Result = | { status: "ok", data: T, } | { status: "error", error: E, }; export type AsyncResult = | Result | ( & { status: "pending", } & { progress: P, } ); export const Result = { fromThrowing, fromThrowingAsync, fromPromise: promiseToResult, ok(data: T): Result & { status: "ok" } { return { status: "ok", data, }; }, error(error: E): Result & { status: "error" } { return { status: "error", error, }; }, map: mapResult, or: (result: Result, fallback: U): T | U => { return result.status === "ok" ? result.data : fallback; }, orThrow: (result: Result): T => { if (result.status === "error") { throw result.error; } return result.data; }, orThrowAsync: async (result: Promise>): Promise => { return Result.orThrow(await result); }, retry, }; import.meta.vitest?.test("Result.ok and Result.error", ({ expect }) => { // Test Result.ok const okResult = Result.ok(42); expect(okResult.status).toBe("ok"); expect(okResult.data).toBe(42); // Test Result.error const error = new Error("Test error"); const errorResult = Result.error(error); expect(errorResult.status).toBe("error"); expect(errorResult.error).toBe(error); }); import.meta.vitest?.test("Result.or", ({ expect }) => { // Test with ok result const okResult: Result = { status: "ok", data: 42 }; expect(Result.or(okResult, 0)).toBe(42); // Test with error result const errorResult: Result = { status: "error", error: "error message" }; expect(Result.or(errorResult, 0)).toBe(0); }); import.meta.vitest?.test("Result.orThrow", ({ expect }) => { // Test with ok result const okResult: Result = { status: "ok", data: 42 }; expect(Result.orThrow(okResult)).toBe(42); // Test with error result const error = new Error("Test error"); const errorResult: Result = { status: "error", error }; expect(() => Result.orThrow(errorResult)).toThrow(error); }); import.meta.vitest?.test("Result.orThrowAsync", async ({ expect }) => { // Test with ok result const okPromise = Promise.resolve({ status: "ok", data: 42 } as Result); expect(await Result.orThrowAsync(okPromise)).toBe(42); // Test with error result const error = new Error("Test error"); const errorPromise = Promise.resolve({ status: "error", error } as Result); await expect(Result.orThrowAsync(errorPromise)).rejects.toThrow(error); }); export const AsyncResult = { fromThrowing, fromPromise: promiseToResult, ok: Result.ok, error: Result.error, pending, map: mapResult, or: (result: AsyncResult, fallback: U): T | U => { if (result.status === "pending") { return fallback; } return Result.or(result, fallback); }, orThrow: (result: AsyncResult): T => { if (result.status === "pending") { throw new Error("Result still pending"); } return Result.orThrow(result); }, retry, }; import.meta.vitest?.test("AsyncResult.or", ({ expect }) => { // Test with ok result const okResult: AsyncResult = { status: "ok", data: 42 }; expect(AsyncResult.or(okResult, 0)).toBe(42); // Test with error result const errorResult: AsyncResult = { status: "error", error: "error message" }; expect(AsyncResult.or(errorResult, 0)).toBe(0); // Test with pending result const pendingResult: AsyncResult = { status: "pending", progress: undefined }; expect(AsyncResult.or(pendingResult, 0)).toBe(0); }); import.meta.vitest?.test("AsyncResult.orThrow", ({ expect }) => { // Test with ok result const okResult: AsyncResult = { status: "ok", data: 42 }; expect(AsyncResult.orThrow(okResult)).toBe(42); // Test with error result const error = new Error("Test error"); const errorResult: AsyncResult = { status: "error", error }; expect(() => AsyncResult.orThrow(errorResult)).toThrow(error); // Test with pending result const pendingResult: AsyncResult = { status: "pending", progress: undefined }; expect(() => AsyncResult.orThrow(pendingResult)).toThrow("Result still pending"); }); function pending(): AsyncResult & { status: "pending" }; function pending

(progress: P): AsyncResult & { status: "pending" }; function pending

(progress?: P): AsyncResult & { status: "pending" } { return { status: "pending", progress: progress!, }; } import.meta.vitest?.test("pending", ({ expect }) => { // Test without progress const pendingResult = pending(); expect(pendingResult.status).toBe("pending"); expect(pendingResult.progress).toBe(undefined); // Test with progress const progressValue = { loaded: 50, total: 100 }; const pendingWithProgress = pending(progressValue); expect(pendingWithProgress.status).toBe("pending"); expect(pendingWithProgress.progress).toBe(progressValue); }); async function promiseToResult(promise: Promise): Promise> { try { const value = await promise; return Result.ok(value); } catch (error) { return Result.error(error); } } import.meta.vitest?.test("promiseToResult", async ({ expect }) => { // Test with resolved promise const resolvedPromise = Promise.resolve(42); const resolvedResult = await promiseToResult(resolvedPromise); expect(resolvedResult.status).toBe("ok"); if (resolvedResult.status === "ok") { expect(resolvedResult.data).toBe(42); } // Test with rejected promise const error = new Error("Test error"); const rejectedPromise = Promise.reject(error); const rejectedResult = await promiseToResult(rejectedPromise); expect(rejectedResult.status).toBe("error"); if (rejectedResult.status === "error") { expect(rejectedResult.error).toBe(error); } }); function fromThrowing(fn: () => T): Result { try { return Result.ok(fn()); } catch (error) { return Result.error(error); } } import.meta.vitest?.test("fromThrowing", ({ expect }) => { // Test with function that succeeds const successFn = () => 42; const successResult = fromThrowing(successFn); expect(successResult.status).toBe("ok"); if (successResult.status === "ok") { expect(successResult.data).toBe(42); } // Test with function that throws const error = new Error("Test error"); const errorFn = () => { throw error; }; const errorResult = fromThrowing(errorFn); expect(errorResult.status).toBe("error"); if (errorResult.status === "error") { expect(errorResult.error).toBe(error); } }); async function fromThrowingAsync(fn: () => Promise): Promise> { try { return Result.ok(await fn()); } catch (error) { return Result.error(error); } } import.meta.vitest?.test("fromThrowingAsync", async ({ expect }) => { // Test with async function that succeeds const successFn = async () => 42; const successResult = await fromThrowingAsync(successFn); expect(successResult.status).toBe("ok"); if (successResult.status === "ok") { expect(successResult.data).toBe(42); } // Test with async function that throws const error = new Error("Test error"); const errorFn = async () => { throw error; }; const errorResult = await fromThrowingAsync(errorFn); expect(errorResult.status).toBe("error"); if (errorResult.status === "error") { expect(errorResult.error).toBe(error); } }); function mapResult(result: Result, fn: (data: T) => U): Result; function mapResult(result: AsyncResult, fn: (data: T) => U): AsyncResult; function mapResult(result: AsyncResult, fn: (data: T) => U): AsyncResult { if (result.status === "error") return { status: "error", error: result.error, }; if (result.status === "pending") return { status: "pending", ..."progress" in result ? { progress: result.progress } : {}, } as any; return Result.ok(fn(result.data)); } import.meta.vitest?.test("mapResult", ({ expect }) => { // Test with ok result const okResult: Result = { status: "ok", data: 42 }; const mappedOk = mapResult(okResult, (n: number) => n * 2); expect(mappedOk.status).toBe("ok"); if (mappedOk.status === "ok") { expect(mappedOk.data).toBe(84); } // Test with error result const errorResult: Result = { status: "error", error: "error message" }; const mappedError = mapResult(errorResult, (n: number) => n * 2); expect(mappedError.status).toBe("error"); if (mappedError.status === "error") { expect(mappedError.error).toBe("error message"); } // Test with pending result (no progress) const pendingResult: AsyncResult = { status: "pending", progress: undefined }; const mappedPending = mapResult(pendingResult, (n: number) => n * 2); expect(mappedPending.status).toBe("pending"); // Test with pending result (with progress) const progressValue = { loaded: 50, total: 100 }; const pendingWithProgress: AsyncResult = { status: "pending", progress: progressValue }; const mappedPendingWithProgress = mapResult(pendingWithProgress, (n: number) => n * 2); expect(mappedPendingWithProgress.status).toBe("pending"); if (mappedPendingWithProgress.status === "pending") { expect(mappedPendingWithProgress.progress).toBe(progressValue); } }); class RetryError extends AggregateError { constructor(public readonly errors: E[]) { const strings = errors.map(e => nicify(e)); const isAllSame = strings.length > 1 && strings.every(s => s === strings[0]); super( errors, deindent` Error after ${errors.length} attempts. ${isAllSame ? deindent` Attempts 1-${errors.length}: ${strings[0]} ` : strings.map((s, i) => deindent` Attempt ${i + 1}: ${s} `).join("\n\n")} `, { cause: errors[errors.length - 1] } ); this.name = "RetryError"; } get attempts() { return this.errors.length; } } RetryError.prototype.name = "RetryError"; import.meta.vitest?.test("RetryError", ({ expect }) => { // Test with single error const singleError = new Error("Single error"); const retryErrorSingle = new RetryError([singleError]); expect(retryErrorSingle.name).toBe("RetryError"); expect(retryErrorSingle.errors).toEqual([singleError]); expect(retryErrorSingle.attempts).toBe(1); expect(retryErrorSingle.cause).toBe(singleError); expect(retryErrorSingle.message).toContain("Error after 1 attempts"); // Test with multiple different errors const error1 = new Error("Error 1"); const error2 = new Error("Error 2"); const retryErrorMultiple = new RetryError([error1, error2]); expect(retryErrorMultiple.name).toBe("RetryError"); expect(retryErrorMultiple.errors).toEqual([error1, error2]); expect(retryErrorMultiple.attempts).toBe(2); expect(retryErrorMultiple.cause).toBe(error2); expect(retryErrorMultiple.message).toContain("Error after 2 attempts"); expect(retryErrorMultiple.message).toContain("Attempt 1"); expect(retryErrorMultiple.message).toContain("Attempt 2"); // Test with multiple identical errors const sameError = new Error("Same error"); const retryErrorSame = new RetryError([sameError, sameError]); expect(retryErrorSame.name).toBe("RetryError"); expect(retryErrorSame.errors).toEqual([sameError, sameError]); expect(retryErrorSame.attempts).toBe(2); expect(retryErrorSame.cause).toBe(sameError); expect(retryErrorSame.message).toContain("Error after 2 attempts"); expect(retryErrorSame.message).toContain("Attempts 1-2"); }); async function retry( fn: (attemptIndex: number) => Result | Promise>, totalAttempts: number, { exponentialDelayBase = 1000 } = {}, ): Promise> & { attempts: number }> { const errors: E[] = []; for (let i = 0; i < totalAttempts; i++) { const res = await fn(i); if (res.status === "ok") { return Object.assign(Result.ok(res.data), { attempts: i + 1 }); } else { errors.push(res.error); if (i < totalAttempts - 1) { await wait((Math.random() + 0.5) * exponentialDelayBase * (2 ** i)); } } } return Object.assign(Result.error(new RetryError(errors)), { attempts: totalAttempts }); } import.meta.vitest?.test("retry", async ({ expect }) => { // Test successful on first attempt const successFn = async () => Result.ok("success"); const successResult = await retry(successFn, 3, { exponentialDelayBase: 0 }); expect(successResult).toEqual({ status: "ok", data: "success", attempts: 1 }); // Test successful after failures let attemptCount = 0; const eventualSuccessFn = async () => { return ++attemptCount < 2 ? Result.error(new Error(`Attempt ${attemptCount} failed`)) : Result.ok("eventual success"); }; const eventualSuccessResult = await retry(eventualSuccessFn, 3, { exponentialDelayBase: 0 }); expect(eventualSuccessResult).toEqual({ status: "ok", data: "eventual success", attempts: 2 }); // Test all attempts fail const errors = [new Error("Error 1"), new Error("Error 2"), new Error("Error 3")]; const allFailFn = async (attempt: number) => { return Result.error(errors[attempt]); }; const allFailResult = await retry(allFailFn, 3, { exponentialDelayBase: 0 }); expect(allFailResult).toEqual({ status: "error", error: expect.any(RetryError), attempts: 3 }); const retryError = (allFailResult as any).error as RetryError; expect(retryError.errors).toEqual(errors); expect(retryError.attempts).toBe(3); });