Skip to content

Commit

Permalink
feat(promise): retry$ helper to make any function auto-retry
Browse files Browse the repository at this point in the history
Useful when there is an async process that fails intermittently (e.g. an
API call).
  • Loading branch information
cristinecula committed Oct 9, 2024
1 parent ad28dd4 commit 498be4c
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 14 deletions.
16 changes: 16 additions & 0 deletions src/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,19 @@ export const log$ =
console.log(response);
return response;
});

export const retry$ =
<T extends unknown[], P>(fn: (...args: T) => P | PromiseLike<P>, n: number) =>
async (...args: T) => {
let r = 0;
let error;
while (r < n) {
try {
return await fn(...args);
} catch (e) {
error = e;
r++;
}
}
throw error;
};
75 changes: 61 additions & 14 deletions test/promise.test.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { assert, aTimeout, nextFrame } from '@open-wc/testing';
import { spy } from 'sinon';
import { debounce$, limit$, ManagedPromise } from '../src/promise';
import { debounce$, limit$, ManagedPromise, retry$ } from '../src/promise';

const nextMicrotask = () => new Promise(queueMicrotask);

suite('limit$', () => {
test('rate limits a function', async () => {
const callback = spy(),
callLater = callback =>
new Promise(resolve =>
requestAnimationFrame(() => resolve(callback()))
callLater = (callback) =>
new Promise((resolve) =>
requestAnimationFrame(() => resolve(callback())),
),
callLater$ = limit$(callLater, 2);

Expand All @@ -30,8 +30,8 @@ suite('limit$', () => {
});

test('is transparent to success', async () => {
const pow = x =>
new Promise(resolve => queueMicrotask(() => resolve(x * x))),
const pow = (x) =>
new Promise((resolve) => queueMicrotask(() => resolve(x * x))),
pow$ = limit$(pow, 3);

assert.equal(await pow$(2), 4);
Expand All @@ -40,7 +40,7 @@ suite('limit$', () => {
test('is transparent to error', async () => {
const broken = () =>
new Promise((resolve, reject) =>
queueMicrotask(() => reject(new Error('broken')))
queueMicrotask(() => reject(new Error('broken'))),
),
broken$ = limit$(broken, 3);

Expand All @@ -53,17 +53,17 @@ suite('limit$', () => {
});

test('also works with sync functions', async () => {
const double = a => a * 2,
const double = (a) => a * 2,
double$ = limit$(double, 2);
assert.equal(await double$(2), 4);
});
});

suite('debounce$', () => {
test('debounces an async function', async () => {
const callLater = callback =>
new Promise(resolve =>
requestAnimationFrame(() => resolve(callback()))
const callLater = (callback) =>
new Promise((resolve) =>
requestAnimationFrame(() => resolve(callback())),
),
callLater$ = debounce$(callLater, 50),
callback = spy();
Expand All @@ -83,8 +83,8 @@ suite('debounce$', () => {
});

test('all debounced calls resolve with the same data', async () => {
const fetch = x =>
new Promise(resolve => requestAnimationFrame(() => resolve(x * 2))),
const fetch = (x) =>
new Promise((resolve) => requestAnimationFrame(() => resolve(x * 2))),
fetch$ = debounce$(fetch, 50),
result1 = fetch$(1);

Expand All @@ -102,7 +102,7 @@ suite('debounce$', () => {

suite('ManagedPromise', () => {
test('can substitute a normal Promise', async () => {
const p = new ManagedPromise(resolve => resolve(10)),
const p = new ManagedPromise((resolve) => resolve(10)),
cb = spy();
p.then(cb);
await p;
Expand Down Expand Up @@ -140,3 +140,50 @@ suite('ManagedPromise', () => {
assert.isTrue(cb.calledOnce);
});
});

suite('retry$', () => {
test('retries a function until it resolves', async () => {
let numCalls = 0;
const fn = retry$(() => {
++numCalls;
if (numCalls === 3) {
return 'ok';
}
throw new Error('fail');
}, 3);

const result = await fn();

assert.equal(numCalls, 3);
assert.equal(result, 'ok');
});

test('retries a function until it reaches max retries', async () => {
let numCalls = 0;
const fn = retry$(() => {
++numCalls;
throw new Error('fail');
}, 3);

try {
await fn();
} catch (e) {
assert.exists(e);
}

assert.equal(numCalls, 3);
});

test('does not retry needlesly', async () => {
let numCalls = 0;
const fn = retry$(() => {
++numCalls;
return 'ok';
}, 3);

const result = await fn();

assert.equal(numCalls, 1);
assert.equal(result, 'ok');
});
});

0 comments on commit 498be4c

Please sign in to comment.