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 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
+ });
@@ -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 };
@@ -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
+ }