summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2025-08-17 15:09:14 -0700
committerElizabeth Hunt <me@liz.coffee>2025-08-17 15:32:01 -0700
commitd9656076ec5c4138201080f0a250b57533357165 (patch)
treec23d32d95c532c8b25a112af4369d0aa0de55382
parent3d61a9b0c29deb576ecd5d0e8de7b4426d5ab41c (diff)
downloadpengueno-d9656076ec5c4138201080f0a250b57533357165.tar.gz
pengueno-d9656076ec5c4138201080f0a250b57533357165.zip
Add some retry behavior
-rw-r--r--lib/trace/util.ts1
-rw-r--r--lib/types/fn/either.ts40
-rw-r--r--package.json2
-rw-r--r--tst/either.test.ts45
4 files changed, 86 insertions, 2 deletions
diff --git a/lib/trace/util.ts b/lib/trace/util.ts
index ec67571..95f8d25 100644
--- a/lib/trace/util.ts
+++ b/lib/trace/util.ts
@@ -1,7 +1,6 @@
import {
IEither,
IMetric,
- isEither,
ITraceable,
ITraceWith,
LogLevel,
diff --git a/lib/types/fn/either.ts b/lib/types/fn/either.ts
index 0f65859..ffe06d4 100644
--- a/lib/types/fn/either.ts
+++ b/lib/types/fn/either.ts
@@ -25,6 +25,7 @@ export interface IEither<E, T> extends Tagged<IEitherTag> {
readonly flatMapAsync: <_T>(mapper: Mapper<T, Promise<IEither<E, _T>>>) => Promise<IEither<E, _T>>;
readonly moveRight: <_T>(t: _T) => IEither<E, _T>;
+ readonly swap: Supplier<IEither<T, E>>;
readonly fold: <_T>(leftFolder: Mapper<E, _T>, rightFolder: Mapper<T, _T>) => _T;
readonly joinRight: <O, _T>(other: IEither<E, O>, mapper: (a: O, b: T) => _T) => IEither<E, _T>;
readonly joinRightAsync: <O, _T>(
@@ -119,6 +120,11 @@ export class Either<E, T> extends _Tagged implements IEither<E, T> {
});
}
+ public swap(): IEither<T, E> {
+ if (isRight(this.self)) return Either.left(this.self.ok);
+ return Either.right(this.self.err);
+ }
+
static left<E, T>(e: E): IEither<E, T> {
return new Either({ err: e, _tag: ELeftTag });
}
@@ -140,4 +146,38 @@ export class Either<E, T> extends _Tagged implements IEither<E, T> {
.then((t: T) => Either.right<E, T>(t))
.catch((e: E) => Either.left<E, T>(e));
}
+
+ static async retrying<E, T>(
+ s: Supplier<Promise<IEither<E, T>>>,
+ attempts: number = 3,
+ interval: Mapper<number, Promise<void>> = (attempt) => Either.attemptWait(attempt),
+ ): Promise<IEither<E, T>> {
+ const res: IEither<T, E> = await Array(attempts)
+ .fill(0)
+ .reduce(
+ async (_result, _i, attempt) => {
+ await interval(attempt);
+ const result: IEither<T, E> = await _result;
+ return result.joinRightAsync(
+ () => s().then((s) => s.swap()),
+ (res, _prevError) => res,
+ );
+ },
+ Promise.resolve(Either.right<T, E>(<E>new Error('No attempts made'))),
+ );
+ return res.swap();
+ }
+
+ static attemptWait(
+ attempt: number,
+ backoffFactor: number = 500,
+ jitter: number = 300,
+ exponent: number = 1.3,
+ ): Promise<void> {
+ if (attempt === 0) {
+ return Promise.resolve();
+ }
+ const wait = Math.pow(exponent, attempt) * backoffFactor + jitter * Math.random() * Math.pow(exponent, attempt);
+ return new Promise((res) => setTimeout(res, wait));
+ }
}
diff --git a/package.json b/package.json
index a77b9ae..d4bb7e5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@emprespresso/pengueno",
- "version": "0.0.9",
+ "version": "0.0.11",
"description": "emprespresso pengueno utils",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
diff --git a/tst/either.test.ts b/tst/either.test.ts
new file mode 100644
index 0000000..790d392
--- /dev/null
+++ b/tst/either.test.ts
@@ -0,0 +1,45 @@
+import { Either } from '@emprespresso/pengueno';
+
+describe('Either.retrying', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('succeeds on first attempt', async () => {
+ const supplier = jest.fn().mockResolvedValue(Either.right<string, string>('success'));
+ const interval = jest.fn().mockResolvedValue(undefined);
+
+ const result = await Either.retrying(supplier, 3, interval);
+
+ expect(result.right().get()).toBe('success');
+ expect(supplier).toHaveBeenCalledTimes(1);
+ });
+
+ test('never succeeds after all attempts', async () => {
+ const supplier = jest.fn().mockResolvedValue(Either.left<string, string>('failed'));
+ const interval = jest.fn().mockResolvedValue(undefined);
+
+ const result = await Either.retrying(supplier, 3, interval);
+
+ expect(result.left().get()).toBe('failed');
+ expect(supplier).toHaveBeenCalledTimes(3);
+ });
+
+ test('attempts correct number of times and calls interval with backoff', async () => {
+ const supplier = jest.fn()
+ .mockResolvedValueOnce(Either.left<string, string>('attempt 1 failed'))
+ .mockResolvedValueOnce(Either.left<string, string>('attempt 2 failed'))
+ .mockResolvedValueOnce(Either.right<string, string>('attempt 3 success'));
+
+ const interval = jest.fn().mockResolvedValue(undefined);
+
+ const result = await Either.retrying(supplier, 3, interval);
+
+ expect(result.right().get()).toBe('attempt 3 success');
+ expect(supplier).toHaveBeenCalledTimes(3);
+ expect(interval).toHaveBeenCalledTimes(3);
+ expect(interval).toHaveBeenNthCalledWith(1, 0);
+ expect(interval).toHaveBeenNthCalledWith(2, 1);
+ expect(interval).toHaveBeenNthCalledWith(3, 2);
+ });
+});