@sylphx/flow 3.19.0 → 3.19.1
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/package.json +1 -3
- package/src/commands/flow/execute-v2.ts +126 -128
- package/src/commands/flow-command.ts +52 -42
- package/src/config/index.ts +0 -20
- package/src/core/agent-loader.ts +2 -2
- package/src/core/attach-manager.ts +5 -1
- package/src/core/cleanup-handler.ts +20 -16
- package/src/core/flow-executor.ts +93 -62
- package/src/core/functional/index.ts +0 -11
- package/src/core/index.ts +1 -1
- package/src/core/project-manager.ts +14 -29
- package/src/core/secrets-manager.ts +15 -18
- package/src/core/session-manager.ts +4 -8
- package/src/core/target-manager.ts +6 -3
- package/src/core/upgrade-manager.ts +1 -1
- package/src/index.ts +1 -1
- package/src/services/auto-upgrade.ts +6 -14
- package/src/services/config-service.ts +7 -23
- package/src/services/index.ts +1 -1
- package/src/targets/claude-code.ts +14 -8
- package/src/targets/opencode.ts +61 -39
- package/src/targets/shared/mcp-transforms.ts +20 -43
- package/src/types/agent.types.ts +5 -3
- package/src/types/mcp.types.ts +38 -1
- package/src/types.ts +4 -0
- package/src/utils/agent-enhancer.ts +1 -1
- package/src/utils/errors.ts +13 -0
- package/src/utils/files/file-operations.ts +16 -0
- package/src/utils/index.ts +1 -1
- package/src/core/error-handling.ts +0 -482
- package/src/core/functional/async.ts +0 -101
- package/src/core/functional/either.ts +0 -109
- package/src/core/functional/error-handler.ts +0 -135
- package/src/core/functional/pipe.ts +0 -189
- package/src/core/functional/validation.ts +0 -138
- package/src/types/mcp-config.types.ts +0 -448
- package/src/utils/error-handler.ts +0 -53
|
@@ -1,482 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Error Handling - 統一錯誤處理
|
|
3
|
-
* Functional, composable error handling system
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { logger } from '../utils/display/logger.js';
|
|
7
|
-
import type { Result } from './result.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Base error class
|
|
11
|
-
*/
|
|
12
|
-
export class BaseError extends Error {
|
|
13
|
-
public readonly code: string;
|
|
14
|
-
public readonly statusCode: number;
|
|
15
|
-
public readonly details?: Record<string, unknown>;
|
|
16
|
-
|
|
17
|
-
constructor(message: string, code: string, statusCode = 500, details?: Record<string, unknown>) {
|
|
18
|
-
super(message);
|
|
19
|
-
this.name = this.constructor.name;
|
|
20
|
-
this.code = code;
|
|
21
|
-
this.statusCode = statusCode;
|
|
22
|
-
this.details = details;
|
|
23
|
-
|
|
24
|
-
// Maintains proper stack trace for where our error was thrown
|
|
25
|
-
Error.captureStackTrace(this, this.constructor);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Validation error
|
|
31
|
-
*/
|
|
32
|
-
export class ValidationError extends BaseError {
|
|
33
|
-
constructor(message: string, details?: Record<string, unknown>) {
|
|
34
|
-
super(message, 'VALIDATION_ERROR', 400, details);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Configuration error
|
|
40
|
-
*/
|
|
41
|
-
export class ConfigurationError extends BaseError {
|
|
42
|
-
constructor(message: string, details?: Record<string, unknown>) {
|
|
43
|
-
super(message, 'CONFIGURATION_ERROR', 500, details);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Storage error
|
|
49
|
-
*/
|
|
50
|
-
export class StorageError extends BaseError {
|
|
51
|
-
constructor(message: string, operation?: string, details?: Record<string, unknown>) {
|
|
52
|
-
super(message, 'STORAGE_ERROR', 500, { operation, ...details });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Network error
|
|
58
|
-
*/
|
|
59
|
-
export class NetworkError extends BaseError {
|
|
60
|
-
constructor(message: string, details?: Record<string, unknown>) {
|
|
61
|
-
super(message, 'NETWORK_ERROR', 503, details);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Authentication error
|
|
67
|
-
*/
|
|
68
|
-
export class AuthenticationError extends BaseError {
|
|
69
|
-
constructor(message: string, details?: Record<string, unknown>) {
|
|
70
|
-
super(message, 'AUTHENTICATION_ERROR', 401, details);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Authorization error
|
|
76
|
-
*/
|
|
77
|
-
export class AuthorizationError extends BaseError {
|
|
78
|
-
constructor(message: string, details?: Record<string, unknown>) {
|
|
79
|
-
super(message, 'AUTHORIZATION_ERROR', 403, details);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Not found error
|
|
85
|
-
*/
|
|
86
|
-
export class NotFoundError extends BaseError {
|
|
87
|
-
constructor(message: string, resource?: string, details?: Record<string, unknown>) {
|
|
88
|
-
super(message, 'NOT_FOUND', 404, { resource, ...details });
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Timeout error
|
|
94
|
-
*/
|
|
95
|
-
export class TimeoutError extends BaseError {
|
|
96
|
-
constructor(message: string, timeout?: number, details?: Record<string, unknown>) {
|
|
97
|
-
super(message, 'TIMEOUT_ERROR', 408, { timeout, ...details });
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Error types
|
|
103
|
-
*/
|
|
104
|
-
export const ErrorTypes = {
|
|
105
|
-
ValidationError,
|
|
106
|
-
ConfigurationError,
|
|
107
|
-
StorageError,
|
|
108
|
-
NetworkError,
|
|
109
|
-
AuthenticationError,
|
|
110
|
-
AuthorizationError,
|
|
111
|
-
NotFoundError,
|
|
112
|
-
TimeoutError,
|
|
113
|
-
} as const;
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Error codes
|
|
117
|
-
*/
|
|
118
|
-
export const ErrorCodes = {
|
|
119
|
-
// Validation errors
|
|
120
|
-
INVALID_INPUT: 'INVALID_INPUT',
|
|
121
|
-
MISSING_REQUIRED_FIELD: 'MISSING_REQUIRED_FIELD',
|
|
122
|
-
INVALID_FORMAT: 'INVALID_FORMAT',
|
|
123
|
-
|
|
124
|
-
// Configuration errors
|
|
125
|
-
MISSING_CONFIG: 'MISSING_CONFIG',
|
|
126
|
-
INVALID_CONFIG: 'INVALID_CONFIG',
|
|
127
|
-
CONFIG_PARSE_ERROR: 'CONFIG_PARSE_ERROR',
|
|
128
|
-
|
|
129
|
-
// Storage errors
|
|
130
|
-
STORAGE_CONNECTION_FAILED: 'STORAGE_CONNECTION_FAILED',
|
|
131
|
-
STORAGE_OPERATION_FAILED: 'STORAGE_OPERATION_FAILED',
|
|
132
|
-
STORAGE_TIMEOUT: 'STORAGE_TIMEOUT',
|
|
133
|
-
STORAGE_FULL: 'STORAGE_FULL',
|
|
134
|
-
|
|
135
|
-
// Network errors
|
|
136
|
-
CONNECTION_FAILED: 'CONNECTION_FAILED',
|
|
137
|
-
REQUEST_FAILED: 'REQUEST_FAILED',
|
|
138
|
-
REQUEST_TIMEOUT: 'REQUEST_TIMEOUT',
|
|
139
|
-
RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',
|
|
140
|
-
|
|
141
|
-
// Authentication errors
|
|
142
|
-
UNAUTHORIZED: 'UNAUTHORIZED',
|
|
143
|
-
INVALID_CREDENTIALS: 'INVALID_CREDENTIALS',
|
|
144
|
-
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
|
|
145
|
-
|
|
146
|
-
// Authorization errors
|
|
147
|
-
FORBIDDEN: 'FORBIDDEN',
|
|
148
|
-
INSUFFICIENT_PERMISSIONS: 'INSUFFICIENT_PERMISSIONS',
|
|
149
|
-
|
|
150
|
-
// Not found errors
|
|
151
|
-
RESOURCE_NOT_FOUND: 'RESOURCE_NOT_FOUND',
|
|
152
|
-
ENDPOINT_NOT_FOUND: 'ENDPOINT_NOT_FOUND',
|
|
153
|
-
|
|
154
|
-
// System errors
|
|
155
|
-
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
|
156
|
-
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
|
|
157
|
-
TIMEOUT: 'TIMEOUT',
|
|
158
|
-
} as const;
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Error handler interface
|
|
162
|
-
*/
|
|
163
|
-
export interface ErrorHandler {
|
|
164
|
-
canHandle(error: Error): boolean;
|
|
165
|
-
handle(error: Error): void | Promise<void>;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Logger error handler
|
|
170
|
-
*/
|
|
171
|
-
export class LoggerErrorHandler implements ErrorHandler {
|
|
172
|
-
constructor(private level: 'error' | 'warn' | 'info' | 'debug' = 'error') {}
|
|
173
|
-
|
|
174
|
-
canHandle(_error: Error): boolean {
|
|
175
|
-
return true; // Logger can handle all errors
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
handle(error: Error): void {
|
|
179
|
-
const errorData = {
|
|
180
|
-
name: error.name,
|
|
181
|
-
message: error.message,
|
|
182
|
-
code: (error as BaseError).code,
|
|
183
|
-
statusCode: (error as BaseError).statusCode,
|
|
184
|
-
stack: error.stack,
|
|
185
|
-
details: (error as BaseError).details,
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
switch (this.level) {
|
|
189
|
-
case 'error':
|
|
190
|
-
logger.error(error.message, errorData);
|
|
191
|
-
break;
|
|
192
|
-
case 'warn':
|
|
193
|
-
logger.warn(error.message, errorData);
|
|
194
|
-
break;
|
|
195
|
-
case 'info':
|
|
196
|
-
logger.info(error.message, errorData);
|
|
197
|
-
break;
|
|
198
|
-
case 'debug':
|
|
199
|
-
logger.debug(error.message, errorData);
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Console error handler
|
|
207
|
-
*/
|
|
208
|
-
export class ConsoleErrorHandler implements ErrorHandler {
|
|
209
|
-
canHandle(_error: Error): boolean {
|
|
210
|
-
return true; // Console can handle all errors
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
handle(error: Error): void {
|
|
214
|
-
if (error instanceof BaseError) {
|
|
215
|
-
console.error(`[${error.code}] ${error.message}`);
|
|
216
|
-
if (error.details) {
|
|
217
|
-
console.error('Details:', error.details);
|
|
218
|
-
}
|
|
219
|
-
} else {
|
|
220
|
-
console.error(error.message);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (process.env.NODE_ENV === 'development' && error.stack) {
|
|
224
|
-
console.error('\nStack trace:');
|
|
225
|
-
console.error(error.stack);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Error handler chain
|
|
232
|
-
*/
|
|
233
|
-
export class ErrorHandlerChain {
|
|
234
|
-
private handlers: ErrorHandler[] = [];
|
|
235
|
-
|
|
236
|
-
constructor(handlers: ErrorHandler[] = []) {
|
|
237
|
-
this.handlers = handlers;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
addHandler(handler: ErrorHandler): void {
|
|
241
|
-
this.handlers.push(handler);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
removeHandler(handler: ErrorHandler): void {
|
|
245
|
-
const index = this.handlers.indexOf(handler);
|
|
246
|
-
if (index > -1) {
|
|
247
|
-
this.handlers.splice(index, 1);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async handle(error: Error): Promise<void> {
|
|
252
|
-
for (const handler of this.handlers) {
|
|
253
|
-
if (handler.canHandle(error)) {
|
|
254
|
-
await handler.handle(error);
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// If no handler can handle the error, use default console handler
|
|
260
|
-
new ConsoleErrorHandler().handle(error);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Global error handler
|
|
266
|
-
*/
|
|
267
|
-
export const globalErrorHandler = new ErrorHandlerChain([new LoggerErrorHandler('error')]);
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Safe function wrapper with error handling
|
|
271
|
-
*/
|
|
272
|
-
export async function withErrorHandling<T>(
|
|
273
|
-
fn: () => Promise<T>,
|
|
274
|
-
errorHandler?: (error: Error) => void | Promise<void>
|
|
275
|
-
): Promise<Result<T>> {
|
|
276
|
-
try {
|
|
277
|
-
const data = await fn();
|
|
278
|
-
return { success: true, data };
|
|
279
|
-
} catch (error) {
|
|
280
|
-
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
281
|
-
|
|
282
|
-
if (errorHandler) {
|
|
283
|
-
await errorHandler(errorObj);
|
|
284
|
-
} else {
|
|
285
|
-
await globalErrorHandler.handle(errorObj);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return { success: false, error: errorObj };
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Safe sync function wrapper with error handling
|
|
294
|
-
*/
|
|
295
|
-
export function withSyncErrorHandling<T>(
|
|
296
|
-
fn: () => T,
|
|
297
|
-
errorHandler?: (error: Error) => void | Promise<void>
|
|
298
|
-
): Result<T> {
|
|
299
|
-
try {
|
|
300
|
-
const data = fn();
|
|
301
|
-
return { success: true, data };
|
|
302
|
-
} catch (error) {
|
|
303
|
-
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
304
|
-
|
|
305
|
-
if (errorHandler) {
|
|
306
|
-
// For sync error handlers, we need to handle them asynchronously
|
|
307
|
-
void errorHandler(errorObj);
|
|
308
|
-
} else {
|
|
309
|
-
void globalErrorHandler.handle(errorObj);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return { success: false, error: errorObj };
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Retry function with error handling
|
|
318
|
-
*/
|
|
319
|
-
export async function withRetry<T>(
|
|
320
|
-
fn: () => Promise<T>,
|
|
321
|
-
options: {
|
|
322
|
-
maxAttempts?: number;
|
|
323
|
-
delay?: number;
|
|
324
|
-
backoff?: 'linear' | 'exponential';
|
|
325
|
-
retryableErrors?: string[];
|
|
326
|
-
onRetry?: (attempt: number, error: Error) => void;
|
|
327
|
-
} = {}
|
|
328
|
-
): Promise<Result<T>> {
|
|
329
|
-
const {
|
|
330
|
-
maxAttempts = 3,
|
|
331
|
-
delay = 1000,
|
|
332
|
-
backoff = 'exponential',
|
|
333
|
-
retryableErrors = [],
|
|
334
|
-
onRetry,
|
|
335
|
-
} = options;
|
|
336
|
-
|
|
337
|
-
let lastError: Error = new Error('Unknown error');
|
|
338
|
-
|
|
339
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
340
|
-
try {
|
|
341
|
-
const data = await fn();
|
|
342
|
-
return { success: true, data };
|
|
343
|
-
} catch (error) {
|
|
344
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
345
|
-
|
|
346
|
-
// Check if error is retryable
|
|
347
|
-
const isRetryable =
|
|
348
|
-
retryableErrors.length === 0 ||
|
|
349
|
-
retryableErrors.includes((lastError as BaseError).code) ||
|
|
350
|
-
retryableErrors.includes(lastError.constructor.name);
|
|
351
|
-
|
|
352
|
-
if (!isRetryable || attempt === maxAttempts) {
|
|
353
|
-
await globalErrorHandler.handle(lastError);
|
|
354
|
-
return { success: false, error: lastError };
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Call retry callback
|
|
358
|
-
if (onRetry) {
|
|
359
|
-
onRetry(attempt, lastError);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Calculate delay
|
|
363
|
-
const retryDelay = backoff === 'exponential' ? delay * 2 ** (attempt - 1) : delay * attempt;
|
|
364
|
-
|
|
365
|
-
// Wait before retry
|
|
366
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
await globalErrorHandler.handle(lastError);
|
|
371
|
-
return { success: false, error: lastError };
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Timeout wrapper
|
|
376
|
-
*/
|
|
377
|
-
export function withTimeout<T>(
|
|
378
|
-
fn: () => Promise<T>,
|
|
379
|
-
timeoutMs: number,
|
|
380
|
-
timeoutMessage = 'Operation timed out'
|
|
381
|
-
): Promise<Result<T>> {
|
|
382
|
-
return new Promise((resolve) => {
|
|
383
|
-
const timeoutId = setTimeout(() => {
|
|
384
|
-
resolve({
|
|
385
|
-
success: false,
|
|
386
|
-
error: new TimeoutError(timeoutMessage, timeoutMs),
|
|
387
|
-
});
|
|
388
|
-
}, timeoutMs);
|
|
389
|
-
|
|
390
|
-
fn()
|
|
391
|
-
.then((data) => {
|
|
392
|
-
clearTimeout(timeoutId);
|
|
393
|
-
resolve({ success: true, data });
|
|
394
|
-
})
|
|
395
|
-
.catch((error) => {
|
|
396
|
-
clearTimeout(timeoutId);
|
|
397
|
-
resolve({
|
|
398
|
-
success: false,
|
|
399
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
400
|
-
});
|
|
401
|
-
});
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Circuit breaker pattern
|
|
407
|
-
*/
|
|
408
|
-
export class CircuitBreaker {
|
|
409
|
-
private failures = 0;
|
|
410
|
-
private lastFailureTime = 0;
|
|
411
|
-
private state: 'closed' | 'open' | 'half-open' = 'closed';
|
|
412
|
-
private readonly config: {
|
|
413
|
-
failureThreshold: number;
|
|
414
|
-
recoveryTimeMs: number;
|
|
415
|
-
monitoringPeriodMs: number;
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
constructor(
|
|
419
|
-
options: {
|
|
420
|
-
failureThreshold?: number;
|
|
421
|
-
recoveryTimeMs?: number;
|
|
422
|
-
monitoringPeriodMs?: number;
|
|
423
|
-
} = {}
|
|
424
|
-
) {
|
|
425
|
-
const { failureThreshold = 5, recoveryTimeMs = 60000, monitoringPeriodMs = 10000 } = options;
|
|
426
|
-
|
|
427
|
-
this.config = { failureThreshold, recoveryTimeMs, monitoringPeriodMs };
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
async execute<T>(fn: () => Promise<T>): Promise<Result<T>> {
|
|
431
|
-
if (this.state === 'open') {
|
|
432
|
-
if (Date.now() - this.lastFailureTime > this.config.recoveryTimeMs) {
|
|
433
|
-
this.state = 'half-open';
|
|
434
|
-
} else {
|
|
435
|
-
return {
|
|
436
|
-
success: false,
|
|
437
|
-
error: new Error('Circuit breaker is open'),
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
try {
|
|
443
|
-
const result = await fn();
|
|
444
|
-
this.onSuccess();
|
|
445
|
-
return { success: true, data: result };
|
|
446
|
-
} catch (error) {
|
|
447
|
-
this.onFailure();
|
|
448
|
-
return {
|
|
449
|
-
success: false,
|
|
450
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
private onSuccess(): void {
|
|
456
|
-
this.failures = 0;
|
|
457
|
-
this.state = 'closed';
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
private onFailure(): void {
|
|
461
|
-
this.failures++;
|
|
462
|
-
this.lastFailureTime = Date.now();
|
|
463
|
-
|
|
464
|
-
if (this.failures >= this.config.failureThreshold) {
|
|
465
|
-
this.state = 'open';
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
getState(): 'closed' | 'open' | 'half-open' {
|
|
470
|
-
return this.state;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
getFailures(): number {
|
|
474
|
-
return this.failures;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
reset(): void {
|
|
478
|
-
this.failures = 0;
|
|
479
|
-
this.state = 'closed';
|
|
480
|
-
this.lastFailureTime = 0;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Async/Promise utilities for functional programming
|
|
3
|
-
* Handle promises with Result types
|
|
4
|
-
*
|
|
5
|
-
* DESIGN RATIONALE:
|
|
6
|
-
* - Convert Promise rejections to Result types
|
|
7
|
-
* - Composable async operations
|
|
8
|
-
* - No unhandled promise rejections
|
|
9
|
-
* - Type-safe async error handling
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { AppError } from './error-types.js';
|
|
13
|
-
import { toAppError } from './error-types.js';
|
|
14
|
-
import type { Result } from './result.js';
|
|
15
|
-
import { failure, isSuccess, success } from './result.js';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Async Result type
|
|
19
|
-
*/
|
|
20
|
-
export type AsyncResult<T, E = AppError> = Promise<Result<T, E>>;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Convert Promise to AsyncResult
|
|
24
|
-
* Catches rejections and converts to Result
|
|
25
|
-
*/
|
|
26
|
-
export const fromPromise = async <T>(
|
|
27
|
-
promise: Promise<T>,
|
|
28
|
-
onError?: (error: unknown) => AppError
|
|
29
|
-
): AsyncResult<T> => {
|
|
30
|
-
try {
|
|
31
|
-
const value = await promise;
|
|
32
|
-
return success(value);
|
|
33
|
-
} catch (error) {
|
|
34
|
-
return failure(onError ? onError(error) : toAppError(error));
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Run async operation with timeout
|
|
40
|
-
*/
|
|
41
|
-
export const withTimeout = <T>(
|
|
42
|
-
promise: Promise<T>,
|
|
43
|
-
timeoutMs: number,
|
|
44
|
-
onTimeout?: () => AppError
|
|
45
|
-
): AsyncResult<T> => {
|
|
46
|
-
return fromPromise(
|
|
47
|
-
Promise.race([
|
|
48
|
-
promise,
|
|
49
|
-
new Promise<T>((_, reject) =>
|
|
50
|
-
setTimeout(() => reject(new Error('Operation timed out')), timeoutMs)
|
|
51
|
-
),
|
|
52
|
-
]),
|
|
53
|
-
onTimeout
|
|
54
|
-
);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Retry async operation
|
|
59
|
-
*/
|
|
60
|
-
export const retry = async <T>(
|
|
61
|
-
fn: () => Promise<T>,
|
|
62
|
-
options: {
|
|
63
|
-
maxAttempts: number;
|
|
64
|
-
delayMs?: number;
|
|
65
|
-
backoff?: number;
|
|
66
|
-
onRetry?: (attempt: number, error: AppError) => void;
|
|
67
|
-
}
|
|
68
|
-
): AsyncResult<T> => {
|
|
69
|
-
const { maxAttempts, delayMs = 1000, backoff = 2, onRetry } = options;
|
|
70
|
-
|
|
71
|
-
let lastError: AppError = { type: 'unknown', message: 'No attempts made', code: 'NO_ATTEMPTS' };
|
|
72
|
-
let currentDelay = delayMs;
|
|
73
|
-
|
|
74
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
75
|
-
const result = await fromPromise(fn());
|
|
76
|
-
|
|
77
|
-
if (isSuccess(result)) {
|
|
78
|
-
return result;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
lastError = result.error;
|
|
82
|
-
|
|
83
|
-
if (attempt < maxAttempts) {
|
|
84
|
-
if (onRetry) {
|
|
85
|
-
onRetry(attempt, lastError);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
await new Promise((resolve) => setTimeout(resolve, currentDelay));
|
|
89
|
-
currentDelay *= backoff;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return failure(lastError);
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Delay execution
|
|
98
|
-
*/
|
|
99
|
-
export const delay = (ms: number): Promise<void> => {
|
|
100
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
101
|
-
};
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Either type for representing values that can be one of two types
|
|
3
|
-
* Commonly used for success/error, but more general than Result
|
|
4
|
-
*
|
|
5
|
-
* DESIGN RATIONALE:
|
|
6
|
-
* - Generic sum type for two possibilities
|
|
7
|
-
* - Left traditionally represents error/failure
|
|
8
|
-
* - Right traditionally represents success/value
|
|
9
|
-
* - Result is a specialized Either<Error, Value>
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export type Either<L, R> = Left<L> | Right<R>;
|
|
13
|
-
|
|
14
|
-
export interface Left<L> {
|
|
15
|
-
readonly _tag: 'Left';
|
|
16
|
-
readonly left: L;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface Right<R> {
|
|
20
|
-
readonly _tag: 'Right';
|
|
21
|
-
readonly right: R;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Constructors
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
export const left = <L>(value: L): Left<L> => ({
|
|
29
|
-
_tag: 'Left',
|
|
30
|
-
left: value,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
export const right = <R>(value: R): Right<R> => ({
|
|
34
|
-
_tag: 'Right',
|
|
35
|
-
right: value,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Type guards
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
export const isLeft = <L, R>(either: Either<L, R>): either is Left<L> => either._tag === 'Left';
|
|
43
|
-
|
|
44
|
-
export const isRight = <L, R>(either: Either<L, R>): either is Right<R> => either._tag === 'Right';
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Transformations
|
|
48
|
-
*/
|
|
49
|
-
|
|
50
|
-
export const map =
|
|
51
|
-
<L, R, R2>(fn: (value: R) => R2) =>
|
|
52
|
-
(either: Either<L, R>): Either<L, R2> => {
|
|
53
|
-
if (isRight(either)) {
|
|
54
|
-
return right(fn(either.right));
|
|
55
|
-
}
|
|
56
|
-
return either;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export const mapLeft =
|
|
60
|
-
<L, L2, R>(fn: (value: L) => L2) =>
|
|
61
|
-
(either: Either<L, R>): Either<L2, R> => {
|
|
62
|
-
if (isLeft(either)) {
|
|
63
|
-
return left(fn(either.left));
|
|
64
|
-
}
|
|
65
|
-
return either;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export const flatMap =
|
|
69
|
-
<L, R, R2>(fn: (value: R) => Either<L, R2>) =>
|
|
70
|
-
(either: Either<L, R>): Either<L, R2> => {
|
|
71
|
-
if (isRight(either)) {
|
|
72
|
-
return fn(either.right);
|
|
73
|
-
}
|
|
74
|
-
return either;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Pattern matching
|
|
79
|
-
*/
|
|
80
|
-
export const match =
|
|
81
|
-
<L, R, T>(onLeft: (left: L) => T, onRight: (right: R) => T) =>
|
|
82
|
-
(either: Either<L, R>): T => {
|
|
83
|
-
if (isLeft(either)) {
|
|
84
|
-
return onLeft(either.left);
|
|
85
|
-
}
|
|
86
|
-
return onRight(either.right);
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Extract value or provide default
|
|
91
|
-
*/
|
|
92
|
-
export const getOrElse =
|
|
93
|
-
<R>(defaultValue: R) =>
|
|
94
|
-
<L>(either: Either<L, R>): R => {
|
|
95
|
-
if (isRight(either)) {
|
|
96
|
-
return either.right;
|
|
97
|
-
}
|
|
98
|
-
return defaultValue;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Swap Left and Right
|
|
103
|
-
*/
|
|
104
|
-
export const swap = <L, R>(either: Either<L, R>): Either<R, L> => {
|
|
105
|
-
if (isLeft(either)) {
|
|
106
|
-
return right(either.left);
|
|
107
|
-
}
|
|
108
|
-
return left(either.right);
|
|
109
|
-
};
|