@litmers/cursorflow-orchestrator 0.1.18 → 0.1.26
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/CHANGELOG.md +25 -0
- package/README.md +25 -7
- package/commands/cursorflow-clean.md +19 -0
- package/commands/cursorflow-runs.md +59 -0
- package/commands/cursorflow-stop.md +55 -0
- package/dist/cli/clean.js +178 -6
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.js +12 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +8 -7
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/logs.js +126 -77
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.d.ts +7 -0
- package/dist/cli/monitor.js +1021 -202
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +39 -21
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +268 -163
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +11 -5
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/runs.d.ts +5 -0
- package/dist/cli/runs.js +214 -0
- package/dist/cli/runs.js.map +1 -0
- package/dist/cli/setup-commands.js +0 -0
- package/dist/cli/signal.js +8 -8
- package/dist/cli/signal.js.map +1 -1
- package/dist/cli/stop.d.ts +5 -0
- package/dist/cli/stop.js +215 -0
- package/dist/cli/stop.js.map +1 -0
- package/dist/cli/tasks.d.ts +10 -0
- package/dist/cli/tasks.js +165 -0
- package/dist/cli/tasks.js.map +1 -0
- package/dist/core/auto-recovery.d.ts +212 -0
- package/dist/core/auto-recovery.js +737 -0
- package/dist/core/auto-recovery.js.map +1 -0
- package/dist/core/failure-policy.d.ts +156 -0
- package/dist/core/failure-policy.js +488 -0
- package/dist/core/failure-policy.js.map +1 -0
- package/dist/core/orchestrator.d.ts +16 -2
- package/dist/core/orchestrator.js +439 -105
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +2 -0
- package/dist/core/reviewer.js +2 -0
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +33 -10
- package/dist/core/runner.js +374 -164
- package/dist/core/runner.js.map +1 -1
- package/dist/services/logging/buffer.d.ts +67 -0
- package/dist/services/logging/buffer.js +309 -0
- package/dist/services/logging/buffer.js.map +1 -0
- package/dist/services/logging/console.d.ts +89 -0
- package/dist/services/logging/console.js +169 -0
- package/dist/services/logging/console.js.map +1 -0
- package/dist/services/logging/file-writer.d.ts +71 -0
- package/dist/services/logging/file-writer.js +516 -0
- package/dist/services/logging/file-writer.js.map +1 -0
- package/dist/services/logging/formatter.d.ts +39 -0
- package/dist/services/logging/formatter.js +227 -0
- package/dist/services/logging/formatter.js.map +1 -0
- package/dist/services/logging/index.d.ts +11 -0
- package/dist/services/logging/index.js +30 -0
- package/dist/services/logging/index.js.map +1 -0
- package/dist/services/logging/parser.d.ts +31 -0
- package/dist/services/logging/parser.js +222 -0
- package/dist/services/logging/parser.js.map +1 -0
- package/dist/services/process/index.d.ts +59 -0
- package/dist/services/process/index.js +257 -0
- package/dist/services/process/index.js.map +1 -0
- package/dist/types/agent.d.ts +20 -0
- package/dist/types/agent.js +6 -0
- package/dist/types/agent.js.map +1 -0
- package/dist/types/config.d.ts +65 -0
- package/dist/types/config.js +6 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/events.d.ts +125 -0
- package/dist/types/events.js +6 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.js +37 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/lane.d.ts +43 -0
- package/dist/types/lane.js +6 -0
- package/dist/types/lane.js.map +1 -0
- package/dist/types/logging.d.ts +71 -0
- package/dist/types/logging.js +16 -0
- package/dist/types/logging.js.map +1 -0
- package/dist/types/review.d.ts +17 -0
- package/dist/types/review.js +6 -0
- package/dist/types/review.js.map +1 -0
- package/dist/types/run.d.ts +32 -0
- package/dist/types/run.js +6 -0
- package/dist/types/run.js.map +1 -0
- package/dist/types/task.d.ts +71 -0
- package/dist/types/task.js +6 -0
- package/dist/types/task.js.map +1 -0
- package/dist/ui/components.d.ts +134 -0
- package/dist/ui/components.js +389 -0
- package/dist/ui/components.js.map +1 -0
- package/dist/ui/log-viewer.d.ts +49 -0
- package/dist/ui/log-viewer.js +449 -0
- package/dist/ui/log-viewer.js.map +1 -0
- package/dist/utils/checkpoint.d.ts +87 -0
- package/dist/utils/checkpoint.js +317 -0
- package/dist/utils/checkpoint.js.map +1 -0
- package/dist/utils/config.d.ts +4 -0
- package/dist/utils/config.js +18 -8
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/cursor-agent.js.map +1 -1
- package/dist/utils/dependency.d.ts +74 -0
- package/dist/utils/dependency.js +420 -0
- package/dist/utils/dependency.js.map +1 -0
- package/dist/utils/doctor.js +17 -11
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +10 -33
- package/dist/utils/enhanced-logger.js +108 -20
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +121 -0
- package/dist/utils/git.js +484 -11
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/health.d.ts +91 -0
- package/dist/utils/health.js +556 -0
- package/dist/utils/health.js.map +1 -0
- package/dist/utils/lock.d.ts +95 -0
- package/dist/utils/lock.js +332 -0
- package/dist/utils/lock.js.map +1 -0
- package/dist/utils/log-buffer.d.ts +17 -0
- package/dist/utils/log-buffer.js +14 -0
- package/dist/utils/log-buffer.js.map +1 -0
- package/dist/utils/log-constants.d.ts +23 -0
- package/dist/utils/log-constants.js +28 -0
- package/dist/utils/log-constants.js.map +1 -0
- package/dist/utils/log-formatter.d.ts +25 -0
- package/dist/utils/log-formatter.js +237 -0
- package/dist/utils/log-formatter.js.map +1 -0
- package/dist/utils/log-service.d.ts +19 -0
- package/dist/utils/log-service.js +47 -0
- package/dist/utils/log-service.js.map +1 -0
- package/dist/utils/logger.d.ts +46 -27
- package/dist/utils/logger.js +82 -60
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/path.d.ts +19 -0
- package/dist/utils/path.js +77 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/process-manager.d.ts +21 -0
- package/dist/utils/process-manager.js +138 -0
- package/dist/utils/process-manager.js.map +1 -0
- package/dist/utils/retry.d.ts +121 -0
- package/dist/utils/retry.js +374 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/run-service.d.ts +88 -0
- package/dist/utils/run-service.js +412 -0
- package/dist/utils/run-service.js.map +1 -0
- package/dist/utils/state.d.ts +62 -3
- package/dist/utils/state.js +317 -11
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/task-service.d.ts +82 -0
- package/dist/utils/task-service.js +348 -0
- package/dist/utils/task-service.js.map +1 -0
- package/dist/utils/template.d.ts +14 -0
- package/dist/utils/template.js +122 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/types.d.ts +2 -271
- package/dist/utils/types.js +16 -0
- package/dist/utils/types.js.map +1 -1
- package/package.json +38 -23
- package/scripts/ai-security-check.js +0 -1
- package/scripts/local-security-gate.sh +0 -0
- package/scripts/monitor-lanes.sh +94 -0
- package/scripts/patches/test-cursor-agent.js +0 -1
- package/scripts/release.sh +0 -0
- package/scripts/setup-security.sh +0 -0
- package/scripts/stream-logs.sh +72 -0
- package/scripts/verify-and-fix.sh +0 -0
- package/src/cli/clean.ts +187 -6
- package/src/cli/index.ts +12 -1
- package/src/cli/init.ts +8 -7
- package/src/cli/logs.ts +124 -77
- package/src/cli/monitor.ts +1815 -898
- package/src/cli/prepare.ts +41 -21
- package/src/cli/resume.ts +753 -626
- package/src/cli/run.ts +12 -5
- package/src/cli/runs.ts +212 -0
- package/src/cli/setup-commands.ts +0 -0
- package/src/cli/signal.ts +8 -7
- package/src/cli/stop.ts +209 -0
- package/src/cli/tasks.ts +154 -0
- package/src/core/auto-recovery.ts +909 -0
- package/src/core/failure-policy.ts +592 -0
- package/src/core/orchestrator.ts +1131 -704
- package/src/core/reviewer.ts +4 -0
- package/src/core/runner.ts +444 -180
- package/src/services/logging/buffer.ts +326 -0
- package/src/services/logging/console.ts +193 -0
- package/src/services/logging/file-writer.ts +526 -0
- package/src/services/logging/formatter.ts +268 -0
- package/src/services/logging/index.ts +16 -0
- package/src/services/logging/parser.ts +232 -0
- package/src/services/process/index.ts +261 -0
- package/src/types/agent.ts +24 -0
- package/src/types/config.ts +79 -0
- package/src/types/events.ts +156 -0
- package/src/types/index.ts +29 -0
- package/src/types/lane.ts +56 -0
- package/src/types/logging.ts +96 -0
- package/src/types/review.ts +20 -0
- package/src/types/run.ts +37 -0
- package/src/types/task.ts +79 -0
- package/src/ui/components.ts +430 -0
- package/src/ui/log-viewer.ts +485 -0
- package/src/utils/checkpoint.ts +374 -0
- package/src/utils/config.ts +18 -8
- package/src/utils/cursor-agent.ts +1 -1
- package/src/utils/dependency.ts +482 -0
- package/src/utils/doctor.ts +18 -11
- package/src/utils/enhanced-logger.ts +122 -60
- package/src/utils/git.ts +517 -11
- package/src/utils/health.ts +596 -0
- package/src/utils/lock.ts +346 -0
- package/src/utils/log-buffer.ts +28 -0
- package/src/utils/log-constants.ts +26 -0
- package/src/utils/log-formatter.ts +245 -0
- package/src/utils/log-service.ts +49 -0
- package/src/utils/logger.ts +100 -51
- package/src/utils/path.ts +45 -0
- package/src/utils/process-manager.ts +100 -0
- package/src/utils/retry.ts +413 -0
- package/src/utils/run-service.ts +433 -0
- package/src/utils/state.ts +385 -11
- package/src/utils/task-service.ts +370 -0
- package/src/utils/template.ts +92 -0
- package/src/utils/types.ts +2 -314
- package/templates/basic.json +21 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced retry logic with exponential backoff and circuit breaker
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as logger from './logger';
|
|
6
|
+
|
|
7
|
+
export interface RetryConfig {
|
|
8
|
+
/** Maximum number of retries */
|
|
9
|
+
maxRetries: number;
|
|
10
|
+
/** Base delay in milliseconds */
|
|
11
|
+
baseDelayMs: number;
|
|
12
|
+
/** Maximum delay in milliseconds */
|
|
13
|
+
maxDelayMs: number;
|
|
14
|
+
/** Backoff multiplier */
|
|
15
|
+
backoffFactor: number;
|
|
16
|
+
/** Add random jitter to delays */
|
|
17
|
+
jitter: boolean;
|
|
18
|
+
/** Timeout for each attempt in milliseconds */
|
|
19
|
+
attemptTimeoutMs?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_RETRY_CONFIG: RetryConfig = {
|
|
23
|
+
maxRetries: 3,
|
|
24
|
+
baseDelayMs: 1000,
|
|
25
|
+
maxDelayMs: 30000,
|
|
26
|
+
backoffFactor: 2,
|
|
27
|
+
jitter: true,
|
|
28
|
+
attemptTimeoutMs: undefined,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export interface CircuitBreakerConfig {
|
|
32
|
+
/** Number of consecutive failures before opening circuit */
|
|
33
|
+
failureThreshold: number;
|
|
34
|
+
/** Time in milliseconds before attempting to close circuit */
|
|
35
|
+
resetTimeoutMs: number;
|
|
36
|
+
/** Number of successful calls needed to close circuit */
|
|
37
|
+
successThreshold: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const DEFAULT_CIRCUIT_BREAKER_CONFIG: CircuitBreakerConfig = {
|
|
41
|
+
failureThreshold: 5,
|
|
42
|
+
resetTimeoutMs: 60000,
|
|
43
|
+
successThreshold: 2,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export enum CircuitState {
|
|
47
|
+
CLOSED = 'CLOSED', // Normal operation
|
|
48
|
+
OPEN = 'OPEN', // Blocking calls
|
|
49
|
+
HALF_OPEN = 'HALF_OPEN', // Testing if service recovered
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Circuit breaker implementation for managing service availability
|
|
54
|
+
*/
|
|
55
|
+
export class CircuitBreaker {
|
|
56
|
+
private state: CircuitState = CircuitState.CLOSED;
|
|
57
|
+
private failureCount: number = 0;
|
|
58
|
+
private successCount: number = 0;
|
|
59
|
+
private lastFailureTime: number = 0;
|
|
60
|
+
private readonly config: CircuitBreakerConfig;
|
|
61
|
+
private readonly name: string;
|
|
62
|
+
|
|
63
|
+
constructor(name: string, config: Partial<CircuitBreakerConfig> = {}) {
|
|
64
|
+
this.name = name;
|
|
65
|
+
this.config = { ...DEFAULT_CIRCUIT_BREAKER_CONFIG, ...config };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if circuit allows a call
|
|
70
|
+
*/
|
|
71
|
+
canCall(): boolean {
|
|
72
|
+
if (this.state === CircuitState.CLOSED) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (this.state === CircuitState.OPEN) {
|
|
77
|
+
// Check if enough time has passed to try again
|
|
78
|
+
if (Date.now() - this.lastFailureTime >= this.config.resetTimeoutMs) {
|
|
79
|
+
this.state = CircuitState.HALF_OPEN;
|
|
80
|
+
this.successCount = 0;
|
|
81
|
+
logger.info(`[CircuitBreaker:${this.name}] Transitioning to HALF_OPEN`);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// HALF_OPEN - allow limited calls
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Record a successful call
|
|
93
|
+
*/
|
|
94
|
+
recordSuccess(): void {
|
|
95
|
+
if (this.state === CircuitState.HALF_OPEN) {
|
|
96
|
+
this.successCount++;
|
|
97
|
+
if (this.successCount >= this.config.successThreshold) {
|
|
98
|
+
this.state = CircuitState.CLOSED;
|
|
99
|
+
this.failureCount = 0;
|
|
100
|
+
logger.info(`[CircuitBreaker:${this.name}] Circuit CLOSED after recovery`);
|
|
101
|
+
}
|
|
102
|
+
} else if (this.state === CircuitState.CLOSED) {
|
|
103
|
+
// Reset failure count on success
|
|
104
|
+
this.failureCount = Math.max(0, this.failureCount - 1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Record a failed call
|
|
110
|
+
*/
|
|
111
|
+
recordFailure(): void {
|
|
112
|
+
this.failureCount++;
|
|
113
|
+
this.lastFailureTime = Date.now();
|
|
114
|
+
|
|
115
|
+
if (this.state === CircuitState.HALF_OPEN) {
|
|
116
|
+
// Immediately open circuit on failure in half-open state
|
|
117
|
+
this.state = CircuitState.OPEN;
|
|
118
|
+
logger.warn(`[CircuitBreaker:${this.name}] Circuit OPEN (failure in half-open state)`);
|
|
119
|
+
} else if (this.state === CircuitState.CLOSED && this.failureCount >= this.config.failureThreshold) {
|
|
120
|
+
this.state = CircuitState.OPEN;
|
|
121
|
+
logger.warn(`[CircuitBreaker:${this.name}] Circuit OPEN (threshold reached: ${this.failureCount} failures)`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get current circuit state
|
|
127
|
+
*/
|
|
128
|
+
getState(): CircuitState {
|
|
129
|
+
return this.state;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get time until circuit might close (for OPEN state)
|
|
134
|
+
*/
|
|
135
|
+
getTimeUntilRetry(): number {
|
|
136
|
+
if (this.state !== CircuitState.OPEN) {
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
return Math.max(0, this.config.resetTimeoutMs - (Date.now() - this.lastFailureTime));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Force reset the circuit breaker
|
|
144
|
+
*/
|
|
145
|
+
reset(): void {
|
|
146
|
+
this.state = CircuitState.CLOSED;
|
|
147
|
+
this.failureCount = 0;
|
|
148
|
+
this.successCount = 0;
|
|
149
|
+
this.lastFailureTime = 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get circuit breaker status for monitoring
|
|
154
|
+
*/
|
|
155
|
+
getStatus(): { state: CircuitState; failureCount: number; timeUntilRetry: number } {
|
|
156
|
+
return {
|
|
157
|
+
state: this.state,
|
|
158
|
+
failureCount: this.failureCount,
|
|
159
|
+
timeUntilRetry: this.getTimeUntilRetry(),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Global circuit breaker registry
|
|
166
|
+
*/
|
|
167
|
+
const circuitBreakers = new Map<string, CircuitBreaker>();
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get or create a circuit breaker by name
|
|
171
|
+
*/
|
|
172
|
+
export function getCircuitBreaker(name: string, config?: Partial<CircuitBreakerConfig>): CircuitBreaker {
|
|
173
|
+
let breaker = circuitBreakers.get(name);
|
|
174
|
+
if (!breaker) {
|
|
175
|
+
breaker = new CircuitBreaker(name, config);
|
|
176
|
+
circuitBreakers.set(name, breaker);
|
|
177
|
+
}
|
|
178
|
+
return breaker;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Calculate delay with exponential backoff and optional jitter
|
|
183
|
+
*/
|
|
184
|
+
export function calculateDelay(attempt: number, config: RetryConfig): number {
|
|
185
|
+
let delay = config.baseDelayMs * Math.pow(config.backoffFactor, attempt);
|
|
186
|
+
delay = Math.min(delay, config.maxDelayMs);
|
|
187
|
+
|
|
188
|
+
if (config.jitter) {
|
|
189
|
+
// Add random jitter of ±25%
|
|
190
|
+
const jitterRange = delay * 0.25;
|
|
191
|
+
delay = delay - jitterRange + Math.random() * jitterRange * 2;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return Math.round(delay);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Sleep for specified milliseconds
|
|
199
|
+
*/
|
|
200
|
+
export function sleep(ms: number): Promise<void> {
|
|
201
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Wrap a promise with timeout
|
|
206
|
+
*/
|
|
207
|
+
export function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message?: string): Promise<T> {
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
const timer = setTimeout(() => {
|
|
210
|
+
reject(new Error(message || `Operation timed out after ${timeoutMs}ms`));
|
|
211
|
+
}, timeoutMs);
|
|
212
|
+
|
|
213
|
+
promise
|
|
214
|
+
.then(result => {
|
|
215
|
+
clearTimeout(timer);
|
|
216
|
+
resolve(result);
|
|
217
|
+
})
|
|
218
|
+
.catch(error => {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
reject(error);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface RetryResult<T> {
|
|
226
|
+
success: boolean;
|
|
227
|
+
result?: T;
|
|
228
|
+
error?: Error;
|
|
229
|
+
attempts: number;
|
|
230
|
+
totalTimeMs: number;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Execute a function with enhanced retry logic
|
|
235
|
+
*/
|
|
236
|
+
export async function withEnhancedRetry<T>(
|
|
237
|
+
fn: () => Promise<T>,
|
|
238
|
+
options: {
|
|
239
|
+
config?: Partial<RetryConfig>;
|
|
240
|
+
circuitBreaker?: CircuitBreaker | string;
|
|
241
|
+
shouldRetry?: (error: Error, attempt: number) => boolean;
|
|
242
|
+
onRetry?: (error: Error, attempt: number, delayMs: number) => void;
|
|
243
|
+
label?: string;
|
|
244
|
+
} = {}
|
|
245
|
+
): Promise<RetryResult<T>> {
|
|
246
|
+
const config = { ...DEFAULT_RETRY_CONFIG, ...options.config };
|
|
247
|
+
const label = options.label || 'operation';
|
|
248
|
+
const startTime = Date.now();
|
|
249
|
+
|
|
250
|
+
// Get circuit breaker if specified
|
|
251
|
+
let breaker: CircuitBreaker | undefined;
|
|
252
|
+
if (options.circuitBreaker) {
|
|
253
|
+
breaker = typeof options.circuitBreaker === 'string'
|
|
254
|
+
? getCircuitBreaker(options.circuitBreaker)
|
|
255
|
+
: options.circuitBreaker;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check circuit breaker before starting
|
|
259
|
+
if (breaker && !breaker.canCall()) {
|
|
260
|
+
const waitTime = breaker.getTimeUntilRetry();
|
|
261
|
+
return {
|
|
262
|
+
success: false,
|
|
263
|
+
error: new Error(`Circuit breaker is OPEN. Retry in ${Math.round(waitTime / 1000)}s`),
|
|
264
|
+
attempts: 0,
|
|
265
|
+
totalTimeMs: Date.now() - startTime,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let lastError: Error | undefined;
|
|
270
|
+
|
|
271
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
272
|
+
try {
|
|
273
|
+
// Execute with optional timeout
|
|
274
|
+
let result: T;
|
|
275
|
+
if (config.attemptTimeoutMs) {
|
|
276
|
+
result = await withTimeout(fn(), config.attemptTimeoutMs, `${label} timed out`);
|
|
277
|
+
} else {
|
|
278
|
+
result = await fn();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Success - record in circuit breaker
|
|
282
|
+
if (breaker) {
|
|
283
|
+
breaker.recordSuccess();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
success: true,
|
|
288
|
+
result,
|
|
289
|
+
attempts: attempt + 1,
|
|
290
|
+
totalTimeMs: Date.now() - startTime,
|
|
291
|
+
};
|
|
292
|
+
} catch (error: any) {
|
|
293
|
+
lastError = error;
|
|
294
|
+
|
|
295
|
+
// Record failure in circuit breaker
|
|
296
|
+
if (breaker) {
|
|
297
|
+
breaker.recordFailure();
|
|
298
|
+
if (!breaker.canCall()) {
|
|
299
|
+
logger.warn(`[${label}] Circuit breaker opened, stopping retries`);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Check if we should retry
|
|
305
|
+
if (options.shouldRetry && !options.shouldRetry(error, attempt)) {
|
|
306
|
+
logger.warn(`[${label}] Error is not retryable: ${error.message}`);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Check if we have more retries
|
|
311
|
+
if (attempt >= config.maxRetries) {
|
|
312
|
+
logger.error(`[${label}] All ${config.maxRetries + 1} attempts failed`);
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Calculate delay
|
|
317
|
+
const delay = calculateDelay(attempt, config);
|
|
318
|
+
|
|
319
|
+
// Notify on retry
|
|
320
|
+
if (options.onRetry) {
|
|
321
|
+
options.onRetry(error, attempt, delay);
|
|
322
|
+
} else {
|
|
323
|
+
logger.warn(`[${label}] Attempt ${attempt + 1} failed: ${error.message}. Retrying in ${delay}ms...`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
await sleep(delay);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
success: false,
|
|
332
|
+
error: lastError,
|
|
333
|
+
attempts: config.maxRetries + 1,
|
|
334
|
+
totalTimeMs: Date.now() - startTime,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Check if an error is transient and worth retrying
|
|
340
|
+
*/
|
|
341
|
+
export function isTransientError(error: Error): boolean {
|
|
342
|
+
const message = error.message.toLowerCase();
|
|
343
|
+
|
|
344
|
+
// Network errors
|
|
345
|
+
if (message.includes('econnreset') ||
|
|
346
|
+
message.includes('econnrefused') ||
|
|
347
|
+
message.includes('etimedout') ||
|
|
348
|
+
message.includes('enotfound') ||
|
|
349
|
+
message.includes('socket hang up')) {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Service unavailable
|
|
354
|
+
if (message.includes('unavailable') ||
|
|
355
|
+
message.includes('503') ||
|
|
356
|
+
message.includes('502') ||
|
|
357
|
+
message.includes('504')) {
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Rate limiting (should retry after delay)
|
|
362
|
+
if (message.includes('rate limit') ||
|
|
363
|
+
message.includes('429') ||
|
|
364
|
+
message.includes('too many requests')) {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Temporary server errors
|
|
369
|
+
if (message.includes('internal server error') ||
|
|
370
|
+
message.includes('500') ||
|
|
371
|
+
message.includes('temporarily')) {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Check if an error is permanent and should not be retried
|
|
380
|
+
*/
|
|
381
|
+
export function isPermanentError(error: Error): boolean {
|
|
382
|
+
const message = error.message.toLowerCase();
|
|
383
|
+
|
|
384
|
+
// Authentication errors
|
|
385
|
+
if (message.includes('unauthorized') ||
|
|
386
|
+
message.includes('401') ||
|
|
387
|
+
message.includes('forbidden') ||
|
|
388
|
+
message.includes('403') ||
|
|
389
|
+
message.includes('not authenticated')) {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Configuration/setup errors
|
|
394
|
+
if (message.includes('not found') ||
|
|
395
|
+
message.includes('404') ||
|
|
396
|
+
message.includes('invalid configuration') ||
|
|
397
|
+
message.includes('invalid api key')) {
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Default retry predicate
|
|
406
|
+
*/
|
|
407
|
+
export function defaultShouldRetry(error: Error, attempt: number): boolean {
|
|
408
|
+
if (isPermanentError(error)) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
return isTransientError(error);
|
|
412
|
+
}
|
|
413
|
+
|