@sylphx/flow 1.0.1 → 1.0.3
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 +12 -0
- package/package.json +10 -9
- package/src/commands/codebase-command.ts +168 -0
- package/src/commands/flow-command.ts +1137 -0
- package/src/commands/flow-orchestrator.ts +296 -0
- package/src/commands/hook-command.ts +444 -0
- package/src/commands/init-command.ts +92 -0
- package/src/commands/init-core.ts +322 -0
- package/src/commands/knowledge-command.ts +161 -0
- package/src/commands/run-command.ts +120 -0
- package/src/components/benchmark-monitor.tsx +331 -0
- package/src/components/reindex-progress.tsx +261 -0
- package/src/composables/functional/index.ts +14 -0
- package/src/composables/functional/useEnvironment.ts +171 -0
- package/src/composables/functional/useFileSystem.ts +139 -0
- package/src/composables/index.ts +5 -0
- package/src/composables/useEnv.ts +13 -0
- package/src/composables/useRuntimeConfig.ts +27 -0
- package/src/composables/useTargetConfig.ts +45 -0
- package/src/config/ai-config.ts +376 -0
- package/src/config/constants.ts +35 -0
- package/src/config/index.ts +27 -0
- package/src/config/rules.ts +43 -0
- package/src/config/servers.ts +371 -0
- package/src/config/targets.ts +126 -0
- package/src/core/agent-loader.ts +141 -0
- package/src/core/agent-manager.ts +174 -0
- package/src/core/ai-sdk.ts +603 -0
- package/src/core/app-factory.ts +381 -0
- package/src/core/builtin-agents.ts +9 -0
- package/src/core/command-system.ts +550 -0
- package/src/core/config-system.ts +550 -0
- package/src/core/connection-pool.ts +390 -0
- package/src/core/di-container.ts +155 -0
- package/src/core/error-handling.ts +519 -0
- package/src/core/formatting/bytes.test.ts +115 -0
- package/src/core/formatting/bytes.ts +64 -0
- package/src/core/functional/async.ts +313 -0
- package/src/core/functional/either.ts +109 -0
- package/src/core/functional/error-handler.ts +135 -0
- package/src/core/functional/error-types.ts +311 -0
- package/src/core/functional/index.ts +19 -0
- package/src/core/functional/option.ts +142 -0
- package/src/core/functional/pipe.ts +189 -0
- package/src/core/functional/result.ts +204 -0
- package/src/core/functional/validation.ts +138 -0
- package/src/core/headless-display.ts +96 -0
- package/src/core/index.ts +6 -0
- package/src/core/installers/file-installer.ts +303 -0
- package/src/core/installers/mcp-installer.ts +213 -0
- package/src/core/interfaces/index.ts +22 -0
- package/src/core/interfaces/repository.interface.ts +91 -0
- package/src/core/interfaces/service.interface.ts +133 -0
- package/src/core/interfaces.ts +129 -0
- package/src/core/loop-controller.ts +200 -0
- package/src/core/result.ts +351 -0
- package/src/core/rule-loader.ts +147 -0
- package/src/core/rule-manager.ts +240 -0
- package/src/core/service-config.ts +252 -0
- package/src/core/session-service.ts +121 -0
- package/src/core/state-detector.ts +389 -0
- package/src/core/storage-factory.ts +115 -0
- package/src/core/stream-handler.ts +288 -0
- package/src/core/target-manager.ts +161 -0
- package/src/core/type-utils.ts +427 -0
- package/src/core/unified-storage.ts +456 -0
- package/src/core/upgrade-manager.ts +300 -0
- package/src/core/validation/limit.test.ts +155 -0
- package/src/core/validation/limit.ts +46 -0
- package/src/core/validation/query.test.ts +44 -0
- package/src/core/validation/query.ts +20 -0
- package/src/db/auto-migrate.ts +322 -0
- package/src/db/base-database-client.ts +144 -0
- package/src/db/cache-db.ts +218 -0
- package/src/db/cache-schema.ts +75 -0
- package/src/db/database.ts +70 -0
- package/src/db/index.ts +252 -0
- package/src/db/memory-db.ts +153 -0
- package/src/db/memory-schema.ts +29 -0
- package/src/db/schema.ts +289 -0
- package/src/db/session-repository.ts +733 -0
- package/src/domains/codebase/index.ts +5 -0
- package/src/domains/codebase/tools.ts +139 -0
- package/src/domains/index.ts +8 -0
- package/src/domains/knowledge/index.ts +10 -0
- package/src/domains/knowledge/resources.ts +537 -0
- package/src/domains/knowledge/tools.ts +174 -0
- package/src/domains/utilities/index.ts +6 -0
- package/src/domains/utilities/time/index.ts +5 -0
- package/src/domains/utilities/time/tools.ts +291 -0
- package/src/index.ts +211 -0
- package/src/services/agent-service.ts +273 -0
- package/src/services/claude-config-service.ts +252 -0
- package/src/services/config-service.ts +258 -0
- package/src/services/evaluation-service.ts +271 -0
- package/src/services/functional/evaluation-logic.ts +296 -0
- package/src/services/functional/file-processor.ts +273 -0
- package/src/services/functional/index.ts +12 -0
- package/src/services/index.ts +13 -0
- package/src/services/mcp-service.ts +432 -0
- package/src/services/memory.service.ts +476 -0
- package/src/services/search/base-indexer.ts +156 -0
- package/src/services/search/codebase-indexer-types.ts +38 -0
- package/src/services/search/codebase-indexer.ts +647 -0
- package/src/services/search/embeddings-provider.ts +455 -0
- package/src/services/search/embeddings.ts +316 -0
- package/src/services/search/functional-indexer.ts +323 -0
- package/src/services/search/index.ts +27 -0
- package/src/services/search/indexer.ts +380 -0
- package/src/services/search/knowledge-indexer.ts +422 -0
- package/src/services/search/semantic-search.ts +244 -0
- package/src/services/search/tfidf.ts +559 -0
- package/src/services/search/unified-search-service.ts +888 -0
- package/src/services/smart-config-service.ts +385 -0
- package/src/services/storage/cache-storage.ts +487 -0
- package/src/services/storage/drizzle-storage.ts +581 -0
- package/src/services/storage/index.ts +15 -0
- package/src/services/storage/lancedb-vector-storage.ts +494 -0
- package/src/services/storage/memory-storage.ts +268 -0
- package/src/services/storage/separated-storage.ts +467 -0
- package/src/services/storage/vector-storage.ts +13 -0
- package/src/shared/agents/index.ts +63 -0
- package/src/shared/files/index.ts +99 -0
- package/src/shared/index.ts +32 -0
- package/src/shared/logging/index.ts +24 -0
- package/src/shared/processing/index.ts +153 -0
- package/src/shared/types/index.ts +25 -0
- package/src/targets/claude-code.ts +574 -0
- package/src/targets/functional/claude-code-logic.ts +185 -0
- package/src/targets/functional/index.ts +6 -0
- package/src/targets/opencode.ts +529 -0
- package/src/types/agent.types.ts +32 -0
- package/src/types/api/batch.ts +108 -0
- package/src/types/api/errors.ts +118 -0
- package/src/types/api/index.ts +55 -0
- package/src/types/api/requests.ts +76 -0
- package/src/types/api/responses.ts +180 -0
- package/src/types/api/websockets.ts +85 -0
- package/src/types/api.types.ts +9 -0
- package/src/types/benchmark.ts +49 -0
- package/src/types/cli.types.ts +87 -0
- package/src/types/common.types.ts +35 -0
- package/src/types/database.types.ts +510 -0
- package/src/types/mcp-config.types.ts +448 -0
- package/src/types/mcp.types.ts +69 -0
- package/src/types/memory-types.ts +63 -0
- package/src/types/provider.types.ts +28 -0
- package/src/types/rule.types.ts +24 -0
- package/src/types/session.types.ts +214 -0
- package/src/types/target-config.types.ts +295 -0
- package/src/types/target.types.ts +140 -0
- package/src/types/todo.types.ts +25 -0
- package/src/types.ts +40 -0
- package/src/utils/advanced-tokenizer.ts +191 -0
- package/src/utils/agent-enhancer.ts +114 -0
- package/src/utils/ai-model-fetcher.ts +19 -0
- package/src/utils/async-file-operations.ts +516 -0
- package/src/utils/audio-player.ts +345 -0
- package/src/utils/cli-output.ts +266 -0
- package/src/utils/codebase-helpers.ts +211 -0
- package/src/utils/console-ui.ts +79 -0
- package/src/utils/database-errors.ts +140 -0
- package/src/utils/debug-logger.ts +49 -0
- package/src/utils/error-handler.ts +53 -0
- package/src/utils/file-operations.ts +310 -0
- package/src/utils/file-scanner.ts +259 -0
- package/src/utils/functional/array.ts +355 -0
- package/src/utils/functional/index.ts +15 -0
- package/src/utils/functional/object.ts +279 -0
- package/src/utils/functional/string.ts +281 -0
- package/src/utils/functional.ts +543 -0
- package/src/utils/help.ts +20 -0
- package/src/utils/immutable-cache.ts +106 -0
- package/src/utils/index.ts +78 -0
- package/src/utils/jsonc.ts +158 -0
- package/src/utils/logger.ts +396 -0
- package/src/utils/mcp-config.ts +249 -0
- package/src/utils/memory-tui.ts +414 -0
- package/src/utils/models-dev.ts +91 -0
- package/src/utils/notifications.ts +169 -0
- package/src/utils/object-utils.ts +51 -0
- package/src/utils/parallel-operations.ts +487 -0
- package/src/utils/paths.ts +143 -0
- package/src/utils/process-manager.ts +155 -0
- package/src/utils/prompts.ts +120 -0
- package/src/utils/search-tool-builder.ts +214 -0
- package/src/utils/secret-utils.ts +179 -0
- package/src/utils/security.ts +537 -0
- package/src/utils/session-manager.ts +168 -0
- package/src/utils/session-title.ts +87 -0
- package/src/utils/settings.ts +182 -0
- package/src/utils/simplified-errors.ts +410 -0
- package/src/utils/sync-utils.ts +159 -0
- package/src/utils/target-config.ts +570 -0
- package/src/utils/target-utils.ts +394 -0
- package/src/utils/template-engine.ts +94 -0
- package/src/utils/test-audio.ts +71 -0
- package/src/utils/todo-context.ts +46 -0
- package/src/utils/token-counter.ts +288 -0
- package/dist/index.d.ts +0 -10
- package/dist/index.js +0 -59554
- package/dist/lancedb.linux-x64-gnu-b7f0jgsz.node +0 -0
- package/dist/lancedb.linux-x64-musl-tgcv22rx.node +0 -0
- package/dist/shared/chunk-25dwp0dp.js +0 -89
- package/dist/shared/chunk-3pjb6063.js +0 -208
- package/dist/shared/chunk-4d6ydpw7.js +0 -2854
- package/dist/shared/chunk-4wjcadjk.js +0 -225
- package/dist/shared/chunk-5j4w74t6.js +0 -30
- package/dist/shared/chunk-5j8m3dh3.js +0 -58
- package/dist/shared/chunk-5thh3qem.js +0 -91
- package/dist/shared/chunk-6g9xy73m.js +0 -252
- package/dist/shared/chunk-7eq34c42.js +0 -23
- package/dist/shared/chunk-c2gwgx3r.js +0 -115
- package/dist/shared/chunk-cjd3mk4c.js +0 -1320
- package/dist/shared/chunk-g5cv6703.js +0 -368
- package/dist/shared/chunk-hpkhykhq.js +0 -574
- package/dist/shared/chunk-m2322pdk.js +0 -122
- package/dist/shared/chunk-nd5fdvaq.js +0 -26
- package/dist/shared/chunk-pgd3m6zf.js +0 -108
- package/dist/shared/chunk-qk8n91hw.js +0 -494
- package/dist/shared/chunk-rkkn8szp.js +0 -16855
- package/dist/shared/chunk-t16rfxh0.js +0 -61
- package/dist/shared/chunk-t4fbfa5v.js +0 -19
- package/dist/shared/chunk-t77h86w6.js +0 -276
- package/dist/shared/chunk-v0ez4aef.js +0 -71
- package/dist/shared/chunk-v29j2r3s.js +0 -32051
- package/dist/shared/chunk-vfbc6ew5.js +0 -765
- package/dist/shared/chunk-vmeqwm1c.js +0 -204
- package/dist/shared/chunk-x66eh37x.js +0 -137
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handling - 統一錯誤處理
|
|
3
|
+
* Functional, composable error handling system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { logger } from '../utils/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(
|
|
18
|
+
message: string,
|
|
19
|
+
code: string,
|
|
20
|
+
statusCode = 500,
|
|
21
|
+
details?: Record<string, unknown>
|
|
22
|
+
) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = this.constructor.name;
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.statusCode = statusCode;
|
|
27
|
+
this.details = details;
|
|
28
|
+
|
|
29
|
+
// Maintains proper stack trace for where our error was thrown
|
|
30
|
+
Error.captureStackTrace(this, this.constructor);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validation error
|
|
36
|
+
*/
|
|
37
|
+
export class ValidationError extends BaseError {
|
|
38
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
39
|
+
super(message, 'VALIDATION_ERROR', 400, details);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Configuration error
|
|
45
|
+
*/
|
|
46
|
+
export class ConfigurationError extends BaseError {
|
|
47
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
48
|
+
super(message, 'CONFIGURATION_ERROR', 500, details);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Storage error
|
|
54
|
+
*/
|
|
55
|
+
export class StorageError extends BaseError {
|
|
56
|
+
constructor(message: string, operation?: string, details?: Record<string, unknown>) {
|
|
57
|
+
super(message, 'STORAGE_ERROR', 500, { operation, ...details });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Network error
|
|
63
|
+
*/
|
|
64
|
+
export class NetworkError extends BaseError {
|
|
65
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
66
|
+
super(message, 'NETWORK_ERROR', 503, details);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Authentication error
|
|
72
|
+
*/
|
|
73
|
+
export class AuthenticationError extends BaseError {
|
|
74
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
75
|
+
super(message, 'AUTHENTICATION_ERROR', 401, details);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Authorization error
|
|
81
|
+
*/
|
|
82
|
+
export class AuthorizationError extends BaseError {
|
|
83
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
84
|
+
super(message, 'AUTHORIZATION_ERROR', 403, details);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Not found error
|
|
90
|
+
*/
|
|
91
|
+
export class NotFoundError extends BaseError {
|
|
92
|
+
constructor(message: string, resource?: string, details?: Record<string, unknown>) {
|
|
93
|
+
super(message, 'NOT_FOUND', 404, { resource, ...details });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Timeout error
|
|
99
|
+
*/
|
|
100
|
+
export class TimeoutError extends BaseError {
|
|
101
|
+
constructor(message: string, timeout?: number, details?: Record<string, unknown>) {
|
|
102
|
+
super(message, 'TIMEOUT_ERROR', 408, { timeout, ...details });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Error types
|
|
108
|
+
*/
|
|
109
|
+
export const ErrorTypes = {
|
|
110
|
+
ValidationError,
|
|
111
|
+
ConfigurationError,
|
|
112
|
+
StorageError,
|
|
113
|
+
NetworkError,
|
|
114
|
+
AuthenticationError,
|
|
115
|
+
AuthorizationError,
|
|
116
|
+
NotFoundError,
|
|
117
|
+
TimeoutError,
|
|
118
|
+
} as const;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Error codes
|
|
122
|
+
*/
|
|
123
|
+
export const ErrorCodes = {
|
|
124
|
+
// Validation errors
|
|
125
|
+
INVALID_INPUT: 'INVALID_INPUT',
|
|
126
|
+
MISSING_REQUIRED_FIELD: 'MISSING_REQUIRED_FIELD',
|
|
127
|
+
INVALID_FORMAT: 'INVALID_FORMAT',
|
|
128
|
+
|
|
129
|
+
// Configuration errors
|
|
130
|
+
MISSING_CONFIG: 'MISSING_CONFIG',
|
|
131
|
+
INVALID_CONFIG: 'INVALID_CONFIG',
|
|
132
|
+
CONFIG_PARSE_ERROR: 'CONFIG_PARSE_ERROR',
|
|
133
|
+
|
|
134
|
+
// Storage errors
|
|
135
|
+
STORAGE_CONNECTION_FAILED: 'STORAGE_CONNECTION_FAILED',
|
|
136
|
+
STORAGE_OPERATION_FAILED: 'STORAGE_OPERATION_FAILED',
|
|
137
|
+
STORAGE_TIMEOUT: 'STORAGE_TIMEOUT',
|
|
138
|
+
STORAGE_FULL: 'STORAGE_FULL',
|
|
139
|
+
|
|
140
|
+
// Network errors
|
|
141
|
+
CONNECTION_FAILED: 'CONNECTION_FAILED',
|
|
142
|
+
REQUEST_FAILED: 'REQUEST_FAILED',
|
|
143
|
+
REQUEST_TIMEOUT: 'REQUEST_TIMEOUT',
|
|
144
|
+
RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',
|
|
145
|
+
|
|
146
|
+
// Authentication errors
|
|
147
|
+
UNAUTHORIZED: 'UNAUTHORIZED',
|
|
148
|
+
INVALID_CREDENTIALS: 'INVALID_CREDENTIALS',
|
|
149
|
+
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
|
|
150
|
+
|
|
151
|
+
// Authorization errors
|
|
152
|
+
FORBIDDEN: 'FORBIDDEN',
|
|
153
|
+
INSUFFICIENT_PERMISSIONS: 'INSUFFICIENT_PERMISSIONS',
|
|
154
|
+
|
|
155
|
+
// Not found errors
|
|
156
|
+
RESOURCE_NOT_FOUND: 'RESOURCE_NOT_FOUND',
|
|
157
|
+
ENDPOINT_NOT_FOUND: 'ENDPOINT_NOT_FOUND',
|
|
158
|
+
|
|
159
|
+
// System errors
|
|
160
|
+
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
|
161
|
+
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
|
|
162
|
+
TIMEOUT: 'TIMEOUT',
|
|
163
|
+
} as const;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Error handler interface
|
|
167
|
+
*/
|
|
168
|
+
export interface ErrorHandler {
|
|
169
|
+
canHandle(error: Error): boolean;
|
|
170
|
+
handle(error: Error): void | Promise<void>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Logger error handler
|
|
175
|
+
*/
|
|
176
|
+
export class LoggerErrorHandler implements ErrorHandler {
|
|
177
|
+
constructor(private level: 'error' | 'warn' | 'info' | 'debug' = 'error') {}
|
|
178
|
+
|
|
179
|
+
canHandle(error: Error): boolean {
|
|
180
|
+
return true; // Logger can handle all errors
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
handle(error: Error): void {
|
|
184
|
+
const errorData = {
|
|
185
|
+
name: error.name,
|
|
186
|
+
message: error.message,
|
|
187
|
+
code: (error as BaseError).code,
|
|
188
|
+
statusCode: (error as BaseError).statusCode,
|
|
189
|
+
stack: error.stack,
|
|
190
|
+
details: (error as BaseError).details,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
switch (this.level) {
|
|
194
|
+
case 'error':
|
|
195
|
+
logger.error(error.message, errorData);
|
|
196
|
+
break;
|
|
197
|
+
case 'warn':
|
|
198
|
+
logger.warn(error.message, errorData);
|
|
199
|
+
break;
|
|
200
|
+
case 'info':
|
|
201
|
+
logger.info(error.message, errorData);
|
|
202
|
+
break;
|
|
203
|
+
case 'debug':
|
|
204
|
+
logger.debug(error.message, errorData);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Console error handler
|
|
212
|
+
*/
|
|
213
|
+
export class ConsoleErrorHandler implements ErrorHandler {
|
|
214
|
+
canHandle(error: Error): boolean {
|
|
215
|
+
return true; // Console can handle all errors
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
handle(error: Error): void {
|
|
219
|
+
if (error instanceof BaseError) {
|
|
220
|
+
console.error(`[${error.code}] ${error.message}`);
|
|
221
|
+
if (error.details) {
|
|
222
|
+
console.error('Details:', error.details);
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
console.error(error.message);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (process.env.NODE_ENV === 'development' && error.stack) {
|
|
229
|
+
console.error('\nStack trace:');
|
|
230
|
+
console.error(error.stack);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Error handler chain
|
|
237
|
+
*/
|
|
238
|
+
export class ErrorHandlerChain {
|
|
239
|
+
private handlers: ErrorHandler[] = [];
|
|
240
|
+
|
|
241
|
+
constructor(handlers: ErrorHandler[] = []) {
|
|
242
|
+
this.handlers = handlers;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
addHandler(handler: ErrorHandler): void {
|
|
246
|
+
this.handlers.push(handler);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
removeHandler(handler: ErrorHandler): void {
|
|
250
|
+
const index = this.handlers.indexOf(handler);
|
|
251
|
+
if (index > -1) {
|
|
252
|
+
this.handlers.splice(index, 1);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async handle(error: Error): Promise<void> {
|
|
257
|
+
for (const handler of this.handlers) {
|
|
258
|
+
if (handler.canHandle(error)) {
|
|
259
|
+
await handler.handle(error);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// If no handler can handle the error, use default console handler
|
|
265
|
+
new ConsoleErrorHandler().handle(error);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Global error handler
|
|
271
|
+
*/
|
|
272
|
+
export const globalErrorHandler = new ErrorHandlerChain([
|
|
273
|
+
new LoggerErrorHandler('error'),
|
|
274
|
+
]);
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Set up global error handlers
|
|
278
|
+
*/
|
|
279
|
+
export function setupGlobalErrorHandlers(): void {
|
|
280
|
+
// Handle uncaught exceptions
|
|
281
|
+
process.on('uncaughtException', (error: Error) => {
|
|
282
|
+
logger.error('Uncaught Exception:', { error: error.message, stack: error.stack });
|
|
283
|
+
globalErrorHandler.handle(error);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Handle unhandled promise rejections
|
|
288
|
+
process.on('unhandledRejection', (reason: unknown, promise: Promise<unknown>) => {
|
|
289
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
290
|
+
logger.error('Unhandled Rejection:', { error: error.message, reason, promise });
|
|
291
|
+
globalErrorHandler.handle(error);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Handle process termination
|
|
295
|
+
process.on('SIGINT', () => {
|
|
296
|
+
logger.info('Process terminated by user');
|
|
297
|
+
process.exit(0);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
process.on('SIGTERM', () => {
|
|
301
|
+
logger.info('Process terminated');
|
|
302
|
+
process.exit(0);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Safe function wrapper with error handling
|
|
308
|
+
*/
|
|
309
|
+
export async function withErrorHandling<T>(
|
|
310
|
+
fn: () => Promise<T>,
|
|
311
|
+
errorHandler?: (error: Error) => void | Promise<void>
|
|
312
|
+
): Promise<Result<T>> {
|
|
313
|
+
try {
|
|
314
|
+
const data = await fn();
|
|
315
|
+
return { success: true, data };
|
|
316
|
+
} catch (error) {
|
|
317
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
318
|
+
|
|
319
|
+
if (errorHandler) {
|
|
320
|
+
await errorHandler(errorObj);
|
|
321
|
+
} else {
|
|
322
|
+
await globalErrorHandler.handle(errorObj);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return { success: false, error: errorObj };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Safe sync function wrapper with error handling
|
|
331
|
+
*/
|
|
332
|
+
export function withSyncErrorHandling<T>(
|
|
333
|
+
fn: () => T,
|
|
334
|
+
errorHandler?: (error: Error) => void | Promise<void>
|
|
335
|
+
): Result<T> {
|
|
336
|
+
try {
|
|
337
|
+
const data = fn();
|
|
338
|
+
return { success: true, data };
|
|
339
|
+
} catch (error) {
|
|
340
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
341
|
+
|
|
342
|
+
if (errorHandler) {
|
|
343
|
+
// For sync error handlers, we need to handle them asynchronously
|
|
344
|
+
void errorHandler(errorObj);
|
|
345
|
+
} else {
|
|
346
|
+
void globalErrorHandler.handle(errorObj);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return { success: false, error: errorObj };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Retry function with error handling
|
|
355
|
+
*/
|
|
356
|
+
export async function withRetry<T>(
|
|
357
|
+
fn: () => Promise<T>,
|
|
358
|
+
options: {
|
|
359
|
+
maxAttempts?: number;
|
|
360
|
+
delay?: number;
|
|
361
|
+
backoff?: 'linear' | 'exponential';
|
|
362
|
+
retryableErrors?: string[];
|
|
363
|
+
onRetry?: (attempt: number, error: Error) => void;
|
|
364
|
+
} = {}
|
|
365
|
+
): Promise<Result<T>> {
|
|
366
|
+
const {
|
|
367
|
+
maxAttempts = 3,
|
|
368
|
+
delay = 1000,
|
|
369
|
+
backoff = 'exponential',
|
|
370
|
+
retryableErrors = [],
|
|
371
|
+
onRetry,
|
|
372
|
+
} = options;
|
|
373
|
+
|
|
374
|
+
let lastError: Error;
|
|
375
|
+
|
|
376
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
377
|
+
try {
|
|
378
|
+
const data = await fn();
|
|
379
|
+
return { success: true, data };
|
|
380
|
+
} catch (error) {
|
|
381
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
382
|
+
|
|
383
|
+
// Check if error is retryable
|
|
384
|
+
const isRetryable = retryableErrors.length === 0 ||
|
|
385
|
+
retryableErrors.includes((lastError as BaseError).code) ||
|
|
386
|
+
retryableErrors.includes(lastError.constructor.name);
|
|
387
|
+
|
|
388
|
+
if (!isRetryable || attempt === maxAttempts) {
|
|
389
|
+
await globalErrorHandler.handle(lastError);
|
|
390
|
+
return { success: false, error: lastError };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Call retry callback
|
|
394
|
+
if (onRetry) {
|
|
395
|
+
onRetry(attempt, lastError);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Calculate delay
|
|
399
|
+
const retryDelay = backoff === 'exponential'
|
|
400
|
+
? delay * Math.pow(2, attempt - 1)
|
|
401
|
+
: delay * attempt;
|
|
402
|
+
|
|
403
|
+
// Wait before retry
|
|
404
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
await globalErrorHandler.handle(lastError!);
|
|
409
|
+
return { success: false, error: lastError! };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Timeout wrapper
|
|
414
|
+
*/
|
|
415
|
+
export async function withTimeout<T>(
|
|
416
|
+
fn: () => Promise<T>,
|
|
417
|
+
timeoutMs: number,
|
|
418
|
+
timeoutMessage = 'Operation timed out'
|
|
419
|
+
): Promise<Result<T>> {
|
|
420
|
+
return new Promise((resolve) => {
|
|
421
|
+
const timeoutId = setTimeout(() => {
|
|
422
|
+
resolve({
|
|
423
|
+
success: false,
|
|
424
|
+
error: new TimeoutError(timeoutMessage, timeoutMs),
|
|
425
|
+
});
|
|
426
|
+
}, timeoutMs);
|
|
427
|
+
|
|
428
|
+
fn()
|
|
429
|
+
.then((data) => {
|
|
430
|
+
clearTimeout(timeoutId);
|
|
431
|
+
resolve({ success: true, data });
|
|
432
|
+
})
|
|
433
|
+
.catch((error) => {
|
|
434
|
+
clearTimeout(timeoutId);
|
|
435
|
+
resolve({
|
|
436
|
+
success: false,
|
|
437
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Circuit breaker pattern
|
|
445
|
+
*/
|
|
446
|
+
export class CircuitBreaker {
|
|
447
|
+
private failures = 0;
|
|
448
|
+
private lastFailureTime = 0;
|
|
449
|
+
private state: 'closed' | 'open' | 'half-open' = 'closed';
|
|
450
|
+
|
|
451
|
+
constructor(
|
|
452
|
+
private options: {
|
|
453
|
+
failureThreshold?: number;
|
|
454
|
+
recoveryTimeMs?: number;
|
|
455
|
+
monitoringPeriodMs?: number;
|
|
456
|
+
} = {}
|
|
457
|
+
) {
|
|
458
|
+
const {
|
|
459
|
+
failureThreshold = 5,
|
|
460
|
+
recoveryTimeMs = 60000,
|
|
461
|
+
monitoringPeriodMs = 10000,
|
|
462
|
+
} = options;
|
|
463
|
+
|
|
464
|
+
this.options = { failureThreshold, recoveryTimeMs, monitoringPeriodMs };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async execute<T>(fn: () => Promise<T>): Promise<Result<T>> {
|
|
468
|
+
if (this.state === 'open') {
|
|
469
|
+
if (Date.now() - this.lastFailureTime > this.options.recoveryTimeMs!) {
|
|
470
|
+
this.state = 'half-open';
|
|
471
|
+
} else {
|
|
472
|
+
return {
|
|
473
|
+
success: false,
|
|
474
|
+
error: new Error('Circuit breaker is open'),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
const result = await fn();
|
|
481
|
+
this.onSuccess();
|
|
482
|
+
return { success: true, data: result };
|
|
483
|
+
} catch (error) {
|
|
484
|
+
this.onFailure();
|
|
485
|
+
return {
|
|
486
|
+
success: false,
|
|
487
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private onSuccess(): void {
|
|
493
|
+
this.failures = 0;
|
|
494
|
+
this.state = 'closed';
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private onFailure(): void {
|
|
498
|
+
this.failures++;
|
|
499
|
+
this.lastFailureTime = Date.now();
|
|
500
|
+
|
|
501
|
+
if (this.failures >= this.options.failureThreshold!) {
|
|
502
|
+
this.state = 'open';
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
getState(): 'closed' | 'open' | 'half-open' {
|
|
507
|
+
return this.state;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
getFailures(): number {
|
|
511
|
+
return this.failures;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
reset(): void {
|
|
515
|
+
this.failures = 0;
|
|
516
|
+
this.state = 'closed';
|
|
517
|
+
this.lastFailureTime = 0;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Byte Formatting Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'bun:test';
|
|
6
|
+
import { formatBytes, formatFileSize } from './bytes.js';
|
|
7
|
+
|
|
8
|
+
describe('formatBytes', () => {
|
|
9
|
+
describe('default options (decimals: 2, longUnits)', () => {
|
|
10
|
+
it('formats 0 bytes', () => {
|
|
11
|
+
expect(formatBytes(0)).toBe('0 Bytes');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('formats bytes', () => {
|
|
15
|
+
expect(formatBytes(500)).toBe('500 Bytes');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('formats kilobytes', () => {
|
|
19
|
+
expect(formatBytes(1024)).toBe('1 KB');
|
|
20
|
+
expect(formatBytes(1536)).toBe('1.5 KB');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('formats megabytes', () => {
|
|
24
|
+
expect(formatBytes(1048576)).toBe('1 MB');
|
|
25
|
+
expect(formatBytes(1572864)).toBe('1.5 MB');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('formats gigabytes', () => {
|
|
29
|
+
expect(formatBytes(1073741824)).toBe('1 GB');
|
|
30
|
+
expect(formatBytes(1610612736)).toBe('1.5 GB');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('formats terabytes', () => {
|
|
34
|
+
expect(formatBytes(1099511627776)).toBe('1 TB');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('rounds to 2 decimal places', () => {
|
|
38
|
+
expect(formatBytes(1587)).toBe('1.55 KB');
|
|
39
|
+
expect(formatBytes(1638400)).toBe('1.56 MB');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('with decimals option', () => {
|
|
44
|
+
it('formats with 0 decimals', () => {
|
|
45
|
+
expect(formatBytes(1536, { decimals: 0 })).toBe('2 KB');
|
|
46
|
+
expect(formatBytes(1024, { decimals: 0 })).toBe('1 KB');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('formats with 1 decimal', () => {
|
|
50
|
+
expect(formatBytes(1536, { decimals: 1 })).toBe('1.5 KB');
|
|
51
|
+
expect(formatBytes(1587, { decimals: 1 })).toBe('1.5 KB');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('formats with 3 decimals', () => {
|
|
55
|
+
expect(formatBytes(1587, { decimals: 3 })).toBe('1.55 KB'); // toFixed trims to 1.550, then trimmed
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('with shortUnits option', () => {
|
|
60
|
+
it('uses short unit for 0 bytes', () => {
|
|
61
|
+
expect(formatBytes(0, { shortUnits: true })).toBe('0 B');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('uses short unit for bytes', () => {
|
|
65
|
+
expect(formatBytes(500, { shortUnits: true })).toBe('500 B');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('uses short units for kilobytes', () => {
|
|
69
|
+
expect(formatBytes(1024, { shortUnits: true })).toBe('1 KB');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('uses short units for megabytes', () => {
|
|
73
|
+
expect(formatBytes(1048576, { shortUnits: true })).toBe('1 MB');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('combined options', () => {
|
|
78
|
+
it('uses short units with 1 decimal', () => {
|
|
79
|
+
expect(formatBytes(1536, { decimals: 1, shortUnits: true })).toBe('1.5 KB');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('uses short units with 0 decimals', () => {
|
|
83
|
+
expect(formatBytes(1536, { decimals: 0, shortUnits: true })).toBe('2 KB');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('edge cases', () => {
|
|
88
|
+
it('handles 1 byte', () => {
|
|
89
|
+
expect(formatBytes(1)).toBe('1 Bytes');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('handles very large numbers', () => {
|
|
93
|
+
const result = formatBytes(1099511627776 * 1024); // 1 PB
|
|
94
|
+
expect(result).toContain('TB'); // Will show in TB since we only go up to TB
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('handles fractional KB', () => {
|
|
98
|
+
expect(formatBytes(1500)).toBe('1.46 KB');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('formatFileSize', () => {
|
|
104
|
+
it('formats with 1 decimal and short units', () => {
|
|
105
|
+
expect(formatFileSize(0)).toBe('0 B');
|
|
106
|
+
expect(formatFileSize(1024)).toBe('1 KB'); // toFixed(1) gives "1.0", trimmed to "1"
|
|
107
|
+
expect(formatFileSize(1536)).toBe('1.5 KB');
|
|
108
|
+
expect(formatFileSize(1048576)).toBe('1 MB'); // toFixed(1) gives "1.0", trimmed to "1"
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('is an alias for formatBytes with specific options', () => {
|
|
112
|
+
const bytes = 1572864;
|
|
113
|
+
expect(formatFileSize(bytes)).toBe(formatBytes(bytes, { decimals: 1, shortUnits: true }));
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte Formatting Utilities
|
|
3
|
+
* Shared utilities for formatting byte sizes with configurable options
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ByteFormatOptions {
|
|
7
|
+
/** Decimal places to round to (default: 2) */
|
|
8
|
+
decimals?: number;
|
|
9
|
+
/** Use short unit names like 'B', 'KB' instead of 'Bytes', 'KB' (default: false) */
|
|
10
|
+
shortUnits?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Format bytes to human-readable size
|
|
15
|
+
* Pure - number to string formatting
|
|
16
|
+
*
|
|
17
|
+
* @param bytes - The number of bytes to format
|
|
18
|
+
* @param options - Formatting options
|
|
19
|
+
* @returns Formatted string like "1.5 MB" or "2.3 KB"
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* formatBytes(0) // '0 Bytes'
|
|
23
|
+
* formatBytes(1024) // '1 KB'
|
|
24
|
+
* formatBytes(1536, { decimals: 1 }) // '1.5 KB'
|
|
25
|
+
* formatBytes(1024, { shortUnits: true }) // '1 KB'
|
|
26
|
+
* formatBytes(0, { shortUnits: true }) // '0 B'
|
|
27
|
+
*/
|
|
28
|
+
export function formatBytes(bytes: number, options: ByteFormatOptions = {}): string {
|
|
29
|
+
const { decimals = 2, shortUnits = false } = options;
|
|
30
|
+
|
|
31
|
+
const units = shortUnits
|
|
32
|
+
? ['B', 'KB', 'MB', 'GB', 'TB']
|
|
33
|
+
: ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
34
|
+
|
|
35
|
+
if (bytes === 0) {
|
|
36
|
+
return `0 ${units[0]}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
40
|
+
const value = bytes / Math.pow(1024, i);
|
|
41
|
+
|
|
42
|
+
// Format with specified decimal places
|
|
43
|
+
const formatted = value.toFixed(decimals);
|
|
44
|
+
|
|
45
|
+
// Remove trailing zeros and decimal point if not needed
|
|
46
|
+
const trimmed = decimals > 0 ? formatted.replace(/\.?0+$/, '') : formatted;
|
|
47
|
+
|
|
48
|
+
return `${trimmed} ${units[i]}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Format file size - alias for formatBytes with 1 decimal place
|
|
53
|
+
* Kept for backward compatibility
|
|
54
|
+
*
|
|
55
|
+
* @param bytes - The number of bytes to format
|
|
56
|
+
* @returns Formatted string like "1.5 MB"
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* formatFileSize(1536) // '1.5 KB'
|
|
60
|
+
* formatFileSize(1048576) // '1.0 MB'
|
|
61
|
+
*/
|
|
62
|
+
export function formatFileSize(bytes: number): string {
|
|
63
|
+
return formatBytes(bytes, { decimals: 1, shortUnits: true });
|
|
64
|
+
}
|