ai-resilience 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -0
- package/dist/index.cjs +326 -0
- package/dist/index.d.cts +144 -0
- package/dist/index.d.ts +144 -0
- package/dist/index.js +276 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# ai-resilience
|
|
2
|
+
|
|
3
|
+
Phase 1 of `ai-resilience` is an axios-retry++ foundation for modern AI and backend systems. It keeps axios compatibility while adding configurable retry strategies, advanced jitter, retry conditions, hooks, structured logging, and strong TypeScript types.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install ai-resilience axios axios-retry
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import axios from "axios";
|
|
15
|
+
import { applyAiResilience, ConsoleRetryLogger } from "ai-resilience";
|
|
16
|
+
|
|
17
|
+
const client = axios.create({ baseURL: "https://api.example.com" });
|
|
18
|
+
|
|
19
|
+
applyAiResilience(client, {
|
|
20
|
+
retries: 3,
|
|
21
|
+
strategy: "exponential",
|
|
22
|
+
baseDelayMs: 150,
|
|
23
|
+
maxDelayMs: 10_000,
|
|
24
|
+
jitter: "full",
|
|
25
|
+
logger: new ConsoleRetryLogger("info"),
|
|
26
|
+
hooks: {
|
|
27
|
+
onRetry: ({ attempt, nextDelayMs }) => {
|
|
28
|
+
console.log(`retry ${attempt} in ${nextDelayMs}ms`);
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- Enhanced retry configuration built on top of `axios-retry`
|
|
37
|
+
- Fixed, linear, and exponential retry strategies
|
|
38
|
+
- None, full, equal, and decorrelated jitter
|
|
39
|
+
- Retry method and status-code controls
|
|
40
|
+
- Async custom retry conditions
|
|
41
|
+
- EventEmitter-based hooks plus direct hook callbacks
|
|
42
|
+
- Structured logger interface and console logger
|
|
43
|
+
- TypeScript-first public API
|
|
44
|
+
|
|
45
|
+
## API
|
|
46
|
+
|
|
47
|
+
### `applyAiResilience(instance, config)`
|
|
48
|
+
|
|
49
|
+
Installs retry behavior on an existing axios instance and returns `{ axios, hooks, config }`.
|
|
50
|
+
|
|
51
|
+
### `createAiResilienceClient(config)`
|
|
52
|
+
|
|
53
|
+
Creates a new axios instance with retry behavior already applied.
|
|
54
|
+
|
|
55
|
+
### Retry config
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
type AiResilienceRetryConfig = {
|
|
59
|
+
retries?: number;
|
|
60
|
+
strategy?: "fixed" | "linear" | "exponential";
|
|
61
|
+
baseDelayMs?: number;
|
|
62
|
+
maxDelayMs?: number;
|
|
63
|
+
jitter?: "none" | "full" | "equal" | "decorrelated";
|
|
64
|
+
retryStatusCodes?: number[];
|
|
65
|
+
retryMethods?: string[];
|
|
66
|
+
retryCondition?: (error) => boolean | Promise<boolean>;
|
|
67
|
+
hooks?: RetryHooks;
|
|
68
|
+
logger?: RetryLogger;
|
|
69
|
+
metadata?: Record<string, unknown>;
|
|
70
|
+
};
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Scripts
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
npm run build
|
|
77
|
+
npm test
|
|
78
|
+
npm run lint
|
|
79
|
+
npm run format
|
|
80
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ConsoleRetryLogger: () => ConsoleRetryLogger,
|
|
34
|
+
RetryHookEmitter: () => RetryHookEmitter,
|
|
35
|
+
applyAiResilience: () => applyAiResilience,
|
|
36
|
+
applyJitter: () => applyJitter,
|
|
37
|
+
calculateRetryDelay: () => calculateRetryDelay,
|
|
38
|
+
createAiResilienceClient: () => createAiResilienceClient,
|
|
39
|
+
createLogEntry: () => createLogEntry,
|
|
40
|
+
defaultRetryConfig: () => defaultRetryConfig,
|
|
41
|
+
isRetryableMethod: () => isRetryableMethod,
|
|
42
|
+
isRetryableStatus: () => isRetryableStatus,
|
|
43
|
+
normalizeRetryConfig: () => normalizeRetryConfig,
|
|
44
|
+
resolveRetryStrategy: () => resolveRetryStrategy,
|
|
45
|
+
retryStrategies: () => retryStrategies,
|
|
46
|
+
shouldRetry: () => shouldRetry
|
|
47
|
+
});
|
|
48
|
+
module.exports = __toCommonJS(index_exports);
|
|
49
|
+
|
|
50
|
+
// src/engine.ts
|
|
51
|
+
var import_axios = __toESM(require("axios"), 1);
|
|
52
|
+
var import_axios_retry2 = __toESM(require("axios-retry"), 1);
|
|
53
|
+
|
|
54
|
+
// src/config.ts
|
|
55
|
+
var import_zod = require("zod");
|
|
56
|
+
var configSchema = import_zod.z.object({
|
|
57
|
+
retries: import_zod.z.number().int().min(0).optional(),
|
|
58
|
+
strategy: import_zod.z.enum(["fixed", "linear", "exponential"]).optional(),
|
|
59
|
+
baseDelayMs: import_zod.z.number().min(0).optional(),
|
|
60
|
+
maxDelayMs: import_zod.z.number().min(0).optional(),
|
|
61
|
+
jitter: import_zod.z.enum(["none", "full", "equal", "decorrelated"]).optional(),
|
|
62
|
+
retryStatusCodes: import_zod.z.array(import_zod.z.number().int().min(100).max(599)).optional(),
|
|
63
|
+
retryMethods: import_zod.z.array(import_zod.z.string().min(1)).optional(),
|
|
64
|
+
metadata: import_zod.z.record(import_zod.z.unknown()).optional()
|
|
65
|
+
});
|
|
66
|
+
var defaultRetryConfig = {
|
|
67
|
+
retries: 3,
|
|
68
|
+
strategy: "exponential",
|
|
69
|
+
baseDelayMs: 150,
|
|
70
|
+
maxDelayMs: 3e4,
|
|
71
|
+
jitter: "full"
|
|
72
|
+
};
|
|
73
|
+
function normalizeRetryConfig(config = {}) {
|
|
74
|
+
configSchema.parse(config);
|
|
75
|
+
return {
|
|
76
|
+
...defaultRetryConfig,
|
|
77
|
+
...config
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/conditions.ts
|
|
82
|
+
var import_axios_retry = __toESM(require("axios-retry"), 1);
|
|
83
|
+
var DEFAULT_RETRY_METHODS = ["delete", "get", "head", "options", "put"];
|
|
84
|
+
var DEFAULT_RETRY_STATUS_CODES = [408, 409, 425, 429, 500, 502, 503, 504];
|
|
85
|
+
function isRetryableMethod(error, retryMethods = DEFAULT_RETRY_METHODS) {
|
|
86
|
+
const method = error.config?.method?.toLowerCase();
|
|
87
|
+
return method ? retryMethods.map((item) => item.toLowerCase()).includes(method) : true;
|
|
88
|
+
}
|
|
89
|
+
function isRetryableStatus(error, retryStatusCodes = DEFAULT_RETRY_STATUS_CODES) {
|
|
90
|
+
const status = error.response?.status;
|
|
91
|
+
return typeof status === "number" && retryStatusCodes.includes(status);
|
|
92
|
+
}
|
|
93
|
+
async function shouldRetry(error, config = {}) {
|
|
94
|
+
if (config.retryCondition) {
|
|
95
|
+
return config.retryCondition(error);
|
|
96
|
+
}
|
|
97
|
+
if (!isRetryableMethod(error, config.retryMethods)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return import_axios_retry.default.isNetworkOrIdempotentRequestError(error) || isRetryableStatus(error, config.retryStatusCodes);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/jitter.ts
|
|
104
|
+
var clamp = (value, max) => Math.min(Math.max(0, value), max);
|
|
105
|
+
function applyJitter(delayMs, options = {}) {
|
|
106
|
+
const strategy = options.strategy ?? "none";
|
|
107
|
+
const maxDelayMs = options.maxDelayMs ?? Number.POSITIVE_INFINITY;
|
|
108
|
+
const random = options.random ?? Math.random;
|
|
109
|
+
if (strategy === "none") {
|
|
110
|
+
return clamp(delayMs, maxDelayMs);
|
|
111
|
+
}
|
|
112
|
+
if (strategy === "full") {
|
|
113
|
+
return clamp(random() * delayMs, maxDelayMs);
|
|
114
|
+
}
|
|
115
|
+
if (strategy === "equal") {
|
|
116
|
+
const half = delayMs / 2;
|
|
117
|
+
return clamp(half + random() * half, maxDelayMs);
|
|
118
|
+
}
|
|
119
|
+
const base = options.baseDelayMs ?? delayMs;
|
|
120
|
+
const previous = options.previousDelayMs ?? base;
|
|
121
|
+
const next = base + random() * Math.max(base, previous * 3 - base);
|
|
122
|
+
return clamp(next, maxDelayMs);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/delay.ts
|
|
126
|
+
function calculateRetryDelay(config, context) {
|
|
127
|
+
const baseDelayMs = config.baseDelayMs ?? 100;
|
|
128
|
+
const maxDelayMs = config.maxDelayMs ?? 3e4;
|
|
129
|
+
const attempt = Math.max(1, context.attempt);
|
|
130
|
+
let delayMs;
|
|
131
|
+
switch (config.strategy ?? "exponential") {
|
|
132
|
+
case "fixed":
|
|
133
|
+
delayMs = baseDelayMs;
|
|
134
|
+
break;
|
|
135
|
+
case "linear":
|
|
136
|
+
delayMs = baseDelayMs * attempt;
|
|
137
|
+
break;
|
|
138
|
+
case "exponential":
|
|
139
|
+
default:
|
|
140
|
+
delayMs = baseDelayMs * 2 ** (attempt - 1);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
return Math.round(
|
|
144
|
+
applyJitter(Math.min(delayMs, maxDelayMs), {
|
|
145
|
+
strategy: config.jitter ?? "full",
|
|
146
|
+
previousDelayMs: context.previousDelayMs,
|
|
147
|
+
baseDelayMs,
|
|
148
|
+
maxDelayMs,
|
|
149
|
+
random: context.random
|
|
150
|
+
})
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/hooks.ts
|
|
155
|
+
var import_eventemitter3 = __toESM(require("eventemitter3"), 1);
|
|
156
|
+
var RetryHookEmitter = class extends import_eventemitter3.default {
|
|
157
|
+
constructor(hooks = {}) {
|
|
158
|
+
super();
|
|
159
|
+
this.hooks = hooks;
|
|
160
|
+
}
|
|
161
|
+
hooks;
|
|
162
|
+
async emitRetry(context) {
|
|
163
|
+
this.emit("retry", context);
|
|
164
|
+
await this.hooks.onRetry?.(context);
|
|
165
|
+
}
|
|
166
|
+
async emitSuccess(context) {
|
|
167
|
+
this.emit("success", context);
|
|
168
|
+
await this.hooks.onSuccess?.(context);
|
|
169
|
+
}
|
|
170
|
+
async emitGiveUp(context) {
|
|
171
|
+
this.emit("giveUp", context);
|
|
172
|
+
await this.hooks.onGiveUp?.(context);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// src/logger.ts
|
|
177
|
+
var ConsoleRetryLogger = class {
|
|
178
|
+
constructor(minLevel = "info") {
|
|
179
|
+
this.minLevel = minLevel;
|
|
180
|
+
}
|
|
181
|
+
minLevel;
|
|
182
|
+
log(entry) {
|
|
183
|
+
if (levelWeight(entry.level) < levelWeight(this.minLevel)) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const payload = {
|
|
187
|
+
event: entry.event,
|
|
188
|
+
timestamp: entry.timestamp,
|
|
189
|
+
...entry.metadata
|
|
190
|
+
};
|
|
191
|
+
console[entry.level === "debug" ? "debug" : entry.level](entry.message, payload);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
function createLogEntry(level, event, message, metadata) {
|
|
195
|
+
return {
|
|
196
|
+
level,
|
|
197
|
+
event,
|
|
198
|
+
message,
|
|
199
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
200
|
+
metadata
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function levelWeight(level) {
|
|
204
|
+
return { debug: 10, info: 20, warn: 30, error: 40 }[level];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/engine.ts
|
|
208
|
+
function applyAiResilience(instance, config = {}) {
|
|
209
|
+
const resolved = normalizeRetryConfig(config);
|
|
210
|
+
const hooks = new RetryHookEmitter(resolved.hooks);
|
|
211
|
+
let previousDelayMs;
|
|
212
|
+
(0, import_axios_retry2.default)(instance, {
|
|
213
|
+
...resolved,
|
|
214
|
+
retries: resolved.retries,
|
|
215
|
+
retryCondition: (error) => shouldRetry(error, resolved),
|
|
216
|
+
retryDelay: (retryCount, error) => {
|
|
217
|
+
const nextDelayMs = calculateRetryDelay(resolved, {
|
|
218
|
+
attempt: retryCount,
|
|
219
|
+
previousDelayMs
|
|
220
|
+
});
|
|
221
|
+
previousDelayMs = nextDelayMs;
|
|
222
|
+
const requestConfig = error.config ?? {};
|
|
223
|
+
void hooks.emitRetry({
|
|
224
|
+
attempt: retryCount,
|
|
225
|
+
retries: resolved.retries,
|
|
226
|
+
error,
|
|
227
|
+
requestConfig,
|
|
228
|
+
nextDelayMs
|
|
229
|
+
});
|
|
230
|
+
resolved.logger?.log(
|
|
231
|
+
createLogEntry("warn", "retry.scheduled", "Retry scheduled", {
|
|
232
|
+
attempt: retryCount,
|
|
233
|
+
retries: resolved.retries,
|
|
234
|
+
delayMs: nextDelayMs,
|
|
235
|
+
method: requestConfig.method,
|
|
236
|
+
url: requestConfig.url,
|
|
237
|
+
...resolved.metadata
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
return nextDelayMs;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
instance.interceptors.response.use(
|
|
244
|
+
async (response) => {
|
|
245
|
+
await hooks.emitSuccess({
|
|
246
|
+
attempt: Number(response.config?.["axios-retry"]?.retryCount ?? 0),
|
|
247
|
+
response,
|
|
248
|
+
requestConfig: response.config
|
|
249
|
+
});
|
|
250
|
+
return response;
|
|
251
|
+
},
|
|
252
|
+
async (error) => {
|
|
253
|
+
const retryState = error.config?.["axios-retry"];
|
|
254
|
+
const attempts = Number(retryState?.retryCount ?? 0);
|
|
255
|
+
if (attempts >= resolved.retries) {
|
|
256
|
+
await hooks.emitGiveUp({
|
|
257
|
+
attempts,
|
|
258
|
+
error,
|
|
259
|
+
requestConfig: error.config ?? {}
|
|
260
|
+
});
|
|
261
|
+
resolved.logger?.log(
|
|
262
|
+
createLogEntry("error", "retry.give_up", "Retry attempts exhausted", {
|
|
263
|
+
attempts,
|
|
264
|
+
method: error.config?.method,
|
|
265
|
+
url: error.config?.url,
|
|
266
|
+
...resolved.metadata
|
|
267
|
+
})
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
return {
|
|
274
|
+
axios: instance,
|
|
275
|
+
hooks,
|
|
276
|
+
config: resolved
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function createAiResilienceClient(config = {}) {
|
|
280
|
+
return applyAiResilience(import_axios.default.create(), config);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/strategies.ts
|
|
284
|
+
var retryStrategies = {
|
|
285
|
+
conservative: {
|
|
286
|
+
retries: 2,
|
|
287
|
+
strategy: "exponential",
|
|
288
|
+
baseDelayMs: 250,
|
|
289
|
+
maxDelayMs: 5e3,
|
|
290
|
+
jitter: "equal"
|
|
291
|
+
},
|
|
292
|
+
balanced: {
|
|
293
|
+
retries: 3,
|
|
294
|
+
strategy: "exponential",
|
|
295
|
+
baseDelayMs: 150,
|
|
296
|
+
maxDelayMs: 15e3,
|
|
297
|
+
jitter: "full"
|
|
298
|
+
},
|
|
299
|
+
aggressive: {
|
|
300
|
+
retries: 5,
|
|
301
|
+
strategy: "exponential",
|
|
302
|
+
baseDelayMs: 100,
|
|
303
|
+
maxDelayMs: 3e4,
|
|
304
|
+
jitter: "decorrelated"
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
function resolveRetryStrategy(strategy) {
|
|
308
|
+
return typeof strategy === "string" ? retryStrategies[strategy] : strategy;
|
|
309
|
+
}
|
|
310
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
311
|
+
0 && (module.exports = {
|
|
312
|
+
ConsoleRetryLogger,
|
|
313
|
+
RetryHookEmitter,
|
|
314
|
+
applyAiResilience,
|
|
315
|
+
applyJitter,
|
|
316
|
+
calculateRetryDelay,
|
|
317
|
+
createAiResilienceClient,
|
|
318
|
+
createLogEntry,
|
|
319
|
+
defaultRetryConfig,
|
|
320
|
+
isRetryableMethod,
|
|
321
|
+
isRetryableStatus,
|
|
322
|
+
normalizeRetryConfig,
|
|
323
|
+
resolveRetryStrategy,
|
|
324
|
+
retryStrategies,
|
|
325
|
+
shouldRetry
|
|
326
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { AxiosInstance, AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
|
|
2
|
+
import EventEmitter from 'eventemitter3';
|
|
3
|
+
import { IAxiosRetryConfig } from 'axios-retry';
|
|
4
|
+
|
|
5
|
+
type RetryHookEvents<T = unknown> = {
|
|
6
|
+
retry: (context: RetryAttemptContext<T>) => void;
|
|
7
|
+
success: (context: RetrySuccessContext<T>) => void;
|
|
8
|
+
giveUp: (context: RetryGiveUpContext<T>) => void;
|
|
9
|
+
};
|
|
10
|
+
declare class RetryHookEmitter<T = unknown> extends EventEmitter<RetryHookEvents<T>> {
|
|
11
|
+
private readonly hooks;
|
|
12
|
+
constructor(hooks?: RetryHooks<T>);
|
|
13
|
+
emitRetry(context: RetryAttemptContext<T>): Promise<void>;
|
|
14
|
+
emitSuccess(context: RetrySuccessContext<T>): Promise<void>;
|
|
15
|
+
emitGiveUp(context: RetryGiveUpContext<T>): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type RetryStrategy = "fixed" | "linear" | "exponential";
|
|
19
|
+
type JitterStrategy = "none" | "full" | "equal" | "decorrelated";
|
|
20
|
+
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
21
|
+
interface RetryAttemptContext<T = unknown> {
|
|
22
|
+
attempt: number;
|
|
23
|
+
retries: number;
|
|
24
|
+
error: AxiosError<T>;
|
|
25
|
+
requestConfig: AxiosRequestConfig;
|
|
26
|
+
nextDelayMs: number;
|
|
27
|
+
}
|
|
28
|
+
interface RetrySuccessContext<T = unknown> {
|
|
29
|
+
attempt: number;
|
|
30
|
+
response: AxiosResponse<T>;
|
|
31
|
+
requestConfig: AxiosRequestConfig;
|
|
32
|
+
}
|
|
33
|
+
interface RetryGiveUpContext<T = unknown> {
|
|
34
|
+
attempts: number;
|
|
35
|
+
error: AxiosError<T>;
|
|
36
|
+
requestConfig: AxiosRequestConfig;
|
|
37
|
+
}
|
|
38
|
+
interface RetryHooks<T = unknown> {
|
|
39
|
+
onRetry?: (context: RetryAttemptContext<T>) => void | Promise<void>;
|
|
40
|
+
onSuccess?: (context: RetrySuccessContext<T>) => void | Promise<void>;
|
|
41
|
+
onGiveUp?: (context: RetryGiveUpContext<T>) => void | Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
interface StructuredLogEntry {
|
|
44
|
+
level: LogLevel;
|
|
45
|
+
event: string;
|
|
46
|
+
message: string;
|
|
47
|
+
timestamp: string;
|
|
48
|
+
metadata?: Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
interface RetryLogger {
|
|
51
|
+
log(entry: StructuredLogEntry): void;
|
|
52
|
+
}
|
|
53
|
+
interface AiResilienceRetryConfig<T = unknown> extends Omit<IAxiosRetryConfig, "retries" | "retryDelay" | "retryCondition" | "onRetry"> {
|
|
54
|
+
retries?: number;
|
|
55
|
+
strategy?: RetryStrategy;
|
|
56
|
+
baseDelayMs?: number;
|
|
57
|
+
maxDelayMs?: number;
|
|
58
|
+
jitter?: JitterStrategy;
|
|
59
|
+
retryStatusCodes?: number[];
|
|
60
|
+
retryMethods?: string[];
|
|
61
|
+
retryCondition?: (error: AxiosError<T>) => boolean | Promise<boolean>;
|
|
62
|
+
hooks?: RetryHooks<T>;
|
|
63
|
+
logger?: RetryLogger;
|
|
64
|
+
metadata?: Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
interface NormalizedAiResilienceRetryConfig<T = unknown> extends AiResilienceRetryConfig<T> {
|
|
67
|
+
retries: number;
|
|
68
|
+
strategy: RetryStrategy;
|
|
69
|
+
baseDelayMs: number;
|
|
70
|
+
maxDelayMs: number;
|
|
71
|
+
jitter: JitterStrategy;
|
|
72
|
+
}
|
|
73
|
+
interface AiResilienceClient<T = unknown> {
|
|
74
|
+
axios: AxiosInstance;
|
|
75
|
+
hooks: RetryHookEmitter<T>;
|
|
76
|
+
config: NormalizedAiResilienceRetryConfig<T>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
declare function applyAiResilience<T = unknown>(instance: AxiosInstance, config?: AiResilienceRetryConfig<T>): AiResilienceClient<T>;
|
|
80
|
+
declare function createAiResilienceClient<T = unknown>(config?: AiResilienceRetryConfig<T>): AiResilienceClient<T>;
|
|
81
|
+
|
|
82
|
+
interface DelayContext {
|
|
83
|
+
attempt: number;
|
|
84
|
+
previousDelayMs?: number;
|
|
85
|
+
random?: () => number;
|
|
86
|
+
}
|
|
87
|
+
declare function calculateRetryDelay<T = unknown>(config: AiResilienceRetryConfig<T>, context: DelayContext): number;
|
|
88
|
+
|
|
89
|
+
interface JitterOptions {
|
|
90
|
+
strategy?: JitterStrategy;
|
|
91
|
+
previousDelayMs?: number;
|
|
92
|
+
baseDelayMs?: number;
|
|
93
|
+
maxDelayMs?: number;
|
|
94
|
+
random?: () => number;
|
|
95
|
+
}
|
|
96
|
+
declare function applyJitter(delayMs: number, options?: JitterOptions): number;
|
|
97
|
+
|
|
98
|
+
declare function isRetryableMethod(error: AxiosError, retryMethods?: string[]): boolean;
|
|
99
|
+
declare function isRetryableStatus(error: AxiosError, retryStatusCodes?: number[]): boolean;
|
|
100
|
+
declare function shouldRetry<T = unknown>(error: AxiosError<T>, config?: AiResilienceRetryConfig<T>): Promise<boolean>;
|
|
101
|
+
|
|
102
|
+
declare class ConsoleRetryLogger implements RetryLogger {
|
|
103
|
+
private readonly minLevel;
|
|
104
|
+
constructor(minLevel?: LogLevel);
|
|
105
|
+
log(entry: StructuredLogEntry): void;
|
|
106
|
+
}
|
|
107
|
+
declare function createLogEntry(level: LogLevel, event: string, message: string, metadata?: Record<string, unknown>): StructuredLogEntry;
|
|
108
|
+
|
|
109
|
+
declare const defaultRetryConfig: {
|
|
110
|
+
readonly retries: 3;
|
|
111
|
+
readonly strategy: "exponential";
|
|
112
|
+
readonly baseDelayMs: 150;
|
|
113
|
+
readonly maxDelayMs: 30000;
|
|
114
|
+
readonly jitter: "full";
|
|
115
|
+
};
|
|
116
|
+
declare function normalizeRetryConfig<T = unknown>(config?: AiResilienceRetryConfig<T>): NormalizedAiResilienceRetryConfig<T>;
|
|
117
|
+
|
|
118
|
+
declare const retryStrategies: {
|
|
119
|
+
readonly conservative: {
|
|
120
|
+
readonly retries: 2;
|
|
121
|
+
readonly strategy: "exponential";
|
|
122
|
+
readonly baseDelayMs: 250;
|
|
123
|
+
readonly maxDelayMs: 5000;
|
|
124
|
+
readonly jitter: "equal";
|
|
125
|
+
};
|
|
126
|
+
readonly balanced: {
|
|
127
|
+
readonly retries: 3;
|
|
128
|
+
readonly strategy: "exponential";
|
|
129
|
+
readonly baseDelayMs: 150;
|
|
130
|
+
readonly maxDelayMs: 15000;
|
|
131
|
+
readonly jitter: "full";
|
|
132
|
+
};
|
|
133
|
+
readonly aggressive: {
|
|
134
|
+
readonly retries: 5;
|
|
135
|
+
readonly strategy: "exponential";
|
|
136
|
+
readonly baseDelayMs: 100;
|
|
137
|
+
readonly maxDelayMs: 30000;
|
|
138
|
+
readonly jitter: "decorrelated";
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
type NamedRetryStrategy = keyof typeof retryStrategies;
|
|
142
|
+
declare function resolveRetryStrategy(strategy: NamedRetryStrategy | AiResilienceRetryConfig): AiResilienceRetryConfig;
|
|
143
|
+
|
|
144
|
+
export { type AiResilienceClient, type AiResilienceRetryConfig, ConsoleRetryLogger, type JitterStrategy, type LogLevel, type RetryAttemptContext, type RetryGiveUpContext, RetryHookEmitter, type RetryHooks, type RetryLogger, type RetryStrategy, type RetrySuccessContext, type StructuredLogEntry, applyAiResilience, applyJitter, calculateRetryDelay, createAiResilienceClient, createLogEntry, defaultRetryConfig, isRetryableMethod, isRetryableStatus, normalizeRetryConfig, resolveRetryStrategy, retryStrategies, shouldRetry };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { AxiosInstance, AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
|
|
2
|
+
import EventEmitter from 'eventemitter3';
|
|
3
|
+
import { IAxiosRetryConfig } from 'axios-retry';
|
|
4
|
+
|
|
5
|
+
type RetryHookEvents<T = unknown> = {
|
|
6
|
+
retry: (context: RetryAttemptContext<T>) => void;
|
|
7
|
+
success: (context: RetrySuccessContext<T>) => void;
|
|
8
|
+
giveUp: (context: RetryGiveUpContext<T>) => void;
|
|
9
|
+
};
|
|
10
|
+
declare class RetryHookEmitter<T = unknown> extends EventEmitter<RetryHookEvents<T>> {
|
|
11
|
+
private readonly hooks;
|
|
12
|
+
constructor(hooks?: RetryHooks<T>);
|
|
13
|
+
emitRetry(context: RetryAttemptContext<T>): Promise<void>;
|
|
14
|
+
emitSuccess(context: RetrySuccessContext<T>): Promise<void>;
|
|
15
|
+
emitGiveUp(context: RetryGiveUpContext<T>): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type RetryStrategy = "fixed" | "linear" | "exponential";
|
|
19
|
+
type JitterStrategy = "none" | "full" | "equal" | "decorrelated";
|
|
20
|
+
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
21
|
+
interface RetryAttemptContext<T = unknown> {
|
|
22
|
+
attempt: number;
|
|
23
|
+
retries: number;
|
|
24
|
+
error: AxiosError<T>;
|
|
25
|
+
requestConfig: AxiosRequestConfig;
|
|
26
|
+
nextDelayMs: number;
|
|
27
|
+
}
|
|
28
|
+
interface RetrySuccessContext<T = unknown> {
|
|
29
|
+
attempt: number;
|
|
30
|
+
response: AxiosResponse<T>;
|
|
31
|
+
requestConfig: AxiosRequestConfig;
|
|
32
|
+
}
|
|
33
|
+
interface RetryGiveUpContext<T = unknown> {
|
|
34
|
+
attempts: number;
|
|
35
|
+
error: AxiosError<T>;
|
|
36
|
+
requestConfig: AxiosRequestConfig;
|
|
37
|
+
}
|
|
38
|
+
interface RetryHooks<T = unknown> {
|
|
39
|
+
onRetry?: (context: RetryAttemptContext<T>) => void | Promise<void>;
|
|
40
|
+
onSuccess?: (context: RetrySuccessContext<T>) => void | Promise<void>;
|
|
41
|
+
onGiveUp?: (context: RetryGiveUpContext<T>) => void | Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
interface StructuredLogEntry {
|
|
44
|
+
level: LogLevel;
|
|
45
|
+
event: string;
|
|
46
|
+
message: string;
|
|
47
|
+
timestamp: string;
|
|
48
|
+
metadata?: Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
interface RetryLogger {
|
|
51
|
+
log(entry: StructuredLogEntry): void;
|
|
52
|
+
}
|
|
53
|
+
interface AiResilienceRetryConfig<T = unknown> extends Omit<IAxiosRetryConfig, "retries" | "retryDelay" | "retryCondition" | "onRetry"> {
|
|
54
|
+
retries?: number;
|
|
55
|
+
strategy?: RetryStrategy;
|
|
56
|
+
baseDelayMs?: number;
|
|
57
|
+
maxDelayMs?: number;
|
|
58
|
+
jitter?: JitterStrategy;
|
|
59
|
+
retryStatusCodes?: number[];
|
|
60
|
+
retryMethods?: string[];
|
|
61
|
+
retryCondition?: (error: AxiosError<T>) => boolean | Promise<boolean>;
|
|
62
|
+
hooks?: RetryHooks<T>;
|
|
63
|
+
logger?: RetryLogger;
|
|
64
|
+
metadata?: Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
interface NormalizedAiResilienceRetryConfig<T = unknown> extends AiResilienceRetryConfig<T> {
|
|
67
|
+
retries: number;
|
|
68
|
+
strategy: RetryStrategy;
|
|
69
|
+
baseDelayMs: number;
|
|
70
|
+
maxDelayMs: number;
|
|
71
|
+
jitter: JitterStrategy;
|
|
72
|
+
}
|
|
73
|
+
interface AiResilienceClient<T = unknown> {
|
|
74
|
+
axios: AxiosInstance;
|
|
75
|
+
hooks: RetryHookEmitter<T>;
|
|
76
|
+
config: NormalizedAiResilienceRetryConfig<T>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
declare function applyAiResilience<T = unknown>(instance: AxiosInstance, config?: AiResilienceRetryConfig<T>): AiResilienceClient<T>;
|
|
80
|
+
declare function createAiResilienceClient<T = unknown>(config?: AiResilienceRetryConfig<T>): AiResilienceClient<T>;
|
|
81
|
+
|
|
82
|
+
interface DelayContext {
|
|
83
|
+
attempt: number;
|
|
84
|
+
previousDelayMs?: number;
|
|
85
|
+
random?: () => number;
|
|
86
|
+
}
|
|
87
|
+
declare function calculateRetryDelay<T = unknown>(config: AiResilienceRetryConfig<T>, context: DelayContext): number;
|
|
88
|
+
|
|
89
|
+
interface JitterOptions {
|
|
90
|
+
strategy?: JitterStrategy;
|
|
91
|
+
previousDelayMs?: number;
|
|
92
|
+
baseDelayMs?: number;
|
|
93
|
+
maxDelayMs?: number;
|
|
94
|
+
random?: () => number;
|
|
95
|
+
}
|
|
96
|
+
declare function applyJitter(delayMs: number, options?: JitterOptions): number;
|
|
97
|
+
|
|
98
|
+
declare function isRetryableMethod(error: AxiosError, retryMethods?: string[]): boolean;
|
|
99
|
+
declare function isRetryableStatus(error: AxiosError, retryStatusCodes?: number[]): boolean;
|
|
100
|
+
declare function shouldRetry<T = unknown>(error: AxiosError<T>, config?: AiResilienceRetryConfig<T>): Promise<boolean>;
|
|
101
|
+
|
|
102
|
+
declare class ConsoleRetryLogger implements RetryLogger {
|
|
103
|
+
private readonly minLevel;
|
|
104
|
+
constructor(minLevel?: LogLevel);
|
|
105
|
+
log(entry: StructuredLogEntry): void;
|
|
106
|
+
}
|
|
107
|
+
declare function createLogEntry(level: LogLevel, event: string, message: string, metadata?: Record<string, unknown>): StructuredLogEntry;
|
|
108
|
+
|
|
109
|
+
declare const defaultRetryConfig: {
|
|
110
|
+
readonly retries: 3;
|
|
111
|
+
readonly strategy: "exponential";
|
|
112
|
+
readonly baseDelayMs: 150;
|
|
113
|
+
readonly maxDelayMs: 30000;
|
|
114
|
+
readonly jitter: "full";
|
|
115
|
+
};
|
|
116
|
+
declare function normalizeRetryConfig<T = unknown>(config?: AiResilienceRetryConfig<T>): NormalizedAiResilienceRetryConfig<T>;
|
|
117
|
+
|
|
118
|
+
declare const retryStrategies: {
|
|
119
|
+
readonly conservative: {
|
|
120
|
+
readonly retries: 2;
|
|
121
|
+
readonly strategy: "exponential";
|
|
122
|
+
readonly baseDelayMs: 250;
|
|
123
|
+
readonly maxDelayMs: 5000;
|
|
124
|
+
readonly jitter: "equal";
|
|
125
|
+
};
|
|
126
|
+
readonly balanced: {
|
|
127
|
+
readonly retries: 3;
|
|
128
|
+
readonly strategy: "exponential";
|
|
129
|
+
readonly baseDelayMs: 150;
|
|
130
|
+
readonly maxDelayMs: 15000;
|
|
131
|
+
readonly jitter: "full";
|
|
132
|
+
};
|
|
133
|
+
readonly aggressive: {
|
|
134
|
+
readonly retries: 5;
|
|
135
|
+
readonly strategy: "exponential";
|
|
136
|
+
readonly baseDelayMs: 100;
|
|
137
|
+
readonly maxDelayMs: 30000;
|
|
138
|
+
readonly jitter: "decorrelated";
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
type NamedRetryStrategy = keyof typeof retryStrategies;
|
|
142
|
+
declare function resolveRetryStrategy(strategy: NamedRetryStrategy | AiResilienceRetryConfig): AiResilienceRetryConfig;
|
|
143
|
+
|
|
144
|
+
export { type AiResilienceClient, type AiResilienceRetryConfig, ConsoleRetryLogger, type JitterStrategy, type LogLevel, type RetryAttemptContext, type RetryGiveUpContext, RetryHookEmitter, type RetryHooks, type RetryLogger, type RetryStrategy, type RetrySuccessContext, type StructuredLogEntry, applyAiResilience, applyJitter, calculateRetryDelay, createAiResilienceClient, createLogEntry, defaultRetryConfig, isRetryableMethod, isRetryableStatus, normalizeRetryConfig, resolveRetryStrategy, retryStrategies, shouldRetry };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// src/engine.ts
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import axiosRetry2 from "axios-retry";
|
|
4
|
+
|
|
5
|
+
// src/config.ts
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
var configSchema = z.object({
|
|
8
|
+
retries: z.number().int().min(0).optional(),
|
|
9
|
+
strategy: z.enum(["fixed", "linear", "exponential"]).optional(),
|
|
10
|
+
baseDelayMs: z.number().min(0).optional(),
|
|
11
|
+
maxDelayMs: z.number().min(0).optional(),
|
|
12
|
+
jitter: z.enum(["none", "full", "equal", "decorrelated"]).optional(),
|
|
13
|
+
retryStatusCodes: z.array(z.number().int().min(100).max(599)).optional(),
|
|
14
|
+
retryMethods: z.array(z.string().min(1)).optional(),
|
|
15
|
+
metadata: z.record(z.unknown()).optional()
|
|
16
|
+
});
|
|
17
|
+
var defaultRetryConfig = {
|
|
18
|
+
retries: 3,
|
|
19
|
+
strategy: "exponential",
|
|
20
|
+
baseDelayMs: 150,
|
|
21
|
+
maxDelayMs: 3e4,
|
|
22
|
+
jitter: "full"
|
|
23
|
+
};
|
|
24
|
+
function normalizeRetryConfig(config = {}) {
|
|
25
|
+
configSchema.parse(config);
|
|
26
|
+
return {
|
|
27
|
+
...defaultRetryConfig,
|
|
28
|
+
...config
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/conditions.ts
|
|
33
|
+
import axiosRetry from "axios-retry";
|
|
34
|
+
var DEFAULT_RETRY_METHODS = ["delete", "get", "head", "options", "put"];
|
|
35
|
+
var DEFAULT_RETRY_STATUS_CODES = [408, 409, 425, 429, 500, 502, 503, 504];
|
|
36
|
+
function isRetryableMethod(error, retryMethods = DEFAULT_RETRY_METHODS) {
|
|
37
|
+
const method = error.config?.method?.toLowerCase();
|
|
38
|
+
return method ? retryMethods.map((item) => item.toLowerCase()).includes(method) : true;
|
|
39
|
+
}
|
|
40
|
+
function isRetryableStatus(error, retryStatusCodes = DEFAULT_RETRY_STATUS_CODES) {
|
|
41
|
+
const status = error.response?.status;
|
|
42
|
+
return typeof status === "number" && retryStatusCodes.includes(status);
|
|
43
|
+
}
|
|
44
|
+
async function shouldRetry(error, config = {}) {
|
|
45
|
+
if (config.retryCondition) {
|
|
46
|
+
return config.retryCondition(error);
|
|
47
|
+
}
|
|
48
|
+
if (!isRetryableMethod(error, config.retryMethods)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return axiosRetry.isNetworkOrIdempotentRequestError(error) || isRetryableStatus(error, config.retryStatusCodes);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/jitter.ts
|
|
55
|
+
var clamp = (value, max) => Math.min(Math.max(0, value), max);
|
|
56
|
+
function applyJitter(delayMs, options = {}) {
|
|
57
|
+
const strategy = options.strategy ?? "none";
|
|
58
|
+
const maxDelayMs = options.maxDelayMs ?? Number.POSITIVE_INFINITY;
|
|
59
|
+
const random = options.random ?? Math.random;
|
|
60
|
+
if (strategy === "none") {
|
|
61
|
+
return clamp(delayMs, maxDelayMs);
|
|
62
|
+
}
|
|
63
|
+
if (strategy === "full") {
|
|
64
|
+
return clamp(random() * delayMs, maxDelayMs);
|
|
65
|
+
}
|
|
66
|
+
if (strategy === "equal") {
|
|
67
|
+
const half = delayMs / 2;
|
|
68
|
+
return clamp(half + random() * half, maxDelayMs);
|
|
69
|
+
}
|
|
70
|
+
const base = options.baseDelayMs ?? delayMs;
|
|
71
|
+
const previous = options.previousDelayMs ?? base;
|
|
72
|
+
const next = base + random() * Math.max(base, previous * 3 - base);
|
|
73
|
+
return clamp(next, maxDelayMs);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/delay.ts
|
|
77
|
+
function calculateRetryDelay(config, context) {
|
|
78
|
+
const baseDelayMs = config.baseDelayMs ?? 100;
|
|
79
|
+
const maxDelayMs = config.maxDelayMs ?? 3e4;
|
|
80
|
+
const attempt = Math.max(1, context.attempt);
|
|
81
|
+
let delayMs;
|
|
82
|
+
switch (config.strategy ?? "exponential") {
|
|
83
|
+
case "fixed":
|
|
84
|
+
delayMs = baseDelayMs;
|
|
85
|
+
break;
|
|
86
|
+
case "linear":
|
|
87
|
+
delayMs = baseDelayMs * attempt;
|
|
88
|
+
break;
|
|
89
|
+
case "exponential":
|
|
90
|
+
default:
|
|
91
|
+
delayMs = baseDelayMs * 2 ** (attempt - 1);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
return Math.round(
|
|
95
|
+
applyJitter(Math.min(delayMs, maxDelayMs), {
|
|
96
|
+
strategy: config.jitter ?? "full",
|
|
97
|
+
previousDelayMs: context.previousDelayMs,
|
|
98
|
+
baseDelayMs,
|
|
99
|
+
maxDelayMs,
|
|
100
|
+
random: context.random
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/hooks.ts
|
|
106
|
+
import EventEmitter from "eventemitter3";
|
|
107
|
+
var RetryHookEmitter = class extends EventEmitter {
|
|
108
|
+
constructor(hooks = {}) {
|
|
109
|
+
super();
|
|
110
|
+
this.hooks = hooks;
|
|
111
|
+
}
|
|
112
|
+
hooks;
|
|
113
|
+
async emitRetry(context) {
|
|
114
|
+
this.emit("retry", context);
|
|
115
|
+
await this.hooks.onRetry?.(context);
|
|
116
|
+
}
|
|
117
|
+
async emitSuccess(context) {
|
|
118
|
+
this.emit("success", context);
|
|
119
|
+
await this.hooks.onSuccess?.(context);
|
|
120
|
+
}
|
|
121
|
+
async emitGiveUp(context) {
|
|
122
|
+
this.emit("giveUp", context);
|
|
123
|
+
await this.hooks.onGiveUp?.(context);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// src/logger.ts
|
|
128
|
+
var ConsoleRetryLogger = class {
|
|
129
|
+
constructor(minLevel = "info") {
|
|
130
|
+
this.minLevel = minLevel;
|
|
131
|
+
}
|
|
132
|
+
minLevel;
|
|
133
|
+
log(entry) {
|
|
134
|
+
if (levelWeight(entry.level) < levelWeight(this.minLevel)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const payload = {
|
|
138
|
+
event: entry.event,
|
|
139
|
+
timestamp: entry.timestamp,
|
|
140
|
+
...entry.metadata
|
|
141
|
+
};
|
|
142
|
+
console[entry.level === "debug" ? "debug" : entry.level](entry.message, payload);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
function createLogEntry(level, event, message, metadata) {
|
|
146
|
+
return {
|
|
147
|
+
level,
|
|
148
|
+
event,
|
|
149
|
+
message,
|
|
150
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
151
|
+
metadata
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function levelWeight(level) {
|
|
155
|
+
return { debug: 10, info: 20, warn: 30, error: 40 }[level];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/engine.ts
|
|
159
|
+
function applyAiResilience(instance, config = {}) {
|
|
160
|
+
const resolved = normalizeRetryConfig(config);
|
|
161
|
+
const hooks = new RetryHookEmitter(resolved.hooks);
|
|
162
|
+
let previousDelayMs;
|
|
163
|
+
axiosRetry2(instance, {
|
|
164
|
+
...resolved,
|
|
165
|
+
retries: resolved.retries,
|
|
166
|
+
retryCondition: (error) => shouldRetry(error, resolved),
|
|
167
|
+
retryDelay: (retryCount, error) => {
|
|
168
|
+
const nextDelayMs = calculateRetryDelay(resolved, {
|
|
169
|
+
attempt: retryCount,
|
|
170
|
+
previousDelayMs
|
|
171
|
+
});
|
|
172
|
+
previousDelayMs = nextDelayMs;
|
|
173
|
+
const requestConfig = error.config ?? {};
|
|
174
|
+
void hooks.emitRetry({
|
|
175
|
+
attempt: retryCount,
|
|
176
|
+
retries: resolved.retries,
|
|
177
|
+
error,
|
|
178
|
+
requestConfig,
|
|
179
|
+
nextDelayMs
|
|
180
|
+
});
|
|
181
|
+
resolved.logger?.log(
|
|
182
|
+
createLogEntry("warn", "retry.scheduled", "Retry scheduled", {
|
|
183
|
+
attempt: retryCount,
|
|
184
|
+
retries: resolved.retries,
|
|
185
|
+
delayMs: nextDelayMs,
|
|
186
|
+
method: requestConfig.method,
|
|
187
|
+
url: requestConfig.url,
|
|
188
|
+
...resolved.metadata
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
return nextDelayMs;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
instance.interceptors.response.use(
|
|
195
|
+
async (response) => {
|
|
196
|
+
await hooks.emitSuccess({
|
|
197
|
+
attempt: Number(response.config?.["axios-retry"]?.retryCount ?? 0),
|
|
198
|
+
response,
|
|
199
|
+
requestConfig: response.config
|
|
200
|
+
});
|
|
201
|
+
return response;
|
|
202
|
+
},
|
|
203
|
+
async (error) => {
|
|
204
|
+
const retryState = error.config?.["axios-retry"];
|
|
205
|
+
const attempts = Number(retryState?.retryCount ?? 0);
|
|
206
|
+
if (attempts >= resolved.retries) {
|
|
207
|
+
await hooks.emitGiveUp({
|
|
208
|
+
attempts,
|
|
209
|
+
error,
|
|
210
|
+
requestConfig: error.config ?? {}
|
|
211
|
+
});
|
|
212
|
+
resolved.logger?.log(
|
|
213
|
+
createLogEntry("error", "retry.give_up", "Retry attempts exhausted", {
|
|
214
|
+
attempts,
|
|
215
|
+
method: error.config?.method,
|
|
216
|
+
url: error.config?.url,
|
|
217
|
+
...resolved.metadata
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
return {
|
|
225
|
+
axios: instance,
|
|
226
|
+
hooks,
|
|
227
|
+
config: resolved
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function createAiResilienceClient(config = {}) {
|
|
231
|
+
return applyAiResilience(axios.create(), config);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/strategies.ts
|
|
235
|
+
var retryStrategies = {
|
|
236
|
+
conservative: {
|
|
237
|
+
retries: 2,
|
|
238
|
+
strategy: "exponential",
|
|
239
|
+
baseDelayMs: 250,
|
|
240
|
+
maxDelayMs: 5e3,
|
|
241
|
+
jitter: "equal"
|
|
242
|
+
},
|
|
243
|
+
balanced: {
|
|
244
|
+
retries: 3,
|
|
245
|
+
strategy: "exponential",
|
|
246
|
+
baseDelayMs: 150,
|
|
247
|
+
maxDelayMs: 15e3,
|
|
248
|
+
jitter: "full"
|
|
249
|
+
},
|
|
250
|
+
aggressive: {
|
|
251
|
+
retries: 5,
|
|
252
|
+
strategy: "exponential",
|
|
253
|
+
baseDelayMs: 100,
|
|
254
|
+
maxDelayMs: 3e4,
|
|
255
|
+
jitter: "decorrelated"
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
function resolveRetryStrategy(strategy) {
|
|
259
|
+
return typeof strategy === "string" ? retryStrategies[strategy] : strategy;
|
|
260
|
+
}
|
|
261
|
+
export {
|
|
262
|
+
ConsoleRetryLogger,
|
|
263
|
+
RetryHookEmitter,
|
|
264
|
+
applyAiResilience,
|
|
265
|
+
applyJitter,
|
|
266
|
+
calculateRetryDelay,
|
|
267
|
+
createAiResilienceClient,
|
|
268
|
+
createLogEntry,
|
|
269
|
+
defaultRetryConfig,
|
|
270
|
+
isRetryableMethod,
|
|
271
|
+
isRetryableStatus,
|
|
272
|
+
normalizeRetryConfig,
|
|
273
|
+
resolveRetryStrategy,
|
|
274
|
+
retryStrategies,
|
|
275
|
+
shouldRetry
|
|
276
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-resilience",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Axios retry++ primitives for resilient AI and backend systems.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest",
|
|
17
|
+
"lint": "eslint . --ext .ts",
|
|
18
|
+
"format": "prettier --write ."
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"axios",
|
|
22
|
+
"retry",
|
|
23
|
+
"axios-retry",
|
|
24
|
+
"resilience",
|
|
25
|
+
"ai"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"axios": "^1.7.9",
|
|
30
|
+
"axios-retry": "^4.5.0",
|
|
31
|
+
"eventemitter3": "^5.0.1",
|
|
32
|
+
"zod": "^3.24.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.10.2",
|
|
36
|
+
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
|
37
|
+
"@typescript-eslint/parser": "^8.18.2",
|
|
38
|
+
"eslint": "^9.17.0",
|
|
39
|
+
"prettier": "^3.4.2",
|
|
40
|
+
"tsup": "^8.3.5",
|
|
41
|
+
"typescript": "^5.7.2",
|
|
42
|
+
"vitest": "^2.1.8"
|
|
43
|
+
}
|
|
44
|
+
}
|