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/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # Airbag
2
+
3
+ Execution Orchestration Layer for JavaScript/TypeScript.
4
+
5
+ Replace imperative `try/catch/finally` blocks with declarative async function wrappers. Airbag handles **loading states**, **error catching**, **retries with exponential backoff**, **timeouts**, and **circuit breaking** — fully type-safe with zero runtime dependencies.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install airbag
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```ts
16
+ import { airbag } from 'airbag';
17
+
18
+ interface User {
19
+ id: string;
20
+ name: string;
21
+ }
22
+
23
+ const fetchUser = async (id: string): Promise<User> => {
24
+ const res = await fetch(`/api/users/${id}`);
25
+ if (!res.ok) throw new Error('Failed to fetch user');
26
+ return res.json() as Promise<User>;
27
+ };
28
+
29
+ const safeFetchUser = airbag(fetchUser, {
30
+ timeout: 5000,
31
+ retries: 3,
32
+ onLoading: (loading) => spinner.toggle(loading),
33
+ onSuccess: (user) => toast.success(`Loaded ${user.name}`),
34
+ onError: (err) => toast.error(err.message),
35
+ });
36
+
37
+ // Fully type-safe — same signature as fetchUser
38
+ const user = await safeFetchUser('user-123');
39
+ ```
40
+
41
+ ## Configuration Hierarchy
42
+
43
+ Three levels of configuration merged in order of specificity:
44
+
45
+ ### Global → Instance → Execution
46
+
47
+ ```ts
48
+ import { createAirbagInstance } from 'airbag';
49
+
50
+ // Level 1: Global defaults
51
+ const { wrap } = createAirbagInstance({
52
+ retries: 3,
53
+ timeout: 10000,
54
+ onError: (err) => logger.error(err),
55
+ });
56
+
57
+ // Level 2: Instance options (merged over global)
58
+ const safeFetch = wrap(fetchUser, {
59
+ name: 'fetchUser',
60
+ timeout: 5000,
61
+ });
62
+
63
+ // Level 3: Execution overrides (merged over instance)
64
+ const user = await safeFetch.with({ timeout: 15000 })('user-123');
65
+ ```
66
+
67
+ ## API
68
+
69
+ ### `airbag(fn, options?)`
70
+
71
+ Wraps an async function with execution orchestration.
72
+
73
+ ```ts
74
+ const wrapped = airbag(myAsyncFn, { timeout: 5000, retries: 3 });
75
+ const result = await wrapped(...originalArgs);
76
+ ```
77
+
78
+ ### `createAirbagInstance(options?)`
79
+
80
+ Creates a factory with shared global defaults.
81
+
82
+ ```ts
83
+ const { wrap, configure, getDefaults } = createAirbagInstance({ retries: 2 });
84
+ ```
85
+
86
+ ### `wrapped.with(overrides)`
87
+
88
+ Creates a new wrapped function with execution-level overrides while sharing the same circuit breaker state.
89
+
90
+ ```ts
91
+ const urgent = wrapped.with({ timeout: 2000 });
92
+ const result = await urgent(...args);
93
+ ```
94
+
95
+ ### `wrapped.reset()`
96
+
97
+ Resets the circuit breaker state back to `closed`.
98
+
99
+ ## Options
100
+
101
+ | Option | Type | Default | Description |
102
+ | --- | --- | --- | --- |
103
+ | `name` | `string` | `'anonymous'` | Identifier for logging and error messages |
104
+ | `timeout` | `number` | `30000` | Timeout in ms (`0` disables) |
105
+ | `retries` | `number` | `0` | Shorthand for `retry.count` |
106
+ | `retry.count` | `number` | `0` | Number of retries after initial failure |
107
+ | `retry.backoff` | `'exponential' \| 'linear' \| 'fixed'` | `'exponential'` | Backoff strategy between retries |
108
+ | `retry.baseDelay` | `number` | `1000` | Base delay in ms |
109
+ | `retry.maxDelay` | `number` | `30000` | Maximum delay cap in ms |
110
+ | `retry.jitter` | `boolean` | `true` | Randomize delays to avoid thundering herd |
111
+ | `circuitBreaker.enabled` | `boolean` | `false` | Enable the circuit breaker |
112
+ | `circuitBreaker.threshold` | `number` | `5` | Consecutive failures before opening |
113
+ | `circuitBreaker.resetTimeout` | `number` | `60000` | Ms before probing with a half-open attempt |
114
+ | `signal` | `AbortSignal` | — | Cancel execution via `AbortController` |
115
+
116
+ ## Callbacks
117
+
118
+ Lifecycle callbacks can be passed **flat** at the top level — no nesting required:
119
+
120
+ ```ts
121
+ airbag(fetchUser, {
122
+ retries: 3,
123
+ onSuccess: (user) => toast.success(user.name),
124
+ onError: (err) => toast.error(err.message),
125
+ });
126
+ ```
127
+
128
+ For reusable callback sets, group them in an `adapter` object instead:
129
+
130
+ ```ts
131
+ const logger: AirbagAdapter = {
132
+ onError: (err, ctx) => log.error(ctx.functionName, err),
133
+ onFinish: (ctx) => log.info(`Done in ${ctx.duration}ms`),
134
+ };
135
+
136
+ airbag(fetchUser, { retries: 3, adapter: logger });
137
+ ```
138
+
139
+ If both flat callbacks and an `adapter` are provided, the `adapter` takes precedence.
140
+
141
+ Every callback receives an `ExecutionContext`:
142
+
143
+ ```ts
144
+ interface ExecutionContext {
145
+ functionName: string;
146
+ duration: number;
147
+ timestamp: number;
148
+ attempt: number;
149
+ maxAttempts: number;
150
+ }
151
+ ```
152
+
153
+ ## Error Types
154
+
155
+ All errors extend `AirbagError` with a `code` and `context` property:
156
+
157
+ | Error | Code | When |
158
+ | --- | --- | --- |
159
+ | `TimeoutError` | `TIMEOUT` | Promise exceeded the configured timeout |
160
+ | `RetryExhaustedError` | `RETRY_EXHAUSTED` | All retry attempts failed |
161
+ | `CircuitOpenError` | `CIRCUIT_OPEN` | Circuit breaker blocked execution |
162
+ | `AbortError` | `ABORTED` | Execution cancelled via `AbortSignal` |
163
+
164
+ ```ts
165
+ import { TimeoutError, RetryExhaustedError } from 'airbag';
166
+
167
+ const result = await safeFetch('id').catch((err) => {
168
+ if (err instanceof TimeoutError) {
169
+ // err.timeout — the configured timeout in ms
170
+ // err.context — execution metadata
171
+ }
172
+ if (err instanceof RetryExhaustedError) {
173
+ // err.cause — the last error that caused the final failure
174
+ }
175
+ });
176
+ ```
177
+
178
+ ## License
179
+
180
+ MIT
@@ -0,0 +1,160 @@
1
+ type BackoffStrategy = 'exponential' | 'linear' | 'fixed';
2
+ type CircuitBreakerState = 'closed' | 'open' | 'half-open';
3
+ type AirbagErrorCode = 'TIMEOUT' | 'RETRY_EXHAUSTED' | 'CIRCUIT_OPEN' | 'ABORTED' | 'EXECUTION_FAILED';
4
+ type Result<T> = {
5
+ ok: true;
6
+ value: T;
7
+ } | {
8
+ ok: false;
9
+ error: Error;
10
+ };
11
+ interface RetryConfig {
12
+ count: number;
13
+ backoff: BackoffStrategy;
14
+ baseDelay: number;
15
+ maxDelay: number;
16
+ jitter: boolean;
17
+ }
18
+ interface CircuitBreakerConfig {
19
+ enabled: boolean;
20
+ threshold: number;
21
+ resetTimeout: number;
22
+ }
23
+ interface ExecutionContext {
24
+ functionName: string;
25
+ duration: number;
26
+ timestamp: number;
27
+ attempt: number;
28
+ maxAttempts: number;
29
+ }
30
+ interface AirbagAdapter<TReturn = unknown> {
31
+ onLoading?: (isLoading: boolean, context: ExecutionContext) => void;
32
+ onSuccess?: (data: TReturn, context: ExecutionContext) => void;
33
+ onError?: (error: Error, context: ExecutionContext) => void;
34
+ onRetry?: (attempt: number, error: Error, context: ExecutionContext) => void;
35
+ onFinish?: (context: ExecutionContext) => void;
36
+ }
37
+ interface AirbagOptions<TReturn = unknown> {
38
+ name?: string;
39
+ timeout?: number;
40
+ retries?: number;
41
+ retry?: Partial<RetryConfig>;
42
+ circuitBreaker?: Partial<CircuitBreakerConfig>;
43
+ signal?: AbortSignal;
44
+ adapter?: AirbagAdapter<TReturn>;
45
+ onLoading?: AirbagAdapter<TReturn>['onLoading'];
46
+ onSuccess?: AirbagAdapter<TReturn>['onSuccess'];
47
+ onError?: AirbagAdapter<TReturn>['onError'];
48
+ onRetry?: AirbagAdapter<TReturn>['onRetry'];
49
+ onFinish?: AirbagAdapter<TReturn>['onFinish'];
50
+ }
51
+ interface AirbagResolvedConfig<TReturn = unknown> {
52
+ name: string;
53
+ adapter: AirbagAdapter<TReturn>;
54
+ retry: RetryConfig;
55
+ timeout: number;
56
+ circuitBreaker: CircuitBreakerConfig;
57
+ signal?: AbortSignal;
58
+ }
59
+ /**
60
+ * Wrapped async function that preserves the original function's
61
+ * argument types and return type while adding execution orchestration.
62
+ *
63
+ * @typeParam TArgs - Tuple of the original function's parameter types
64
+ * @typeParam TReturn - The resolved type of the original function's promise
65
+ */
66
+ interface WrappedFunction<TArgs extends unknown[], TReturn> {
67
+ (...args: TArgs): Promise<TReturn>;
68
+ /** Creates a new wrapped function with execution-level option overrides. */
69
+ with(overrides: AirbagOptions<TReturn>): WrappedFunction<TArgs, TReturn>;
70
+ /** Resets the circuit breaker state for this wrapped function. */
71
+ reset(): void;
72
+ }
73
+ interface CircuitBreakerInstance {
74
+ canExecute(): boolean;
75
+ recordSuccess(): void;
76
+ recordFailure(): void;
77
+ getState(): CircuitBreakerState;
78
+ reset(): void;
79
+ }
80
+ interface AirbagInstance {
81
+ /** Wraps an async function with airbag orchestration using instance-level defaults. */
82
+ wrap<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => Promise<TReturn>, options?: AirbagOptions<TReturn>): WrappedFunction<TArgs, TReturn>;
83
+ /** Merges new options into the instance-level defaults. */
84
+ configure(options: AirbagOptions): void;
85
+ /** Returns the fully resolved configuration with current defaults. */
86
+ getDefaults(): AirbagResolvedConfig;
87
+ }
88
+
89
+ /**
90
+ * Wraps an async function with execution orchestration.
91
+ *
92
+ * The returned function has the same signature as the original but adds:
93
+ * - Automatic loading state management
94
+ * - Configurable retries with exponential backoff
95
+ * - Timeout enforcement
96
+ * - Circuit breaker protection
97
+ * - Structured error reporting via adapters
98
+ *
99
+ * @typeParam TArgs - Tuple of the original function's parameter types
100
+ * @typeParam TReturn - The resolved type of the original function's promise
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * const safeFetch = airbag(fetchUser, {
105
+ * name: 'fetchUser',
106
+ * timeout: 5000,
107
+ * retry: { count: 3 },
108
+ * adapter: {
109
+ * onError: (err) => toast.error(err.message),
110
+ * },
111
+ * });
112
+ *
113
+ * const user = await safeFetch('user-123');
114
+ * ```
115
+ */
116
+ declare function airbag<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => Promise<TReturn>, options?: AirbagOptions<TReturn>): WrappedFunction<TArgs, TReturn>;
117
+
118
+ /**
119
+ * Creates a pre-configured airbag instance with shared global defaults.
120
+ *
121
+ * All functions wrapped through this instance inherit the global options
122
+ * before applying their own instance-level and execution-level overrides.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * const { wrap } = createAirbagInstance({
127
+ * retry: { count: 3 },
128
+ * adapter: {
129
+ * onError: (err) => logger.error(err),
130
+ * },
131
+ * });
132
+ *
133
+ * const safeFetch = wrap(fetchUser, { timeout: 5000 });
134
+ * const user = await safeFetch('user-123');
135
+ * ```
136
+ */
137
+ declare function createAirbagInstance(globalOptions?: AirbagOptions): AirbagInstance;
138
+
139
+ declare class AirbagError extends Error {
140
+ readonly code: AirbagErrorCode;
141
+ readonly context: ExecutionContext;
142
+ constructor(message: string, code: AirbagErrorCode, context: ExecutionContext, options?: {
143
+ cause?: Error;
144
+ });
145
+ }
146
+ declare class TimeoutError extends AirbagError {
147
+ readonly timeout: number;
148
+ constructor(context: ExecutionContext, timeout: number);
149
+ }
150
+ declare class RetryExhaustedError extends AirbagError {
151
+ constructor(context: ExecutionContext, cause: Error);
152
+ }
153
+ declare class CircuitOpenError extends AirbagError {
154
+ constructor(context: ExecutionContext);
155
+ }
156
+ declare class AbortError extends AirbagError {
157
+ constructor(context: ExecutionContext);
158
+ }
159
+
160
+ export { AbortError, type AirbagAdapter, AirbagError, type AirbagErrorCode, type AirbagInstance, type AirbagOptions, type AirbagResolvedConfig, type BackoffStrategy, type CircuitBreakerConfig, type CircuitBreakerInstance, type CircuitBreakerState, CircuitOpenError, type ExecutionContext, type Result, type RetryConfig, RetryExhaustedError, TimeoutError, type WrappedFunction, airbag, createAirbagInstance };
@@ -0,0 +1,160 @@
1
+ type BackoffStrategy = 'exponential' | 'linear' | 'fixed';
2
+ type CircuitBreakerState = 'closed' | 'open' | 'half-open';
3
+ type AirbagErrorCode = 'TIMEOUT' | 'RETRY_EXHAUSTED' | 'CIRCUIT_OPEN' | 'ABORTED' | 'EXECUTION_FAILED';
4
+ type Result<T> = {
5
+ ok: true;
6
+ value: T;
7
+ } | {
8
+ ok: false;
9
+ error: Error;
10
+ };
11
+ interface RetryConfig {
12
+ count: number;
13
+ backoff: BackoffStrategy;
14
+ baseDelay: number;
15
+ maxDelay: number;
16
+ jitter: boolean;
17
+ }
18
+ interface CircuitBreakerConfig {
19
+ enabled: boolean;
20
+ threshold: number;
21
+ resetTimeout: number;
22
+ }
23
+ interface ExecutionContext {
24
+ functionName: string;
25
+ duration: number;
26
+ timestamp: number;
27
+ attempt: number;
28
+ maxAttempts: number;
29
+ }
30
+ interface AirbagAdapter<TReturn = unknown> {
31
+ onLoading?: (isLoading: boolean, context: ExecutionContext) => void;
32
+ onSuccess?: (data: TReturn, context: ExecutionContext) => void;
33
+ onError?: (error: Error, context: ExecutionContext) => void;
34
+ onRetry?: (attempt: number, error: Error, context: ExecutionContext) => void;
35
+ onFinish?: (context: ExecutionContext) => void;
36
+ }
37
+ interface AirbagOptions<TReturn = unknown> {
38
+ name?: string;
39
+ timeout?: number;
40
+ retries?: number;
41
+ retry?: Partial<RetryConfig>;
42
+ circuitBreaker?: Partial<CircuitBreakerConfig>;
43
+ signal?: AbortSignal;
44
+ adapter?: AirbagAdapter<TReturn>;
45
+ onLoading?: AirbagAdapter<TReturn>['onLoading'];
46
+ onSuccess?: AirbagAdapter<TReturn>['onSuccess'];
47
+ onError?: AirbagAdapter<TReturn>['onError'];
48
+ onRetry?: AirbagAdapter<TReturn>['onRetry'];
49
+ onFinish?: AirbagAdapter<TReturn>['onFinish'];
50
+ }
51
+ interface AirbagResolvedConfig<TReturn = unknown> {
52
+ name: string;
53
+ adapter: AirbagAdapter<TReturn>;
54
+ retry: RetryConfig;
55
+ timeout: number;
56
+ circuitBreaker: CircuitBreakerConfig;
57
+ signal?: AbortSignal;
58
+ }
59
+ /**
60
+ * Wrapped async function that preserves the original function's
61
+ * argument types and return type while adding execution orchestration.
62
+ *
63
+ * @typeParam TArgs - Tuple of the original function's parameter types
64
+ * @typeParam TReturn - The resolved type of the original function's promise
65
+ */
66
+ interface WrappedFunction<TArgs extends unknown[], TReturn> {
67
+ (...args: TArgs): Promise<TReturn>;
68
+ /** Creates a new wrapped function with execution-level option overrides. */
69
+ with(overrides: AirbagOptions<TReturn>): WrappedFunction<TArgs, TReturn>;
70
+ /** Resets the circuit breaker state for this wrapped function. */
71
+ reset(): void;
72
+ }
73
+ interface CircuitBreakerInstance {
74
+ canExecute(): boolean;
75
+ recordSuccess(): void;
76
+ recordFailure(): void;
77
+ getState(): CircuitBreakerState;
78
+ reset(): void;
79
+ }
80
+ interface AirbagInstance {
81
+ /** Wraps an async function with airbag orchestration using instance-level defaults. */
82
+ wrap<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => Promise<TReturn>, options?: AirbagOptions<TReturn>): WrappedFunction<TArgs, TReturn>;
83
+ /** Merges new options into the instance-level defaults. */
84
+ configure(options: AirbagOptions): void;
85
+ /** Returns the fully resolved configuration with current defaults. */
86
+ getDefaults(): AirbagResolvedConfig;
87
+ }
88
+
89
+ /**
90
+ * Wraps an async function with execution orchestration.
91
+ *
92
+ * The returned function has the same signature as the original but adds:
93
+ * - Automatic loading state management
94
+ * - Configurable retries with exponential backoff
95
+ * - Timeout enforcement
96
+ * - Circuit breaker protection
97
+ * - Structured error reporting via adapters
98
+ *
99
+ * @typeParam TArgs - Tuple of the original function's parameter types
100
+ * @typeParam TReturn - The resolved type of the original function's promise
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * const safeFetch = airbag(fetchUser, {
105
+ * name: 'fetchUser',
106
+ * timeout: 5000,
107
+ * retry: { count: 3 },
108
+ * adapter: {
109
+ * onError: (err) => toast.error(err.message),
110
+ * },
111
+ * });
112
+ *
113
+ * const user = await safeFetch('user-123');
114
+ * ```
115
+ */
116
+ declare function airbag<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => Promise<TReturn>, options?: AirbagOptions<TReturn>): WrappedFunction<TArgs, TReturn>;
117
+
118
+ /**
119
+ * Creates a pre-configured airbag instance with shared global defaults.
120
+ *
121
+ * All functions wrapped through this instance inherit the global options
122
+ * before applying their own instance-level and execution-level overrides.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * const { wrap } = createAirbagInstance({
127
+ * retry: { count: 3 },
128
+ * adapter: {
129
+ * onError: (err) => logger.error(err),
130
+ * },
131
+ * });
132
+ *
133
+ * const safeFetch = wrap(fetchUser, { timeout: 5000 });
134
+ * const user = await safeFetch('user-123');
135
+ * ```
136
+ */
137
+ declare function createAirbagInstance(globalOptions?: AirbagOptions): AirbagInstance;
138
+
139
+ declare class AirbagError extends Error {
140
+ readonly code: AirbagErrorCode;
141
+ readonly context: ExecutionContext;
142
+ constructor(message: string, code: AirbagErrorCode, context: ExecutionContext, options?: {
143
+ cause?: Error;
144
+ });
145
+ }
146
+ declare class TimeoutError extends AirbagError {
147
+ readonly timeout: number;
148
+ constructor(context: ExecutionContext, timeout: number);
149
+ }
150
+ declare class RetryExhaustedError extends AirbagError {
151
+ constructor(context: ExecutionContext, cause: Error);
152
+ }
153
+ declare class CircuitOpenError extends AirbagError {
154
+ constructor(context: ExecutionContext);
155
+ }
156
+ declare class AbortError extends AirbagError {
157
+ constructor(context: ExecutionContext);
158
+ }
159
+
160
+ export { AbortError, type AirbagAdapter, AirbagError, type AirbagErrorCode, type AirbagInstance, type AirbagOptions, type AirbagResolvedConfig, type BackoffStrategy, type CircuitBreakerConfig, type CircuitBreakerInstance, type CircuitBreakerState, CircuitOpenError, type ExecutionContext, type Result, type RetryConfig, RetryExhaustedError, TimeoutError, type WrappedFunction, airbag, createAirbagInstance };