airbag 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/dist/index.mjs ADDED
@@ -0,0 +1,353 @@
1
+ // src/errors.ts
2
+ var AirbagError = class extends Error {
3
+ constructor(message, code, context, options) {
4
+ super(message, options);
5
+ this.name = "AirbagError";
6
+ this.code = code;
7
+ this.context = context;
8
+ Object.setPrototypeOf(this, new.target.prototype);
9
+ }
10
+ };
11
+ var TimeoutError = class extends AirbagError {
12
+ constructor(context, timeout) {
13
+ super(
14
+ `"${context.functionName}" timed out after ${timeout}ms`,
15
+ "TIMEOUT",
16
+ context
17
+ );
18
+ this.name = "TimeoutError";
19
+ this.timeout = timeout;
20
+ }
21
+ };
22
+ var RetryExhaustedError = class extends AirbagError {
23
+ constructor(context, cause) {
24
+ super(
25
+ `"${context.functionName}" failed after ${context.maxAttempts} attempt(s): ${cause.message}`,
26
+ "RETRY_EXHAUSTED",
27
+ context,
28
+ { cause }
29
+ );
30
+ this.name = "RetryExhaustedError";
31
+ }
32
+ };
33
+ var CircuitOpenError = class extends AirbagError {
34
+ constructor(context) {
35
+ super(
36
+ `Circuit breaker is open for "${context.functionName}"`,
37
+ "CIRCUIT_OPEN",
38
+ context
39
+ );
40
+ this.name = "CircuitOpenError";
41
+ }
42
+ };
43
+ var AbortError = class extends AirbagError {
44
+ constructor(context) {
45
+ super(
46
+ `Execution of "${context.functionName}" was aborted`,
47
+ "ABORTED",
48
+ context
49
+ );
50
+ this.name = "AbortError";
51
+ }
52
+ };
53
+
54
+ // src/config.ts
55
+ var DEFAULTS = {
56
+ name: "anonymous",
57
+ adapter: {},
58
+ retry: {
59
+ count: 0,
60
+ backoff: "exponential",
61
+ baseDelay: 1e3,
62
+ maxDelay: 3e4,
63
+ jitter: true
64
+ },
65
+ timeout: 3e4,
66
+ circuitBreaker: {
67
+ enabled: false,
68
+ threshold: 5,
69
+ resetTimeout: 6e4
70
+ }
71
+ };
72
+ function resolveConfig(...layers) {
73
+ const resolved = {
74
+ name: DEFAULTS.name,
75
+ adapter: { ...DEFAULTS.adapter },
76
+ retry: { ...DEFAULTS.retry },
77
+ timeout: DEFAULTS.timeout,
78
+ circuitBreaker: { ...DEFAULTS.circuitBreaker }
79
+ };
80
+ for (const layer of layers) {
81
+ if (!layer) continue;
82
+ if (layer.name !== void 0) resolved.name = layer.name;
83
+ if (layer.timeout !== void 0) resolved.timeout = layer.timeout;
84
+ if (layer.signal !== void 0) resolved.signal = layer.signal;
85
+ const flatAdapter = {};
86
+ if (layer.onLoading) flatAdapter.onLoading = layer.onLoading;
87
+ if (layer.onSuccess) flatAdapter.onSuccess = layer.onSuccess;
88
+ if (layer.onError) flatAdapter.onError = layer.onError;
89
+ if (layer.onRetry) flatAdapter.onRetry = layer.onRetry;
90
+ if (layer.onFinish) flatAdapter.onFinish = layer.onFinish;
91
+ resolved.adapter = { ...resolved.adapter, ...flatAdapter, ...layer.adapter ?? {} };
92
+ if (layer.retries !== void 0) {
93
+ resolved.retry = { ...resolved.retry, count: layer.retries };
94
+ }
95
+ if (layer.retry) {
96
+ resolved.retry = { ...resolved.retry, ...layer.retry };
97
+ }
98
+ if (layer.circuitBreaker) {
99
+ resolved.circuitBreaker = { ...resolved.circuitBreaker, ...layer.circuitBreaker };
100
+ }
101
+ }
102
+ return resolved;
103
+ }
104
+
105
+ // src/circuit-breaker.ts
106
+ function createCircuitBreaker(config) {
107
+ let state = "closed";
108
+ let failureCount = 0;
109
+ let lastFailureTime = 0;
110
+ return {
111
+ canExecute() {
112
+ if (!config.enabled) return true;
113
+ if (state === "closed") return true;
114
+ if (state === "open") {
115
+ const elapsed = Date.now() - lastFailureTime;
116
+ if (elapsed >= config.resetTimeout) {
117
+ state = "half-open";
118
+ return true;
119
+ }
120
+ return false;
121
+ }
122
+ return true;
123
+ },
124
+ recordSuccess() {
125
+ if (!config.enabled) return;
126
+ if (state === "half-open") {
127
+ state = "closed";
128
+ failureCount = 0;
129
+ }
130
+ },
131
+ recordFailure() {
132
+ if (!config.enabled) return;
133
+ failureCount++;
134
+ lastFailureTime = Date.now();
135
+ if (state === "half-open" || failureCount >= config.threshold) {
136
+ state = "open";
137
+ }
138
+ },
139
+ getState() {
140
+ return state;
141
+ },
142
+ reset() {
143
+ state = "closed";
144
+ failureCount = 0;
145
+ lastFailureTime = 0;
146
+ }
147
+ };
148
+ }
149
+
150
+ // src/utils.ts
151
+ var InternalTimeoutError = class _InternalTimeoutError extends Error {
152
+ constructor(ms) {
153
+ super(`Timed out after ${ms}ms`);
154
+ this.name = "InternalTimeoutError";
155
+ this.ms = ms;
156
+ Object.setPrototypeOf(this, _InternalTimeoutError.prototype);
157
+ }
158
+ };
159
+ var InternalAbortError = class _InternalAbortError extends Error {
160
+ constructor() {
161
+ super("Operation aborted");
162
+ this.name = "InternalAbortError";
163
+ Object.setPrototypeOf(this, _InternalAbortError.prototype);
164
+ }
165
+ };
166
+ function toError(value) {
167
+ if (value instanceof Error) return value;
168
+ if (typeof value === "string") return new Error(value);
169
+ return new Error(String(value));
170
+ }
171
+ function buildContext(functionName, startTime, attempt, maxAttempts) {
172
+ return {
173
+ functionName,
174
+ duration: Date.now() - startTime,
175
+ timestamp: Date.now(),
176
+ attempt,
177
+ maxAttempts
178
+ };
179
+ }
180
+ function calculateDelay(attemptIndex, config) {
181
+ let delay;
182
+ switch (config.backoff) {
183
+ case "exponential":
184
+ delay = config.baseDelay * Math.pow(2, attemptIndex);
185
+ break;
186
+ case "linear":
187
+ delay = config.baseDelay * (attemptIndex + 1);
188
+ break;
189
+ case "fixed":
190
+ delay = config.baseDelay;
191
+ break;
192
+ }
193
+ if (config.jitter) {
194
+ delay = delay * (0.5 + Math.random() * 0.5);
195
+ }
196
+ return Math.min(delay, config.maxDelay);
197
+ }
198
+ function wait(ms, signal) {
199
+ return new Promise((resolve, reject) => {
200
+ if (signal?.aborted) {
201
+ reject(new InternalAbortError());
202
+ return;
203
+ }
204
+ const timer = setTimeout(resolve, ms);
205
+ const onAbort = () => {
206
+ clearTimeout(timer);
207
+ reject(new InternalAbortError());
208
+ };
209
+ signal?.addEventListener("abort", onAbort, { once: true });
210
+ });
211
+ }
212
+ function withTimeout(promise, ms, signal) {
213
+ if (ms <= 0 || !Number.isFinite(ms)) return promise;
214
+ return new Promise((resolve, reject) => {
215
+ let settled = false;
216
+ if (signal?.aborted) {
217
+ reject(new InternalAbortError());
218
+ return;
219
+ }
220
+ const timer = setTimeout(() => {
221
+ if (!settled) {
222
+ settled = true;
223
+ reject(new InternalTimeoutError(ms));
224
+ }
225
+ }, ms);
226
+ const onAbort = () => {
227
+ if (!settled) {
228
+ settled = true;
229
+ clearTimeout(timer);
230
+ reject(new InternalAbortError());
231
+ }
232
+ };
233
+ signal?.addEventListener("abort", onAbort, { once: true });
234
+ promise.then(
235
+ (value) => {
236
+ if (!settled) {
237
+ settled = true;
238
+ clearTimeout(timer);
239
+ signal?.removeEventListener("abort", onAbort);
240
+ resolve(value);
241
+ }
242
+ },
243
+ (error) => {
244
+ if (!settled) {
245
+ settled = true;
246
+ clearTimeout(timer);
247
+ signal?.removeEventListener("abort", onAbort);
248
+ reject(error);
249
+ }
250
+ }
251
+ );
252
+ });
253
+ }
254
+ function safeInvoke(fn, args) {
255
+ return new Promise((resolve, reject) => {
256
+ Promise.resolve().then(() => fn(...args)).then(resolve, reject);
257
+ });
258
+ }
259
+ function settle(promise) {
260
+ return promise.then((value) => ({ ok: true, value })).catch((error) => ({ ok: false, error: toError(error) }));
261
+ }
262
+
263
+ // src/airbag.ts
264
+ function emitSuccess(config, ctx, value) {
265
+ config.adapter.onSuccess?.(value, ctx);
266
+ config.adapter.onLoading?.(false, ctx);
267
+ config.adapter.onFinish?.(ctx);
268
+ return value;
269
+ }
270
+ function emitFailure(config, ctx, error) {
271
+ config.adapter.onError?.(error, ctx);
272
+ config.adapter.onLoading?.(false, ctx);
273
+ config.adapter.onFinish?.(ctx);
274
+ throw error;
275
+ }
276
+ async function execute(fn, args, config, breaker) {
277
+ const startTime = Date.now();
278
+ const maxAttempts = Math.max(1, config.retry.count + 1);
279
+ const getContext = (attempt) => buildContext(config.name, startTime, attempt, maxAttempts);
280
+ if (!breaker.canExecute()) {
281
+ const ctx2 = getContext(0);
282
+ throw new CircuitOpenError(ctx2);
283
+ }
284
+ config.adapter.onLoading?.(true, getContext(0));
285
+ let lastError = new Error("No attempts executed");
286
+ let lastAttempt = 0;
287
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
288
+ lastAttempt = attempt;
289
+ if (attempt > 0) {
290
+ config.adapter.onRetry?.(attempt, lastError, getContext(attempt));
291
+ const delayed = await settle(
292
+ wait(calculateDelay(attempt - 1, config.retry), config.signal)
293
+ );
294
+ if (!delayed.ok) {
295
+ const ctx2 = getContext(attempt);
296
+ return emitFailure(config, ctx2, new AbortError(ctx2));
297
+ }
298
+ }
299
+ const result = await settle(
300
+ withTimeout(safeInvoke(fn, args), config.timeout, config.signal)
301
+ );
302
+ if (result.ok) {
303
+ breaker.recordSuccess();
304
+ return emitSuccess(config, getContext(attempt), result.value);
305
+ }
306
+ if (result.error instanceof InternalAbortError) {
307
+ const ctx2 = getContext(attempt);
308
+ return emitFailure(config, ctx2, new AbortError(ctx2));
309
+ }
310
+ lastError = result.error;
311
+ }
312
+ breaker.recordFailure();
313
+ const ctx = getContext(lastAttempt);
314
+ const finalError = lastError instanceof InternalTimeoutError ? new TimeoutError(ctx, config.timeout) : new RetryExhaustedError(ctx, lastError);
315
+ return emitFailure(config, ctx, finalError);
316
+ }
317
+ function createWrapped(fn, optionLayers, sharedBreaker) {
318
+ const config = resolveConfig(...optionLayers);
319
+ const breaker = sharedBreaker ?? createCircuitBreaker(config.circuitBreaker);
320
+ const wrapped = ((...args) => execute(fn, args, config, breaker));
321
+ wrapped.with = (overrides) => createWrapped(fn, [...optionLayers, overrides], breaker);
322
+ wrapped.reset = () => breaker.reset();
323
+ return wrapped;
324
+ }
325
+ function airbag(fn, options) {
326
+ return createWrapped(fn, [options]);
327
+ }
328
+
329
+ // src/instance.ts
330
+ function createAirbagInstance(globalOptions) {
331
+ let globals = { ...globalOptions };
332
+ return {
333
+ wrap(fn, options) {
334
+ return createWrapped(fn, [globals, options]);
335
+ },
336
+ configure(options) {
337
+ globals = { ...globals, ...options };
338
+ },
339
+ getDefaults() {
340
+ return resolveConfig(globals);
341
+ }
342
+ };
343
+ }
344
+ export {
345
+ AbortError,
346
+ AirbagError,
347
+ CircuitOpenError,
348
+ RetryExhaustedError,
349
+ TimeoutError,
350
+ airbag,
351
+ createAirbagInstance
352
+ };
353
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/config.ts","../src/circuit-breaker.ts","../src/utils.ts","../src/airbag.ts","../src/instance.ts"],"sourcesContent":["import type { AirbagErrorCode, ExecutionContext } from './interfaces';\r\n\r\nexport class AirbagError extends Error {\r\n readonly code: AirbagErrorCode;\r\n readonly context: ExecutionContext;\r\n\r\n constructor(\r\n message: string,\r\n code: AirbagErrorCode,\r\n context: ExecutionContext,\r\n options?: { cause?: Error }\r\n ) {\r\n super(message, options);\r\n this.name = 'AirbagError';\r\n this.code = code;\r\n this.context = context;\r\n Object.setPrototypeOf(this, new.target.prototype);\r\n }\r\n}\r\n\r\nexport class TimeoutError extends AirbagError {\r\n readonly timeout: number;\r\n\r\n constructor(context: ExecutionContext, timeout: number) {\r\n super(\r\n `\"${context.functionName}\" timed out after ${timeout}ms`,\r\n 'TIMEOUT',\r\n context\r\n );\r\n this.name = 'TimeoutError';\r\n this.timeout = timeout;\r\n }\r\n}\r\n\r\nexport class RetryExhaustedError extends AirbagError {\r\n constructor(context: ExecutionContext, cause: Error) {\r\n super(\r\n `\"${context.functionName}\" failed after ${context.maxAttempts} attempt(s): ${cause.message}`,\r\n 'RETRY_EXHAUSTED',\r\n context,\r\n { cause }\r\n );\r\n this.name = 'RetryExhaustedError';\r\n }\r\n}\r\n\r\nexport class CircuitOpenError extends AirbagError {\r\n constructor(context: ExecutionContext) {\r\n super(\r\n `Circuit breaker is open for \"${context.functionName}\"`,\r\n 'CIRCUIT_OPEN',\r\n context\r\n );\r\n this.name = 'CircuitOpenError';\r\n }\r\n}\r\n\r\nexport class AbortError extends AirbagError {\r\n constructor(context: ExecutionContext) {\r\n super(\r\n `Execution of \"${context.functionName}\" was aborted`,\r\n 'ABORTED',\r\n context\r\n );\r\n this.name = 'AbortError';\r\n }\r\n}\r\n","import type { AirbagAdapter, AirbagOptions, AirbagResolvedConfig } from './interfaces';\n\nconst DEFAULTS: AirbagResolvedConfig = {\n name: 'anonymous',\n adapter: {},\n retry: {\n count: 0,\n backoff: 'exponential',\n baseDelay: 1000,\n maxDelay: 30_000,\n jitter: true,\n },\n timeout: 30_000,\n circuitBreaker: {\n enabled: false,\n threshold: 5,\n resetTimeout: 60_000,\n },\n};\n\nexport function resolveConfig<TReturn>(\n ...layers: Array<AirbagOptions<TReturn> | undefined>\n): AirbagResolvedConfig<TReturn> {\n const resolved: AirbagResolvedConfig<TReturn> = {\n name: DEFAULTS.name,\n adapter: { ...DEFAULTS.adapter } as AirbagResolvedConfig<TReturn>['adapter'],\n retry: { ...DEFAULTS.retry },\n timeout: DEFAULTS.timeout,\n circuitBreaker: { ...DEFAULTS.circuitBreaker },\n };\n\n for (const layer of layers) {\n if (!layer) continue;\n\n if (layer.name !== undefined) resolved.name = layer.name;\n if (layer.timeout !== undefined) resolved.timeout = layer.timeout;\n if (layer.signal !== undefined) resolved.signal = layer.signal;\n\n const flatAdapter: Partial<AirbagAdapter<TReturn>> = {};\n if (layer.onLoading) flatAdapter.onLoading = layer.onLoading;\n if (layer.onSuccess) flatAdapter.onSuccess = layer.onSuccess;\n if (layer.onError) flatAdapter.onError = layer.onError;\n if (layer.onRetry) flatAdapter.onRetry = layer.onRetry;\n if (layer.onFinish) flatAdapter.onFinish = layer.onFinish;\n\n resolved.adapter = { ...resolved.adapter, ...flatAdapter, ...(layer.adapter ?? {}) };\n\n if (layer.retries !== undefined) {\n resolved.retry = { ...resolved.retry, count: layer.retries };\n }\n\n if (layer.retry) {\n resolved.retry = { ...resolved.retry, ...layer.retry };\n }\n\n if (layer.circuitBreaker) {\n resolved.circuitBreaker = { ...resolved.circuitBreaker, ...layer.circuitBreaker };\n }\n }\n\n return resolved;\n}\n","import type { CircuitBreakerConfig, CircuitBreakerState, CircuitBreakerInstance } from './interfaces';\r\n\r\nexport function createCircuitBreaker(config: CircuitBreakerConfig): CircuitBreakerInstance {\r\n let state: CircuitBreakerState = 'closed';\r\n let failureCount = 0;\r\n let lastFailureTime = 0;\r\n\r\n return {\r\n canExecute(): boolean {\r\n if (!config.enabled) return true;\r\n if (state === 'closed') return true;\r\n\r\n if (state === 'open') {\r\n const elapsed = Date.now() - lastFailureTime;\r\n if (elapsed >= config.resetTimeout) {\r\n state = 'half-open';\r\n return true;\r\n }\r\n return false;\r\n }\r\n\r\n return true;\r\n },\r\n\r\n recordSuccess(): void {\r\n if (!config.enabled) return;\r\n if (state === 'half-open') {\r\n state = 'closed';\r\n failureCount = 0;\r\n }\r\n },\r\n\r\n recordFailure(): void {\r\n if (!config.enabled) return;\r\n failureCount++;\r\n lastFailureTime = Date.now();\r\n if (state === 'half-open' || failureCount >= config.threshold) {\r\n state = 'open';\r\n }\r\n },\r\n\r\n getState(): CircuitBreakerState {\r\n return state;\r\n },\r\n\r\n reset(): void {\r\n state = 'closed';\r\n failureCount = 0;\r\n lastFailureTime = 0;\r\n },\r\n };\r\n}\r\n","import type { RetryConfig, ExecutionContext, Result } from './interfaces';\r\n\r\nexport class InternalTimeoutError extends Error {\r\n readonly ms: number;\r\n\r\n constructor(ms: number) {\r\n super(`Timed out after ${ms}ms`);\r\n this.name = 'InternalTimeoutError';\r\n this.ms = ms;\r\n Object.setPrototypeOf(this, InternalTimeoutError.prototype);\r\n }\r\n}\r\n\r\nexport class InternalAbortError extends Error {\r\n constructor() {\r\n super('Operation aborted');\r\n this.name = 'InternalAbortError';\r\n Object.setPrototypeOf(this, InternalAbortError.prototype);\r\n }\r\n}\r\n\r\nexport function toError(value: unknown): Error {\r\n if (value instanceof Error) return value;\r\n if (typeof value === 'string') return new Error(value);\r\n return new Error(String(value));\r\n}\r\n\r\nexport function buildContext(\r\n functionName: string,\r\n startTime: number,\r\n attempt: number,\r\n maxAttempts: number\r\n): ExecutionContext {\r\n return {\r\n functionName,\r\n duration: Date.now() - startTime,\r\n timestamp: Date.now(),\r\n attempt,\r\n maxAttempts,\r\n };\r\n}\r\n\r\nexport function calculateDelay(attemptIndex: number, config: RetryConfig): number {\r\n let delay: number;\r\n\r\n switch (config.backoff) {\r\n case 'exponential':\r\n delay = config.baseDelay * Math.pow(2, attemptIndex);\r\n break;\r\n case 'linear':\r\n delay = config.baseDelay * (attemptIndex + 1);\r\n break;\r\n case 'fixed':\r\n delay = config.baseDelay;\r\n break;\r\n }\r\n\r\n if (config.jitter) {\r\n delay = delay * (0.5 + Math.random() * 0.5);\r\n }\r\n\r\n return Math.min(delay, config.maxDelay);\r\n}\r\n\r\nexport function wait(ms: number, signal?: AbortSignal): Promise<void> {\r\n return new Promise<void>((resolve, reject) => {\r\n if (signal?.aborted) {\r\n reject(new InternalAbortError());\r\n return;\r\n }\r\n\r\n const timer = setTimeout(resolve, ms);\r\n\r\n const onAbort = () => {\r\n clearTimeout(timer);\r\n reject(new InternalAbortError());\r\n };\r\n\r\n signal?.addEventListener('abort', onAbort, { once: true });\r\n });\r\n}\r\n\r\nexport function withTimeout<T>(\r\n promise: Promise<T>,\r\n ms: number,\r\n signal?: AbortSignal\r\n): Promise<T> {\r\n if (ms <= 0 || !Number.isFinite(ms)) return promise;\r\n\r\n return new Promise<T>((resolve, reject) => {\r\n let settled = false;\r\n\r\n if (signal?.aborted) {\r\n reject(new InternalAbortError());\r\n return;\r\n }\r\n\r\n const timer = setTimeout(() => {\r\n if (!settled) {\r\n settled = true;\r\n reject(new InternalTimeoutError(ms));\r\n }\r\n }, ms);\r\n\r\n const onAbort = () => {\r\n if (!settled) {\r\n settled = true;\r\n clearTimeout(timer);\r\n reject(new InternalAbortError());\r\n }\r\n };\r\n\r\n signal?.addEventListener('abort', onAbort, { once: true });\r\n\r\n promise.then(\r\n (value) => {\r\n if (!settled) {\r\n settled = true;\r\n clearTimeout(timer);\r\n signal?.removeEventListener('abort', onAbort);\r\n resolve(value);\r\n }\r\n },\r\n (error) => {\r\n if (!settled) {\r\n settled = true;\r\n clearTimeout(timer);\r\n signal?.removeEventListener('abort', onAbort);\r\n reject(error);\r\n }\r\n }\r\n );\r\n });\r\n}\r\n\r\n/**\r\n * Safely invokes an async function, converting synchronous throws\r\n * into promise rejections so they can be handled uniformly.\r\n */\r\nexport function safeInvoke<TArgs extends unknown[], TReturn>(\r\n fn: (...args: TArgs) => Promise<TReturn>,\r\n args: TArgs\r\n): Promise<TReturn> {\r\n return new Promise<TReturn>((resolve, reject) => {\r\n Promise.resolve()\r\n .then(() => fn(...args))\r\n .then(resolve, reject);\r\n });\r\n}\r\n\r\n/**\r\n * Wraps a promise into a discriminated Result union,\r\n * guaranteeing the promise never rejects.\r\n */\r\nexport function settle<T>(promise: Promise<T>): Promise<Result<T>> {\r\n return promise\r\n .then((value): Result<T> => ({ ok: true, value }))\r\n .catch((error): Result<T> => ({ ok: false, error: toError(error) }));\r\n}\r\n","import type {\r\n AirbagOptions,\r\n AirbagResolvedConfig,\r\n CircuitBreakerInstance,\r\n ExecutionContext,\r\n WrappedFunction,\r\n} from './interfaces';\r\nimport { AirbagError, TimeoutError, RetryExhaustedError, CircuitOpenError, AbortError } from './errors';\r\nimport { resolveConfig } from './config';\r\nimport { createCircuitBreaker } from './circuit-breaker';\r\nimport {\r\n InternalTimeoutError,\r\n InternalAbortError,\r\n buildContext,\r\n calculateDelay,\r\n wait,\r\n withTimeout,\r\n safeInvoke,\r\n settle,\r\n} from './utils';\r\n\r\nfunction emitSuccess<TReturn>(\r\n config: AirbagResolvedConfig<TReturn>,\r\n ctx: ExecutionContext,\r\n value: TReturn\r\n): TReturn {\r\n config.adapter.onSuccess?.(value, ctx);\r\n config.adapter.onLoading?.(false, ctx);\r\n config.adapter.onFinish?.(ctx);\r\n return value;\r\n}\r\n\r\nfunction emitFailure<TReturn>(\r\n config: AirbagResolvedConfig<TReturn>,\r\n ctx: ExecutionContext,\r\n error: AirbagError\r\n): never {\r\n config.adapter.onError?.(error, ctx);\r\n config.adapter.onLoading?.(false, ctx);\r\n config.adapter.onFinish?.(ctx);\r\n throw error;\r\n}\r\n\r\nasync function execute<TArgs extends unknown[], TReturn>(\r\n fn: (...args: TArgs) => Promise<TReturn>,\r\n args: TArgs,\r\n config: AirbagResolvedConfig<TReturn>,\r\n breaker: CircuitBreakerInstance\r\n): Promise<TReturn> {\r\n const startTime = Date.now();\r\n const maxAttempts = Math.max(1, config.retry.count + 1);\r\n const getContext = (attempt: number): ExecutionContext =>\r\n buildContext(config.name, startTime, attempt, maxAttempts);\r\n\r\n if (!breaker.canExecute()) {\r\n const ctx = getContext(0);\r\n throw new CircuitOpenError(ctx);\r\n }\r\n\r\n config.adapter.onLoading?.(true, getContext(0));\r\n\r\n let lastError: Error = new Error('No attempts executed');\r\n let lastAttempt = 0;\r\n\r\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\r\n lastAttempt = attempt;\r\n\r\n if (attempt > 0) {\r\n config.adapter.onRetry?.(attempt, lastError, getContext(attempt));\r\n\r\n const delayed = await settle(\r\n wait(calculateDelay(attempt - 1, config.retry), config.signal)\r\n );\r\n\r\n if (!delayed.ok) {\r\n const ctx = getContext(attempt);\r\n return emitFailure(config, ctx, new AbortError(ctx));\r\n }\r\n }\r\n\r\n const result = await settle(\r\n withTimeout(safeInvoke(fn, args), config.timeout, config.signal)\r\n );\r\n\r\n if (result.ok) {\r\n breaker.recordSuccess();\r\n return emitSuccess(config, getContext(attempt), result.value);\r\n }\r\n\r\n if (result.error instanceof InternalAbortError) {\r\n const ctx = getContext(attempt);\r\n return emitFailure(config, ctx, new AbortError(ctx));\r\n }\r\n\r\n lastError = result.error;\r\n }\r\n\r\n breaker.recordFailure();\r\n const ctx = getContext(lastAttempt);\r\n\r\n const finalError = lastError instanceof InternalTimeoutError\r\n ? new TimeoutError(ctx, config.timeout)\r\n : new RetryExhaustedError(ctx, lastError);\r\n\r\n return emitFailure(config, ctx, finalError);\r\n}\r\n\r\nexport function createWrapped<TArgs extends unknown[], TReturn>(\r\n fn: (...args: TArgs) => Promise<TReturn>,\r\n optionLayers: Array<AirbagOptions<TReturn> | undefined>,\r\n sharedBreaker?: CircuitBreakerInstance\r\n): WrappedFunction<TArgs, TReturn> {\r\n const config = resolveConfig<TReturn>(...optionLayers);\r\n const breaker = sharedBreaker ?? createCircuitBreaker(config.circuitBreaker);\r\n\r\n const wrapped = ((...args: TArgs): Promise<TReturn> =>\r\n execute(fn, args, config, breaker)\r\n ) as WrappedFunction<TArgs, TReturn>;\r\n\r\n wrapped.with = (overrides: AirbagOptions<TReturn>): WrappedFunction<TArgs, TReturn> =>\r\n createWrapped(fn, [...optionLayers, overrides], breaker);\r\n\r\n wrapped.reset = (): void => breaker.reset();\r\n\r\n return wrapped;\r\n}\r\n\r\n/**\r\n * Wraps an async function with execution orchestration.\r\n *\r\n * The returned function has the same signature as the original but adds:\r\n * - Automatic loading state management\r\n * - Configurable retries with exponential backoff\r\n * - Timeout enforcement\r\n * - Circuit breaker protection\r\n * - Structured error reporting via adapters\r\n *\r\n * @typeParam TArgs - Tuple of the original function's parameter types\r\n * @typeParam TReturn - The resolved type of the original function's promise\r\n *\r\n * @example\r\n * ```ts\r\n * const safeFetch = airbag(fetchUser, {\r\n * name: 'fetchUser',\r\n * timeout: 5000,\r\n * retry: { count: 3 },\r\n * adapter: {\r\n * onError: (err) => toast.error(err.message),\r\n * },\r\n * });\r\n *\r\n * const user = await safeFetch('user-123');\r\n * ```\r\n */\r\nexport function airbag<TArgs extends unknown[], TReturn>(\r\n fn: (...args: TArgs) => Promise<TReturn>,\r\n options?: AirbagOptions<TReturn>\r\n): WrappedFunction<TArgs, TReturn> {\r\n return createWrapped(fn, [options]);\r\n}\r\n","import type { AirbagInstance, AirbagOptions, AirbagResolvedConfig, WrappedFunction } from './interfaces';\r\nimport { resolveConfig } from './config';\r\nimport { createWrapped } from './airbag';\r\n\r\n/**\r\n * Creates a pre-configured airbag instance with shared global defaults.\r\n *\r\n * All functions wrapped through this instance inherit the global options\r\n * before applying their own instance-level and execution-level overrides.\r\n *\r\n * @example\r\n * ```ts\r\n * const { wrap } = createAirbagInstance({\r\n * retry: { count: 3 },\r\n * adapter: {\r\n * onError: (err) => logger.error(err),\r\n * },\r\n * });\r\n *\r\n * const safeFetch = wrap(fetchUser, { timeout: 5000 });\r\n * const user = await safeFetch('user-123');\r\n * ```\r\n */\r\nexport function createAirbagInstance(globalOptions?: AirbagOptions): AirbagInstance {\r\n let globals: AirbagOptions = { ...globalOptions };\r\n\r\n return {\r\n wrap<TArgs extends unknown[], TReturn>(\r\n fn: (...args: TArgs) => Promise<TReturn>,\r\n options?: AirbagOptions<TReturn>\r\n ): WrappedFunction<TArgs, TReturn> {\r\n return createWrapped(fn, [globals as AirbagOptions<TReturn>, options]);\r\n },\r\n\r\n configure(options: AirbagOptions): void {\r\n globals = { ...globals, ...options };\r\n },\r\n\r\n getDefaults(): AirbagResolvedConfig {\r\n return resolveConfig(globals);\r\n },\r\n };\r\n}\r\n"],"mappings":"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","names":["ctx"]}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "airbag",
3
+ "version": "0.1.0",
4
+ "description": "Execution Orchestration Layer for JavaScript/TypeScript — declarative async function wrapping with retries, timeouts, and circuit breaking",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.mts",
12
+ "default": "./dist/index.mjs"
13
+ },
14
+ "require": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "sideEffects": false,
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "dev": "tsup --watch",
27
+ "typecheck": "tsc --noEmit",
28
+ "prepublishOnly": "npm run build"
29
+ },
30
+ "keywords": [
31
+ "error-handling",
32
+ "retry",
33
+ "timeout",
34
+ "circuit-breaker",
35
+ "async",
36
+ "promise",
37
+ "typescript",
38
+ "try-catch",
39
+ "orchestration"
40
+ ],
41
+ "license": "MIT",
42
+ "devDependencies": {
43
+ "tsup": "^8.0.0",
44
+ "typescript": "^5.3.0"
45
+ },
46
+ "engines": {
47
+ "node": ">=16"
48
+ }
49
+ }