Skip to content

Commit f21372f

Browse files
committed
fix: add max delay to retries
1 parent 94e53b6 commit f21372f

5 files changed

Lines changed: 47 additions & 4 deletions

File tree

.changeset/every-ducks-yell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@gelatocloud/gasless": patch
3+
---
4+
5+
fix: add max delay to retries

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,6 @@ const receipt = await relayer.sendTransactionSync(
578578
},
579579
{
580580
retries: {
581-
max: 3, // Retry up to 3 times (default: 0, max: 10)
582581
max: 3, // Retry up to 3 times (default: 0, max: 5)
583582
delay: 1000, // Wait 1s between retries (default: 200ms)
584583
backoff: 'fixed', // 'exponential' (default) or 'fixed'
@@ -592,11 +591,10 @@ const receipt = await relayer.sendTransactionSync(
592591

593592
| Option | Type | Default | Description |
594593
|--------|------|---------|-------------|
595-
| `max` | `number` | `0` | Maximum number of retries (0–10). Clamped to 10. |
596-
| `delay` | `number` | `200` | Delay in milliseconds before each retry. |
597594
| `max` | `number` | `0` | Maximum number of retries (0–5). Clamped to 5. |
598595
| `delay` | `number` | `200` | Base delay in milliseconds before each retry. |
599596
| `backoff` | `'fixed' \| 'exponential'` | `'exponential'` | Backoff strategy. `'fixed'` keeps constant delay; `'exponential'` doubles each retry (`delay × 2^attempt`). |
597+
| `maxDelay` | `number` | `10000` | Maximum delay in ms. Caps exponential backoff so it never exceeds this value. |
600598
| `errorCodes` | `number[]` | `[4211]` | RPC error codes that trigger a retry. Default is `SimulationFailedRpcError`. |
601599

602600
Works with both async and sync relayer methods:

src/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,7 @@ const receipt = await relayer.sendTransactionSync(
594594
| `max` | `number` | `0` | Maximum number of retries (0–5). Clamped to 5. |
595595
| `delay` | `number` | `200` | Base delay in milliseconds before each retry. |
596596
| `backoff` | `'fixed' \| 'exponential'` | `'exponential'` | Backoff strategy. `'fixed'` keeps constant delay; `'exponential'` doubles each retry (`delay × 2^attempt`). |
597+
| `maxDelay` | `number` | `10000` | Maximum delay in ms. Caps exponential backoff so it never exceeds this value. |
597598
| `errorCodes` | `number[]` | `[4211]` | RPC error codes that trigger a retry. Default is `SimulationFailedRpcError`. |
598599

599600
Works with both async and sync relayer methods:

src/utils/withRetries.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,42 @@ describe('withRetries', () => {
132132
expect(result).toBe('ok');
133133
});
134134

135+
it('caps exponential backoff at maxDelay', async () => {
136+
const fn = vi
137+
.fn()
138+
.mockRejectedValueOnce({ code: SimulationFailedRpcError.code })
139+
.mockRejectedValueOnce({ code: SimulationFailedRpcError.code })
140+
.mockRejectedValueOnce({ code: SimulationFailedRpcError.code })
141+
.mockResolvedValue('ok');
142+
143+
// delay=1000, exponential: 1000, 2000, 4000 — but maxDelay=2500 caps it
144+
const promise = withRetries(fn, {
145+
backoff: 'exponential',
146+
delay: 1000,
147+
max: 3,
148+
maxDelay: 2500
149+
});
150+
151+
// First call fails immediately
152+
await vi.advanceTimersByTimeAsync(0);
153+
expect(fn).toHaveBeenCalledTimes(1);
154+
155+
// Retry 0: delay = min(1000 * 2^0, 2500) = 1000ms
156+
await vi.advanceTimersByTimeAsync(1000);
157+
expect(fn).toHaveBeenCalledTimes(2);
158+
159+
// Retry 1: delay = min(1000 * 2^1, 2500) = 2000ms
160+
await vi.advanceTimersByTimeAsync(2000);
161+
expect(fn).toHaveBeenCalledTimes(3);
162+
163+
// Retry 2: delay = min(1000 * 2^2, 2500) = 2500ms (capped)
164+
await vi.advanceTimersByTimeAsync(2500);
165+
expect(fn).toHaveBeenCalledTimes(4);
166+
167+
const result = await promise;
168+
expect(result).toBe('ok');
169+
});
170+
135171
it('clamps max retries to MAX_RETRIES', async () => {
136172
const fn = vi.fn().mockRejectedValue({ code: SimulationFailedRpcError.code });
137173

src/utils/withRetries.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export interface WithRetriesOptions {
1111
delay?: number;
1212
/** Backoff strategy. 'fixed' keeps constant delay, 'exponential' doubles each retry. Default: 'exponential' */
1313
backoff?: 'fixed' | 'exponential';
14+
/** Maximum delay in ms. Caps exponential backoff so it never exceeds this value. Default: 10000 */
15+
maxDelay?: number;
1416
}
1517

1618
export async function withRetries<T>(
@@ -21,14 +23,15 @@ export async function withRetries<T>(
2123
const errorCodes = options?.errorCodes ?? [SimulationFailedRpcError.code];
2224
const delay = options?.delay ?? 200;
2325
const backoff = options?.backoff ?? 'exponential';
26+
const maxDelay = options?.maxDelay ?? 12_000;
2427

2528
for (let attempt = 0; attempt <= max; attempt++) {
2629
try {
2730
return await fn();
2831
} catch (error) {
2932
const code = (error as { code?: number }).code;
3033
if (attempt < max && code !== undefined && errorCodes.includes(code)) {
31-
const wait = backoff === 'exponential' ? delay * 2 ** attempt : delay;
34+
const wait = backoff === 'exponential' ? Math.min(delay * 2 ** attempt, maxDelay) : delay;
3235
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
3336
continue;
3437
}

0 commit comments

Comments
 (0)