UNPKG

neverthrow-result-utils

Version:

Extra utilities for working with Neverthrow results

624 lines (602 loc) 12.8 kB
import delay from "delay"; import { err, ok, ResultAsync, errAsync } from "neverthrow"; export class ResultUtils { /** * Version of combine() with improved typing for heterogeneous lists * @param asyncResultList */ static combine< T, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, E, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13, E14, E15 >( asyncResultList: [ ResultAsync<T, E>, ResultAsync<T2, E2>, ResultAsync<T3, E3>, ResultAsync<T4, E4>, ResultAsync<T5, E5>, ResultAsync<T6, E6>, ResultAsync<T7, E7>, ResultAsync<T8, E8>, ResultAsync<T9, E9>, ResultAsync<T10, E10>, ResultAsync<T11, E11>, ResultAsync<T12, E12>, ResultAsync<T13, E13>, ResultAsync<T14, E14>, ResultAsync<T15, E15> ] ): ResultAsync< [T, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15], | E | E2 | E3 | E4 | E5 | E6 | E7 | E8 | E9 | E10 | E11 | E12 | E13 | E14 | E15 >; static combine< T, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, E, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13, E14 >( asyncResultList: [ ResultAsync<T, E>, ResultAsync<T2, E2>, ResultAsync<T3, E3>, ResultAsync<T4, E4>, ResultAsync<T5, E5>, ResultAsync<T6, E6>, ResultAsync<T7, E7>, ResultAsync<T8, E8>, ResultAsync<T9, E9>, ResultAsync<T10, E10>, ResultAsync<T11, E11>, ResultAsync<T12, E12>, ResultAsync<T13, E13>, ResultAsync<T14, E14> ] ): ResultAsync< [T, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14], E | E2 | E3 | E4 | E5 | E6 | E7 | E8 | E9 | E10 | E11 | E12 | E13 | E14 >; static combine< T, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, E, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13 >( asyncResultList: [ ResultAsync<T, E>, ResultAsync<T2, E2>, ResultAsync<T3, E3>, ResultAsync<T4, E4>, ResultAsync<T5, E5>, ResultAsync<T6, E6>, ResultAsync<T7, E7>, ResultAsync<T8, E8>, ResultAsync<T9, E9>, ResultAsync<T10, E10>, ResultAsync<T11, E11>, ResultAsync<T12, E12>, ResultAsync<T13, E13> ] ): ResultAsync< [T, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13], E | E2 | E3 | E4 | E5 | E6 | E7 | E8 | E9 | E10 | E11 | E12 | E13 >; static combine< T, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, E, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12 >( asyncResultList: [ ResultAsync<T, E>, ResultAsync<T2, E2>, ResultAsync<T3, E3>, ResultAsync<T4, E4>, ResultAsync<T5, E5>, ResultAsync<T6, E6>, ResultAsync<T7, E7>, ResultAsync<T8, E8>, ResultAsync<T9, E9>, ResultAsync<T10, E10>, ResultAsync<T11, E11>, ResultAsync<T12, E12> ] ): ResultAsync< [T, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12], E | E2 | E3 | E4 | E5 | E6 | E7 | E8 | E9 | E10 | E11 | E12 >; static combine< T, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, E, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11 >( asyncResultList: [ ResultAsync<T, E>, ResultAsync<T2, E2>, ResultAsync<T3, E3>, ResultAsync<T4, E4>, ResultAsync<T5, E5>, ResultAsync<T6, E6>, ResultAsync<T7, E7>, ResultAsync<T8, E8>, ResultAsync<T9, E9>, ResultAsync<T10, E10>, ResultAsync<T11, E11> ] ): ResultAsync< [T, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11], E | E2 | E3 | E4 | E5 | E6 | E7 | E8 | E9 | E10 | E11 >; static combine< T, T2, T3, T4, T5, T6, T7, T8, T9, T10, E, E2, E3, E4, E5, E6, E7, E8, E9, E10 >( asyncResultList: [ ResultAsync<T, E>, ResultAsync<T2, E2>, ResultAsync<T3, E3>, ResultAsync<T4, E4>, ResultAsync<T5, E5>, ResultAsync<T6, E6>, ResultAsync<T7, E7>, ResultAsync<T8, E8>, ResultAsync<T9, E9>, ResultAsync<T10, E10> ] ): ResultAsync< [T, T2, T3, T4, T5, T6, T7, T8, T9, T10], E | E2 | E3 | E4 | E5 | E6 | E7 | E8 | E9 | E10 >; static combine< T, T2, T3, T4, T5, T6, T7, T8, T9, E, E2, E3, E4, E5, E6, E7, E8, E9 >( asyncResultList: [ ResultAsync<T, E>, ResultAsync<T2, E2>, ResultAsync<T3, E3>, ResultAsync<T4, E4>, ResultAsync<T5, E5>, ResultAsync<T6, E6>, ResultAsync<T7, E7>, ResultAsync<T8, E8>, ResultAsync<T9, E9> ] ): ResultAsync< [T, T2, T3, T4, T5, T6, T7, T8, T9], E | E2 | E3 | E4 | E5 | E6 | E7 | E8 | E9 >; static combine< T, T2, T3, T4, T5, T6, T7, T8, E, E2, E3, E4, E5, E6, E7, E8 >( asyncResultList: [ ResultAsync<T, E>, ResultAsync<T2, E2>, ResultAsync<T3, E3>, ResultAsync<T4, E4>, ResultAsync<T5, E5>, ResultAsync<T6, E6>, ResultAsync<T7, E7>, ResultAsync<T8, E8> ] ): ResultAsync< [T, T2, T3, T4, T5, T6, T7, T8], E | E2 | E3 | E4 | E5 | E6 | E7 | E8 >; static combine<T, T2, T3, T4, T5, T6, T7, E, E2, E3, E4, E5, E6, E7>( asyncResultList: [ ResultAsync<T, E>, ResultAsync<T2, E2>, ResultAsync<T3, E3>, ResultAsync<T4, E4>, ResultAsync<T5, E5>, ResultAsync<T6, E6>, ResultAsync<T7, E7> ] ): ResultAsync< [T, T2, T3, T4, T5, T6, T7], E | E2 | E3 | E4 | E5 | E6 | E7 >; static combine<T, T2, T3, T4, T5, T6, E, E2, E3, E4, E5, E6>( asyncResultList: [ ResultAsync<T, E>, ResultAsync<T2, E2>, ResultAsync<T3, E3>, ResultAsync<T4, E4>, ResultAsync<T5, E5>, ResultAsync<T6, E6> ] ): ResultAsync<[T, T2, T3, T4, T5, T6], E | E2 | E3 | E4 | E5 | E6>; static combine<T, T2, T3, T4, T5, E, E2, E3, E4, E5>( asyncResultList: [ ResultAsync<T, E>, ResultAsync<T2, E2>, ResultAsync<T3, E3>, ResultAsync<T4, E4>, ResultAsync<T5, E5> ] ): ResultAsync<[T, T2, T3, T4, T5], E | E2 | E3 | E4 | E5>; static combine<T, T2, T3, T4, E, E2, E3, E4>( asyncResultList: [ ResultAsync<T, E>, ResultAsync<T2, E2>, ResultAsync<T3, E3>, ResultAsync<T4, E4> ] ): ResultAsync<[T, T2, T3, T4], E | E2 | E3 | E4>; static combine<T, T2, T3, E, E2, E3>( asyncResultList: [ ResultAsync<T, E>, ResultAsync<T2, E2>, ResultAsync<T3, E3> ] ): ResultAsync<[T, T2, T3], E | E2 | E3>; static combine<T, T2, E, E2>( asyncResultList: [ResultAsync<T, E>, ResultAsync<T2, E2>] ): ResultAsync<[T, T2], E | E2>; static combine<T, E>( asyncResultList: ResultAsync<T, E>[] ): ResultAsync<T[], E>; static combine<T, E>( asyncResultList: ResultAsync<T, E>[] ): ResultAsync<T[], E> { return ResultAsync.combine(asyncResultList) as ResultAsync<T[], E>; } /** * Given a list of ResultAsync, it will return the result only from the * first that completes. * @param asyncResultList * @returns */ static race<T, E>(asyncResultList: ResultAsync<T, E>[]): ResultAsync<T, E> { return ResultAsync.fromPromise(Promise.race(asyncResultList), (e) => { return e as E; }).andThen((result) => { return result.isErr() ? err(result.error) : ok(result.value); }); } /** * Executes a list of functions that return ResultAsync one at a time, * rather than in parrallel. Semantically identical to combine, but the * order or execution is different. * @param funcList * @returns */ static executeSerially<T, T2, T3, E, E2, E3>( funcList: [ () => ResultAsync<T, E>, () => ResultAsync<T2, E2>, () => ResultAsync<T3, E3> ] ): ResultAsync<[T, T2, T3], E | E2 | E3>; static executeSerially<T, T2, E, E2>( funcList: [() => ResultAsync<T, E>, () => ResultAsync<T2, E2>] ): ResultAsync<[T, T2], E | E2>; static executeSerially<T, E>( funcList: (() => ResultAsync<T, E>)[] ): ResultAsync<T[], E>; static executeSerially<T, E>( funcList: (() => ResultAsync<T, E>)[] ): ResultAsync<T[], E> { const executor = async () => { const results = new Array<T>(); for (const func of funcList) { const result = await func(); if (result.isErr()) { throw result.error; } else { results.push(result.value); } } return results; }; return ResultAsync.fromPromise(executor(), (e) => { return e as E; }); } /** * backoffAndRetry() is a powerful method that executes a function that returns * a ResultAsync. If that result is OK, then the result is returned directly. * If it is an error, it will check to see if the returned error is in the * acceptableErrors list. If so, it will delay and re-execute the function. * If it's not an acceptable error type, the error result is returned. * Each failure will backoff exponentially. * * This is an excellent wrapper for functions that can fail inconsistently * in some ways were we expect a retry to succeed. For example, a function * that communicates with a database over a sketchy connection might just need * to retry in a few seconds. * @param func * @param acceptableErrors A list of error types that are considered retryable. Logical error types that will never succeed should not be in this list, but error types that represent temporary runtime errors should. * @param maxAttempts This is the maximum number of times a retry will be attempted; set to null to use infinite retries. * @param baseSeconds This is the delay in seconds for the first retry attempt. It will double with each successive retry. * @returns */ static backoffAndRetry<T, E extends Error>( func: () => ResultAsync<T, E>, acceptableErrors: Function[], maxAttempts?: number, baseSeconds = 5 ): ResultAsync<T, E> { if (maxAttempts != null && maxAttempts < 1) { throw new Error("maxAttempts must be 1 or more!"); } if (baseSeconds < 1) { throw new Error("baseSeconds must be 1 or more!"); } const runAndCheck = ( currentAttempt: number, nextAttemptSecs: number, lastError: E | null ): ResultAsync<T, E> => { if (maxAttempts != null && currentAttempt > maxAttempts) { if (lastError == null) { throw new Error( "Error before first function run; logical error! maxAttempts must be 1 or more!" ); } return errAsync(lastError); } // Check the result. If it's not an error, we're done! // If it's an error, check the error type against acceptableErrors. If it's in the list, // wait some amount of time and try again. // If it is not in the list, return the error and stop. return func().orElse((e) => { let retry = false; for (const acceptableError of acceptableErrors) { if (e instanceof acceptableError) { retry = true; break; } } if (retry) { return ResultAsync.fromSafePromise<void, never>( delay(nextAttemptSecs) ).andThen(() => { return runAndCheck( ++currentAttempt, nextAttemptSecs * 2, e ); }); } return errAsync(e); }); }; return runAndCheck(1, baseSeconds, null); } /** * filter() is a normal filter method that works with async callbacks. * This works like a nomral array filter() call, except the callback returns * a ResultAsync<boolean> instead of just boolean. * @param arr the source array * @param callback a function that returns a ResultAsync<boolean>; if it returns true then the source value is included in the result. * @returns a ResultAsync containing an array of source values where the callback returns true. */ static filter<T, E extends Error>( arr: T[], callback: (val: T) => ResultAsync<boolean, E> ): ResultAsync<T[], E> { const filterVals = new Array<T>(); return ResultUtils.combine( arr.map((val) => { return callback(val).map((result) => { if (result) { filterVals.push(val); } }); }) ).map(() => { return filterVals; }); } /** * map() is a way to combine multiple async callbacks. * This works like a normal array map() call, except the callbacks return ResultAsync. * It will combine all the results and return a single ResultAsync with an array of the mapped * values. * @param arr The source array of objects * @param callback A function that returns a ResultAsync and a value * @returns A ResultAsync containing an array of the mapped values or the first error. */ static map<T, U, E extends Error>( arr: T[], callback: (val: T) => ResultAsync<U, E> ): ResultAsync<U[], E> { const mapVals = new Array<U>(); return ResultUtils.combine( arr.map((val) => { return callback(val).map((result) => { mapVals.push(result); }); }) ).map(() => { return mapVals; }); } static delay(ms: number): ResultAsync<void, never> { return ResultAsync.fromSafePromise( new Promise((resolve) => { setTimeout(() => { resolve(); }, ms); }) ); } }