Skip to content

Commit 57b600a

Browse files
refactor: retry logic
1 parent ead0b5a commit 57b600a

4 files changed

Lines changed: 39 additions & 9 deletions

File tree

packages/anticapture-client/dist/with-retry-and-timeout.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"use strict";
22
Object.defineProperty(exports, "__esModule", { value: true });
33
exports.withRetryAndTimeout = withRetryAndTimeout;
4-
function isRetryable(err) {
4+
function isRetryable(err, timedOut) {
5+
if (timedOut)
6+
return true;
57
const e = err;
68
if (!e)
79
return false;
@@ -18,16 +20,17 @@ async function withRetryAndTimeout(fn, opts) {
1820
let lastErr;
1921
while (attempt <= retries) {
2022
const ac = new AbortController();
21-
const timer = setTimeout(() => ac.abort(), timeoutMs);
23+
let timedOut = false;
24+
const timer = setTimeout(() => { timedOut = true; ac.abort(); }, timeoutMs);
2225
try {
2326
return await fn(ac.signal);
2427
}
2528
catch (err) {
2629
lastErr = err;
27-
if (!isRetryable(err) || attempt === retries)
30+
if (!isRetryable(err, timedOut) || attempt === retries)
2831
throw err;
2932
const delay = baseDelayMs * 2 ** attempt;
30-
console.warn(`[AnticaptureClient] Retry ${attempt + 1}/${retries} after error: ${err.message}`);
33+
console.warn(`[AnticaptureClient] Retry ${attempt + 1}/${retries} after ${timedOut ? 'timeout' : 'error'}: ${err.message}`);
3134
await sleep(delay);
3235
attempt += 1;
3336
}

packages/anticapture-client/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44
"description": "Shared AnticaptureClient for REST SDK communication",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
7+
"exports": {
8+
".": {
9+
"types": "./dist/index.d.ts",
10+
"default": "./dist/index.js"
11+
},
12+
"./test-doubles": {
13+
"types": "./dist/test-doubles.d.ts",
14+
"default": "./dist/test-doubles.js"
15+
}
16+
},
717
"files": [
818
"dist"
919
],

packages/anticapture-client/src/with-retry-and-timeout.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ export interface RetryOptions {
44
baseDelayMs?: number; // default 1000
55
}
66

7-
function isRetryable(err: unknown): boolean {
7+
function isRetryable(err: unknown, timedOut: boolean): boolean {
8+
if (timedOut) return true;
89
const e = err as { status?: number; code?: string };
910
if (!e) return false;
1011
if (e.code === 'ECONNRESET' || e.code === 'ETIMEDOUT' || e.code === 'ENETUNREACH') return true;
@@ -23,14 +24,15 @@ export async function withRetryAndTimeout<T>(
2324
let lastErr: unknown;
2425
while (attempt <= retries) {
2526
const ac = new AbortController();
26-
const timer = setTimeout(() => ac.abort(), timeoutMs);
27+
let timedOut = false;
28+
const timer = setTimeout(() => { timedOut = true; ac.abort(); }, timeoutMs);
2729
try {
2830
return await fn(ac.signal);
2931
} catch (err) {
3032
lastErr = err;
31-
if (!isRetryable(err) || attempt === retries) throw err;
33+
if (!isRetryable(err, timedOut) || attempt === retries) throw err;
3234
const delay = baseDelayMs * 2 ** attempt;
33-
console.warn(`[AnticaptureClient] Retry ${attempt + 1}/${retries} after error: ${(err as Error).message}`);
35+
console.warn(`[AnticaptureClient] Retry ${attempt + 1}/${retries} after ${timedOut ? 'timeout' : 'error'}: ${(err as Error).message}`);
3436
await sleep(delay);
3537
attempt += 1;
3638
} finally {

packages/anticapture-client/tests/with-retry-and-timeout.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,26 @@ describe('withRetryAndTimeout', () => {
3737
expect(fn).toHaveBeenCalledTimes(3); // initial + 2 retries
3838
});
3939

40-
it('aborts and throws on timeout', async () => {
40+
it('aborts and throws on timeout when no retries left', async () => {
4141
const fn = vi.fn((signal?: AbortSignal) => new Promise((_, reject) => {
4242
signal?.addEventListener('abort', () => reject(new Error('aborted')));
4343
}));
4444
await expect(withRetryAndTimeout(fn, { retries: 0, timeoutMs: 10 }))
4545
.rejects.toThrow();
4646
});
47+
48+
it('retries on timeout and eventually succeeds', async () => {
49+
let attempts = 0;
50+
const fn = vi.fn((signal?: AbortSignal) => new Promise<string>((resolve, reject) => {
51+
attempts += 1;
52+
if (attempts < 3) {
53+
signal?.addEventListener('abort', () => reject(new Error('aborted')));
54+
return;
55+
}
56+
resolve('ok');
57+
}));
58+
const result = await withRetryAndTimeout(fn, { retries: 4, timeoutMs: 10, baseDelayMs: 1 });
59+
expect(result).toBe('ok');
60+
expect(fn).toHaveBeenCalledTimes(3);
61+
});
4762
});

0 commit comments

Comments
 (0)