sol-trade-sdk 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 +390 -0
- package/dist/chunk-MMQAMIKR.mjs +3735 -0
- package/dist/chunk-NEZDFAYA.mjs +7744 -0
- package/dist/clients-VITWK7B6.mjs +1370 -0
- package/dist/index-1BK_FXsW.d.mts +2327 -0
- package/dist/index-1BK_FXsW.d.ts +2327 -0
- package/dist/index.d.mts +2659 -0
- package/dist/index.d.ts +2659 -0
- package/dist/index.js +13265 -0
- package/dist/index.mjs +562 -0
- package/dist/perf/index.d.mts +2 -0
- package/dist/perf/index.d.ts +2 -0
- package/dist/perf/index.js +3742 -0
- package/dist/perf/index.mjs +214 -0
- package/package.json +101 -0
- package/src/__tests__/complete_sdk.test.ts +354 -0
- package/src/__tests__/hotpath.test.ts +486 -0
- package/src/__tests__/nonce.test.ts +45 -0
- package/src/__tests__/sdk.test.ts +425 -0
- package/src/address-lookup/index.ts +197 -0
- package/src/cache/cache.ts +308 -0
- package/src/calc/index.ts +1058 -0
- package/src/calc/pumpfun.ts +124 -0
- package/src/common/bonding_curve.ts +272 -0
- package/src/common/compute-budget.ts +148 -0
- package/src/common/confirm-any-signature.ts +184 -0
- package/src/common/fast-timing.ts +481 -0
- package/src/common/fast_fn.ts +150 -0
- package/src/common/gas-fee-strategy.ts +253 -0
- package/src/common/map-pool.ts +23 -0
- package/src/common/nonce.ts +40 -0
- package/src/common/sdk-log.ts +460 -0
- package/src/common/seed.ts +381 -0
- package/src/common/spl-token.ts +578 -0
- package/src/common/subscription-handle.ts +644 -0
- package/src/common/trading-utils.ts +239 -0
- package/src/common/wsol-manager.ts +325 -0
- package/src/compute/compute_budget_manager.ts +187 -0
- package/src/compute/index.ts +21 -0
- package/src/constants/index.ts +96 -0
- package/src/execution/execution.ts +532 -0
- package/src/execution/index.ts +42 -0
- package/src/hotpath/executor.ts +464 -0
- package/src/hotpath/index.ts +64 -0
- package/src/hotpath/state.ts +435 -0
- package/src/index.ts +2117 -0
- package/src/instruction/bonk_builder.ts +730 -0
- package/src/instruction/index.ts +24 -0
- package/src/instruction/meteora_damm_v2_builder.ts +509 -0
- package/src/instruction/pumpfun_builder.ts +1183 -0
- package/src/instruction/pumpswap.ts +1123 -0
- package/src/instruction/raydium_amm_v4_builder.ts +692 -0
- package/src/instruction/raydium_cpmm_builder.ts +795 -0
- package/src/middleware/traits.ts +407 -0
- package/src/params/index.ts +483 -0
- package/src/perf/compiler-optimization.ts +529 -0
- package/src/perf/hardware.ts +631 -0
- package/src/perf/index.ts +9 -0
- package/src/perf/kernel-bypass.ts +656 -0
- package/src/perf/protocol.ts +682 -0
- package/src/perf/realtime.ts +592 -0
- package/src/perf/simd.ts +668 -0
- package/src/perf/syscall-bypass.ts +331 -0
- package/src/perf/ultra-low-latency.ts +505 -0
- package/src/perf/zero-copy.ts +589 -0
- package/src/pool/pool.ts +294 -0
- package/src/rpc/client.ts +345 -0
- package/src/sdk-errors.ts +13 -0
- package/src/security/index.ts +26 -0
- package/src/security/secure-key.ts +303 -0
- package/src/security/validators.ts +281 -0
- package/src/seed/pda.ts +262 -0
- package/src/serialization/index.ts +28 -0
- package/src/serialization/serialization.ts +288 -0
- package/src/swqos/clients.ts +1754 -0
- package/src/swqos/index.ts +50 -0
- package/src/swqos/providers.ts +1707 -0
- package/src/trading/core/async-executor.ts +702 -0
- package/src/trading/core/confirmation-monitor.ts +711 -0
- package/src/trading/core/index.ts +82 -0
- package/src/trading/core/retry-handler.ts +683 -0
- package/src/trading/core/transaction-pool.ts +780 -0
- package/src/trading/executor.ts +385 -0
- package/src/trading/factory.ts +282 -0
- package/src/trading/index.ts +30 -0
- package/src/types.ts +8 -0
- package/src/utils/index.ts +155 -0
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry Handler for Sol Trade SDK
|
|
3
|
+
* Implements retry strategies with circuit breaker pattern.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { TradeError } from '../../index';
|
|
7
|
+
|
|
8
|
+
// ===== Types =====
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Retry strategy types
|
|
12
|
+
*/
|
|
13
|
+
export enum RetryStrategy {
|
|
14
|
+
/** No retry - fail immediately */
|
|
15
|
+
None = 'None',
|
|
16
|
+
/** Fixed delay between retries */
|
|
17
|
+
Fixed = 'Fixed',
|
|
18
|
+
/** Linear increasing delay */
|
|
19
|
+
Linear = 'Linear',
|
|
20
|
+
/** Exponential backoff */
|
|
21
|
+
Exponential = 'Exponential',
|
|
22
|
+
/** Exponential backoff with jitter */
|
|
23
|
+
ExponentialJitter = 'ExponentialJitter',
|
|
24
|
+
/** Custom retry strategy */
|
|
25
|
+
Custom = 'Custom',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Configuration for retry behavior
|
|
30
|
+
*/
|
|
31
|
+
export interface RetryConfig {
|
|
32
|
+
/** Maximum number of retry attempts */
|
|
33
|
+
maxAttempts: number;
|
|
34
|
+
/** Initial delay between retries in milliseconds */
|
|
35
|
+
initialDelayMs: number;
|
|
36
|
+
/** Maximum delay between retries in milliseconds */
|
|
37
|
+
maxDelayMs: number;
|
|
38
|
+
/** Multiplier for exponential backoff */
|
|
39
|
+
backoffMultiplier: number;
|
|
40
|
+
/** Retry strategy to use */
|
|
41
|
+
strategy: RetryStrategy;
|
|
42
|
+
/** Whether to retry on all errors or specific ones */
|
|
43
|
+
retryAllErrors: boolean;
|
|
44
|
+
/** Specific error codes to retry on */
|
|
45
|
+
retryableErrorCodes: number[];
|
|
46
|
+
/** Specific error messages to retry on */
|
|
47
|
+
retryableErrorMessages: string[];
|
|
48
|
+
/** Whether to retry on timeout */
|
|
49
|
+
retryOnTimeout: boolean;
|
|
50
|
+
/** Timeout for each attempt in milliseconds */
|
|
51
|
+
attemptTimeoutMs: number;
|
|
52
|
+
/** Callback before each retry attempt */
|
|
53
|
+
onRetry?: (attempt: number, error: Error, nextDelayMs: number) => void;
|
|
54
|
+
/** Callback when all attempts exhausted */
|
|
55
|
+
onExhausted?: (lastError: Error, attempts: number) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Result of a retry operation
|
|
60
|
+
*/
|
|
61
|
+
export interface RetryResult<T> {
|
|
62
|
+
/** Whether the operation succeeded */
|
|
63
|
+
success: boolean;
|
|
64
|
+
/** Result value if succeeded */
|
|
65
|
+
value?: T;
|
|
66
|
+
/** Error if failed */
|
|
67
|
+
error?: Error;
|
|
68
|
+
/** Number of attempts made */
|
|
69
|
+
attempts: number;
|
|
70
|
+
/** Total time elapsed in milliseconds */
|
|
71
|
+
totalTimeMs: number;
|
|
72
|
+
/** Whether the result was from cache */
|
|
73
|
+
fromCache?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Circuit breaker states
|
|
78
|
+
*/
|
|
79
|
+
export enum CircuitState {
|
|
80
|
+
/** Normal operation - requests allowed */
|
|
81
|
+
Closed = 'Closed',
|
|
82
|
+
/** Failure threshold reached - requests blocked */
|
|
83
|
+
Open = 'Open',
|
|
84
|
+
/** Testing if service has recovered */
|
|
85
|
+
HalfOpen = 'HalfOpen',
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Circuit breaker configuration
|
|
90
|
+
*/
|
|
91
|
+
export interface CircuitBreakerConfig {
|
|
92
|
+
/** Number of failures before opening circuit */
|
|
93
|
+
failureThreshold: number;
|
|
94
|
+
/** Time to wait before attempting reset in milliseconds */
|
|
95
|
+
resetTimeoutMs: number;
|
|
96
|
+
/** Number of successes required to close circuit from half-open */
|
|
97
|
+
successThreshold: number;
|
|
98
|
+
/** Time window for counting failures in milliseconds */
|
|
99
|
+
failureWindowMs: number;
|
|
100
|
+
/** Half-open request probability (0-1) */
|
|
101
|
+
halfOpenProbability: number;
|
|
102
|
+
/** Callback when circuit opens */
|
|
103
|
+
onOpen?: () => void;
|
|
104
|
+
/** Callback when circuit closes */
|
|
105
|
+
onClose?: () => void;
|
|
106
|
+
/** Callback when circuit enters half-open */
|
|
107
|
+
onHalfOpen?: () => void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Circuit breaker statistics
|
|
112
|
+
*/
|
|
113
|
+
export interface CircuitStats {
|
|
114
|
+
/** Current circuit state */
|
|
115
|
+
state: CircuitState;
|
|
116
|
+
/** Total requests made */
|
|
117
|
+
totalRequests: number;
|
|
118
|
+
/** Total successful requests */
|
|
119
|
+
successfulRequests: number;
|
|
120
|
+
/** Total failed requests */
|
|
121
|
+
failedRequests: number;
|
|
122
|
+
/** Current consecutive failures */
|
|
123
|
+
consecutiveFailures: number;
|
|
124
|
+
/** Last failure timestamp */
|
|
125
|
+
lastFailureTime?: number;
|
|
126
|
+
/** Last success timestamp */
|
|
127
|
+
lastSuccessTime?: number;
|
|
128
|
+
/** Circuit open timestamp */
|
|
129
|
+
circuitOpenTime?: number;
|
|
130
|
+
/** Current failure rate (0-1) */
|
|
131
|
+
failureRate: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ===== Default Configurations =====
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get default retry configuration
|
|
138
|
+
*/
|
|
139
|
+
export function defaultRetryConfig(): RetryConfig {
|
|
140
|
+
return {
|
|
141
|
+
maxAttempts: 3,
|
|
142
|
+
initialDelayMs: 100,
|
|
143
|
+
maxDelayMs: 10000,
|
|
144
|
+
backoffMultiplier: 2,
|
|
145
|
+
strategy: RetryStrategy.Exponential,
|
|
146
|
+
retryAllErrors: false,
|
|
147
|
+
retryableErrorCodes: [429, 500, 502, 503, 504],
|
|
148
|
+
retryableErrorMessages: ['timeout', 'rate limit', 'temporarily unavailable'],
|
|
149
|
+
retryOnTimeout: true,
|
|
150
|
+
attemptTimeoutMs: 30000,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get aggressive retry configuration
|
|
156
|
+
*/
|
|
157
|
+
export function aggressiveRetryConfig(): RetryConfig {
|
|
158
|
+
return {
|
|
159
|
+
maxAttempts: 5,
|
|
160
|
+
initialDelayMs: 50,
|
|
161
|
+
maxDelayMs: 5000,
|
|
162
|
+
backoffMultiplier: 1.5,
|
|
163
|
+
strategy: RetryStrategy.ExponentialJitter,
|
|
164
|
+
retryAllErrors: true,
|
|
165
|
+
retryableErrorCodes: [],
|
|
166
|
+
retryableErrorMessages: [],
|
|
167
|
+
retryOnTimeout: true,
|
|
168
|
+
attemptTimeoutMs: 15000,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get conservative retry configuration
|
|
174
|
+
*/
|
|
175
|
+
export function conservativeRetryConfig(): RetryConfig {
|
|
176
|
+
return {
|
|
177
|
+
maxAttempts: 2,
|
|
178
|
+
initialDelayMs: 500,
|
|
179
|
+
maxDelayMs: 30000,
|
|
180
|
+
backoffMultiplier: 2,
|
|
181
|
+
strategy: RetryStrategy.Exponential,
|
|
182
|
+
retryAllErrors: false,
|
|
183
|
+
retryableErrorCodes: [503, 504],
|
|
184
|
+
retryableErrorMessages: ['service unavailable'],
|
|
185
|
+
retryOnTimeout: false,
|
|
186
|
+
attemptTimeoutMs: 60000,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get default circuit breaker configuration
|
|
192
|
+
*/
|
|
193
|
+
export function defaultCircuitBreakerConfig(): CircuitBreakerConfig {
|
|
194
|
+
return {
|
|
195
|
+
failureThreshold: 5,
|
|
196
|
+
resetTimeoutMs: 30000,
|
|
197
|
+
successThreshold: 2,
|
|
198
|
+
failureWindowMs: 60000,
|
|
199
|
+
halfOpenProbability: 0.5,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ===== Exponential Backoff =====
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Calculates delays for exponential backoff strategies
|
|
207
|
+
*/
|
|
208
|
+
export class ExponentialBackoff {
|
|
209
|
+
constructor(private config: RetryConfig) {}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Calculate delay for a specific attempt
|
|
213
|
+
*/
|
|
214
|
+
calculateDelay(attempt: number): number {
|
|
215
|
+
switch (this.config.strategy) {
|
|
216
|
+
case RetryStrategy.None:
|
|
217
|
+
return 0;
|
|
218
|
+
case RetryStrategy.Fixed:
|
|
219
|
+
return this.config.initialDelayMs;
|
|
220
|
+
case RetryStrategy.Linear:
|
|
221
|
+
return Math.min(
|
|
222
|
+
this.config.initialDelayMs * attempt,
|
|
223
|
+
this.config.maxDelayMs
|
|
224
|
+
);
|
|
225
|
+
case RetryStrategy.Exponential:
|
|
226
|
+
return Math.min(
|
|
227
|
+
this.config.initialDelayMs * Math.pow(this.config.backoffMultiplier, attempt - 1),
|
|
228
|
+
this.config.maxDelayMs
|
|
229
|
+
);
|
|
230
|
+
case RetryStrategy.ExponentialJitter:
|
|
231
|
+
const baseDelay = Math.min(
|
|
232
|
+
this.config.initialDelayMs * Math.pow(this.config.backoffMultiplier, attempt - 1),
|
|
233
|
+
this.config.maxDelayMs
|
|
234
|
+
);
|
|
235
|
+
// Add random jitter (±25%)
|
|
236
|
+
const jitter = baseDelay * 0.25 * (Math.random() * 2 - 1);
|
|
237
|
+
return Math.max(0, baseDelay + jitter);
|
|
238
|
+
default:
|
|
239
|
+
return this.config.initialDelayMs;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Calculate all delays for all attempts
|
|
245
|
+
*/
|
|
246
|
+
calculateAllDelays(): number[] {
|
|
247
|
+
const delays: number[] = [];
|
|
248
|
+
for (let i = 1; i <= this.config.maxAttempts; i++) {
|
|
249
|
+
delays.push(this.calculateDelay(i));
|
|
250
|
+
}
|
|
251
|
+
return delays;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get total estimated time for all retries
|
|
256
|
+
*/
|
|
257
|
+
getTotalEstimatedTime(): number {
|
|
258
|
+
const delays = this.calculateAllDelays();
|
|
259
|
+
return delays.reduce((sum, delay) => sum + delay, 0);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ===== Circuit Breaker =====
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Circuit breaker implementation for fault tolerance
|
|
267
|
+
*/
|
|
268
|
+
export class CircuitBreaker {
|
|
269
|
+
private state: CircuitState = CircuitState.Closed;
|
|
270
|
+
private stats: CircuitStats;
|
|
271
|
+
private failureTimes: number[] = [];
|
|
272
|
+
private consecutiveSuccesses: number = 0;
|
|
273
|
+
private halfOpenAttempts: number = 0;
|
|
274
|
+
|
|
275
|
+
constructor(private config: CircuitBreakerConfig = defaultCircuitBreakerConfig()) {
|
|
276
|
+
this.stats = this.initializeStats();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Execute a function with circuit breaker protection
|
|
281
|
+
*/
|
|
282
|
+
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
283
|
+
if (!this.canExecute()) {
|
|
284
|
+
throw new TradeError(503, 'Circuit breaker is open');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const result = await fn();
|
|
289
|
+
this.recordSuccess();
|
|
290
|
+
return result;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
this.recordFailure();
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Check if execution is allowed
|
|
299
|
+
*/
|
|
300
|
+
canExecute(): boolean {
|
|
301
|
+
this.updateState();
|
|
302
|
+
|
|
303
|
+
switch (this.state) {
|
|
304
|
+
case CircuitState.Closed:
|
|
305
|
+
return true;
|
|
306
|
+
case CircuitState.Open:
|
|
307
|
+
return false;
|
|
308
|
+
case CircuitState.HalfOpen:
|
|
309
|
+
// Allow some requests through in half-open state
|
|
310
|
+
return Math.random() < this.config.halfOpenProbability;
|
|
311
|
+
default:
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Force open the circuit
|
|
318
|
+
*/
|
|
319
|
+
forceOpen(): void {
|
|
320
|
+
this.transitionTo(CircuitState.Open);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Force close the circuit
|
|
325
|
+
*/
|
|
326
|
+
forceClose(): void {
|
|
327
|
+
this.transitionTo(CircuitState.Closed);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get current circuit state
|
|
332
|
+
*/
|
|
333
|
+
getState(): CircuitState {
|
|
334
|
+
this.updateState();
|
|
335
|
+
return this.state;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get circuit statistics
|
|
340
|
+
*/
|
|
341
|
+
getStats(): CircuitStats {
|
|
342
|
+
this.updateStats();
|
|
343
|
+
return { ...this.stats };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Reset circuit statistics
|
|
348
|
+
*/
|
|
349
|
+
reset(): void {
|
|
350
|
+
this.state = CircuitState.Closed;
|
|
351
|
+
this.stats = this.initializeStats();
|
|
352
|
+
this.failureTimes = [];
|
|
353
|
+
this.consecutiveSuccesses = 0;
|
|
354
|
+
this.halfOpenAttempts = 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private recordSuccess(): void {
|
|
358
|
+
this.stats.totalRequests++;
|
|
359
|
+
this.stats.successfulRequests++;
|
|
360
|
+
this.stats.lastSuccessTime = Date.now();
|
|
361
|
+
this.consecutiveSuccesses++;
|
|
362
|
+
|
|
363
|
+
if (this.state === CircuitState.HalfOpen) {
|
|
364
|
+
if (this.consecutiveSuccesses >= this.config.successThreshold) {
|
|
365
|
+
this.transitionTo(CircuitState.Closed);
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
this.stats.consecutiveFailures = 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.updateStats();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private recordFailure(): void {
|
|
375
|
+
const now = Date.now();
|
|
376
|
+
this.stats.totalRequests++;
|
|
377
|
+
this.stats.failedRequests++;
|
|
378
|
+
this.stats.consecutiveFailures++;
|
|
379
|
+
this.stats.lastFailureTime = now;
|
|
380
|
+
this.consecutiveSuccesses = 0;
|
|
381
|
+
|
|
382
|
+
this.failureTimes.push(now);
|
|
383
|
+
this.cleanupOldFailures();
|
|
384
|
+
|
|
385
|
+
if (this.state === CircuitState.HalfOpen) {
|
|
386
|
+
this.transitionTo(CircuitState.Open);
|
|
387
|
+
} else if (this.failureTimes.length >= this.config.failureThreshold) {
|
|
388
|
+
this.transitionTo(CircuitState.Open);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
this.updateStats();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private updateState(): void {
|
|
395
|
+
if (this.state === CircuitState.Open) {
|
|
396
|
+
const timeSinceOpen = Date.now() - (this.stats.circuitOpenTime || 0);
|
|
397
|
+
if (timeSinceOpen >= this.config.resetTimeoutMs) {
|
|
398
|
+
this.transitionTo(CircuitState.HalfOpen);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private transitionTo(newState: CircuitState): void {
|
|
404
|
+
const oldState = this.state;
|
|
405
|
+
this.state = newState;
|
|
406
|
+
|
|
407
|
+
if (newState === CircuitState.Open) {
|
|
408
|
+
this.stats.circuitOpenTime = Date.now();
|
|
409
|
+
if (this.config.onOpen) this.config.onOpen();
|
|
410
|
+
} else if (newState === CircuitState.Closed) {
|
|
411
|
+
this.consecutiveSuccesses = 0;
|
|
412
|
+
this.failureTimes = [];
|
|
413
|
+
this.stats.consecutiveFailures = 0;
|
|
414
|
+
if (this.config.onClose) this.config.onClose();
|
|
415
|
+
} else if (newState === CircuitState.HalfOpen) {
|
|
416
|
+
this.halfOpenAttempts = 0;
|
|
417
|
+
if (this.config.onHalfOpen) this.config.onHalfOpen();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private cleanupOldFailures(): void {
|
|
422
|
+
const cutoff = Date.now() - this.config.failureWindowMs;
|
|
423
|
+
this.failureTimes = this.failureTimes.filter(time => time > cutoff);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private updateStats(): void {
|
|
427
|
+
const totalRequests = this.stats.totalRequests;
|
|
428
|
+
this.stats.failureRate = totalRequests > 0
|
|
429
|
+
? this.stats.failedRequests / totalRequests
|
|
430
|
+
: 0;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private initializeStats(): CircuitStats {
|
|
434
|
+
return {
|
|
435
|
+
state: CircuitState.Closed,
|
|
436
|
+
totalRequests: 0,
|
|
437
|
+
successfulRequests: 0,
|
|
438
|
+
failedRequests: 0,
|
|
439
|
+
consecutiveFailures: 0,
|
|
440
|
+
failureRate: 0,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ===== Retry Handler =====
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Handles retry logic with configurable strategies
|
|
449
|
+
*/
|
|
450
|
+
export class RetryHandler {
|
|
451
|
+
private backoff: ExponentialBackoff;
|
|
452
|
+
private circuitBreaker?: CircuitBreaker;
|
|
453
|
+
|
|
454
|
+
constructor(
|
|
455
|
+
private config: RetryConfig = defaultRetryConfig(),
|
|
456
|
+
circuitBreakerConfig?: CircuitBreakerConfig
|
|
457
|
+
) {
|
|
458
|
+
this.backoff = new ExponentialBackoff(config);
|
|
459
|
+
if (circuitBreakerConfig) {
|
|
460
|
+
this.circuitBreaker = new CircuitBreaker(circuitBreakerConfig);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Execute a function with retry logic
|
|
466
|
+
*/
|
|
467
|
+
async execute<T>(fn: () => Promise<T>): Promise<RetryResult<T>> {
|
|
468
|
+
const startTime = Date.now();
|
|
469
|
+
|
|
470
|
+
// Check circuit breaker
|
|
471
|
+
if (this.circuitBreaker && !this.circuitBreaker.canExecute()) {
|
|
472
|
+
return {
|
|
473
|
+
success: false,
|
|
474
|
+
error: new TradeError(503, 'Circuit breaker is open'),
|
|
475
|
+
attempts: 0,
|
|
476
|
+
totalTimeMs: Date.now() - startTime,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
let lastError: Error | undefined;
|
|
481
|
+
|
|
482
|
+
for (let attempt = 1; attempt <= this.config.maxAttempts; attempt++) {
|
|
483
|
+
try {
|
|
484
|
+
let result: T;
|
|
485
|
+
|
|
486
|
+
if (this.circuitBreaker) {
|
|
487
|
+
result = await this.circuitBreaker.execute(fn);
|
|
488
|
+
} else {
|
|
489
|
+
result = await fn();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
success: true,
|
|
494
|
+
value: result,
|
|
495
|
+
attempts: attempt,
|
|
496
|
+
totalTimeMs: Date.now() - startTime,
|
|
497
|
+
};
|
|
498
|
+
} catch (error) {
|
|
499
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
500
|
+
|
|
501
|
+
// Check if we should retry this error
|
|
502
|
+
if (!this.shouldRetry(lastError, attempt)) {
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Calculate delay for next attempt
|
|
507
|
+
if (attempt < this.config.maxAttempts) {
|
|
508
|
+
const delayMs = this.backoff.calculateDelay(attempt);
|
|
509
|
+
|
|
510
|
+
if (this.config.onRetry) {
|
|
511
|
+
this.config.onRetry(attempt, lastError, delayMs);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
await this.sleep(delayMs);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// All attempts exhausted
|
|
520
|
+
if (this.config.onExhausted && lastError) {
|
|
521
|
+
this.config.onExhausted(lastError, this.config.maxAttempts);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
success: false,
|
|
526
|
+
error: lastError,
|
|
527
|
+
attempts: this.config.maxAttempts,
|
|
528
|
+
totalTimeMs: Date.now() - startTime,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Execute with timeout for each attempt
|
|
534
|
+
*/
|
|
535
|
+
async executeWithTimeout<T>(fn: () => Promise<T>, timeoutMs?: number): Promise<RetryResult<T>> {
|
|
536
|
+
const actualTimeout = timeoutMs ?? this.config.attemptTimeoutMs;
|
|
537
|
+
|
|
538
|
+
return this.execute(async () => {
|
|
539
|
+
return Promise.race([
|
|
540
|
+
fn(),
|
|
541
|
+
new Promise<T>((_, reject) => {
|
|
542
|
+
setTimeout(() => {
|
|
543
|
+
reject(new TradeError(408, `Operation timed out after ${actualTimeout}ms`));
|
|
544
|
+
}, actualTimeout);
|
|
545
|
+
}),
|
|
546
|
+
]);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Get current retry configuration
|
|
552
|
+
*/
|
|
553
|
+
getConfig(): RetryConfig {
|
|
554
|
+
return { ...this.config };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Update retry configuration
|
|
559
|
+
*/
|
|
560
|
+
updateConfig(config: Partial<RetryConfig>): void {
|
|
561
|
+
this.config = { ...this.config, ...config };
|
|
562
|
+
this.backoff = new ExponentialBackoff(this.config);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Get circuit breaker stats if available
|
|
567
|
+
*/
|
|
568
|
+
getCircuitStats(): CircuitStats | undefined {
|
|
569
|
+
return this.circuitBreaker?.getStats();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Reset circuit breaker if available
|
|
574
|
+
*/
|
|
575
|
+
resetCircuit(): void {
|
|
576
|
+
this.circuitBreaker?.reset();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private shouldRetry(error: Error, attempt: number): boolean {
|
|
580
|
+
// Don't retry if we've reached max attempts
|
|
581
|
+
if (attempt >= this.config.maxAttempts) {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Check if retrying all errors
|
|
586
|
+
if (this.config.retryAllErrors) {
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Check error code
|
|
591
|
+
const tradeError = error as TradeError;
|
|
592
|
+
if (tradeError.code !== undefined &&
|
|
593
|
+
this.config.retryableErrorCodes.includes(tradeError.code)) {
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Check error message
|
|
598
|
+
const errorMessage = error.message.toLowerCase();
|
|
599
|
+
for (const retryableMessage of this.config.retryableErrorMessages) {
|
|
600
|
+
if (errorMessage.includes(retryableMessage.toLowerCase())) {
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Check for timeout
|
|
606
|
+
if (this.config.retryOnTimeout &&
|
|
607
|
+
(errorMessage.includes('timeout') || errorMessage.includes('timed out'))) {
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private sleep(ms: number): Promise<void> {
|
|
615
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ===== Convenience Functions =====
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Create a new retry handler
|
|
623
|
+
*/
|
|
624
|
+
export function createRetryHandler(
|
|
625
|
+
config?: Partial<RetryConfig>,
|
|
626
|
+
circuitBreakerConfig?: Partial<CircuitBreakerConfig>
|
|
627
|
+
): RetryHandler {
|
|
628
|
+
return new RetryHandler(
|
|
629
|
+
{ ...defaultRetryConfig(), ...config },
|
|
630
|
+
circuitBreakerConfig ? { ...defaultCircuitBreakerConfig(), ...circuitBreakerConfig } : undefined
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Execute a function with retry logic
|
|
636
|
+
*/
|
|
637
|
+
export async function withRetry<T>(
|
|
638
|
+
fn: () => Promise<T>,
|
|
639
|
+
config?: Partial<RetryConfig>
|
|
640
|
+
): Promise<T> {
|
|
641
|
+
const handler = new RetryHandler({ ...defaultRetryConfig(), ...config });
|
|
642
|
+
const result = await handler.execute(fn);
|
|
643
|
+
|
|
644
|
+
if (!result.success) {
|
|
645
|
+
throw result.error || new TradeError(500, 'Operation failed after retries');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return result.value as T;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Execute a function with circuit breaker protection
|
|
653
|
+
*/
|
|
654
|
+
export async function withCircuitBreaker<T>(
|
|
655
|
+
fn: () => Promise<T>,
|
|
656
|
+
circuitBreakerConfig?: Partial<CircuitBreakerConfig>
|
|
657
|
+
): Promise<T> {
|
|
658
|
+
const config = { ...defaultCircuitBreakerConfig(), ...circuitBreakerConfig };
|
|
659
|
+
const breaker = new CircuitBreaker(config);
|
|
660
|
+
return breaker.execute(fn);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Calculate exponential backoff delay
|
|
665
|
+
*/
|
|
666
|
+
export function calculateBackoff(
|
|
667
|
+
attempt: number,
|
|
668
|
+
initialDelayMs: number = 100,
|
|
669
|
+
multiplier: number = 2,
|
|
670
|
+
maxDelayMs: number = 10000,
|
|
671
|
+
jitter: boolean = false
|
|
672
|
+
): number {
|
|
673
|
+
let delay = Math.min(
|
|
674
|
+
initialDelayMs * Math.pow(multiplier, attempt - 1),
|
|
675
|
+
maxDelayMs
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
if (jitter) {
|
|
679
|
+
delay += delay * 0.25 * (Math.random() * 2 - 1);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return Math.max(0, delay);
|
|
683
|
+
}
|