@sparkleideas/shared 3.0.0-alpha.7
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 +323 -0
- package/__tests__/hooks/bash-safety.test.ts +289 -0
- package/__tests__/hooks/file-organization.test.ts +335 -0
- package/__tests__/hooks/git-commit.test.ts +336 -0
- package/__tests__/hooks/index.ts +23 -0
- package/__tests__/hooks/session-hooks.test.ts +357 -0
- package/__tests__/hooks/task-hooks.test.ts +193 -0
- package/docs/EVENTS_IMPLEMENTATION_SUMMARY.md +388 -0
- package/docs/EVENTS_QUICK_REFERENCE.md +470 -0
- package/docs/EVENTS_README.md +352 -0
- package/package.json +39 -0
- package/src/core/config/defaults.ts +207 -0
- package/src/core/config/index.ts +15 -0
- package/src/core/config/loader.ts +271 -0
- package/src/core/config/schema.ts +188 -0
- package/src/core/config/validator.ts +209 -0
- package/src/core/event-bus.ts +236 -0
- package/src/core/index.ts +22 -0
- package/src/core/interfaces/agent.interface.ts +251 -0
- package/src/core/interfaces/coordinator.interface.ts +363 -0
- package/src/core/interfaces/event.interface.ts +267 -0
- package/src/core/interfaces/index.ts +19 -0
- package/src/core/interfaces/memory.interface.ts +332 -0
- package/src/core/interfaces/task.interface.ts +223 -0
- package/src/core/orchestrator/event-coordinator.ts +122 -0
- package/src/core/orchestrator/health-monitor.ts +214 -0
- package/src/core/orchestrator/index.ts +89 -0
- package/src/core/orchestrator/lifecycle-manager.ts +263 -0
- package/src/core/orchestrator/session-manager.ts +279 -0
- package/src/core/orchestrator/task-manager.ts +317 -0
- package/src/events/domain-events.ts +584 -0
- package/src/events/event-store.test.ts +387 -0
- package/src/events/event-store.ts +588 -0
- package/src/events/example-usage.ts +293 -0
- package/src/events/index.ts +90 -0
- package/src/events/projections.ts +561 -0
- package/src/events/state-reconstructor.ts +349 -0
- package/src/events.ts +367 -0
- package/src/hooks/INTEGRATION.md +658 -0
- package/src/hooks/README.md +532 -0
- package/src/hooks/example-usage.ts +499 -0
- package/src/hooks/executor.ts +379 -0
- package/src/hooks/hooks.test.ts +421 -0
- package/src/hooks/index.ts +131 -0
- package/src/hooks/registry.ts +333 -0
- package/src/hooks/safety/bash-safety.ts +604 -0
- package/src/hooks/safety/file-organization.ts +473 -0
- package/src/hooks/safety/git-commit.ts +623 -0
- package/src/hooks/safety/index.ts +46 -0
- package/src/hooks/session-hooks.ts +559 -0
- package/src/hooks/task-hooks.ts +513 -0
- package/src/hooks/types.ts +357 -0
- package/src/hooks/verify-exports.test.ts +125 -0
- package/src/index.ts +195 -0
- package/src/mcp/connection-pool.ts +438 -0
- package/src/mcp/index.ts +183 -0
- package/src/mcp/server.ts +774 -0
- package/src/mcp/session-manager.ts +428 -0
- package/src/mcp/tool-registry.ts +566 -0
- package/src/mcp/transport/http.ts +557 -0
- package/src/mcp/transport/index.ts +294 -0
- package/src/mcp/transport/stdio.ts +324 -0
- package/src/mcp/transport/websocket.ts +484 -0
- package/src/mcp/types.ts +565 -0
- package/src/plugin-interface.ts +663 -0
- package/src/plugin-loader.ts +638 -0
- package/src/plugin-registry.ts +604 -0
- package/src/plugins/index.ts +34 -0
- package/src/plugins/official/hive-mind-plugin.ts +330 -0
- package/src/plugins/official/index.ts +24 -0
- package/src/plugins/official/maestro-plugin.ts +508 -0
- package/src/plugins/types.ts +108 -0
- package/src/resilience/bulkhead.ts +277 -0
- package/src/resilience/circuit-breaker.ts +326 -0
- package/src/resilience/index.ts +26 -0
- package/src/resilience/rate-limiter.ts +420 -0
- package/src/resilience/retry.ts +224 -0
- package/src/security/index.ts +39 -0
- package/src/security/input-validation.ts +265 -0
- package/src/security/secure-random.ts +159 -0
- package/src/services/index.ts +16 -0
- package/src/services/v3-progress.service.ts +505 -0
- package/src/types/agent.types.ts +144 -0
- package/src/types/index.ts +22 -0
- package/src/types/mcp.types.ts +300 -0
- package/src/types/memory.types.ts +263 -0
- package/src/types/swarm.types.ts +255 -0
- package/src/types/task.types.ts +205 -0
- package/src/types.ts +367 -0
- package/src/utils/secure-logger.d.ts +69 -0
- package/src/utils/secure-logger.d.ts.map +1 -0
- package/src/utils/secure-logger.js +208 -0
- package/src/utils/secure-logger.js.map +1 -0
- package/src/utils/secure-logger.ts +257 -0
- package/tmp.json +0 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bulkhead Pattern
|
|
3
|
+
*
|
|
4
|
+
* Isolates failures by limiting concurrent executions.
|
|
5
|
+
*
|
|
6
|
+
* @module v3/shared/resilience/bulkhead
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { EventEmitter } from 'events';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Bulkhead options
|
|
13
|
+
*/
|
|
14
|
+
export interface BulkheadOptions {
|
|
15
|
+
/** Name for identification */
|
|
16
|
+
name: string;
|
|
17
|
+
|
|
18
|
+
/** Maximum concurrent executions */
|
|
19
|
+
maxConcurrent: number;
|
|
20
|
+
|
|
21
|
+
/** Maximum queue size */
|
|
22
|
+
maxQueue: number;
|
|
23
|
+
|
|
24
|
+
/** Timeout for queued items in ms */
|
|
25
|
+
queueTimeout: number;
|
|
26
|
+
|
|
27
|
+
/** Callback when rejected */
|
|
28
|
+
onRejected?: (reason: 'full' | 'timeout') => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Bulkhead statistics
|
|
33
|
+
*/
|
|
34
|
+
export interface BulkheadStats {
|
|
35
|
+
active: number;
|
|
36
|
+
queued: number;
|
|
37
|
+
maxConcurrent: number;
|
|
38
|
+
maxQueue: number;
|
|
39
|
+
completed: number;
|
|
40
|
+
rejected: number;
|
|
41
|
+
timedOut: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Queued item
|
|
46
|
+
*/
|
|
47
|
+
interface QueuedItem<T> {
|
|
48
|
+
fn: () => Promise<T>;
|
|
49
|
+
resolve: (value: T) => void;
|
|
50
|
+
reject: (error: Error) => void;
|
|
51
|
+
queuedAt: number;
|
|
52
|
+
timeoutId?: ReturnType<typeof setTimeout>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Default options
|
|
57
|
+
*/
|
|
58
|
+
const DEFAULT_OPTIONS: Omit<BulkheadOptions, 'name'> = {
|
|
59
|
+
maxConcurrent: 10,
|
|
60
|
+
maxQueue: 100,
|
|
61
|
+
queueTimeout: 30000,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Bulkhead
|
|
66
|
+
*
|
|
67
|
+
* Limits concurrent executions to prevent resource exhaustion.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* const bulkhead = new Bulkhead({
|
|
71
|
+
* name: 'database',
|
|
72
|
+
* maxConcurrent: 10,
|
|
73
|
+
* maxQueue: 50,
|
|
74
|
+
* });
|
|
75
|
+
*
|
|
76
|
+
* try {
|
|
77
|
+
* const result = await bulkhead.execute(() => dbQuery());
|
|
78
|
+
* } catch (error) {
|
|
79
|
+
* if (error.message.includes('Bulkhead full')) {
|
|
80
|
+
* // Handle capacity exceeded
|
|
81
|
+
* }
|
|
82
|
+
* }
|
|
83
|
+
*/
|
|
84
|
+
export class Bulkhead extends EventEmitter {
|
|
85
|
+
private readonly options: BulkheadOptions;
|
|
86
|
+
private active = 0;
|
|
87
|
+
private readonly queue: Array<QueuedItem<unknown>> = [];
|
|
88
|
+
private completed = 0;
|
|
89
|
+
private rejected = 0;
|
|
90
|
+
private timedOut = 0;
|
|
91
|
+
|
|
92
|
+
constructor(options: BulkheadOptions) {
|
|
93
|
+
super();
|
|
94
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Execute a function within the bulkhead
|
|
99
|
+
*/
|
|
100
|
+
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
101
|
+
// If there's room for execution, run immediately
|
|
102
|
+
if (this.active < this.options.maxConcurrent) {
|
|
103
|
+
return this.runNow(fn);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check if queue is full
|
|
107
|
+
if (this.queue.length >= this.options.maxQueue) {
|
|
108
|
+
this.rejected++;
|
|
109
|
+
this.options.onRejected?.('full');
|
|
110
|
+
throw new Error(`Bulkhead '${this.options.name}' is full. Max concurrent: ${this.options.maxConcurrent}, queue: ${this.options.maxQueue}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Add to queue
|
|
114
|
+
return this.addToQueue(fn);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get current statistics
|
|
119
|
+
*/
|
|
120
|
+
getStats(): BulkheadStats {
|
|
121
|
+
return {
|
|
122
|
+
active: this.active,
|
|
123
|
+
queued: this.queue.length,
|
|
124
|
+
maxConcurrent: this.options.maxConcurrent,
|
|
125
|
+
maxQueue: this.options.maxQueue,
|
|
126
|
+
completed: this.completed,
|
|
127
|
+
rejected: this.rejected,
|
|
128
|
+
timedOut: this.timedOut,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if there's capacity available
|
|
134
|
+
*/
|
|
135
|
+
hasCapacity(): boolean {
|
|
136
|
+
return this.active < this.options.maxConcurrent || this.queue.length < this.options.maxQueue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get available capacity (concurrent + queue)
|
|
141
|
+
*/
|
|
142
|
+
availableCapacity(): number {
|
|
143
|
+
const concurrentAvailable = this.options.maxConcurrent - this.active;
|
|
144
|
+
const queueAvailable = this.options.maxQueue - this.queue.length;
|
|
145
|
+
return concurrentAvailable + queueAvailable;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Reset statistics
|
|
150
|
+
*/
|
|
151
|
+
resetStats(): void {
|
|
152
|
+
this.completed = 0;
|
|
153
|
+
this.rejected = 0;
|
|
154
|
+
this.timedOut = 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Run function immediately
|
|
159
|
+
*/
|
|
160
|
+
private async runNow<T>(fn: () => Promise<T>): Promise<T> {
|
|
161
|
+
this.active++;
|
|
162
|
+
this.emit('acquire');
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const result = await fn();
|
|
166
|
+
this.completed++;
|
|
167
|
+
return result;
|
|
168
|
+
} finally {
|
|
169
|
+
this.active--;
|
|
170
|
+
this.emit('release');
|
|
171
|
+
this.processQueue();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Add function to queue
|
|
177
|
+
*/
|
|
178
|
+
private addToQueue<T>(fn: () => Promise<T>): Promise<T> {
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const item: QueuedItem<T> = {
|
|
181
|
+
fn,
|
|
182
|
+
resolve,
|
|
183
|
+
reject,
|
|
184
|
+
queuedAt: Date.now(),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Set timeout for queued item
|
|
188
|
+
item.timeoutId = setTimeout(() => {
|
|
189
|
+
const index = this.queue.indexOf(item as QueuedItem<unknown>);
|
|
190
|
+
if (index !== -1) {
|
|
191
|
+
this.queue.splice(index, 1);
|
|
192
|
+
this.timedOut++;
|
|
193
|
+
this.options.onRejected?.('timeout');
|
|
194
|
+
reject(new Error(`Bulkhead '${this.options.name}' queue timeout after ${this.options.queueTimeout}ms`));
|
|
195
|
+
}
|
|
196
|
+
}, this.options.queueTimeout);
|
|
197
|
+
|
|
198
|
+
this.queue.push(item as QueuedItem<unknown>);
|
|
199
|
+
this.emit('queued', { queueLength: this.queue.length });
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Process next item in queue
|
|
205
|
+
*/
|
|
206
|
+
private processQueue(): void {
|
|
207
|
+
if (this.active >= this.options.maxConcurrent) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const item = this.queue.shift();
|
|
212
|
+
if (!item) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Clear timeout
|
|
217
|
+
if (item.timeoutId) {
|
|
218
|
+
clearTimeout(item.timeoutId);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Execute the queued function
|
|
222
|
+
this.active++;
|
|
223
|
+
this.emit('acquire');
|
|
224
|
+
|
|
225
|
+
item.fn()
|
|
226
|
+
.then((result) => {
|
|
227
|
+
this.completed++;
|
|
228
|
+
item.resolve(result);
|
|
229
|
+
})
|
|
230
|
+
.catch((error) => {
|
|
231
|
+
item.reject(error);
|
|
232
|
+
})
|
|
233
|
+
.finally(() => {
|
|
234
|
+
this.active--;
|
|
235
|
+
this.emit('release');
|
|
236
|
+
this.processQueue();
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Create a semaphore for limiting concurrent access
|
|
243
|
+
*/
|
|
244
|
+
export function createSemaphore(maxConcurrent: number): {
|
|
245
|
+
acquire: () => Promise<void>;
|
|
246
|
+
release: () => void;
|
|
247
|
+
available: () => number;
|
|
248
|
+
} {
|
|
249
|
+
let current = 0;
|
|
250
|
+
const waiting: Array<() => void> = [];
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
async acquire(): Promise<void> {
|
|
254
|
+
if (current < maxConcurrent) {
|
|
255
|
+
current++;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return new Promise<void>((resolve) => {
|
|
260
|
+
waiting.push(resolve);
|
|
261
|
+
});
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
release(): void {
|
|
265
|
+
const next = waiting.shift();
|
|
266
|
+
if (next) {
|
|
267
|
+
next();
|
|
268
|
+
} else {
|
|
269
|
+
current = Math.max(0, current - 1);
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
available(): number {
|
|
274
|
+
return maxConcurrent - current;
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit Breaker Pattern
|
|
3
|
+
*
|
|
4
|
+
* Prevents cascading failures by breaking the circuit after failures.
|
|
5
|
+
*
|
|
6
|
+
* @module v3/shared/resilience/circuit-breaker
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { EventEmitter } from 'events';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Circuit breaker states
|
|
13
|
+
*/
|
|
14
|
+
export enum CircuitBreakerState {
|
|
15
|
+
/** Circuit is closed, requests flow normally */
|
|
16
|
+
CLOSED = 'CLOSED',
|
|
17
|
+
|
|
18
|
+
/** Circuit is open, requests are rejected immediately */
|
|
19
|
+
OPEN = 'OPEN',
|
|
20
|
+
|
|
21
|
+
/** Circuit is testing if service recovered */
|
|
22
|
+
HALF_OPEN = 'HALF_OPEN',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Circuit breaker options
|
|
27
|
+
*/
|
|
28
|
+
export interface CircuitBreakerOptions {
|
|
29
|
+
/** Name for identification */
|
|
30
|
+
name: string;
|
|
31
|
+
|
|
32
|
+
/** Failure threshold before opening circuit (default: 5) */
|
|
33
|
+
failureThreshold: number;
|
|
34
|
+
|
|
35
|
+
/** Success threshold in half-open state to close circuit (default: 3) */
|
|
36
|
+
successThreshold: number;
|
|
37
|
+
|
|
38
|
+
/** Time to wait before testing again in ms (default: 30000) */
|
|
39
|
+
timeout: number;
|
|
40
|
+
|
|
41
|
+
/** Time window to track failures in ms (default: 60000) */
|
|
42
|
+
rollingWindow: number;
|
|
43
|
+
|
|
44
|
+
/** Volume threshold - minimum requests before tripping (default: 10) */
|
|
45
|
+
volumeThreshold: number;
|
|
46
|
+
|
|
47
|
+
/** Custom failure detection */
|
|
48
|
+
isFailure?: (error: Error) => boolean;
|
|
49
|
+
|
|
50
|
+
/** Fallback function when circuit is open */
|
|
51
|
+
fallback?: <T>(error: Error) => T | Promise<T>;
|
|
52
|
+
|
|
53
|
+
/** Callback when state changes */
|
|
54
|
+
onStateChange?: (from: CircuitBreakerState, to: CircuitBreakerState) => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Circuit breaker statistics
|
|
59
|
+
*/
|
|
60
|
+
export interface CircuitBreakerStats {
|
|
61
|
+
state: CircuitBreakerState;
|
|
62
|
+
failures: number;
|
|
63
|
+
successes: number;
|
|
64
|
+
totalRequests: number;
|
|
65
|
+
rejectedRequests: number;
|
|
66
|
+
lastFailure: Date | null;
|
|
67
|
+
lastSuccess: Date | null;
|
|
68
|
+
openSince: Date | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Default options
|
|
73
|
+
*/
|
|
74
|
+
const DEFAULT_OPTIONS: Omit<CircuitBreakerOptions, 'name'> = {
|
|
75
|
+
failureThreshold: 5,
|
|
76
|
+
successThreshold: 3,
|
|
77
|
+
timeout: 30000,
|
|
78
|
+
rollingWindow: 60000,
|
|
79
|
+
volumeThreshold: 10,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Request tracking entry
|
|
84
|
+
*/
|
|
85
|
+
interface RequestEntry {
|
|
86
|
+
timestamp: number;
|
|
87
|
+
success: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Circuit Breaker
|
|
92
|
+
*
|
|
93
|
+
* Implements the circuit breaker pattern to prevent cascading failures.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* const breaker = new CircuitBreaker({
|
|
97
|
+
* name: 'external-api',
|
|
98
|
+
* failureThreshold: 5,
|
|
99
|
+
* timeout: 30000,
|
|
100
|
+
* });
|
|
101
|
+
*
|
|
102
|
+
* try {
|
|
103
|
+
* const result = await breaker.execute(() => fetchExternalAPI());
|
|
104
|
+
* } catch (error) {
|
|
105
|
+
* if (error.message === 'Circuit is open') {
|
|
106
|
+
* // Handle circuit open case
|
|
107
|
+
* }
|
|
108
|
+
* }
|
|
109
|
+
*/
|
|
110
|
+
export class CircuitBreaker extends EventEmitter {
|
|
111
|
+
private readonly options: CircuitBreakerOptions;
|
|
112
|
+
private state: CircuitBreakerState = CircuitBreakerState.CLOSED;
|
|
113
|
+
private requests: RequestEntry[] = [];
|
|
114
|
+
private halfOpenSuccesses = 0;
|
|
115
|
+
private openedAt: Date | null = null;
|
|
116
|
+
private lastFailure: Date | null = null;
|
|
117
|
+
private lastSuccess: Date | null = null;
|
|
118
|
+
private rejectedCount = 0;
|
|
119
|
+
private timeoutId?: ReturnType<typeof setTimeout>;
|
|
120
|
+
|
|
121
|
+
constructor(options: CircuitBreakerOptions) {
|
|
122
|
+
super();
|
|
123
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Execute a function through the circuit breaker
|
|
128
|
+
*/
|
|
129
|
+
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
130
|
+
// Clean up old requests
|
|
131
|
+
this.cleanOldRequests();
|
|
132
|
+
|
|
133
|
+
// Check if circuit should be tested
|
|
134
|
+
this.checkState();
|
|
135
|
+
|
|
136
|
+
// If open, reject immediately or use fallback
|
|
137
|
+
if (this.state === CircuitBreakerState.OPEN) {
|
|
138
|
+
this.rejectedCount++;
|
|
139
|
+
const error = new Error(`Circuit breaker '${this.options.name}' is open`);
|
|
140
|
+
|
|
141
|
+
if (this.options.fallback) {
|
|
142
|
+
return this.options.fallback(error);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const result = await fn();
|
|
150
|
+
this.onSuccess();
|
|
151
|
+
return result;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
154
|
+
|
|
155
|
+
// Check if this should be counted as failure
|
|
156
|
+
const isFailure = this.options.isFailure?.(err) ?? true;
|
|
157
|
+
|
|
158
|
+
if (isFailure) {
|
|
159
|
+
this.onFailure(err);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get current state
|
|
168
|
+
*/
|
|
169
|
+
getState(): CircuitBreakerState {
|
|
170
|
+
this.checkState();
|
|
171
|
+
return this.state;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get statistics
|
|
176
|
+
*/
|
|
177
|
+
getStats(): CircuitBreakerStats {
|
|
178
|
+
this.cleanOldRequests();
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
state: this.state,
|
|
182
|
+
failures: this.requests.filter((r) => !r.success).length,
|
|
183
|
+
successes: this.requests.filter((r) => r.success).length,
|
|
184
|
+
totalRequests: this.requests.length,
|
|
185
|
+
rejectedRequests: this.rejectedCount,
|
|
186
|
+
lastFailure: this.lastFailure,
|
|
187
|
+
lastSuccess: this.lastSuccess,
|
|
188
|
+
openSince: this.openedAt,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Force reset the circuit breaker
|
|
194
|
+
*/
|
|
195
|
+
reset(): void {
|
|
196
|
+
const previousState = this.state;
|
|
197
|
+
this.state = CircuitBreakerState.CLOSED;
|
|
198
|
+
this.requests = [];
|
|
199
|
+
this.halfOpenSuccesses = 0;
|
|
200
|
+
this.openedAt = null;
|
|
201
|
+
|
|
202
|
+
if (this.timeoutId) {
|
|
203
|
+
clearTimeout(this.timeoutId);
|
|
204
|
+
this.timeoutId = undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (previousState !== this.state) {
|
|
208
|
+
this.notifyStateChange(previousState, this.state);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Handle successful request
|
|
214
|
+
*/
|
|
215
|
+
private onSuccess(): void {
|
|
216
|
+
this.lastSuccess = new Date();
|
|
217
|
+
this.requests.push({ timestamp: Date.now(), success: true });
|
|
218
|
+
|
|
219
|
+
if (this.state === CircuitBreakerState.HALF_OPEN) {
|
|
220
|
+
this.halfOpenSuccesses++;
|
|
221
|
+
|
|
222
|
+
if (this.halfOpenSuccesses >= this.options.successThreshold) {
|
|
223
|
+
this.transitionTo(CircuitBreakerState.CLOSED);
|
|
224
|
+
this.halfOpenSuccesses = 0;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Handle failed request
|
|
231
|
+
*/
|
|
232
|
+
private onFailure(error: Error): void {
|
|
233
|
+
this.lastFailure = new Date();
|
|
234
|
+
this.requests.push({ timestamp: Date.now(), success: false });
|
|
235
|
+
|
|
236
|
+
if (this.state === CircuitBreakerState.HALF_OPEN) {
|
|
237
|
+
// Failed during half-open, go back to open
|
|
238
|
+
this.transitionTo(CircuitBreakerState.OPEN);
|
|
239
|
+
this.halfOpenSuccesses = 0;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check if we should open the circuit
|
|
244
|
+
const failures = this.requests.filter((r) => !r.success).length;
|
|
245
|
+
const totalRequests = this.requests.length;
|
|
246
|
+
|
|
247
|
+
if (
|
|
248
|
+
totalRequests >= this.options.volumeThreshold &&
|
|
249
|
+
failures >= this.options.failureThreshold
|
|
250
|
+
) {
|
|
251
|
+
this.transitionTo(CircuitBreakerState.OPEN);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Check if state should change based on timeout
|
|
257
|
+
*/
|
|
258
|
+
private checkState(): void {
|
|
259
|
+
if (this.state === CircuitBreakerState.OPEN && this.openedAt) {
|
|
260
|
+
const elapsed = Date.now() - this.openedAt.getTime();
|
|
261
|
+
|
|
262
|
+
if (elapsed >= this.options.timeout) {
|
|
263
|
+
this.transitionTo(CircuitBreakerState.HALF_OPEN);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Transition to new state
|
|
270
|
+
*/
|
|
271
|
+
private transitionTo(newState: CircuitBreakerState): void {
|
|
272
|
+
const previousState = this.state;
|
|
273
|
+
|
|
274
|
+
if (previousState === newState) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
this.state = newState;
|
|
279
|
+
|
|
280
|
+
if (newState === CircuitBreakerState.OPEN) {
|
|
281
|
+
this.openedAt = new Date();
|
|
282
|
+
this.scheduleHalfOpen();
|
|
283
|
+
} else if (newState === CircuitBreakerState.CLOSED) {
|
|
284
|
+
this.openedAt = null;
|
|
285
|
+
this.requests = [];
|
|
286
|
+
|
|
287
|
+
if (this.timeoutId) {
|
|
288
|
+
clearTimeout(this.timeoutId);
|
|
289
|
+
this.timeoutId = undefined;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this.notifyStateChange(previousState, newState);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Schedule transition to half-open
|
|
298
|
+
*/
|
|
299
|
+
private scheduleHalfOpen(): void {
|
|
300
|
+
if (this.timeoutId) {
|
|
301
|
+
clearTimeout(this.timeoutId);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.timeoutId = setTimeout(() => {
|
|
305
|
+
if (this.state === CircuitBreakerState.OPEN) {
|
|
306
|
+
this.transitionTo(CircuitBreakerState.HALF_OPEN);
|
|
307
|
+
}
|
|
308
|
+
}, this.options.timeout);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Notify state change
|
|
313
|
+
*/
|
|
314
|
+
private notifyStateChange(from: CircuitBreakerState, to: CircuitBreakerState): void {
|
|
315
|
+
this.emit('stateChange', { from, to });
|
|
316
|
+
this.options.onStateChange?.(from, to);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Clean old requests outside rolling window
|
|
321
|
+
*/
|
|
322
|
+
private cleanOldRequests(): void {
|
|
323
|
+
const cutoff = Date.now() - this.options.rollingWindow;
|
|
324
|
+
this.requests = this.requests.filter((r) => r.timestamp >= cutoff);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resilience Patterns
|
|
3
|
+
*
|
|
4
|
+
* Production-ready resilience utilities:
|
|
5
|
+
* - Retry with exponential backoff
|
|
6
|
+
* - Circuit breaker pattern
|
|
7
|
+
* - Rate limiting
|
|
8
|
+
*
|
|
9
|
+
* @module v3/shared/resilience
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Retry
|
|
13
|
+
export { retry, RetryError } from './retry.js';
|
|
14
|
+
export type { RetryOptions, RetryResult } from './retry.js';
|
|
15
|
+
|
|
16
|
+
// Circuit Breaker
|
|
17
|
+
export { CircuitBreaker } from './circuit-breaker.js';
|
|
18
|
+
export type { CircuitBreakerOptions, CircuitBreakerStats } from './circuit-breaker.js';
|
|
19
|
+
|
|
20
|
+
// Rate Limiter
|
|
21
|
+
export { SlidingWindowRateLimiter, TokenBucketRateLimiter } from './rate-limiter.js';
|
|
22
|
+
export type { RateLimiter, RateLimiterOptions, RateLimitResult } from './rate-limiter.js';
|
|
23
|
+
|
|
24
|
+
// Bulkhead
|
|
25
|
+
export { Bulkhead } from './bulkhead.js';
|
|
26
|
+
export type { BulkheadOptions, BulkheadStats } from './bulkhead.js';
|