neverthrow-result-utils
Version:
Extra utilities for working with Neverthrow results
624 lines (602 loc) • 12.8 kB
text/typescript
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);
})
);
}
}