unreal-engine-mcp-server 0.2.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/.dockerignore +57 -0
- package/.env.production +25 -0
- package/.eslintrc.json +54 -0
- package/.github/workflows/publish-mcp.yml +75 -0
- package/Dockerfile +54 -0
- package/LICENSE +21 -0
- package/Public/icon.png +0 -0
- package/README.md +209 -0
- package/claude_desktop_config_example.json +13 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +7 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +484 -0
- package/dist/prompts/index.d.ts +14 -0
- package/dist/prompts/index.js +38 -0
- package/dist/python-utils.d.ts +29 -0
- package/dist/python-utils.js +54 -0
- package/dist/resources/actors.d.ts +13 -0
- package/dist/resources/actors.js +83 -0
- package/dist/resources/assets.d.ts +23 -0
- package/dist/resources/assets.js +245 -0
- package/dist/resources/levels.d.ts +17 -0
- package/dist/resources/levels.js +94 -0
- package/dist/tools/actors.d.ts +51 -0
- package/dist/tools/actors.js +459 -0
- package/dist/tools/animation.d.ts +196 -0
- package/dist/tools/animation.js +579 -0
- package/dist/tools/assets.d.ts +21 -0
- package/dist/tools/assets.js +304 -0
- package/dist/tools/audio.d.ts +170 -0
- package/dist/tools/audio.js +416 -0
- package/dist/tools/blueprint.d.ts +144 -0
- package/dist/tools/blueprint.js +652 -0
- package/dist/tools/build_environment_advanced.d.ts +66 -0
- package/dist/tools/build_environment_advanced.js +484 -0
- package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
- package/dist/tools/consolidated-tool-definitions.js +607 -0
- package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
- package/dist/tools/consolidated-tool-handlers.js +1050 -0
- package/dist/tools/debug.d.ts +185 -0
- package/dist/tools/debug.js +265 -0
- package/dist/tools/editor.d.ts +88 -0
- package/dist/tools/editor.js +365 -0
- package/dist/tools/engine.d.ts +30 -0
- package/dist/tools/engine.js +36 -0
- package/dist/tools/foliage.d.ts +155 -0
- package/dist/tools/foliage.js +525 -0
- package/dist/tools/introspection.d.ts +98 -0
- package/dist/tools/introspection.js +683 -0
- package/dist/tools/landscape.d.ts +158 -0
- package/dist/tools/landscape.js +375 -0
- package/dist/tools/level.d.ts +110 -0
- package/dist/tools/level.js +362 -0
- package/dist/tools/lighting.d.ts +159 -0
- package/dist/tools/lighting.js +1179 -0
- package/dist/tools/materials.d.ts +34 -0
- package/dist/tools/materials.js +146 -0
- package/dist/tools/niagara.d.ts +145 -0
- package/dist/tools/niagara.js +289 -0
- package/dist/tools/performance.d.ts +163 -0
- package/dist/tools/performance.js +412 -0
- package/dist/tools/physics.d.ts +189 -0
- package/dist/tools/physics.js +784 -0
- package/dist/tools/rc.d.ts +110 -0
- package/dist/tools/rc.js +363 -0
- package/dist/tools/sequence.d.ts +112 -0
- package/dist/tools/sequence.js +675 -0
- package/dist/tools/tool-definitions.d.ts +4919 -0
- package/dist/tools/tool-definitions.js +891 -0
- package/dist/tools/tool-handlers.d.ts +47 -0
- package/dist/tools/tool-handlers.js +830 -0
- package/dist/tools/ui.d.ts +171 -0
- package/dist/tools/ui.js +337 -0
- package/dist/tools/visual.d.ts +29 -0
- package/dist/tools/visual.js +67 -0
- package/dist/types/env.d.ts +10 -0
- package/dist/types/env.js +18 -0
- package/dist/types/index.d.ts +323 -0
- package/dist/types/index.js +28 -0
- package/dist/types/tool-types.d.ts +274 -0
- package/dist/types/tool-types.js +13 -0
- package/dist/unreal-bridge.d.ts +126 -0
- package/dist/unreal-bridge.js +992 -0
- package/dist/utils/cache-manager.d.ts +64 -0
- package/dist/utils/cache-manager.js +176 -0
- package/dist/utils/error-handler.d.ts +66 -0
- package/dist/utils/error-handler.js +243 -0
- package/dist/utils/errors.d.ts +133 -0
- package/dist/utils/errors.js +256 -0
- package/dist/utils/http.d.ts +26 -0
- package/dist/utils/http.js +135 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +32 -0
- package/dist/utils/normalize.d.ts +17 -0
- package/dist/utils/normalize.js +49 -0
- package/dist/utils/response-validator.d.ts +34 -0
- package/dist/utils/response-validator.js +121 -0
- package/dist/utils/safe-json.d.ts +4 -0
- package/dist/utils/safe-json.js +97 -0
- package/dist/utils/stdio-redirect.d.ts +2 -0
- package/dist/utils/stdio-redirect.js +20 -0
- package/dist/utils/validation.d.ts +50 -0
- package/dist/utils/validation.js +173 -0
- package/mcp-config-example.json +14 -0
- package/package.json +63 -0
- package/server.json +60 -0
- package/src/cli.ts +7 -0
- package/src/index.ts +543 -0
- package/src/prompts/index.ts +51 -0
- package/src/python/editor_compat.py +181 -0
- package/src/python-utils.ts +57 -0
- package/src/resources/actors.ts +92 -0
- package/src/resources/assets.ts +251 -0
- package/src/resources/levels.ts +83 -0
- package/src/tools/actors.ts +480 -0
- package/src/tools/animation.ts +713 -0
- package/src/tools/assets.ts +305 -0
- package/src/tools/audio.ts +548 -0
- package/src/tools/blueprint.ts +736 -0
- package/src/tools/build_environment_advanced.ts +526 -0
- package/src/tools/consolidated-tool-definitions.ts +619 -0
- package/src/tools/consolidated-tool-handlers.ts +1093 -0
- package/src/tools/debug.ts +368 -0
- package/src/tools/editor.ts +360 -0
- package/src/tools/engine.ts +32 -0
- package/src/tools/foliage.ts +652 -0
- package/src/tools/introspection.ts +778 -0
- package/src/tools/landscape.ts +523 -0
- package/src/tools/level.ts +410 -0
- package/src/tools/lighting.ts +1316 -0
- package/src/tools/materials.ts +148 -0
- package/src/tools/niagara.ts +312 -0
- package/src/tools/performance.ts +549 -0
- package/src/tools/physics.ts +924 -0
- package/src/tools/rc.ts +437 -0
- package/src/tools/sequence.ts +791 -0
- package/src/tools/tool-definitions.ts +907 -0
- package/src/tools/tool-handlers.ts +941 -0
- package/src/tools/ui.ts +499 -0
- package/src/tools/visual.ts +60 -0
- package/src/types/env.ts +27 -0
- package/src/types/index.ts +414 -0
- package/src/types/tool-types.ts +343 -0
- package/src/unreal-bridge.ts +1118 -0
- package/src/utils/cache-manager.ts +213 -0
- package/src/utils/error-handler.ts +320 -0
- package/src/utils/errors.ts +312 -0
- package/src/utils/http.ts +184 -0
- package/src/utils/logger.ts +30 -0
- package/src/utils/normalize.ts +54 -0
- package/src/utils/response-validator.ts +145 -0
- package/src/utils/safe-json.ts +112 -0
- package/src/utils/stdio-redirect.ts +18 -0
- package/src/utils/validation.ts +212 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced error types for better error handling and recovery
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export enum ErrorCode {
|
|
6
|
+
// Connection errors
|
|
7
|
+
CONNECTION_FAILED = 'CONNECTION_FAILED',
|
|
8
|
+
CONNECTION_TIMEOUT = 'CONNECTION_TIMEOUT',
|
|
9
|
+
CONNECTION_REFUSED = 'CONNECTION_REFUSED',
|
|
10
|
+
|
|
11
|
+
// API errors
|
|
12
|
+
API_ERROR = 'API_ERROR',
|
|
13
|
+
INVALID_RESPONSE = 'INVALID_RESPONSE',
|
|
14
|
+
RATE_LIMITED = 'RATE_LIMITED',
|
|
15
|
+
|
|
16
|
+
// Validation errors
|
|
17
|
+
VALIDATION_ERROR = 'VALIDATION_ERROR',
|
|
18
|
+
INVALID_PARAMETERS = 'INVALID_PARAMETERS',
|
|
19
|
+
MISSING_REQUIRED_FIELD = 'MISSING_REQUIRED_FIELD',
|
|
20
|
+
|
|
21
|
+
// Resource errors
|
|
22
|
+
RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND',
|
|
23
|
+
RESOURCE_LOCKED = 'RESOURCE_LOCKED',
|
|
24
|
+
RESOURCE_UNAVAILABLE = 'RESOURCE_UNAVAILABLE',
|
|
25
|
+
|
|
26
|
+
// Permission errors
|
|
27
|
+
UNAUTHORIZED = 'UNAUTHORIZED',
|
|
28
|
+
FORBIDDEN = 'FORBIDDEN',
|
|
29
|
+
|
|
30
|
+
// System errors
|
|
31
|
+
INTERNAL_ERROR = 'INTERNAL_ERROR',
|
|
32
|
+
CIRCUIT_BREAKER_OPEN = 'CIRCUIT_BREAKER_OPEN',
|
|
33
|
+
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ErrorMetadata {
|
|
37
|
+
code: ErrorCode;
|
|
38
|
+
statusCode?: number;
|
|
39
|
+
retriable: boolean;
|
|
40
|
+
context?: Record<string, any>;
|
|
41
|
+
timestamp: Date;
|
|
42
|
+
correlationId?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Base application error with metadata
|
|
47
|
+
*/
|
|
48
|
+
export class AppError extends Error {
|
|
49
|
+
public readonly metadata: ErrorMetadata;
|
|
50
|
+
|
|
51
|
+
constructor(message: string, metadata: Partial<ErrorMetadata> = {}) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = this.constructor.name;
|
|
54
|
+
this.metadata = {
|
|
55
|
+
code: metadata.code || ErrorCode.INTERNAL_ERROR,
|
|
56
|
+
retriable: metadata.retriable || false,
|
|
57
|
+
timestamp: new Date(),
|
|
58
|
+
...metadata
|
|
59
|
+
};
|
|
60
|
+
Error.captureStackTrace(this, this.constructor);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
toJSON() {
|
|
64
|
+
return {
|
|
65
|
+
name: this.name,
|
|
66
|
+
message: this.message,
|
|
67
|
+
...this.metadata,
|
|
68
|
+
stack: this.stack
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Connection-related errors
|
|
75
|
+
*/
|
|
76
|
+
export class ConnectionError extends AppError {
|
|
77
|
+
constructor(message: string, metadata: Partial<ErrorMetadata> = {}) {
|
|
78
|
+
super(message, {
|
|
79
|
+
code: ErrorCode.CONNECTION_FAILED,
|
|
80
|
+
retriable: true,
|
|
81
|
+
statusCode: 503,
|
|
82
|
+
...metadata
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* API-related errors
|
|
89
|
+
*/
|
|
90
|
+
export class ApiError extends AppError {
|
|
91
|
+
constructor(message: string, statusCode: number, metadata: Partial<ErrorMetadata> = {}) {
|
|
92
|
+
super(message, {
|
|
93
|
+
code: ErrorCode.API_ERROR,
|
|
94
|
+
statusCode,
|
|
95
|
+
retriable: statusCode >= 500 || statusCode === 429,
|
|
96
|
+
...metadata
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validation errors
|
|
103
|
+
*/
|
|
104
|
+
export class ValidationError extends AppError {
|
|
105
|
+
constructor(message: string, metadata: Partial<ErrorMetadata> = {}) {
|
|
106
|
+
super(message, {
|
|
107
|
+
code: ErrorCode.VALIDATION_ERROR,
|
|
108
|
+
statusCode: 400,
|
|
109
|
+
retriable: false,
|
|
110
|
+
...metadata
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resource errors
|
|
117
|
+
*/
|
|
118
|
+
export class ResourceError extends AppError {
|
|
119
|
+
constructor(message: string, code: ErrorCode, metadata: Partial<ErrorMetadata> = {}) {
|
|
120
|
+
super(message, {
|
|
121
|
+
code,
|
|
122
|
+
statusCode: code === ErrorCode.RESOURCE_NOT_FOUND ? 404 : 409,
|
|
123
|
+
retriable: code === ErrorCode.RESOURCE_LOCKED,
|
|
124
|
+
...metadata
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Circuit Breaker implementation for fault tolerance
|
|
131
|
+
*/
|
|
132
|
+
export enum CircuitState {
|
|
133
|
+
CLOSED = 'CLOSED',
|
|
134
|
+
OPEN = 'OPEN',
|
|
135
|
+
HALF_OPEN = 'HALF_OPEN'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface CircuitBreakerOptions {
|
|
139
|
+
threshold: number;
|
|
140
|
+
timeout: number;
|
|
141
|
+
resetTimeout: number;
|
|
142
|
+
onStateChange?: (oldState: CircuitState, newState: CircuitState) => void;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export class CircuitBreaker {
|
|
146
|
+
private state: CircuitState = CircuitState.CLOSED;
|
|
147
|
+
private failures = 0;
|
|
148
|
+
private successCount = 0;
|
|
149
|
+
private lastFailureTime?: Date;
|
|
150
|
+
private readonly options: CircuitBreakerOptions;
|
|
151
|
+
|
|
152
|
+
constructor(options: Partial<CircuitBreakerOptions> = {}) {
|
|
153
|
+
this.options = {
|
|
154
|
+
threshold: options.threshold || 5,
|
|
155
|
+
timeout: options.timeout || 60000, // 1 minute
|
|
156
|
+
resetTimeout: options.resetTimeout || 30000, // 30 seconds
|
|
157
|
+
onStateChange: options.onStateChange
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Execute function with circuit breaker protection
|
|
163
|
+
*/
|
|
164
|
+
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
165
|
+
// Check circuit state
|
|
166
|
+
if (this.state === CircuitState.OPEN) {
|
|
167
|
+
if (this.shouldAttemptReset()) {
|
|
168
|
+
this.transitionTo(CircuitState.HALF_OPEN);
|
|
169
|
+
} else {
|
|
170
|
+
throw new AppError('Circuit breaker is open', {
|
|
171
|
+
code: ErrorCode.CIRCUIT_BREAKER_OPEN,
|
|
172
|
+
retriable: true,
|
|
173
|
+
context: {
|
|
174
|
+
failures: this.failures,
|
|
175
|
+
lastFailure: this.lastFailureTime
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const result = await fn();
|
|
183
|
+
this.onSuccess();
|
|
184
|
+
return result;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
this.onFailure();
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private onSuccess(): void {
|
|
192
|
+
this.failures = 0;
|
|
193
|
+
if (this.state === CircuitState.HALF_OPEN) {
|
|
194
|
+
this.successCount++;
|
|
195
|
+
if (this.successCount >= 3) {
|
|
196
|
+
this.transitionTo(CircuitState.CLOSED);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private onFailure(): void {
|
|
202
|
+
this.failures++;
|
|
203
|
+
this.lastFailureTime = new Date();
|
|
204
|
+
this.successCount = 0;
|
|
205
|
+
|
|
206
|
+
if (this.failures >= this.options.threshold) {
|
|
207
|
+
this.transitionTo(CircuitState.OPEN);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private shouldAttemptReset(): boolean {
|
|
212
|
+
return (
|
|
213
|
+
this.lastFailureTime !== undefined &&
|
|
214
|
+
Date.now() - this.lastFailureTime.getTime() >= this.options.resetTimeout
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private transitionTo(newState: CircuitState): void {
|
|
219
|
+
const oldState = this.state;
|
|
220
|
+
this.state = newState;
|
|
221
|
+
|
|
222
|
+
if (newState === CircuitState.CLOSED) {
|
|
223
|
+
this.failures = 0;
|
|
224
|
+
this.successCount = 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (this.options.onStateChange && oldState !== newState) {
|
|
228
|
+
this.options.onStateChange(oldState, newState);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
getState(): CircuitState {
|
|
233
|
+
return this.state;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
getMetrics() {
|
|
237
|
+
return {
|
|
238
|
+
state: this.state,
|
|
239
|
+
failures: this.failures,
|
|
240
|
+
successCount: this.successCount,
|
|
241
|
+
lastFailureTime: this.lastFailureTime
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Error recovery strategies
|
|
248
|
+
*/
|
|
249
|
+
export class ErrorRecovery {
|
|
250
|
+
private static circuitBreakers = new Map<string, CircuitBreaker>();
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get or create circuit breaker for a service
|
|
254
|
+
*/
|
|
255
|
+
static getCircuitBreaker(service: string, options?: Partial<CircuitBreakerOptions>): CircuitBreaker {
|
|
256
|
+
if (!this.circuitBreakers.has(service)) {
|
|
257
|
+
this.circuitBreakers.set(service, new CircuitBreaker(options));
|
|
258
|
+
}
|
|
259
|
+
const breaker = this.circuitBreakers.get(service);
|
|
260
|
+
if (!breaker) {
|
|
261
|
+
throw new Error(`Circuit breaker for service ${service} could not be created`);
|
|
262
|
+
}
|
|
263
|
+
return breaker;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Wrap function with error recovery
|
|
268
|
+
*/
|
|
269
|
+
static async withRecovery<T>(
|
|
270
|
+
fn: () => Promise<T>,
|
|
271
|
+
options: {
|
|
272
|
+
service: string;
|
|
273
|
+
fallback?: () => T | Promise<T>;
|
|
274
|
+
onError?: (error: Error) => void;
|
|
275
|
+
}
|
|
276
|
+
): Promise<T> {
|
|
277
|
+
const breaker = this.getCircuitBreaker(options.service);
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
return await breaker.execute(fn);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (options.onError) {
|
|
283
|
+
options.onError(error as Error);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Try fallback if available
|
|
287
|
+
if (options.fallback) {
|
|
288
|
+
return await options.fallback();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Check if error is retriable
|
|
297
|
+
*/
|
|
298
|
+
static isRetriable(error: Error): boolean {
|
|
299
|
+
if (error instanceof AppError) {
|
|
300
|
+
return error.metadata.retriable;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check for network errors
|
|
304
|
+
const message = error.message.toLowerCase();
|
|
305
|
+
return (
|
|
306
|
+
message.includes('timeout') ||
|
|
307
|
+
message.includes('network') ||
|
|
308
|
+
message.includes('econnrefused') ||
|
|
309
|
+
message.includes('econnreset')
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import https from 'https';
|
|
4
|
+
|
|
5
|
+
// Connection pooling configuration for better performance
|
|
6
|
+
const httpAgent = new http.Agent({
|
|
7
|
+
keepAlive: true,
|
|
8
|
+
keepAliveMsecs: 30000,
|
|
9
|
+
maxSockets: 10,
|
|
10
|
+
maxFreeSockets: 5,
|
|
11
|
+
timeout: 30000
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const httpsAgent = new https.Agent({
|
|
15
|
+
keepAlive: true,
|
|
16
|
+
keepAliveMsecs: 30000,
|
|
17
|
+
maxSockets: 10,
|
|
18
|
+
maxFreeSockets: 5,
|
|
19
|
+
timeout: 30000
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Retry configuration interface
|
|
23
|
+
interface RetryConfig {
|
|
24
|
+
maxRetries: number;
|
|
25
|
+
initialDelay: number;
|
|
26
|
+
maxDelay: number;
|
|
27
|
+
backoffMultiplier: number;
|
|
28
|
+
retryableStatuses: number[];
|
|
29
|
+
retryableErrors: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const defaultRetryConfig: RetryConfig = {
|
|
33
|
+
maxRetries: 3,
|
|
34
|
+
initialDelay: 1000,
|
|
35
|
+
maxDelay: 10000,
|
|
36
|
+
backoffMultiplier: 2,
|
|
37
|
+
retryableStatuses: [408, 429, 500, 502, 503, 504],
|
|
38
|
+
retryableErrors: ['ECONNABORTED', 'ETIMEDOUT', 'ECONNRESET', 'ENOTFOUND']
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Calculate exponential backoff delay with jitter
|
|
43
|
+
*/
|
|
44
|
+
function calculateBackoff(attempt: number, config: RetryConfig): number {
|
|
45
|
+
const delay = Math.min(
|
|
46
|
+
config.initialDelay * Math.pow(config.backoffMultiplier, attempt - 1),
|
|
47
|
+
config.maxDelay
|
|
48
|
+
);
|
|
49
|
+
// Add jitter to prevent thundering herd
|
|
50
|
+
return delay + Math.random() * 1000;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Enhanced HTTP client factory with connection pooling and retry logic
|
|
55
|
+
*/
|
|
56
|
+
export function createHttpClient(baseURL: string): AxiosInstance {
|
|
57
|
+
const client = axios.create({
|
|
58
|
+
baseURL,
|
|
59
|
+
headers: {
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
'Accept': 'application/json'
|
|
62
|
+
},
|
|
63
|
+
timeout: 15000,
|
|
64
|
+
httpAgent,
|
|
65
|
+
httpsAgent,
|
|
66
|
+
// Ensure proper handling of request body transformation
|
|
67
|
+
transformRequest: [(data, headers) => {
|
|
68
|
+
// Remove Content-Length if it's set incorrectly
|
|
69
|
+
delete headers['Content-Length'];
|
|
70
|
+
delete headers['content-length'];
|
|
71
|
+
|
|
72
|
+
// Properly stringify JSON data
|
|
73
|
+
if (data && typeof data === 'object') {
|
|
74
|
+
const jsonStr = JSON.stringify(data);
|
|
75
|
+
// Let axios set Content-Length automatically
|
|
76
|
+
return jsonStr;
|
|
77
|
+
}
|
|
78
|
+
return data;
|
|
79
|
+
}],
|
|
80
|
+
// Optimize response handling
|
|
81
|
+
maxContentLength: 50 * 1024 * 1024, // 50MB
|
|
82
|
+
maxBodyLength: 50 * 1024 * 1024,
|
|
83
|
+
decompress: true
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Add request interceptor for timing
|
|
87
|
+
client.interceptors.request.use(
|
|
88
|
+
(config) => {
|
|
89
|
+
// Add metadata for performance tracking
|
|
90
|
+
(config as any).metadata = { startTime: Date.now() };
|
|
91
|
+
return config;
|
|
92
|
+
},
|
|
93
|
+
(error) => {
|
|
94
|
+
return Promise.reject(error);
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Add response interceptor for timing and logging
|
|
99
|
+
client.interceptors.response.use(
|
|
100
|
+
(response) => {
|
|
101
|
+
const duration = Date.now() - ((response.config as any).metadata?.startTime || 0);
|
|
102
|
+
if (duration > 5000) {
|
|
103
|
+
console.warn(`[HTTP] Slow request: ${response.config.url} took ${duration}ms`);
|
|
104
|
+
}
|
|
105
|
+
return response;
|
|
106
|
+
},
|
|
107
|
+
(error) => {
|
|
108
|
+
return Promise.reject(error);
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return client;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Execute request with retry logic for resilience
|
|
117
|
+
*/
|
|
118
|
+
export async function requestWithRetry<T = any>(
|
|
119
|
+
client: AxiosInstance,
|
|
120
|
+
config: AxiosRequestConfig,
|
|
121
|
+
retryConfig: Partial<RetryConfig> = {}
|
|
122
|
+
): Promise<T> {
|
|
123
|
+
const retry = { ...defaultRetryConfig, ...retryConfig };
|
|
124
|
+
let lastError: Error | null = null;
|
|
125
|
+
|
|
126
|
+
for (let attempt = 1; attempt <= retry.maxRetries; attempt++) {
|
|
127
|
+
try {
|
|
128
|
+
const response = await client.request<T>(config);
|
|
129
|
+
|
|
130
|
+
// Check if we should retry based on status
|
|
131
|
+
if (retry.retryableStatuses.includes(response.status)) {
|
|
132
|
+
throw new Error(`Retryable status: ${response.status}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return response.data;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
lastError = error as Error;
|
|
138
|
+
const axiosError = error as AxiosError;
|
|
139
|
+
|
|
140
|
+
// Check if error is retryable
|
|
141
|
+
const isRetryable =
|
|
142
|
+
retry.retryableErrors.includes(axiosError.code || '') ||
|
|
143
|
+
(axiosError.response && retry.retryableStatuses.includes(axiosError.response.status));
|
|
144
|
+
|
|
145
|
+
if (!isRetryable || attempt === retry.maxRetries) {
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Calculate delay and wait
|
|
150
|
+
const delay = calculateBackoff(attempt, retry);
|
|
151
|
+
console.error(`[HTTP] Retry attempt ${attempt}/${retry.maxRetries} after ${Math.round(delay)}ms`);
|
|
152
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
throw lastError || new Error('Request failed after retries');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Batch multiple requests for efficiency
|
|
161
|
+
*/
|
|
162
|
+
export async function batchRequests<T = any>(
|
|
163
|
+
client: AxiosInstance,
|
|
164
|
+
requests: AxiosRequestConfig[],
|
|
165
|
+
options: { concurrency?: number; throwOnError?: boolean } = {}
|
|
166
|
+
): Promise<(T | Error)[]> {
|
|
167
|
+
const { concurrency = 5, throwOnError = false } = options;
|
|
168
|
+
const results: (T | Error)[] = [];
|
|
169
|
+
|
|
170
|
+
// Process requests in batches
|
|
171
|
+
for (let i = 0; i < requests.length; i += concurrency) {
|
|
172
|
+
const batch = requests.slice(i, i + concurrency);
|
|
173
|
+
const batchPromises = batch.map(req =>
|
|
174
|
+
client.request<T>(req)
|
|
175
|
+
.then(res => res.data)
|
|
176
|
+
.catch(err => throwOnError ? Promise.reject(err) : err)
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const batchResults = await Promise.all(batchPromises);
|
|
180
|
+
results.push(...batchResults);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return results;
|
|
184
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
2
|
+
|
|
3
|
+
export class Logger {
|
|
4
|
+
private level: LogLevel;
|
|
5
|
+
|
|
6
|
+
constructor(private scope: string, level: LogLevel = 'info') {
|
|
7
|
+
const envLevel = (process.env.LOG_LEVEL || process.env.LOGLEVEL || level).toString().toLowerCase();
|
|
8
|
+
this.level = (['debug', 'info', 'warn', 'error'] as LogLevel[]).includes(envLevel as LogLevel)
|
|
9
|
+
? (envLevel as LogLevel)
|
|
10
|
+
: 'info';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private shouldLog(level: LogLevel) {
|
|
14
|
+
const order: LogLevel[] = ['debug', 'info', 'warn', 'error'];
|
|
15
|
+
return order.indexOf(level) >= order.indexOf(this.level);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
debug(...args: any[]) {
|
|
19
|
+
if (this.shouldLog('debug')) console.error(`[${this.scope}]`, ...args);
|
|
20
|
+
}
|
|
21
|
+
info(...args: any[]) {
|
|
22
|
+
if (this.shouldLog('info')) console.error(`[${this.scope}]`, ...args);
|
|
23
|
+
}
|
|
24
|
+
warn(...args: any[]) {
|
|
25
|
+
if (this.shouldLog('warn')) console.error(`[${this.scope}]`, ...args);
|
|
26
|
+
}
|
|
27
|
+
error(...args: any[]) {
|
|
28
|
+
if (this.shouldLog('error')) console.error(`[${this.scope}]`, ...args);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type Vec3Array = [number, number, number];
|
|
2
|
+
export type Rot3Array = [number, number, number];
|
|
3
|
+
export interface Vec3Obj { x: number; y: number; z: number; }
|
|
4
|
+
export interface Rot3Obj { pitch: number; yaw: number; roll: number; }
|
|
5
|
+
|
|
6
|
+
export function toVec3Object(input: any): Vec3Obj | null {
|
|
7
|
+
try {
|
|
8
|
+
if (Array.isArray(input) && input.length === 3) {
|
|
9
|
+
const [x, y, z] = input;
|
|
10
|
+
if ([x, y, z].every(v => typeof v === 'number' && isFinite(v))) {
|
|
11
|
+
return { x, y, z };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (input && typeof input === 'object') {
|
|
15
|
+
const x = Number((input as any).x ?? (input as any).X);
|
|
16
|
+
const y = Number((input as any).y ?? (input as any).Y);
|
|
17
|
+
const z = Number((input as any).z ?? (input as any).Z);
|
|
18
|
+
if ([x, y, z].every(v => typeof v === 'number' && !isNaN(v) && isFinite(v))) {
|
|
19
|
+
return { x, y, z };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function toVec3Array(input: any): Vec3Array | null {
|
|
27
|
+
const obj = toVec3Object(input);
|
|
28
|
+
return obj ? [obj.x, obj.y, obj.z] : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function toRotObject(input: any): Rot3Obj | null {
|
|
32
|
+
try {
|
|
33
|
+
if (Array.isArray(input) && input.length === 3) {
|
|
34
|
+
const [pitch, yaw, roll] = input;
|
|
35
|
+
if ([pitch, yaw, roll].every(v => typeof v === 'number' && isFinite(v))) {
|
|
36
|
+
return { pitch, yaw, roll };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (input && typeof input === 'object') {
|
|
40
|
+
const pitch = Number((input as any).pitch ?? (input as any).Pitch);
|
|
41
|
+
const yaw = Number((input as any).yaw ?? (input as any).Yaw);
|
|
42
|
+
const roll = Number((input as any).roll ?? (input as any).Roll);
|
|
43
|
+
if ([pitch, yaw, roll].every(v => typeof v === 'number' && !isNaN(v) && isFinite(v))) {
|
|
44
|
+
return { pitch, yaw, roll };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function toRotArray(input: any): Rot3Array | null {
|
|
52
|
+
const obj = toRotObject(input);
|
|
53
|
+
return obj ? [obj.pitch, obj.yaw, obj.roll] : null;
|
|
54
|
+
}
|