@stacksjs/bun-router 0.0.19 → 0.0.20
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/dist/chunk-1ahs68ys.js +18 -0
- package/dist/{chunk-y3xq6wq6.js → chunk-cgptvjdf.js} +1 -1
- package/dist/chunk-g3ybefhg.js +1923 -0
- package/dist/cli.js +3 -3
- package/dist/container/index.js +211 -1149
- package/dist/index.js +2206 -366
- package/package.json +1 -2
- package/dist/chunk-2mx7fq49.js +0 -4
- package/dist/chunk-a8p83068.js +0 -103
package/dist/index.js
CHANGED
|
@@ -64,11 +64,48 @@ import {
|
|
|
64
64
|
signRequest,
|
|
65
65
|
singleFileUpload,
|
|
66
66
|
verifySignature
|
|
67
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-cgptvjdf.js";
|
|
68
68
|
import {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
AuthenticationException,
|
|
70
|
+
AuthorizationException,
|
|
71
|
+
BindingBuilder,
|
|
72
|
+
BusinessLogicException,
|
|
73
|
+
CircuitBreakerOpenException,
|
|
74
|
+
CircularDependencyException,
|
|
75
|
+
ConfigurationException,
|
|
76
|
+
Container,
|
|
77
|
+
ContextualContainer,
|
|
78
|
+
DatabaseException,
|
|
79
|
+
DefaultServiceProviderManager,
|
|
80
|
+
EnvironmentManager,
|
|
81
|
+
ErrorFactory,
|
|
82
|
+
ExternalServiceException,
|
|
83
|
+
FileSystemException,
|
|
84
|
+
HandlerResolutionException,
|
|
85
|
+
InternalServerException,
|
|
86
|
+
MethodNotAllowedException,
|
|
87
|
+
MiddlewareConfigurationException,
|
|
88
|
+
ModelBindingException,
|
|
89
|
+
NotFoundException,
|
|
90
|
+
RateLimitException,
|
|
91
|
+
RouteConflictException,
|
|
92
|
+
RouteNotFoundException,
|
|
93
|
+
RouterErrorFactory,
|
|
94
|
+
RouterException,
|
|
95
|
+
ServiceUnavailableException,
|
|
96
|
+
TimeoutException,
|
|
97
|
+
ValidationException,
|
|
98
|
+
createContainer,
|
|
99
|
+
detectRouteConflicts,
|
|
100
|
+
findSimilarRoutes,
|
|
101
|
+
getContainer,
|
|
102
|
+
setContainer
|
|
103
|
+
} from "./chunk-g3ybefhg.js";
|
|
104
|
+
import {
|
|
105
|
+
__export,
|
|
106
|
+
__require
|
|
107
|
+
} from "./chunk-1ahs68ys.js";
|
|
108
|
+
|
|
72
109
|
// src/auth.ts
|
|
73
110
|
import { timingSafeEqual } from "crypto";
|
|
74
111
|
function base64UrlEncode(str) {
|
|
@@ -215,123 +252,1800 @@ class ApiKeyManager {
|
|
|
215
252
|
...options
|
|
216
253
|
};
|
|
217
254
|
}
|
|
218
|
-
generateKey(owner, scopes = [], expiresIn) {
|
|
219
|
-
const key = `${this.options.keyPrefix}${this.randomString(32)}`;
|
|
220
|
-
this.keys.set(key, {
|
|
221
|
-
owner,
|
|
222
|
-
scopes,
|
|
223
|
-
expiresAt: expiresIn ? new Date(Date.now() + expiresIn * 1000) : undefined
|
|
255
|
+
generateKey(owner, scopes = [], expiresIn) {
|
|
256
|
+
const key = `${this.options.keyPrefix}${this.randomString(32)}`;
|
|
257
|
+
this.keys.set(key, {
|
|
258
|
+
owner,
|
|
259
|
+
scopes,
|
|
260
|
+
expiresAt: expiresIn ? new Date(Date.now() + expiresIn * 1000) : undefined
|
|
261
|
+
});
|
|
262
|
+
return key;
|
|
263
|
+
}
|
|
264
|
+
validateKey(key, requiredScopes = []) {
|
|
265
|
+
const keyInfo = this.keys.get(key);
|
|
266
|
+
if (!keyInfo) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
if (keyInfo.expiresAt && keyInfo.expiresAt < new Date) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
if (requiredScopes.length > 0) {
|
|
273
|
+
return requiredScopes.every((scope) => keyInfo.scopes.includes(scope));
|
|
274
|
+
}
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
getKeyInfo(key) {
|
|
278
|
+
return this.keys.get(key) || null;
|
|
279
|
+
}
|
|
280
|
+
revokeKey(key) {
|
|
281
|
+
return this.keys.delete(key);
|
|
282
|
+
}
|
|
283
|
+
extractFromRequest(req) {
|
|
284
|
+
const { source, keyName } = this.options;
|
|
285
|
+
switch (source) {
|
|
286
|
+
case "header":
|
|
287
|
+
return req.headers.get(keyName || "X-API-Key") || null;
|
|
288
|
+
case "query":
|
|
289
|
+
return new URL(req.url).searchParams.get(keyName || "api_key") || null;
|
|
290
|
+
case "cookie":
|
|
291
|
+
if (req.cookies) {
|
|
292
|
+
const key = keyName || "api_key";
|
|
293
|
+
if (typeof req.cookies.get === "function") {
|
|
294
|
+
return req.cookies.get(key) || null;
|
|
295
|
+
}
|
|
296
|
+
return req.cookies[key] || null;
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
default:
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
randomString(length) {
|
|
304
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
305
|
+
let result = "";
|
|
306
|
+
for (let i = 0;i < length; i++) {
|
|
307
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
308
|
+
}
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
class OAuth2Helper {
|
|
314
|
+
config;
|
|
315
|
+
constructor(config2) {
|
|
316
|
+
this.config = config2;
|
|
317
|
+
}
|
|
318
|
+
getAuthorizationUrl(additionalParams = {}) {
|
|
319
|
+
const params = new URLSearchParams({
|
|
320
|
+
client_id: this.config.clientId,
|
|
321
|
+
redirect_uri: this.config.redirectUri,
|
|
322
|
+
response_type: "code",
|
|
323
|
+
...additionalParams
|
|
324
|
+
});
|
|
325
|
+
if (this.config.scope) {
|
|
326
|
+
params.set("scope", this.config.scope);
|
|
327
|
+
}
|
|
328
|
+
if (this.config.state) {
|
|
329
|
+
params.set("state", this.config.state);
|
|
330
|
+
}
|
|
331
|
+
return `${this.config.authorizeUrl}?${params.toString()}`;
|
|
332
|
+
}
|
|
333
|
+
async exchangeCodeForToken(code) {
|
|
334
|
+
const params = new URLSearchParams({
|
|
335
|
+
grant_type: "authorization_code",
|
|
336
|
+
code,
|
|
337
|
+
redirect_uri: this.config.redirectUri,
|
|
338
|
+
client_id: this.config.clientId,
|
|
339
|
+
client_secret: this.config.clientSecret
|
|
340
|
+
});
|
|
341
|
+
const response = await fetch(this.config.tokenUrl, {
|
|
342
|
+
method: "POST",
|
|
343
|
+
headers: {
|
|
344
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
345
|
+
},
|
|
346
|
+
body: params.toString()
|
|
347
|
+
});
|
|
348
|
+
if (!response.ok) {
|
|
349
|
+
throw new Error(`OAuth token exchange failed: ${response.status} ${response.statusText}`);
|
|
350
|
+
}
|
|
351
|
+
return await response.json();
|
|
352
|
+
}
|
|
353
|
+
async refreshToken(refreshToken) {
|
|
354
|
+
const params = new URLSearchParams({
|
|
355
|
+
grant_type: "refresh_token",
|
|
356
|
+
refresh_token: refreshToken,
|
|
357
|
+
client_id: this.config.clientId,
|
|
358
|
+
client_secret: this.config.clientSecret
|
|
359
|
+
});
|
|
360
|
+
const response = await fetch(this.config.tokenUrl, {
|
|
361
|
+
method: "POST",
|
|
362
|
+
headers: {
|
|
363
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
364
|
+
},
|
|
365
|
+
body: params.toString()
|
|
366
|
+
});
|
|
367
|
+
if (!response.ok) {
|
|
368
|
+
throw new Error(`OAuth token refresh failed: ${response.status} ${response.statusText}`);
|
|
369
|
+
}
|
|
370
|
+
return await response.json();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
var Auth = {
|
|
374
|
+
JWT,
|
|
375
|
+
ApiKeyManager,
|
|
376
|
+
OAuth2Helper
|
|
377
|
+
};
|
|
378
|
+
var auth_default = Auth;
|
|
379
|
+
// src/errors/index.ts
|
|
380
|
+
var exports_errors = {};
|
|
381
|
+
__export(exports_errors, {
|
|
382
|
+
withCircuitBreaker: () => withCircuitBreaker,
|
|
383
|
+
globalCircuitBreakerRegistry: () => globalCircuitBreakerRegistry,
|
|
384
|
+
findSimilarRoutes: () => findSimilarRoutes,
|
|
385
|
+
detectRouteConflicts: () => detectRouteConflicts,
|
|
386
|
+
createGracefulDegradationMiddleware: () => createGracefulDegradationMiddleware,
|
|
387
|
+
createErrorReporting: () => createErrorReporting,
|
|
388
|
+
createCircuitBreakerMiddleware: () => createCircuitBreakerMiddleware,
|
|
389
|
+
createCircuitBreaker: () => createCircuitBreaker,
|
|
390
|
+
createAdvancedErrorHandler: () => createAdvancedErrorHandler,
|
|
391
|
+
circuitBreaker: () => circuitBreaker,
|
|
392
|
+
ValidationException: () => ValidationException,
|
|
393
|
+
TimeoutException: () => TimeoutException,
|
|
394
|
+
ServiceUnavailableException: () => ServiceUnavailableException,
|
|
395
|
+
ServiceHealthMonitor: () => ServiceHealthMonitor,
|
|
396
|
+
SentryReporter: () => SentryReporter,
|
|
397
|
+
RouterException: () => RouterException,
|
|
398
|
+
RouterErrorFactory: () => RouterErrorFactory,
|
|
399
|
+
RouteNotFoundException: () => RouteNotFoundException,
|
|
400
|
+
RouteConflictException: () => RouteConflictException,
|
|
401
|
+
RateLimitException: () => RateLimitException,
|
|
402
|
+
NotFoundException: () => NotFoundException,
|
|
403
|
+
ModelBindingException: () => ModelBindingException,
|
|
404
|
+
MiddlewareConfigurationException: () => MiddlewareConfigurationException,
|
|
405
|
+
MethodNotAllowedException: () => MethodNotAllowedException,
|
|
406
|
+
InternalServerException: () => InternalServerException,
|
|
407
|
+
HandlerResolutionException: () => HandlerResolutionException,
|
|
408
|
+
GracefulDegradationManager: () => GracefulDegradationManager,
|
|
409
|
+
FileSystemException: () => FileSystemException,
|
|
410
|
+
ExternalServiceException: () => ExternalServiceException,
|
|
411
|
+
ErrorReportingPresets: () => ErrorReportingPresets,
|
|
412
|
+
ErrorReportingManager: () => ErrorReportingManager,
|
|
413
|
+
ErrorFactory: () => ErrorFactory,
|
|
414
|
+
DegradationStrategies: () => DegradationStrategies,
|
|
415
|
+
DatabaseException: () => DatabaseException,
|
|
416
|
+
CustomReporter: () => CustomReporter,
|
|
417
|
+
ConfigurationException: () => ConfigurationException,
|
|
418
|
+
CircularDependencyException: () => CircularDependencyException,
|
|
419
|
+
CircuitBreakerRegistry: () => CircuitBreakerRegistry,
|
|
420
|
+
CircuitBreakerPresets: () => CircuitBreakerPresets,
|
|
421
|
+
CircuitBreakerOpenException: () => CircuitBreakerOpenException,
|
|
422
|
+
CircuitBreakerHttpClient: () => CircuitBreakerHttpClient,
|
|
423
|
+
CircuitBreaker: () => CircuitBreaker,
|
|
424
|
+
BusinessLogicException: () => BusinessLogicException,
|
|
425
|
+
BugsnagReporter: () => BugsnagReporter,
|
|
426
|
+
AuthorizationException: () => AuthorizationException,
|
|
427
|
+
AuthenticationException: () => AuthenticationException
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// src/errors/circuit-breaker.ts
|
|
431
|
+
class CircuitBreaker {
|
|
432
|
+
config;
|
|
433
|
+
state = "CLOSED";
|
|
434
|
+
metrics;
|
|
435
|
+
nextAttemptTime;
|
|
436
|
+
halfOpenCallCount = 0;
|
|
437
|
+
recentRequests = [];
|
|
438
|
+
constructor(config2) {
|
|
439
|
+
const defaults = {
|
|
440
|
+
failureThreshold: 5,
|
|
441
|
+
recoveryTimeout: 60000,
|
|
442
|
+
timeout: 30000,
|
|
443
|
+
monitoringPeriod: 60000,
|
|
444
|
+
minimumRequests: 10,
|
|
445
|
+
errorThresholdPercentage: 50,
|
|
446
|
+
halfOpenMaxCalls: 3,
|
|
447
|
+
resetTimeout: 60000
|
|
448
|
+
};
|
|
449
|
+
this.config = {
|
|
450
|
+
...defaults,
|
|
451
|
+
...config2
|
|
452
|
+
};
|
|
453
|
+
this.metrics = {
|
|
454
|
+
totalRequests: 0,
|
|
455
|
+
successfulRequests: 0,
|
|
456
|
+
failedRequests: 0,
|
|
457
|
+
timeouts: 0,
|
|
458
|
+
averageResponseTime: 0,
|
|
459
|
+
state: this.state,
|
|
460
|
+
consecutiveFailures: 0,
|
|
461
|
+
consecutiveSuccesses: 0
|
|
462
|
+
};
|
|
463
|
+
setInterval(() => this.cleanupOldRequests(), this.config.monitoringPeriod / 4).unref?.();
|
|
464
|
+
}
|
|
465
|
+
async execute(call) {
|
|
466
|
+
if (this.state === "OPEN") {
|
|
467
|
+
if (this.nextAttemptTime && new Date < this.nextAttemptTime) {
|
|
468
|
+
const error2 = new CircuitBreakerOpenException(this.config.name, this.nextAttemptTime, call.context);
|
|
469
|
+
if (call.fallback) {
|
|
470
|
+
try {
|
|
471
|
+
return await call.fallback();
|
|
472
|
+
} catch (fallbackError) {
|
|
473
|
+
console.error(fallbackError);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
throw error2;
|
|
477
|
+
} else {
|
|
478
|
+
this.transitionToHalfOpen();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (this.state === "HALF_OPEN" && this.halfOpenCallCount >= this.config.halfOpenMaxCalls) {
|
|
482
|
+
const error2 = new CircuitBreakerOpenException(this.config.name, this.nextAttemptTime || new Date(Date.now() + this.config.recoveryTimeout), call.context);
|
|
483
|
+
if (call.fallback) {
|
|
484
|
+
try {
|
|
485
|
+
return await call.fallback();
|
|
486
|
+
} catch (fallbackError) {
|
|
487
|
+
console.error(fallbackError);
|
|
488
|
+
throw error2;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
throw error2;
|
|
492
|
+
}
|
|
493
|
+
const startTime = Date.now();
|
|
494
|
+
let result;
|
|
495
|
+
let error = null;
|
|
496
|
+
try {
|
|
497
|
+
if (this.state === "HALF_OPEN") {
|
|
498
|
+
this.halfOpenCallCount++;
|
|
499
|
+
}
|
|
500
|
+
const timeout = call.timeout || this.config.timeout;
|
|
501
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
502
|
+
setTimeout(() => {
|
|
503
|
+
reject(new TimeoutException(`Circuit breaker timeout for ${this.config.name}`, timeout, call.context));
|
|
504
|
+
}, timeout);
|
|
505
|
+
});
|
|
506
|
+
result = await Promise.race([call.execute(), timeoutPromise]);
|
|
507
|
+
this.onSuccess(Date.now() - startTime);
|
|
508
|
+
return result;
|
|
509
|
+
} catch (err) {
|
|
510
|
+
error = err;
|
|
511
|
+
this.onFailure(error, Date.now() - startTime);
|
|
512
|
+
if (call.fallback) {
|
|
513
|
+
try {
|
|
514
|
+
return await call.fallback();
|
|
515
|
+
} catch (fallbackError) {
|
|
516
|
+
console.error(fallbackError);
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
519
|
+
} else {
|
|
520
|
+
throw error;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
getState() {
|
|
525
|
+
return this.state;
|
|
526
|
+
}
|
|
527
|
+
getMetrics() {
|
|
528
|
+
return {
|
|
529
|
+
...this.metrics,
|
|
530
|
+
state: this.state,
|
|
531
|
+
nextAttemptTime: this.nextAttemptTime
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
forceOpen() {
|
|
535
|
+
this.transitionToOpen();
|
|
536
|
+
}
|
|
537
|
+
forceClosed() {
|
|
538
|
+
this.transitionToClosed();
|
|
539
|
+
}
|
|
540
|
+
reset() {
|
|
541
|
+
this.metrics = {
|
|
542
|
+
totalRequests: 0,
|
|
543
|
+
successfulRequests: 0,
|
|
544
|
+
failedRequests: 0,
|
|
545
|
+
timeouts: 0,
|
|
546
|
+
averageResponseTime: 0,
|
|
547
|
+
state: this.state,
|
|
548
|
+
consecutiveFailures: 0,
|
|
549
|
+
consecutiveSuccesses: 0
|
|
550
|
+
};
|
|
551
|
+
this.recentRequests = [];
|
|
552
|
+
}
|
|
553
|
+
onSuccess(responseTime) {
|
|
554
|
+
this.metrics.totalRequests++;
|
|
555
|
+
this.metrics.successfulRequests++;
|
|
556
|
+
this.metrics.consecutiveSuccesses++;
|
|
557
|
+
this.metrics.consecutiveFailures = 0;
|
|
558
|
+
this.metrics.lastSuccessTime = new Date;
|
|
559
|
+
this.updateAverageResponseTime(responseTime);
|
|
560
|
+
this.recentRequests.push({
|
|
561
|
+
timestamp: new Date,
|
|
562
|
+
success: true,
|
|
563
|
+
responseTime
|
|
564
|
+
});
|
|
565
|
+
if (this.state === "HALF_OPEN") {
|
|
566
|
+
if (this.metrics.consecutiveSuccesses >= this.config.halfOpenMaxCalls) {
|
|
567
|
+
this.transitionToClosed();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (this.config.onSuccess) {
|
|
571
|
+
this.config.onSuccess(this.config.name);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
onFailure(error, responseTime) {
|
|
575
|
+
this.metrics.totalRequests++;
|
|
576
|
+
this.metrics.failedRequests++;
|
|
577
|
+
this.metrics.consecutiveFailures++;
|
|
578
|
+
this.metrics.consecutiveSuccesses = 0;
|
|
579
|
+
this.metrics.lastFailureTime = new Date;
|
|
580
|
+
if (error instanceof TimeoutException) {
|
|
581
|
+
this.metrics.timeouts++;
|
|
582
|
+
}
|
|
583
|
+
this.updateAverageResponseTime(responseTime);
|
|
584
|
+
this.recentRequests.push({
|
|
585
|
+
timestamp: new Date,
|
|
586
|
+
success: false,
|
|
587
|
+
responseTime
|
|
588
|
+
});
|
|
589
|
+
if (this.shouldTrip(error)) {
|
|
590
|
+
if (this.state === "CLOSED") {
|
|
591
|
+
this.transitionToOpen();
|
|
592
|
+
} else if (this.state === "HALF_OPEN") {
|
|
593
|
+
this.transitionToOpen();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (this.config.onFailure) {
|
|
597
|
+
this.config.onFailure(error, this.config.name);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
shouldTrip(error) {
|
|
601
|
+
if (this.config.shouldTripOnError && !this.config.shouldTripOnError(error)) {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
if (this.metrics.consecutiveFailures >= this.config.failureThreshold) {
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
const recentRequests = this.getRecentRequests();
|
|
608
|
+
if (recentRequests.length >= this.config.minimumRequests) {
|
|
609
|
+
const failedRequests = recentRequests.filter((r) => !r.success).length;
|
|
610
|
+
const errorRate = failedRequests / recentRequests.length * 100;
|
|
611
|
+
if (errorRate >= this.config.errorThresholdPercentage) {
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
transitionToClosed() {
|
|
618
|
+
this.state = "CLOSED";
|
|
619
|
+
this.nextAttemptTime = undefined;
|
|
620
|
+
this.halfOpenCallCount = 0;
|
|
621
|
+
this.metrics.state = this.state;
|
|
622
|
+
if (this.config.onStateChange) {
|
|
623
|
+
this.config.onStateChange(this.state, this.config.name);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
transitionToOpen() {
|
|
627
|
+
this.state = "OPEN";
|
|
628
|
+
this.nextAttemptTime = new Date(Date.now() + this.config.recoveryTimeout);
|
|
629
|
+
this.halfOpenCallCount = 0;
|
|
630
|
+
this.metrics.state = this.state;
|
|
631
|
+
this.metrics.nextAttemptTime = this.nextAttemptTime;
|
|
632
|
+
if (this.config.onStateChange) {
|
|
633
|
+
this.config.onStateChange(this.state, this.config.name);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
transitionToHalfOpen() {
|
|
637
|
+
this.state = "HALF_OPEN";
|
|
638
|
+
this.halfOpenCallCount = 0;
|
|
639
|
+
this.metrics.state = this.state;
|
|
640
|
+
if (this.config.onStateChange) {
|
|
641
|
+
this.config.onStateChange(this.state, this.config.name);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
updateAverageResponseTime(responseTime) {
|
|
645
|
+
if (this.metrics.totalRequests === 1) {
|
|
646
|
+
this.metrics.averageResponseTime = responseTime;
|
|
647
|
+
} else {
|
|
648
|
+
const alpha = 0.1;
|
|
649
|
+
this.metrics.averageResponseTime = alpha * responseTime + (1 - alpha) * this.metrics.averageResponseTime;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
getRecentRequests() {
|
|
653
|
+
const cutoff = new Date(Date.now() - this.config.monitoringPeriod);
|
|
654
|
+
return this.recentRequests.filter((r) => r.timestamp > cutoff);
|
|
655
|
+
}
|
|
656
|
+
cleanupOldRequests() {
|
|
657
|
+
const cutoff = new Date(Date.now() - this.config.monitoringPeriod);
|
|
658
|
+
this.recentRequests = this.recentRequests.filter((r) => r.timestamp > cutoff);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
class CircuitBreakerRegistry {
|
|
663
|
+
breakers = new Map;
|
|
664
|
+
configs = new Map;
|
|
665
|
+
register(config2) {
|
|
666
|
+
const breaker = new CircuitBreaker(config2);
|
|
667
|
+
this.breakers.set(config2.name, breaker);
|
|
668
|
+
this.configs.set(config2.name, config2);
|
|
669
|
+
return breaker;
|
|
670
|
+
}
|
|
671
|
+
get(name) {
|
|
672
|
+
return this.breakers.get(name);
|
|
673
|
+
}
|
|
674
|
+
async execute(name, call) {
|
|
675
|
+
const breaker = this.breakers.get(name);
|
|
676
|
+
if (!breaker) {
|
|
677
|
+
throw new Error(`Circuit breaker '${name}' not found`);
|
|
678
|
+
}
|
|
679
|
+
return breaker.execute(call);
|
|
680
|
+
}
|
|
681
|
+
getAllMetrics() {
|
|
682
|
+
const metrics = {};
|
|
683
|
+
for (const [name, breaker] of this.breakers) {
|
|
684
|
+
metrics[name] = breaker.getMetrics();
|
|
685
|
+
}
|
|
686
|
+
return metrics;
|
|
687
|
+
}
|
|
688
|
+
getBreakersByState(state) {
|
|
689
|
+
const names = [];
|
|
690
|
+
for (const [name, breaker] of this.breakers) {
|
|
691
|
+
if (breaker.getState() === state) {
|
|
692
|
+
names.push(name);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return names;
|
|
696
|
+
}
|
|
697
|
+
forceAllClosed() {
|
|
698
|
+
for (const breaker of this.breakers.values()) {
|
|
699
|
+
breaker.forceClosed();
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
resetAll() {
|
|
703
|
+
for (const breaker of this.breakers.values()) {
|
|
704
|
+
breaker.reset();
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
remove(name) {
|
|
708
|
+
this.configs.delete(name);
|
|
709
|
+
return this.breakers.delete(name);
|
|
710
|
+
}
|
|
711
|
+
getNames() {
|
|
712
|
+
return Array.from(this.breakers.keys());
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
function circuitBreaker(config2) {
|
|
716
|
+
const breaker = new CircuitBreaker(config2);
|
|
717
|
+
return function(target, propertyName, descriptor) {
|
|
718
|
+
const method = descriptor.value;
|
|
719
|
+
descriptor.value = async function(...args) {
|
|
720
|
+
return breaker.execute({
|
|
721
|
+
execute: () => method.apply(this, args),
|
|
722
|
+
context: {
|
|
723
|
+
method: propertyName,
|
|
724
|
+
timestamp: new Date
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
};
|
|
728
|
+
return descriptor;
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
class CircuitBreakerHttpClient {
|
|
733
|
+
registry;
|
|
734
|
+
constructor(registry) {
|
|
735
|
+
this.registry = registry || new CircuitBreakerRegistry;
|
|
736
|
+
}
|
|
737
|
+
async fetch(url, options = {}) {
|
|
738
|
+
const { circuitBreaker: circuitBreaker2, fallback, context, ...fetchOptions } = options;
|
|
739
|
+
let breakerName;
|
|
740
|
+
if (typeof circuitBreaker2 === "string") {
|
|
741
|
+
breakerName = circuitBreaker2;
|
|
742
|
+
} else if (circuitBreaker2) {
|
|
743
|
+
breakerName = circuitBreaker2.name;
|
|
744
|
+
if (!this.registry.get(breakerName)) {
|
|
745
|
+
this.registry.register(circuitBreaker2);
|
|
746
|
+
}
|
|
747
|
+
} else {
|
|
748
|
+
const hostname = new URL(url).hostname;
|
|
749
|
+
breakerName = `http-${hostname}`;
|
|
750
|
+
if (!this.registry.get(breakerName)) {
|
|
751
|
+
this.registry.register({
|
|
752
|
+
name: breakerName,
|
|
753
|
+
failureThreshold: 5,
|
|
754
|
+
recoveryTimeout: 60000,
|
|
755
|
+
timeout: 30000,
|
|
756
|
+
monitoringPeriod: 60000,
|
|
757
|
+
minimumRequests: 10,
|
|
758
|
+
errorThresholdPercentage: 50,
|
|
759
|
+
halfOpenMaxCalls: 3,
|
|
760
|
+
resetTimeout: 60000
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return this.registry.execute(breakerName, {
|
|
765
|
+
execute: async () => {
|
|
766
|
+
const response = await fetch(url, fetchOptions);
|
|
767
|
+
if (response.status >= 500) {
|
|
768
|
+
throw new ExternalServiceException(`HTTP ${response.status}: ${response.statusText}`, breakerName, url, response.status, undefined, context);
|
|
769
|
+
}
|
|
770
|
+
return response;
|
|
771
|
+
},
|
|
772
|
+
fallback,
|
|
773
|
+
context
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
getRegistry() {
|
|
777
|
+
return this.registry;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function createCircuitBreakerMiddleware(registry) {
|
|
781
|
+
return async (req, next) => {
|
|
782
|
+
const serviceName = req.context?.serviceName;
|
|
783
|
+
if (!serviceName) {
|
|
784
|
+
return next();
|
|
785
|
+
}
|
|
786
|
+
const breaker = registry.get(serviceName);
|
|
787
|
+
if (!breaker) {
|
|
788
|
+
return next();
|
|
789
|
+
}
|
|
790
|
+
return breaker.execute({
|
|
791
|
+
execute: next,
|
|
792
|
+
context: {
|
|
793
|
+
requestId: req.requestId,
|
|
794
|
+
route: req.route,
|
|
795
|
+
method: req.method,
|
|
796
|
+
url: req.url
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
var CircuitBreakerPresets = {
|
|
802
|
+
critical: (name) => ({
|
|
803
|
+
name,
|
|
804
|
+
failureThreshold: 3,
|
|
805
|
+
recoveryTimeout: 30000,
|
|
806
|
+
timeout: 5000,
|
|
807
|
+
monitoringPeriod: 30000,
|
|
808
|
+
minimumRequests: 5,
|
|
809
|
+
errorThresholdPercentage: 30,
|
|
810
|
+
halfOpenMaxCalls: 2,
|
|
811
|
+
resetTimeout: 30000
|
|
812
|
+
}),
|
|
813
|
+
standard: (name) => ({
|
|
814
|
+
name,
|
|
815
|
+
failureThreshold: 5,
|
|
816
|
+
recoveryTimeout: 60000,
|
|
817
|
+
timeout: 30000,
|
|
818
|
+
monitoringPeriod: 60000,
|
|
819
|
+
minimumRequests: 10,
|
|
820
|
+
errorThresholdPercentage: 50,
|
|
821
|
+
halfOpenMaxCalls: 3,
|
|
822
|
+
resetTimeout: 60000
|
|
823
|
+
}),
|
|
824
|
+
tolerant: (name) => ({
|
|
825
|
+
name,
|
|
826
|
+
failureThreshold: 10,
|
|
827
|
+
recoveryTimeout: 120000,
|
|
828
|
+
timeout: 60000,
|
|
829
|
+
monitoringPeriod: 120000,
|
|
830
|
+
minimumRequests: 20,
|
|
831
|
+
errorThresholdPercentage: 70,
|
|
832
|
+
halfOpenMaxCalls: 5,
|
|
833
|
+
resetTimeout: 120000
|
|
834
|
+
}),
|
|
835
|
+
database: (name) => ({
|
|
836
|
+
name,
|
|
837
|
+
failureThreshold: 3,
|
|
838
|
+
recoveryTimeout: 60000,
|
|
839
|
+
timeout: 1e4,
|
|
840
|
+
monitoringPeriod: 60000,
|
|
841
|
+
minimumRequests: 5,
|
|
842
|
+
errorThresholdPercentage: 40,
|
|
843
|
+
halfOpenMaxCalls: 2,
|
|
844
|
+
resetTimeout: 60000,
|
|
845
|
+
shouldTripOnError: (error) => {
|
|
846
|
+
return !(error.message.includes("validation") || error.message.includes("constraint"));
|
|
847
|
+
}
|
|
848
|
+
}),
|
|
849
|
+
externalApi: (name) => ({
|
|
850
|
+
name,
|
|
851
|
+
failureThreshold: 5,
|
|
852
|
+
recoveryTimeout: 90000,
|
|
853
|
+
timeout: 15000,
|
|
854
|
+
monitoringPeriod: 90000,
|
|
855
|
+
minimumRequests: 8,
|
|
856
|
+
errorThresholdPercentage: 60,
|
|
857
|
+
halfOpenMaxCalls: 3,
|
|
858
|
+
resetTimeout: 90000,
|
|
859
|
+
shouldTripOnError: (error) => {
|
|
860
|
+
if (error instanceof ExternalServiceException) {
|
|
861
|
+
return error.statusCode >= 500;
|
|
862
|
+
}
|
|
863
|
+
return true;
|
|
864
|
+
}
|
|
865
|
+
})
|
|
866
|
+
};
|
|
867
|
+
var globalCircuitBreakerRegistry = new CircuitBreakerRegistry;
|
|
868
|
+
function createCircuitBreaker(config2) {
|
|
869
|
+
return globalCircuitBreakerRegistry.register(config2);
|
|
870
|
+
}
|
|
871
|
+
async function withCircuitBreaker(name, call) {
|
|
872
|
+
return globalCircuitBreakerRegistry.execute(name, call);
|
|
873
|
+
}
|
|
874
|
+
// src/errors/error-reporting.ts
|
|
875
|
+
class SentryReporter {
|
|
876
|
+
config;
|
|
877
|
+
sentry;
|
|
878
|
+
constructor(config2) {
|
|
879
|
+
this.config = config2;
|
|
880
|
+
this.initializeSentry();
|
|
881
|
+
}
|
|
882
|
+
async initializeSentry() {
|
|
883
|
+
try {
|
|
884
|
+
const Sentry = await import("@sentry/node");
|
|
885
|
+
Sentry.init({
|
|
886
|
+
dsn: this.config.dsn,
|
|
887
|
+
environment: this.config.environment || "production",
|
|
888
|
+
release: this.config.release,
|
|
889
|
+
sampleRate: this.config.sampleRate || 1,
|
|
890
|
+
beforeSend: (event, hint) => {
|
|
891
|
+
const error = hint.originalException;
|
|
892
|
+
if (error && this.config.beforeSend) {
|
|
893
|
+
const filteredError = this.config.beforeSend(error, error.context);
|
|
894
|
+
return filteredError ? event : null;
|
|
895
|
+
}
|
|
896
|
+
return event;
|
|
897
|
+
},
|
|
898
|
+
ignoreErrors: this.config.filters?.ignoreErrors,
|
|
899
|
+
denyUrls: this.config.filters?.ignoreUrls,
|
|
900
|
+
allowUrls: this.config.filters?.allowUrls,
|
|
901
|
+
initialScope: {
|
|
902
|
+
tags: this.config.tags,
|
|
903
|
+
extra: this.config.extra,
|
|
904
|
+
user: this.config.user
|
|
905
|
+
},
|
|
906
|
+
maxBreadcrumbs: this.config.breadcrumbs?.maxBreadcrumbs || 100,
|
|
907
|
+
tracesSampleRate: this.config.performance?.tracesSampleRate || 0.1
|
|
908
|
+
});
|
|
909
|
+
this.sentry = Sentry;
|
|
910
|
+
} catch (error) {
|
|
911
|
+
console.warn("Failed to initialize Sentry:", error);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
async report(error, context) {
|
|
915
|
+
if (!this.sentry || !this.config.enabled) {
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
try {
|
|
919
|
+
if (this.shouldIgnoreError(error)) {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
const mergedContext = { ...error.context, ...context };
|
|
923
|
+
return this.sentry.withScope((scope) => {
|
|
924
|
+
if (mergedContext.requestId) {
|
|
925
|
+
scope.setTag("requestId", mergedContext.requestId);
|
|
926
|
+
}
|
|
927
|
+
if (mergedContext.userId) {
|
|
928
|
+
scope.setUser({ id: mergedContext.userId });
|
|
929
|
+
}
|
|
930
|
+
if (mergedContext.traceId) {
|
|
931
|
+
scope.setTag("traceId", mergedContext.traceId);
|
|
932
|
+
}
|
|
933
|
+
if (mergedContext.route) {
|
|
934
|
+
scope.setTag("route", mergedContext.route);
|
|
935
|
+
}
|
|
936
|
+
if (mergedContext.method) {
|
|
937
|
+
scope.setTag("method", mergedContext.method);
|
|
938
|
+
}
|
|
939
|
+
scope.setTag("errorCode", error.code);
|
|
940
|
+
scope.setTag("severity", error.severity);
|
|
941
|
+
scope.setTag("category", error.category);
|
|
942
|
+
scope.setLevel(this.mapSeverityToLevel(error.severity));
|
|
943
|
+
scope.setExtra("statusCode", error.statusCode);
|
|
944
|
+
scope.setExtra("retryable", error.retryable);
|
|
945
|
+
scope.setExtra("timestamp", error.timestamp);
|
|
946
|
+
scope.setExtra("context", mergedContext);
|
|
947
|
+
const fingerprint = this.generateFingerprint(error);
|
|
948
|
+
if (fingerprint.length > 0) {
|
|
949
|
+
scope.setFingerprint(fingerprint);
|
|
950
|
+
}
|
|
951
|
+
return this.sentry.captureException(error);
|
|
952
|
+
});
|
|
953
|
+
} catch (reportingError) {
|
|
954
|
+
console.error("Failed to report error to Sentry:", reportingError);
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
addBreadcrumb(message, category = "default", level = "info", data) {
|
|
959
|
+
if (!this.sentry || !this.config.breadcrumbs?.enabled) {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
this.sentry.addBreadcrumb({
|
|
963
|
+
message,
|
|
964
|
+
category,
|
|
965
|
+
level,
|
|
966
|
+
data,
|
|
967
|
+
timestamp: Date.now() / 1000
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
setUser(user) {
|
|
971
|
+
if (!this.sentry)
|
|
972
|
+
return;
|
|
973
|
+
this.sentry.setUser(user);
|
|
974
|
+
}
|
|
975
|
+
setTag(key, value) {
|
|
976
|
+
if (!this.sentry)
|
|
977
|
+
return;
|
|
978
|
+
this.sentry.setTag(key, value);
|
|
979
|
+
}
|
|
980
|
+
setExtra(key, value) {
|
|
981
|
+
if (!this.sentry)
|
|
982
|
+
return;
|
|
983
|
+
this.sentry.setExtra(key, value);
|
|
984
|
+
}
|
|
985
|
+
setContext(key, context) {
|
|
986
|
+
if (!this.sentry)
|
|
987
|
+
return;
|
|
988
|
+
this.sentry.setContext(key, context);
|
|
989
|
+
}
|
|
990
|
+
async close(timeout = 2000) {
|
|
991
|
+
if (!this.sentry)
|
|
992
|
+
return true;
|
|
993
|
+
return this.sentry.close(timeout);
|
|
994
|
+
}
|
|
995
|
+
shouldIgnoreError(error) {
|
|
996
|
+
const filters = this.config.filters;
|
|
997
|
+
if (!filters)
|
|
998
|
+
return false;
|
|
999
|
+
if (filters.ignoreCodes?.includes(error.code)) {
|
|
1000
|
+
return true;
|
|
1001
|
+
}
|
|
1002
|
+
if (filters.ignoreErrors?.some((pattern) => {
|
|
1003
|
+
if (typeof pattern === "string") {
|
|
1004
|
+
return error.message.includes(pattern);
|
|
1005
|
+
}
|
|
1006
|
+
return pattern.test(error.message);
|
|
1007
|
+
})) {
|
|
1008
|
+
return true;
|
|
1009
|
+
}
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
mapSeverityToLevel(severity) {
|
|
1013
|
+
switch (severity) {
|
|
1014
|
+
case "low":
|
|
1015
|
+
return "info";
|
|
1016
|
+
case "medium":
|
|
1017
|
+
return "warning";
|
|
1018
|
+
case "high":
|
|
1019
|
+
return "error";
|
|
1020
|
+
case "critical":
|
|
1021
|
+
return "fatal";
|
|
1022
|
+
default:
|
|
1023
|
+
return "error";
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
generateFingerprint(error) {
|
|
1027
|
+
const fingerprint = [];
|
|
1028
|
+
fingerprint.push(error.code);
|
|
1029
|
+
fingerprint.push(error.category);
|
|
1030
|
+
if (error.context.route) {
|
|
1031
|
+
fingerprint.push(error.context.route);
|
|
1032
|
+
}
|
|
1033
|
+
return fingerprint;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
class BugsnagReporter {
|
|
1038
|
+
config;
|
|
1039
|
+
bugsnag;
|
|
1040
|
+
constructor(config2) {
|
|
1041
|
+
this.config = config2;
|
|
1042
|
+
this.initializeBugsnag();
|
|
1043
|
+
}
|
|
1044
|
+
async initializeBugsnag() {
|
|
1045
|
+
try {
|
|
1046
|
+
const Bugsnag = await import("@bugsnag/js");
|
|
1047
|
+
Bugsnag.start({
|
|
1048
|
+
apiKey: this.config.apiKey,
|
|
1049
|
+
appVersion: this.config.release,
|
|
1050
|
+
releaseStage: this.config.environment || "production",
|
|
1051
|
+
enabledReleaseStages: ["production", "staging"],
|
|
1052
|
+
maxBreadcrumbs: this.config.breadcrumbs?.maxBreadcrumbs || 25,
|
|
1053
|
+
onError: (event) => {
|
|
1054
|
+
const error = event.originalError;
|
|
1055
|
+
if (error && this.config.beforeSend) {
|
|
1056
|
+
const filteredError = this.config.beforeSend(error, error.context);
|
|
1057
|
+
return filteredError !== null;
|
|
1058
|
+
}
|
|
1059
|
+
return !this.shouldIgnoreError(error);
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
this.bugsnag = Bugsnag;
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
console.warn("Failed to initialize Bugsnag:", error);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
async report(error, context) {
|
|
1068
|
+
if (!this.bugsnag || !this.config.enabled) {
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
try {
|
|
1072
|
+
if (this.shouldIgnoreError(error)) {
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
const mergedContext = { ...error.context, ...context };
|
|
1076
|
+
this.bugsnag.notify(error, (event) => {
|
|
1077
|
+
event.severity = this.mapSeverityToBugsnag(error.severity);
|
|
1078
|
+
event.context = mergedContext.route || mergedContext.url;
|
|
1079
|
+
if (mergedContext.userId) {
|
|
1080
|
+
event.setUser(mergedContext.userId, undefined, undefined);
|
|
1081
|
+
}
|
|
1082
|
+
event.addMetadata("error", {
|
|
1083
|
+
code: error.code,
|
|
1084
|
+
category: error.category,
|
|
1085
|
+
statusCode: error.statusCode,
|
|
1086
|
+
retryable: error.retryable,
|
|
1087
|
+
timestamp: error.timestamp
|
|
1088
|
+
});
|
|
1089
|
+
event.addMetadata("request", {
|
|
1090
|
+
requestId: mergedContext.requestId,
|
|
1091
|
+
method: mergedContext.method,
|
|
1092
|
+
url: mergedContext.url,
|
|
1093
|
+
userAgent: mergedContext.userAgent,
|
|
1094
|
+
ip: mergedContext.ip
|
|
1095
|
+
});
|
|
1096
|
+
event.groupingHash = this.generateGroupingHash(error);
|
|
1097
|
+
});
|
|
1098
|
+
return "reported";
|
|
1099
|
+
} catch (reportingError) {
|
|
1100
|
+
console.error("Failed to report error to Bugsnag:", reportingError);
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
addBreadcrumb(message, category = "manual", _level = "info", data) {
|
|
1105
|
+
if (!this.bugsnag || !this.config.breadcrumbs?.enabled) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
this.bugsnag.leaveBreadcrumb(message, data, category);
|
|
1109
|
+
}
|
|
1110
|
+
setUser(user) {
|
|
1111
|
+
if (!this.bugsnag)
|
|
1112
|
+
return;
|
|
1113
|
+
this.bugsnag.setUser(user.id, user.email, user.username);
|
|
1114
|
+
}
|
|
1115
|
+
setTag(key, value) {
|
|
1116
|
+
if (!this.bugsnag)
|
|
1117
|
+
return;
|
|
1118
|
+
this.bugsnag.addMetadata("tags", { [key]: value });
|
|
1119
|
+
}
|
|
1120
|
+
setExtra(key, value) {
|
|
1121
|
+
if (!this.bugsnag)
|
|
1122
|
+
return;
|
|
1123
|
+
this.bugsnag.addMetadata("extra", { [key]: value });
|
|
1124
|
+
}
|
|
1125
|
+
setContext(key, context) {
|
|
1126
|
+
if (!this.bugsnag)
|
|
1127
|
+
return;
|
|
1128
|
+
this.bugsnag.addMetadata(key, context);
|
|
1129
|
+
}
|
|
1130
|
+
async close() {
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
shouldIgnoreError(error) {
|
|
1134
|
+
const filters = this.config.filters;
|
|
1135
|
+
if (!filters)
|
|
1136
|
+
return false;
|
|
1137
|
+
if (filters.ignoreCodes?.includes(error.code)) {
|
|
1138
|
+
return true;
|
|
1139
|
+
}
|
|
1140
|
+
if (filters.ignoreErrors?.some((pattern) => {
|
|
1141
|
+
if (typeof pattern === "string") {
|
|
1142
|
+
return error.message.includes(pattern);
|
|
1143
|
+
}
|
|
1144
|
+
return pattern.test(error.message);
|
|
1145
|
+
})) {
|
|
1146
|
+
return true;
|
|
1147
|
+
}
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
mapSeverityToBugsnag(severity) {
|
|
1151
|
+
switch (severity) {
|
|
1152
|
+
case "low":
|
|
1153
|
+
return "info";
|
|
1154
|
+
case "medium":
|
|
1155
|
+
return "warning";
|
|
1156
|
+
case "high":
|
|
1157
|
+
return "error";
|
|
1158
|
+
case "critical":
|
|
1159
|
+
return "error";
|
|
1160
|
+
default:
|
|
1161
|
+
return "warning";
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
generateGroupingHash(error) {
|
|
1165
|
+
return `${error.code}-${error.category}-${error.context.route || "unknown"}`;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
class CustomReporter {
|
|
1170
|
+
config;
|
|
1171
|
+
reportHandler;
|
|
1172
|
+
constructor(config2, reportHandler) {
|
|
1173
|
+
this.config = config2;
|
|
1174
|
+
this.reportHandler = reportHandler;
|
|
1175
|
+
}
|
|
1176
|
+
async report(error, context) {
|
|
1177
|
+
if (!this.config.enabled) {
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
try {
|
|
1181
|
+
const mergedContext = { ...error.context, ...context };
|
|
1182
|
+
if (this.config.beforeSend) {
|
|
1183
|
+
const filteredError = this.config.beforeSend(error, mergedContext);
|
|
1184
|
+
if (!filteredError) {
|
|
1185
|
+
return null;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
const report = {
|
|
1189
|
+
error,
|
|
1190
|
+
context: mergedContext,
|
|
1191
|
+
level: this.mapSeverityToLevel(error.severity),
|
|
1192
|
+
tags: this.config.tags,
|
|
1193
|
+
extra: this.config.extra,
|
|
1194
|
+
user: this.config.user
|
|
1195
|
+
};
|
|
1196
|
+
return await this.reportHandler(report);
|
|
1197
|
+
} catch (reportingError) {
|
|
1198
|
+
console.error("Failed to report error with custom handler:", reportingError);
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
addBreadcrumb() {}
|
|
1203
|
+
setUser() {}
|
|
1204
|
+
setTag() {}
|
|
1205
|
+
setExtra() {}
|
|
1206
|
+
setContext() {}
|
|
1207
|
+
async close() {
|
|
1208
|
+
return true;
|
|
1209
|
+
}
|
|
1210
|
+
mapSeverityToLevel(severity) {
|
|
1211
|
+
switch (severity) {
|
|
1212
|
+
case "low":
|
|
1213
|
+
return "info";
|
|
1214
|
+
case "medium":
|
|
1215
|
+
return "warning";
|
|
1216
|
+
case "high":
|
|
1217
|
+
return "error";
|
|
1218
|
+
case "critical":
|
|
1219
|
+
return "fatal";
|
|
1220
|
+
default:
|
|
1221
|
+
return "error";
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
class ErrorReportingManager {
|
|
1227
|
+
reporters = [];
|
|
1228
|
+
breadcrumbs = [];
|
|
1229
|
+
maxBreadcrumbs = 50;
|
|
1230
|
+
constructor(configs) {
|
|
1231
|
+
this.initializeReporters(configs);
|
|
1232
|
+
}
|
|
1233
|
+
initializeReporters(configs) {
|
|
1234
|
+
for (const config2 of configs) {
|
|
1235
|
+
if (!config2.enabled)
|
|
1236
|
+
continue;
|
|
1237
|
+
switch (config2.service) {
|
|
1238
|
+
case "sentry":
|
|
1239
|
+
this.reporters.push(new SentryReporter(config2));
|
|
1240
|
+
break;
|
|
1241
|
+
case "bugsnag":
|
|
1242
|
+
this.reporters.push(new BugsnagReporter(config2));
|
|
1243
|
+
break;
|
|
1244
|
+
case "custom":
|
|
1245
|
+
break;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
addReporter(reporter) {
|
|
1250
|
+
this.reporters.push(reporter);
|
|
1251
|
+
}
|
|
1252
|
+
async report(error, context) {
|
|
1253
|
+
const reportIds = [];
|
|
1254
|
+
this.addBreadcrumb(`Error occurred: ${error.message}`, "error", {
|
|
1255
|
+
code: error.code,
|
|
1256
|
+
severity: error.severity
|
|
1257
|
+
});
|
|
1258
|
+
const promises = this.reporters.map(async (reporter) => {
|
|
1259
|
+
try {
|
|
1260
|
+
const reportId = await reporter.report(error, context);
|
|
1261
|
+
if (reportId) {
|
|
1262
|
+
reportIds.push(reportId);
|
|
1263
|
+
}
|
|
1264
|
+
} catch (reportingError) {
|
|
1265
|
+
console.error("Reporter failed:", reportingError);
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
await Promise.allSettled(promises);
|
|
1269
|
+
return reportIds;
|
|
1270
|
+
}
|
|
1271
|
+
addBreadcrumb(message, category = "default", data) {
|
|
1272
|
+
this.breadcrumbs.push({
|
|
1273
|
+
message,
|
|
1274
|
+
timestamp: new Date,
|
|
1275
|
+
data: { category, ...data }
|
|
1276
|
+
});
|
|
1277
|
+
if (this.breadcrumbs.length > this.maxBreadcrumbs) {
|
|
1278
|
+
this.breadcrumbs = this.breadcrumbs.slice(-this.maxBreadcrumbs);
|
|
1279
|
+
}
|
|
1280
|
+
this.reporters.forEach((reporter) => {
|
|
1281
|
+
reporter.addBreadcrumb(message, category, "info", data);
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
setUser(user) {
|
|
1285
|
+
this.reporters.forEach((reporter) => reporter.setUser(user));
|
|
1286
|
+
}
|
|
1287
|
+
setTag(key, value) {
|
|
1288
|
+
this.reporters.forEach((reporter) => reporter.setTag(key, value));
|
|
1289
|
+
}
|
|
1290
|
+
setExtra(key, value) {
|
|
1291
|
+
this.reporters.forEach((reporter) => reporter.setExtra(key, value));
|
|
1292
|
+
}
|
|
1293
|
+
setContext(key, context) {
|
|
1294
|
+
this.reporters.forEach((reporter) => reporter.setContext(key, context));
|
|
1295
|
+
}
|
|
1296
|
+
async close(timeout = 5000) {
|
|
1297
|
+
const promises = this.reporters.map((reporter) => reporter.close(timeout));
|
|
1298
|
+
const results = await Promise.allSettled(promises);
|
|
1299
|
+
return results.every((result) => result.status === "fulfilled" && result.value);
|
|
1300
|
+
}
|
|
1301
|
+
getBreadcrumbs() {
|
|
1302
|
+
return [...this.breadcrumbs];
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
function createErrorReporting(configs) {
|
|
1306
|
+
return new ErrorReportingManager(configs);
|
|
1307
|
+
}
|
|
1308
|
+
var ErrorReportingPresets = {
|
|
1309
|
+
sentry: (dsn, environment = "production") => ({
|
|
1310
|
+
enabled: true,
|
|
1311
|
+
service: "sentry",
|
|
1312
|
+
dsn,
|
|
1313
|
+
environment,
|
|
1314
|
+
sampleRate: 1,
|
|
1315
|
+
breadcrumbs: {
|
|
1316
|
+
enabled: true,
|
|
1317
|
+
maxBreadcrumbs: 100
|
|
1318
|
+
},
|
|
1319
|
+
performance: {
|
|
1320
|
+
enabled: true,
|
|
1321
|
+
tracesSampleRate: 0.1
|
|
1322
|
+
},
|
|
1323
|
+
filters: {
|
|
1324
|
+
ignoreErrors: [
|
|
1325
|
+
"Non-Error promise rejection captured",
|
|
1326
|
+
"ResizeObserver loop limit exceeded"
|
|
1327
|
+
],
|
|
1328
|
+
ignoreCodes: ["VALIDATION_ERROR"]
|
|
1329
|
+
}
|
|
1330
|
+
}),
|
|
1331
|
+
bugsnag: (apiKey, environment = "production") => ({
|
|
1332
|
+
enabled: true,
|
|
1333
|
+
service: "bugsnag",
|
|
1334
|
+
apiKey,
|
|
1335
|
+
environment,
|
|
1336
|
+
breadcrumbs: {
|
|
1337
|
+
enabled: true,
|
|
1338
|
+
maxBreadcrumbs: 25
|
|
1339
|
+
},
|
|
1340
|
+
filters: {
|
|
1341
|
+
ignoreCodes: ["VALIDATION_ERROR"]
|
|
1342
|
+
}
|
|
1343
|
+
}),
|
|
1344
|
+
development: () => ({
|
|
1345
|
+
enabled: false,
|
|
1346
|
+
service: "custom",
|
|
1347
|
+
environment: "development"
|
|
1348
|
+
})
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
// src/errors/graceful-degradation.ts
|
|
1352
|
+
class ServiceHealthMonitor {
|
|
1353
|
+
healthStatus = new Map;
|
|
1354
|
+
healthCheckIntervals = new Map;
|
|
1355
|
+
config;
|
|
1356
|
+
constructor(config2) {
|
|
1357
|
+
this.config = config2;
|
|
1358
|
+
this.initializeHealthChecks();
|
|
1359
|
+
}
|
|
1360
|
+
initializeHealthChecks() {
|
|
1361
|
+
for (const [serviceName, healthConfig] of Object.entries(this.config.healthChecks)) {
|
|
1362
|
+
if (!healthConfig.enabled)
|
|
1363
|
+
continue;
|
|
1364
|
+
this.healthStatus.set(serviceName, {
|
|
1365
|
+
name: serviceName,
|
|
1366
|
+
status: "healthy",
|
|
1367
|
+
lastCheck: new Date,
|
|
1368
|
+
responseTime: 0,
|
|
1369
|
+
errorRate: 0,
|
|
1370
|
+
availability: 100,
|
|
1371
|
+
consecutiveFailures: 0
|
|
1372
|
+
});
|
|
1373
|
+
const interval = setInterval(() => this.performHealthCheck(serviceName, healthConfig), healthConfig.interval);
|
|
1374
|
+
interval.unref?.();
|
|
1375
|
+
this.healthCheckIntervals.set(serviceName, interval);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
async performHealthCheck(serviceName, config2) {
|
|
1379
|
+
const startTime = Date.now();
|
|
1380
|
+
let isHealthy = false;
|
|
1381
|
+
try {
|
|
1382
|
+
const controller = new AbortController;
|
|
1383
|
+
const timeoutId = setTimeout(() => controller.abort(), config2.timeout);
|
|
1384
|
+
const response = await fetch(config2.endpoint, {
|
|
1385
|
+
signal: controller.signal,
|
|
1386
|
+
headers: config2.headers,
|
|
1387
|
+
method: "GET"
|
|
1388
|
+
});
|
|
1389
|
+
clearTimeout(timeoutId);
|
|
1390
|
+
isHealthy = config2.expectedStatus.includes(response.status);
|
|
1391
|
+
if (isHealthy && config2.expectedBody) {
|
|
1392
|
+
const body = await response.text();
|
|
1393
|
+
if (typeof config2.expectedBody === "string") {
|
|
1394
|
+
isHealthy = body.includes(config2.expectedBody);
|
|
1395
|
+
} else {
|
|
1396
|
+
isHealthy = config2.expectedBody.test(body);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
} catch (error) {
|
|
1400
|
+
console.error(error);
|
|
1401
|
+
isHealthy = false;
|
|
1402
|
+
}
|
|
1403
|
+
const responseTime = Date.now() - startTime;
|
|
1404
|
+
this.updateHealthStatus(serviceName, isHealthy, responseTime);
|
|
1405
|
+
if (isHealthy && config2.onHealthy) {
|
|
1406
|
+
config2.onHealthy();
|
|
1407
|
+
} else if (!isHealthy && config2.onUnhealthy) {
|
|
1408
|
+
config2.onUnhealthy(new Error(`Health check failed for ${serviceName}`));
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
updateHealthStatus(serviceName, isHealthy, responseTime) {
|
|
1412
|
+
const current = this.healthStatus.get(serviceName);
|
|
1413
|
+
if (!current)
|
|
1414
|
+
return;
|
|
1415
|
+
const updated = {
|
|
1416
|
+
...current,
|
|
1417
|
+
lastCheck: new Date,
|
|
1418
|
+
responseTime,
|
|
1419
|
+
consecutiveFailures: isHealthy ? 0 : current.consecutiveFailures + 1
|
|
1420
|
+
};
|
|
1421
|
+
if (updated.consecutiveFailures === 0) {
|
|
1422
|
+
updated.status = "healthy";
|
|
1423
|
+
} else if (updated.consecutiveFailures < 3) {
|
|
1424
|
+
updated.status = "degraded";
|
|
1425
|
+
} else {
|
|
1426
|
+
updated.status = "unhealthy";
|
|
1427
|
+
}
|
|
1428
|
+
const healthyCount = isHealthy ? 1 : 0;
|
|
1429
|
+
updated.availability = current.availability * 0.9 + healthyCount * 10;
|
|
1430
|
+
this.healthStatus.set(serviceName, updated);
|
|
1431
|
+
}
|
|
1432
|
+
getServiceHealth(serviceName) {
|
|
1433
|
+
return this.healthStatus.get(serviceName);
|
|
1434
|
+
}
|
|
1435
|
+
getAllServiceHealth() {
|
|
1436
|
+
return Array.from(this.healthStatus.values());
|
|
1437
|
+
}
|
|
1438
|
+
isServiceHealthy(serviceName) {
|
|
1439
|
+
const health = this.healthStatus.get(serviceName);
|
|
1440
|
+
return health?.status === "healthy";
|
|
1441
|
+
}
|
|
1442
|
+
isServiceDegraded(serviceName) {
|
|
1443
|
+
const health = this.healthStatus.get(serviceName);
|
|
1444
|
+
return health?.status === "degraded";
|
|
1445
|
+
}
|
|
1446
|
+
stop() {
|
|
1447
|
+
for (const interval of this.healthCheckIntervals.values()) {
|
|
1448
|
+
clearInterval(interval);
|
|
1449
|
+
}
|
|
1450
|
+
this.healthCheckIntervals.clear();
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
class GracefulDegradationManager {
|
|
1455
|
+
config;
|
|
1456
|
+
healthMonitor;
|
|
1457
|
+
cache = new Map;
|
|
1458
|
+
metrics = new Map;
|
|
1459
|
+
constructor(config2) {
|
|
1460
|
+
this.config = config2;
|
|
1461
|
+
this.healthMonitor = new ServiceHealthMonitor(config2);
|
|
1462
|
+
}
|
|
1463
|
+
async handleFailure(serviceName, error, request, originalHandler) {
|
|
1464
|
+
if (!this.config.enabled) {
|
|
1465
|
+
throw error;
|
|
1466
|
+
}
|
|
1467
|
+
const strategy = this.config.fallbackStrategies[serviceName];
|
|
1468
|
+
if (!strategy) {
|
|
1469
|
+
throw error;
|
|
1470
|
+
}
|
|
1471
|
+
const context = {
|
|
1472
|
+
request,
|
|
1473
|
+
serviceName,
|
|
1474
|
+
error,
|
|
1475
|
+
attemptCount: 1,
|
|
1476
|
+
startTime: new Date
|
|
1477
|
+
};
|
|
1478
|
+
return this.executeFallbackStrategy(strategy, context, originalHandler);
|
|
1479
|
+
}
|
|
1480
|
+
async executeFallbackStrategy(strategy, context, originalHandler) {
|
|
1481
|
+
if (context.attemptCount <= strategy.retries) {
|
|
1482
|
+
try {
|
|
1483
|
+
if (context.attemptCount > 1) {
|
|
1484
|
+
const delay = this.calculateBackoffDelay(strategy.backoff, context.attemptCount);
|
|
1485
|
+
await this.sleep(delay);
|
|
1486
|
+
}
|
|
1487
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1488
|
+
setTimeout(() => reject(new Error("Timeout")), strategy.timeout);
|
|
1489
|
+
});
|
|
1490
|
+
const result = await Promise.race([originalHandler(), timeoutPromise]);
|
|
1491
|
+
this.updateMetrics(context.serviceName, true, Date.now() - context.startTime.getTime());
|
|
1492
|
+
return result;
|
|
1493
|
+
} catch (retryError) {
|
|
1494
|
+
console.error(retryError);
|
|
1495
|
+
context.attemptCount++;
|
|
1496
|
+
if (context.attemptCount <= strategy.retries) {
|
|
1497
|
+
return this.executeFallbackStrategy(strategy, context, originalHandler);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
this.updateMetrics(context.serviceName, false, Date.now() - context.startTime.getTime());
|
|
1502
|
+
switch (strategy.type) {
|
|
1503
|
+
case "cache":
|
|
1504
|
+
return this.handleCacheFallback(strategy, context);
|
|
1505
|
+
case "static":
|
|
1506
|
+
return this.handleStaticFallback(strategy, context);
|
|
1507
|
+
case "simplified":
|
|
1508
|
+
return this.handleSimplifiedFallback(strategy, context);
|
|
1509
|
+
case "redirect":
|
|
1510
|
+
return this.handleRedirectFallback(strategy, context);
|
|
1511
|
+
case "custom":
|
|
1512
|
+
return this.handleCustomFallback(strategy, context);
|
|
1513
|
+
default:
|
|
1514
|
+
throw context.error;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
async handleCacheFallback(strategy, context) {
|
|
1518
|
+
if (!strategy.cacheConfig) {
|
|
1519
|
+
throw context.error;
|
|
1520
|
+
}
|
|
1521
|
+
const cacheKey = this.generateCacheKey(strategy.cacheConfig.key, context);
|
|
1522
|
+
const cached = this.cache.get(cacheKey);
|
|
1523
|
+
if (cached && (cached.expires > new Date || strategy.cacheConfig.staleWhileRevalidate)) {
|
|
1524
|
+
const headers = new Headers({
|
|
1525
|
+
"Content-Type": "application/json",
|
|
1526
|
+
"X-Cache": "HIT",
|
|
1527
|
+
"X-Degraded": "true",
|
|
1528
|
+
"X-Service-Status": this.healthMonitor.getServiceHealth(context.serviceName)?.status || "unknown"
|
|
1529
|
+
});
|
|
1530
|
+
if (strategy.cacheConfig.staleWhileRevalidate && cached.expires <= new Date) {
|
|
1531
|
+
headers.set("X-Cache", "STALE");
|
|
1532
|
+
}
|
|
1533
|
+
return new Response(JSON.stringify(cached.data), {
|
|
1534
|
+
status: 200,
|
|
1535
|
+
headers
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
throw context.error;
|
|
1539
|
+
}
|
|
1540
|
+
async handleStaticFallback(strategy, context) {
|
|
1541
|
+
if (!strategy.staticResponse) {
|
|
1542
|
+
throw context.error;
|
|
1543
|
+
}
|
|
1544
|
+
const headers = new Headers(strategy.staticResponse.headers || {});
|
|
1545
|
+
headers.set("X-Degraded", "true");
|
|
1546
|
+
headers.set("X-Fallback-Type", "static");
|
|
1547
|
+
headers.set("X-Service-Status", this.healthMonitor.getServiceHealth(context.serviceName)?.status || "unknown");
|
|
1548
|
+
return new Response(typeof strategy.staticResponse.body === "string" ? strategy.staticResponse.body : JSON.stringify(strategy.staticResponse.body), {
|
|
1549
|
+
status: strategy.staticResponse.status,
|
|
1550
|
+
headers
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
async handleSimplifiedFallback(strategy, context) {
|
|
1554
|
+
const simplifiedData = {
|
|
1555
|
+
message: "Service temporarily unavailable. Showing simplified view.",
|
|
1556
|
+
status: "degraded",
|
|
1557
|
+
timestamp: new Date().toISOString(),
|
|
1558
|
+
requestId: context.request.requestId
|
|
1559
|
+
};
|
|
1560
|
+
return new Response(JSON.stringify(simplifiedData), {
|
|
1561
|
+
status: 200,
|
|
1562
|
+
headers: {
|
|
1563
|
+
"Content-Type": "application/json",
|
|
1564
|
+
"X-Degraded": "true",
|
|
1565
|
+
"X-Fallback-Type": "simplified",
|
|
1566
|
+
"X-Service-Status": this.healthMonitor.getServiceHealth(context.serviceName)?.status || "unknown"
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
async handleRedirectFallback(strategy, context) {
|
|
1571
|
+
if (!strategy.redirectConfig) {
|
|
1572
|
+
throw context.error;
|
|
1573
|
+
}
|
|
1574
|
+
return new Response(null, {
|
|
1575
|
+
status: strategy.redirectConfig.permanent ? 301 : 302,
|
|
1576
|
+
headers: {
|
|
1577
|
+
Location: strategy.redirectConfig.url,
|
|
1578
|
+
"X-Degraded": "true",
|
|
1579
|
+
"X-Fallback-Type": "redirect"
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
async handleCustomFallback(strategy, context) {
|
|
1584
|
+
if (!strategy.fallbackHandler) {
|
|
1585
|
+
throw context.error;
|
|
1586
|
+
}
|
|
1587
|
+
try {
|
|
1588
|
+
const response = await strategy.fallbackHandler(context.error, context.error.context);
|
|
1589
|
+
response.headers.set("X-Degraded", "true");
|
|
1590
|
+
response.headers.set("X-Fallback-Type", "custom");
|
|
1591
|
+
response.headers.set("X-Service-Status", this.healthMonitor.getServiceHealth(context.serviceName)?.status || "unknown");
|
|
1592
|
+
return response;
|
|
1593
|
+
} catch (fallbackError) {
|
|
1594
|
+
console.error(fallbackError);
|
|
1595
|
+
throw context.error;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
cacheResponse(serviceName, key, data, ttl) {
|
|
1599
|
+
const strategy = this.config.fallbackStrategies[serviceName];
|
|
1600
|
+
if (strategy?.type === "cache" && strategy.cacheConfig) {
|
|
1601
|
+
const cacheKey = this.generateCacheKey(key, { serviceName });
|
|
1602
|
+
this.cache.set(cacheKey, {
|
|
1603
|
+
data,
|
|
1604
|
+
expires: new Date(Date.now() + ttl * 1000)
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
getServiceMetrics(serviceName) {
|
|
1609
|
+
const metrics = this.metrics.get(serviceName);
|
|
1610
|
+
if (!metrics || metrics.requests === 0) {
|
|
1611
|
+
return { errorRate: 0, avgResponseTime: 0, totalRequests: 0 };
|
|
1612
|
+
}
|
|
1613
|
+
return {
|
|
1614
|
+
errorRate: metrics.errors / metrics.requests * 100,
|
|
1615
|
+
avgResponseTime: metrics.totalTime / metrics.requests,
|
|
1616
|
+
totalRequests: metrics.requests
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
getSystemHealth() {
|
|
1620
|
+
const services = this.healthMonitor.getAllServiceHealth();
|
|
1621
|
+
const unhealthyCount = services.filter((s) => s.status === "unhealthy").length;
|
|
1622
|
+
const degradedCount = services.filter((s) => s.status === "degraded").length;
|
|
1623
|
+
let status = "healthy";
|
|
1624
|
+
if (unhealthyCount > 0) {
|
|
1625
|
+
status = unhealthyCount > services.length / 2 ? "unhealthy" : "degraded";
|
|
1626
|
+
} else if (degradedCount > 0) {
|
|
1627
|
+
status = "degraded";
|
|
1628
|
+
}
|
|
1629
|
+
return { status, services };
|
|
1630
|
+
}
|
|
1631
|
+
calculateBackoffDelay(backoff, attempt) {
|
|
1632
|
+
if (backoff.type === "fixed") {
|
|
1633
|
+
return backoff.delay;
|
|
1634
|
+
}
|
|
1635
|
+
const delay = backoff.delay * 2 ** (attempt - 1);
|
|
1636
|
+
return Math.min(delay, backoff.maxDelay || 30000);
|
|
1637
|
+
}
|
|
1638
|
+
generateCacheKey(template, context) {
|
|
1639
|
+
return template.replace("{service}", context.serviceName).replace("{url}", context.request.url).replace("{method}", context.request.method).replace("{userId}", String(context.request.user?.id || "anonymous"));
|
|
1640
|
+
}
|
|
1641
|
+
updateMetrics(serviceName, success, responseTime) {
|
|
1642
|
+
const current = this.metrics.get(serviceName) || { requests: 0, errors: 0, totalTime: 0 };
|
|
1643
|
+
current.requests++;
|
|
1644
|
+
current.totalTime += responseTime;
|
|
1645
|
+
if (!success) {
|
|
1646
|
+
current.errors++;
|
|
1647
|
+
}
|
|
1648
|
+
this.metrics.set(serviceName, current);
|
|
1649
|
+
}
|
|
1650
|
+
sleep(ms) {
|
|
1651
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1652
|
+
}
|
|
1653
|
+
stop() {
|
|
1654
|
+
this.healthMonitor.stop();
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
function createGracefulDegradationMiddleware(config2) {
|
|
1658
|
+
const manager = new GracefulDegradationManager(config2);
|
|
1659
|
+
return async (req, next) => {
|
|
1660
|
+
try {
|
|
1661
|
+
const response = await next();
|
|
1662
|
+
const serviceName = req.context?.serviceName;
|
|
1663
|
+
if (serviceName && response.ok) {
|
|
1664
|
+
const strategy = config2.fallbackStrategies[serviceName];
|
|
1665
|
+
if (strategy?.type === "cache" && strategy.cacheConfig) {
|
|
1666
|
+
try {
|
|
1667
|
+
const data = await response.clone().json();
|
|
1668
|
+
manager.cacheResponse(serviceName, strategy.cacheConfig.key, data, strategy.cacheConfig.ttl);
|
|
1669
|
+
} catch {}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
return response;
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
const serviceName = req.context?.serviceName;
|
|
1675
|
+
if (serviceName && error instanceof Error) {
|
|
1676
|
+
const routerError = error;
|
|
1677
|
+
try {
|
|
1678
|
+
return await manager.handleFailure(serviceName, routerError, req, next);
|
|
1679
|
+
} catch (fallbackError) {
|
|
1680
|
+
console.error(fallbackError);
|
|
1681
|
+
throw error;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
throw error;
|
|
1685
|
+
}
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
var DegradationStrategies = {
|
|
1689
|
+
cacheFirst: (ttl = 3600, staleWhileRevalidate = true) => ({
|
|
1690
|
+
type: "cache",
|
|
1691
|
+
priority: 1,
|
|
1692
|
+
timeout: 5000,
|
|
1693
|
+
retries: 2,
|
|
1694
|
+
backoff: { type: "exponential", delay: 1000, maxDelay: 5000 },
|
|
1695
|
+
cacheConfig: {
|
|
1696
|
+
key: "{service}:{url}:{method}",
|
|
1697
|
+
ttl,
|
|
1698
|
+
staleWhileRevalidate
|
|
1699
|
+
}
|
|
1700
|
+
}),
|
|
1701
|
+
staticFallback: (body, status = 200) => ({
|
|
1702
|
+
type: "static",
|
|
1703
|
+
priority: 2,
|
|
1704
|
+
timeout: 1000,
|
|
1705
|
+
retries: 0,
|
|
1706
|
+
backoff: { type: "fixed", delay: 0 },
|
|
1707
|
+
staticResponse: {
|
|
1708
|
+
status,
|
|
1709
|
+
body,
|
|
1710
|
+
headers: { "Content-Type": "application/json" }
|
|
1711
|
+
}
|
|
1712
|
+
}),
|
|
1713
|
+
simplifiedView: () => ({
|
|
1714
|
+
type: "simplified",
|
|
1715
|
+
priority: 3,
|
|
1716
|
+
timeout: 1000,
|
|
1717
|
+
retries: 0,
|
|
1718
|
+
backoff: { type: "fixed", delay: 0 }
|
|
1719
|
+
}),
|
|
1720
|
+
redirectToMaintenance: (url) => ({
|
|
1721
|
+
type: "redirect",
|
|
1722
|
+
priority: 4,
|
|
1723
|
+
timeout: 1000,
|
|
1724
|
+
retries: 0,
|
|
1725
|
+
backoff: { type: "fixed", delay: 0 },
|
|
1726
|
+
redirectConfig: {
|
|
1727
|
+
url,
|
|
1728
|
+
permanent: false
|
|
1729
|
+
}
|
|
1730
|
+
})
|
|
1731
|
+
};
|
|
1732
|
+
|
|
1733
|
+
// src/errors/error-handler.ts
|
|
1734
|
+
class AdvancedErrorHandler {
|
|
1735
|
+
errorReporting;
|
|
1736
|
+
gracefulDegradation;
|
|
1737
|
+
circuitBreakerRegistry;
|
|
1738
|
+
config;
|
|
1739
|
+
constructor(config2 = {}) {
|
|
1740
|
+
this.config = {
|
|
1741
|
+
development: false,
|
|
1742
|
+
includeStackTrace: false,
|
|
1743
|
+
sanitizeErrors: true,
|
|
1744
|
+
logErrors: true,
|
|
1745
|
+
...config2
|
|
1746
|
+
};
|
|
1747
|
+
this.initializeComponents();
|
|
1748
|
+
}
|
|
1749
|
+
initializeComponents() {
|
|
1750
|
+
if (this.config.errorReporting?.length) {
|
|
1751
|
+
this.errorReporting = new ErrorReportingManager(this.config.errorReporting);
|
|
1752
|
+
}
|
|
1753
|
+
if (this.config.gracefulDegradation?.enabled) {
|
|
1754
|
+
this.gracefulDegradation = new GracefulDegradationManager(this.config.gracefulDegradation);
|
|
1755
|
+
}
|
|
1756
|
+
if (this.config.circuitBreakers?.length) {
|
|
1757
|
+
this.circuitBreakerRegistry = new CircuitBreakerRegistry;
|
|
1758
|
+
for (const config2 of this.config.circuitBreakers) {
|
|
1759
|
+
this.circuitBreakerRegistry.register(config2);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
middleware() {
|
|
1764
|
+
return async (req, next) => {
|
|
1765
|
+
try {
|
|
1766
|
+
const errorContext = {
|
|
1767
|
+
requestId: req.requestId,
|
|
1768
|
+
userId: req.user?.id,
|
|
1769
|
+
traceId: req.traceId,
|
|
1770
|
+
spanId: req.spanId,
|
|
1771
|
+
route: req.params?.route || req.url,
|
|
1772
|
+
method: req.method,
|
|
1773
|
+
url: req.url,
|
|
1774
|
+
userAgent: req.userAgent?.() || undefined,
|
|
1775
|
+
ip: req.ip?.() || undefined,
|
|
1776
|
+
timestamp: new Date
|
|
1777
|
+
};
|
|
1778
|
+
req.context = { ...req.context, errorContext };
|
|
1779
|
+
if (this.circuitBreakerRegistry && req.context?.serviceName) {
|
|
1780
|
+
const breaker = this.circuitBreakerRegistry.get(req.context.serviceName);
|
|
1781
|
+
if (breaker) {
|
|
1782
|
+
return await breaker.execute({
|
|
1783
|
+
execute: next,
|
|
1784
|
+
context: errorContext
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
return await next();
|
|
1789
|
+
} catch (error) {
|
|
1790
|
+
return this.handleError(error, req);
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
async handleError(error, req) {
|
|
1795
|
+
const routerError = this.normalizeError(error, req);
|
|
1796
|
+
if (this.errorReporting) {
|
|
1797
|
+
this.errorReporting.addBreadcrumb(`Error in ${req.method} ${req.url}`, "error", {
|
|
1798
|
+
code: routerError.code,
|
|
1799
|
+
severity: routerError.severity,
|
|
1800
|
+
statusCode: routerError.statusCode
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
if (this.config.logErrors) {
|
|
1804
|
+
this.logError(routerError, req);
|
|
1805
|
+
}
|
|
1806
|
+
if (this.gracefulDegradation && req.context?.serviceName) {
|
|
1807
|
+
try {
|
|
1808
|
+
return await this.gracefulDegradation.handleFailure(req.context.serviceName, routerError, req, async () => {
|
|
1809
|
+
throw routerError;
|
|
1810
|
+
});
|
|
1811
|
+
} catch {}
|
|
1812
|
+
}
|
|
1813
|
+
if (this.errorReporting) {
|
|
1814
|
+
try {
|
|
1815
|
+
await this.errorReporting.report(routerError, routerError.context);
|
|
1816
|
+
} catch (reportingError) {
|
|
1817
|
+
console.error("Failed to report error:", reportingError);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
if (this.config.globalErrorHandler) {
|
|
1821
|
+
try {
|
|
1822
|
+
return await this.config.globalErrorHandler(routerError, req);
|
|
1823
|
+
} catch (handlerError) {
|
|
1824
|
+
console.error("Global error handler failed:", handlerError);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
const customPage = this.config.customErrorPages?.[routerError.statusCode];
|
|
1828
|
+
if (customPage) {
|
|
1829
|
+
try {
|
|
1830
|
+
if (typeof customPage === "string") {
|
|
1831
|
+
return new Response(customPage, {
|
|
1832
|
+
status: routerError.statusCode,
|
|
1833
|
+
headers: { "Content-Type": "text/html" }
|
|
1834
|
+
});
|
|
1835
|
+
} else {
|
|
1836
|
+
return await customPage(routerError, req);
|
|
1837
|
+
}
|
|
1838
|
+
} catch (pageError) {
|
|
1839
|
+
console.error("Custom error page failed:", pageError);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
return this.createErrorResponse(routerError, req);
|
|
1843
|
+
}
|
|
1844
|
+
normalizeError(error, req) {
|
|
1845
|
+
const errorContext = {
|
|
1846
|
+
requestId: req.requestId,
|
|
1847
|
+
userId: req.user?.id,
|
|
1848
|
+
traceId: req.traceId,
|
|
1849
|
+
spanId: req.spanId,
|
|
1850
|
+
route: req.params?.route || req.url,
|
|
1851
|
+
method: req.method,
|
|
1852
|
+
url: req.url,
|
|
1853
|
+
userAgent: req.userAgent?.() || undefined,
|
|
1854
|
+
ip: req.ip?.() || undefined,
|
|
1855
|
+
timestamp: new Date,
|
|
1856
|
+
metadata: req.context
|
|
1857
|
+
};
|
|
1858
|
+
if (error instanceof RouterException) {
|
|
1859
|
+
return error;
|
|
1860
|
+
}
|
|
1861
|
+
if (error instanceof Error) {
|
|
1862
|
+
if (error.name === "ValidationError") {
|
|
1863
|
+
return ErrorFactory.validation(error.message, {}, errorContext);
|
|
1864
|
+
}
|
|
1865
|
+
if (error.name === "UnauthorizedError" || error.message.includes("unauthorized")) {
|
|
1866
|
+
return ErrorFactory.authentication(error.message, errorContext);
|
|
1867
|
+
}
|
|
1868
|
+
if (error.name === "ForbiddenError" || error.message.includes("forbidden")) {
|
|
1869
|
+
return ErrorFactory.authorization(error.message, [], errorContext);
|
|
1870
|
+
}
|
|
1871
|
+
if (error.name === "NotFoundError" || error.message.includes("not found")) {
|
|
1872
|
+
return ErrorFactory.notFound(undefined, undefined, errorContext);
|
|
1873
|
+
}
|
|
1874
|
+
if (error.name === "TimeoutError" || error.message.includes("timeout")) {
|
|
1875
|
+
return ErrorFactory.timeout(30000, error.message, errorContext);
|
|
1876
|
+
}
|
|
1877
|
+
return ErrorFactory.internalServer(error.message, error, errorContext);
|
|
1878
|
+
}
|
|
1879
|
+
if (typeof error === "string") {
|
|
1880
|
+
return ErrorFactory.internalServer(error, undefined, errorContext);
|
|
1881
|
+
}
|
|
1882
|
+
return ErrorFactory.internalServer("An unknown error occurred", undefined, errorContext);
|
|
1883
|
+
}
|
|
1884
|
+
createErrorResponse(error, _req) {
|
|
1885
|
+
const isDevelopment = this.config.development;
|
|
1886
|
+
const includeStackTrace = this.config.includeStackTrace || isDevelopment;
|
|
1887
|
+
const sanitizeErrors = this.config.sanitizeErrors && !isDevelopment;
|
|
1888
|
+
let message = error.message;
|
|
1889
|
+
if (sanitizeErrors && error.statusCode >= 500) {
|
|
1890
|
+
message = "An internal server error occurred";
|
|
1891
|
+
}
|
|
1892
|
+
const errorResponse = {
|
|
1893
|
+
error: {
|
|
1894
|
+
code: error.code,
|
|
1895
|
+
message,
|
|
1896
|
+
statusCode: error.statusCode,
|
|
1897
|
+
timestamp: error.timestamp.toISOString(),
|
|
1898
|
+
requestId: error.context.requestId,
|
|
1899
|
+
traceId: error.context.traceId
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
if (isDevelopment) {
|
|
1903
|
+
errorResponse.error.category = error.category;
|
|
1904
|
+
errorResponse.error.severity = error.severity;
|
|
1905
|
+
errorResponse.error.retryable = error.retryable;
|
|
1906
|
+
errorResponse.error.context = error.context;
|
|
1907
|
+
}
|
|
1908
|
+
if (includeStackTrace) {
|
|
1909
|
+
errorResponse.error.stack = error.stack;
|
|
1910
|
+
if (error.cause) {
|
|
1911
|
+
errorResponse.error.cause = {
|
|
1912
|
+
name: error.cause.name,
|
|
1913
|
+
message: error.cause.message,
|
|
1914
|
+
stack: error.cause.stack
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
if ("fields" in error) {
|
|
1919
|
+
errorResponse.error.fields = error.fields;
|
|
1920
|
+
}
|
|
1921
|
+
if ("allowedMethods" in error) {
|
|
1922
|
+
errorResponse.error.allowedMethods = error.allowedMethods;
|
|
1923
|
+
}
|
|
1924
|
+
if ("retryAfter" in error) {
|
|
1925
|
+
errorResponse.error.retryAfter = error.retryAfter;
|
|
1926
|
+
}
|
|
1927
|
+
const headers = new Headers({
|
|
1928
|
+
"Content-Type": "application/json",
|
|
1929
|
+
"X-Error-Code": error.code,
|
|
1930
|
+
"X-Request-ID": error.context.requestId || "",
|
|
1931
|
+
"X-Trace-ID": error.context.traceId || ""
|
|
224
1932
|
});
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
validateKey(key, requiredScopes = []) {
|
|
228
|
-
const keyInfo = this.keys.get(key);
|
|
229
|
-
if (!keyInfo) {
|
|
230
|
-
return false;
|
|
1933
|
+
if ("allowedMethods" in error) {
|
|
1934
|
+
headers.set("Allow", error.allowedMethods.join(", "));
|
|
231
1935
|
}
|
|
232
|
-
if (
|
|
233
|
-
|
|
1936
|
+
if ("retryAfter" in error) {
|
|
1937
|
+
headers.set("Retry-After", error.retryAfter.toString());
|
|
234
1938
|
}
|
|
235
|
-
if (
|
|
236
|
-
|
|
1939
|
+
if ("limit" in error) {
|
|
1940
|
+
headers.set("X-RateLimit-Limit", error.limit.toString());
|
|
1941
|
+
headers.set("X-RateLimit-Remaining", error.remaining.toString());
|
|
237
1942
|
}
|
|
238
|
-
return
|
|
1943
|
+
return new Response(JSON.stringify(errorResponse), {
|
|
1944
|
+
status: error.statusCode,
|
|
1945
|
+
headers
|
|
1946
|
+
});
|
|
239
1947
|
}
|
|
240
|
-
|
|
241
|
-
|
|
1948
|
+
logError(error, req) {
|
|
1949
|
+
const logData = {
|
|
1950
|
+
error: error.toJSON(),
|
|
1951
|
+
request: {
|
|
1952
|
+
method: req.method,
|
|
1953
|
+
url: req.url,
|
|
1954
|
+
headers: Object.fromEntries(req.headers.entries()),
|
|
1955
|
+
userAgent: req.userAgent?.() || undefined,
|
|
1956
|
+
ip: req.ip?.() || undefined,
|
|
1957
|
+
requestId: req.requestId,
|
|
1958
|
+
traceId: req.traceId
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1961
|
+
switch (error.severity) {
|
|
1962
|
+
case "critical":
|
|
1963
|
+
console.error("[CRITICAL ERROR]", logData);
|
|
1964
|
+
break;
|
|
1965
|
+
case "high":
|
|
1966
|
+
console.error("[ERROR]", logData);
|
|
1967
|
+
break;
|
|
1968
|
+
case "medium":
|
|
1969
|
+
console.warn("[WARNING]", logData);
|
|
1970
|
+
break;
|
|
1971
|
+
case "low":
|
|
1972
|
+
console.warn("[INFO]", logData);
|
|
1973
|
+
break;
|
|
1974
|
+
default:
|
|
1975
|
+
console.error("[ERROR]", logData);
|
|
1976
|
+
}
|
|
242
1977
|
}
|
|
243
|
-
|
|
244
|
-
return
|
|
1978
|
+
getStats() {
|
|
1979
|
+
return {
|
|
1980
|
+
errorReporting: this.errorReporting ? {
|
|
1981
|
+
breadcrumbs: this.errorReporting.getBreadcrumbs().length
|
|
1982
|
+
} : undefined,
|
|
1983
|
+
gracefulDegradation: this.gracefulDegradation ? this.gracefulDegradation.getSystemHealth() : undefined,
|
|
1984
|
+
circuitBreakers: this.circuitBreakerRegistry ? this.circuitBreakerRegistry.getAllMetrics() : undefined
|
|
1985
|
+
};
|
|
245
1986
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
return new URL(req.url).searchParams.get(keyName || "api_key") || null;
|
|
253
|
-
case "cookie":
|
|
254
|
-
if (req.cookies) {
|
|
255
|
-
const key = keyName || "api_key";
|
|
256
|
-
if (typeof req.cookies.get === "function") {
|
|
257
|
-
return req.cookies.get(key) || null;
|
|
258
|
-
}
|
|
259
|
-
return req.cookies[key] || null;
|
|
260
|
-
}
|
|
261
|
-
return null;
|
|
262
|
-
default:
|
|
263
|
-
return null;
|
|
1987
|
+
async cleanup() {
|
|
1988
|
+
if (this.errorReporting) {
|
|
1989
|
+
await this.errorReporting.close();
|
|
1990
|
+
}
|
|
1991
|
+
if (this.gracefulDegradation) {
|
|
1992
|
+
this.gracefulDegradation.stop();
|
|
264
1993
|
}
|
|
265
1994
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
1995
|
+
}
|
|
1996
|
+
function createAdvancedErrorHandler(config2 = {}) {
|
|
1997
|
+
const handler = new AdvancedErrorHandler(config2);
|
|
1998
|
+
return {
|
|
1999
|
+
middleware: handler.middleware(),
|
|
2000
|
+
getStats: () => handler.getStats(),
|
|
2001
|
+
cleanup: () => handler.cleanup()
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
// src/request/context.ts
|
|
2005
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
2006
|
+
var storage = new AsyncLocalStorage;
|
|
2007
|
+
var contextEnabled = false;
|
|
2008
|
+
var syncCurrent = null;
|
|
2009
|
+
function enableRequestContext() {
|
|
2010
|
+
contextEnabled = true;
|
|
2011
|
+
}
|
|
2012
|
+
function runWithRequest(initial, fn) {
|
|
2013
|
+
if (!contextEnabled) {
|
|
2014
|
+
syncCurrent = initial;
|
|
2015
|
+
const result = fn();
|
|
2016
|
+
if (result instanceof Promise) {
|
|
2017
|
+
return result.finally(() => {
|
|
2018
|
+
syncCurrent = null;
|
|
2019
|
+
});
|
|
271
2020
|
}
|
|
2021
|
+
syncCurrent = null;
|
|
272
2022
|
return result;
|
|
273
2023
|
}
|
|
2024
|
+
return storage.run({ current: initial }, fn);
|
|
274
2025
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
2026
|
+
function setCurrentRequest(req) {
|
|
2027
|
+
const store = storage.getStore();
|
|
2028
|
+
if (store) {
|
|
2029
|
+
store.current = req;
|
|
2030
|
+
return;
|
|
280
2031
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
client_id: this.config.clientId,
|
|
284
|
-
redirect_uri: this.config.redirectUri,
|
|
285
|
-
response_type: "code",
|
|
286
|
-
...additionalParams
|
|
287
|
-
});
|
|
288
|
-
if (this.config.scope) {
|
|
289
|
-
params.set("scope", this.config.scope);
|
|
290
|
-
}
|
|
291
|
-
if (this.config.state) {
|
|
292
|
-
params.set("state", this.config.state);
|
|
293
|
-
}
|
|
294
|
-
return `${this.config.authorizeUrl}?${params.toString()}`;
|
|
2032
|
+
if (syncCurrent !== null) {
|
|
2033
|
+
syncCurrent = req;
|
|
295
2034
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
client_id: this.config.clientId,
|
|
302
|
-
client_secret: this.config.clientSecret
|
|
303
|
-
});
|
|
304
|
-
const response = await fetch(this.config.tokenUrl, {
|
|
305
|
-
method: "POST",
|
|
306
|
-
headers: {
|
|
307
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
308
|
-
},
|
|
309
|
-
body: params.toString()
|
|
310
|
-
});
|
|
311
|
-
if (!response.ok) {
|
|
312
|
-
throw new Error(`OAuth token exchange failed: ${response.status} ${response.statusText}`);
|
|
313
|
-
}
|
|
314
|
-
return await response.json();
|
|
2035
|
+
}
|
|
2036
|
+
function getCurrentRequest() {
|
|
2037
|
+
if (!contextEnabled) {
|
|
2038
|
+
contextEnabled = true;
|
|
2039
|
+
return syncCurrent ?? undefined;
|
|
315
2040
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
});
|
|
323
|
-
const response = await fetch(this.config.tokenUrl, {
|
|
324
|
-
method: "POST",
|
|
325
|
-
headers: {
|
|
326
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
327
|
-
},
|
|
328
|
-
body: params.toString()
|
|
329
|
-
});
|
|
330
|
-
if (!response.ok) {
|
|
331
|
-
throw new Error(`OAuth token refresh failed: ${response.status} ${response.statusText}`);
|
|
332
|
-
}
|
|
333
|
-
return await response.json();
|
|
2041
|
+
return storage.getStore()?.current ?? syncCurrent ?? undefined;
|
|
2042
|
+
}
|
|
2043
|
+
function request() {
|
|
2044
|
+
const req = getCurrentRequest();
|
|
2045
|
+
if (!req) {
|
|
2046
|
+
throw new Error("request() called outside a request scope. It can only be invoked from within a route handler or middleware.");
|
|
334
2047
|
}
|
|
2048
|
+
return req;
|
|
335
2049
|
}
|
|
336
2050
|
// src/response/response-factory.ts
|
|
337
2051
|
var response = {
|
|
@@ -429,8 +2143,8 @@ var response = {
|
|
|
429
2143
|
redirectTemporary: (url) => {
|
|
430
2144
|
return response.redirect(url, 302);
|
|
431
2145
|
},
|
|
432
|
-
back: (
|
|
433
|
-
const referer =
|
|
2146
|
+
back: (request2, fallback = "/") => {
|
|
2147
|
+
const referer = request2.headers.get("referer") || fallback;
|
|
434
2148
|
return response.redirect(referer);
|
|
435
2149
|
},
|
|
436
2150
|
view: (html, status = 200, headers = {}) => {
|
|
@@ -675,7 +2389,7 @@ class MiddlewarePipeline {
|
|
|
675
2389
|
}
|
|
676
2390
|
this.compiledPipelines.set(routeKey, compiled);
|
|
677
2391
|
}
|
|
678
|
-
async execute(routeKey,
|
|
2392
|
+
async execute(routeKey, request2, finalHandler) {
|
|
679
2393
|
const startTime = performance.now();
|
|
680
2394
|
this.stats.totalExecutions++;
|
|
681
2395
|
const pipeline = this.compiledPipelines.get(routeKey);
|
|
@@ -685,7 +2399,7 @@ class MiddlewarePipeline {
|
|
|
685
2399
|
}
|
|
686
2400
|
this.stats.cacheHits++;
|
|
687
2401
|
const context = {
|
|
688
|
-
request,
|
|
2402
|
+
request: request2,
|
|
689
2403
|
dependencies: new Map,
|
|
690
2404
|
metadata: {}
|
|
691
2405
|
};
|
|
@@ -700,7 +2414,7 @@ class MiddlewarePipeline {
|
|
|
700
2414
|
this.stats.skippedMiddleware++;
|
|
701
2415
|
return await next();
|
|
702
2416
|
}
|
|
703
|
-
const result2 = await middleware2.handler(
|
|
2417
|
+
const result2 = await middleware2.handler(request2, next);
|
|
704
2418
|
if (result2 instanceof Response && middleware2.canShortCircuit) {
|
|
705
2419
|
this.stats.shortCircuits++;
|
|
706
2420
|
return result2;
|
|
@@ -1778,12 +3492,12 @@ class StreamHandler {
|
|
|
1778
3492
|
}
|
|
1779
3493
|
}
|
|
1780
3494
|
class FileStreamHandler {
|
|
1781
|
-
static async streamFile(filePath,
|
|
3495
|
+
static async streamFile(filePath, request2, options = {}) {
|
|
1782
3496
|
const { contentType, enableRanges = true, chunkSize: _chunkSize = 64 * 1024 } = options;
|
|
1783
3497
|
try {
|
|
1784
3498
|
const file = Bun.file(filePath);
|
|
1785
3499
|
const fileSize = file.size;
|
|
1786
|
-
const range = enableRanges ?
|
|
3500
|
+
const range = enableRanges ? request2.headers.get("range") : null;
|
|
1787
3501
|
if (range && enableRanges) {
|
|
1788
3502
|
return FileStreamHandler.handleRangeRequest(file, range, contentType);
|
|
1789
3503
|
}
|
|
@@ -1862,8 +3576,8 @@ function closeReadableStreamAfterError(controller, error) {
|
|
|
1862
3576
|
function registerFileStreaming(RouterClass) {
|
|
1863
3577
|
Object.defineProperties(RouterClass.prototype, {
|
|
1864
3578
|
streamFile: {
|
|
1865
|
-
value(filePath,
|
|
1866
|
-
return FileStreamHandler.streamFile(filePath,
|
|
3579
|
+
value(filePath, request2, options) {
|
|
3580
|
+
return FileStreamHandler.streamFile(filePath, request2, options);
|
|
1867
3581
|
},
|
|
1868
3582
|
writable: true,
|
|
1869
3583
|
configurable: true
|
|
@@ -3218,6 +4932,7 @@ var SubdomainPatterns = {
|
|
|
3218
4932
|
};
|
|
3219
4933
|
|
|
3220
4934
|
// src/url.ts
|
|
4935
|
+
import process3 from "process";
|
|
3221
4936
|
var namedRouteRegistry = new Map;
|
|
3222
4937
|
function registerNamedRoute(name, path) {
|
|
3223
4938
|
namedRouteRegistry.set(name, path);
|
|
@@ -3225,6 +4940,54 @@ function registerNamedRoute(name, path) {
|
|
|
3225
4940
|
function getNamedRoutePath(name) {
|
|
3226
4941
|
return namedRouteRegistry.get(name);
|
|
3227
4942
|
}
|
|
4943
|
+
function getNamedRoutes() {
|
|
4944
|
+
return new Map(namedRouteRegistry);
|
|
4945
|
+
}
|
|
4946
|
+
function clearNamedRoutes() {
|
|
4947
|
+
namedRouteRegistry.clear();
|
|
4948
|
+
}
|
|
4949
|
+
function url(name, params = {}, options = {}) {
|
|
4950
|
+
const path = namedRouteRegistry.get(name);
|
|
4951
|
+
if (!path) {
|
|
4952
|
+
const known = [...namedRouteRegistry.keys()].sort().join(", ") || "(none registered)";
|
|
4953
|
+
throw new Error(`Named route '${name}' is not defined. Known routes: ${known}`);
|
|
4954
|
+
}
|
|
4955
|
+
const consumed = new Set;
|
|
4956
|
+
let resolved = path;
|
|
4957
|
+
for (const [key, value] of Object.entries(params)) {
|
|
4958
|
+
const required = `{${key}}`;
|
|
4959
|
+
const optional = `{${key}?}`;
|
|
4960
|
+
const encoded = encodeURIComponent(String(value));
|
|
4961
|
+
if (resolved.includes(required)) {
|
|
4962
|
+
resolved = resolved.replaceAll(required, encoded);
|
|
4963
|
+
consumed.add(key);
|
|
4964
|
+
} else if (resolved.includes(optional)) {
|
|
4965
|
+
resolved = resolved.replaceAll(optional, encoded);
|
|
4966
|
+
consumed.add(key);
|
|
4967
|
+
}
|
|
4968
|
+
}
|
|
4969
|
+
if (resolved.includes("?}")) {
|
|
4970
|
+
resolved = resolved.replace(/\{[^}]+\?\}/g, "").replace(/\/{2,}/g, "/");
|
|
4971
|
+
if (resolved.length > 1 && resolved.endsWith("/"))
|
|
4972
|
+
resolved = resolved.slice(0, -1);
|
|
4973
|
+
}
|
|
4974
|
+
const missing = resolved.match(/\{([^}?]+)\}/g);
|
|
4975
|
+
if (missing && missing.length > 0)
|
|
4976
|
+
throw new Error(`Missing required params for route '${name}': ${missing.join(", ")}`);
|
|
4977
|
+
const leftover = Object.entries(params).filter(([k]) => !consumed.has(k));
|
|
4978
|
+
if (leftover.length > 0) {
|
|
4979
|
+
const qs = new URLSearchParams;
|
|
4980
|
+
for (const [k, v] of leftover)
|
|
4981
|
+
qs.set(k, String(v));
|
|
4982
|
+
resolved += `?${qs.toString()}`;
|
|
4983
|
+
}
|
|
4984
|
+
if (options.absolute) {
|
|
4985
|
+
const base = (options.base ?? process3.env.APP_URL ?? "http://localhost").replace(/\/$/, "");
|
|
4986
|
+
const prefixed = base.startsWith("http") ? base : `https://${base}`;
|
|
4987
|
+
return `${prefixed}${resolved.startsWith("/") ? resolved : `/${resolved}`}`;
|
|
4988
|
+
}
|
|
4989
|
+
return resolved;
|
|
4990
|
+
}
|
|
3228
4991
|
|
|
3229
4992
|
// src/utils.ts
|
|
3230
4993
|
import { join as join3 } from "path";
|
|
@@ -3778,24 +5541,24 @@ class FluentRouter {
|
|
|
3778
5541
|
getRoutes() {
|
|
3779
5542
|
return [...this.routes, ...this.subdomainRouter.getAllRoutes()];
|
|
3780
5543
|
}
|
|
3781
|
-
async handle(
|
|
3782
|
-
const
|
|
3783
|
-
const domain =
|
|
5544
|
+
async handle(request2) {
|
|
5545
|
+
const url2 = new URL(request2.url);
|
|
5546
|
+
const domain = url2.hostname;
|
|
3784
5547
|
const match = this.subdomainRouter.findDomainGroup(domain);
|
|
3785
5548
|
if (match) {
|
|
3786
|
-
const enhancedReq =
|
|
5549
|
+
const enhancedReq = request2;
|
|
3787
5550
|
enhancedReq.domainParams = match.parameters;
|
|
3788
5551
|
for (const route of match.group.getRoutes()) {
|
|
3789
|
-
const params = this.matchRouteParams(route,
|
|
5552
|
+
const params = this.matchRouteParams(route, request2.method, url2.pathname);
|
|
3790
5553
|
if (params) {
|
|
3791
5554
|
return await this.executeRoute(route, enhancedReq, params);
|
|
3792
5555
|
}
|
|
3793
5556
|
}
|
|
3794
5557
|
}
|
|
3795
5558
|
for (const route of this.routes) {
|
|
3796
|
-
const params = this.matchRouteParams(route,
|
|
5559
|
+
const params = this.matchRouteParams(route, request2.method, url2.pathname);
|
|
3797
5560
|
if (params) {
|
|
3798
|
-
return await this.executeRoute(route,
|
|
5561
|
+
return await this.executeRoute(route, request2, params);
|
|
3799
5562
|
}
|
|
3800
5563
|
}
|
|
3801
5564
|
return null;
|
|
@@ -3807,11 +5570,11 @@ class FluentRouter {
|
|
|
3807
5570
|
const params = {};
|
|
3808
5571
|
return matchPath(route.path, pathname, params) ? params : null;
|
|
3809
5572
|
}
|
|
3810
|
-
async executeRoute(route,
|
|
3811
|
-
|
|
5573
|
+
async executeRoute(route, request2, params) {
|
|
5574
|
+
request2.params = params;
|
|
3812
5575
|
const allMiddleware = [...this.globalMiddleware];
|
|
3813
5576
|
for (const conditional of this.conditionalMiddleware) {
|
|
3814
|
-
if (await conditional.condition(
|
|
5577
|
+
if (await conditional.condition(request2)) {
|
|
3815
5578
|
allMiddleware.push(...conditional.middleware);
|
|
3816
5579
|
}
|
|
3817
5580
|
}
|
|
@@ -3820,10 +5583,10 @@ class FluentRouter {
|
|
|
3820
5583
|
const next = async () => {
|
|
3821
5584
|
if (index < allMiddleware.length) {
|
|
3822
5585
|
const middleware2 = allMiddleware[index++];
|
|
3823
|
-
const result = await middleware2(
|
|
5586
|
+
const result = await middleware2(request2, next);
|
|
3824
5587
|
return result ?? new Response(null);
|
|
3825
5588
|
}
|
|
3826
|
-
return await route.handler(
|
|
5589
|
+
return await route.handler(request2);
|
|
3827
5590
|
};
|
|
3828
5591
|
return await next();
|
|
3829
5592
|
}
|
|
@@ -3861,27 +5624,27 @@ var RouteFactory = {
|
|
|
3861
5624
|
};
|
|
3862
5625
|
var RouterUtils = {
|
|
3863
5626
|
route: (name, params = {}, query = {}) => {
|
|
3864
|
-
let
|
|
5627
|
+
let url2 = getNamedRoutePath(name) ?? `/${name}`;
|
|
3865
5628
|
for (const [key, value] of Object.entries(params)) {
|
|
3866
5629
|
const encoded = encodeURIComponent(value);
|
|
3867
|
-
|
|
3868
|
-
|
|
5630
|
+
url2 = url2.replace(`{${key}}`, encoded);
|
|
5631
|
+
url2 = url2.replace(`{${key}?}`, encoded);
|
|
3869
5632
|
}
|
|
3870
|
-
if (
|
|
3871
|
-
|
|
3872
|
-
if (
|
|
3873
|
-
|
|
5633
|
+
if (url2.includes("?}")) {
|
|
5634
|
+
url2 = url2.replace(/\{[^}]+\?\}/g, "").replace(/\/{2,}/g, "/");
|
|
5635
|
+
if (url2.length > 1 && url2.endsWith("/"))
|
|
5636
|
+
url2 = url2.slice(0, -1);
|
|
3874
5637
|
}
|
|
3875
5638
|
const queryString = new URLSearchParams(query).toString();
|
|
3876
5639
|
if (queryString) {
|
|
3877
|
-
|
|
5640
|
+
url2 += `?${queryString}`;
|
|
3878
5641
|
}
|
|
3879
|
-
return
|
|
5642
|
+
return url2;
|
|
3880
5643
|
},
|
|
3881
|
-
redirect: (
|
|
5644
|
+
redirect: (url2, status = 302) => {
|
|
3882
5645
|
return new Response(null, {
|
|
3883
5646
|
status,
|
|
3884
|
-
headers: { Location:
|
|
5647
|
+
headers: { Location: url2 }
|
|
3885
5648
|
});
|
|
3886
5649
|
},
|
|
3887
5650
|
json: (data, status = 200) => {
|
|
@@ -4079,10 +5842,10 @@ function registerHttpMethods(RouterClass) {
|
|
|
4079
5842
|
route.constraints = constraints;
|
|
4080
5843
|
}
|
|
4081
5844
|
route.pattern = {
|
|
4082
|
-
exec: (
|
|
5845
|
+
exec: (url2) => {
|
|
4083
5846
|
const params = {};
|
|
4084
5847
|
const constraintsRecord = route.constraints && !Array.isArray(route.constraints) ? route.constraints : undefined;
|
|
4085
|
-
const isMatch = matchPath(routePath,
|
|
5848
|
+
const isMatch = matchPath(routePath, url2.pathname, params, constraintsRecord);
|
|
4086
5849
|
if (!isMatch) {
|
|
4087
5850
|
return null;
|
|
4088
5851
|
}
|
|
@@ -4209,18 +5972,18 @@ function registerHttpMethods(RouterClass) {
|
|
|
4209
5972
|
if (!route) {
|
|
4210
5973
|
throw new Error(`Route with name "${name}" not found`);
|
|
4211
5974
|
}
|
|
4212
|
-
let
|
|
5975
|
+
let url2 = route.path;
|
|
4213
5976
|
for (const [param, value] of Object.entries(params)) {
|
|
4214
|
-
|
|
4215
|
-
|
|
5977
|
+
url2 = url2.replace(`{${param}}`, encodeURIComponent(value));
|
|
5978
|
+
url2 = url2.replace(`{${param}?}`, encodeURIComponent(value));
|
|
4216
5979
|
}
|
|
4217
|
-
if (
|
|
4218
|
-
|
|
4219
|
-
if (
|
|
4220
|
-
|
|
5980
|
+
if (url2.includes("{")) {
|
|
5981
|
+
url2 = url2.replace(/\{[^}]+\?\}/g, "").replace(/\/{2,}/g, "/");
|
|
5982
|
+
if (url2.length > 1 && url2.endsWith("/")) {
|
|
5983
|
+
url2 = url2.slice(0, -1);
|
|
4221
5984
|
}
|
|
4222
5985
|
}
|
|
4223
|
-
return
|
|
5986
|
+
return url2;
|
|
4224
5987
|
},
|
|
4225
5988
|
writable: true,
|
|
4226
5989
|
configurable: true
|
|
@@ -4238,9 +6001,9 @@ function registerHttpMethods(RouterClass) {
|
|
|
4238
6001
|
function registerRedirectMethods(RouterClass) {
|
|
4239
6002
|
Object.defineProperties(RouterClass.prototype, {
|
|
4240
6003
|
redirect: {
|
|
4241
|
-
value(
|
|
6004
|
+
value(url2, status = 302) {
|
|
4242
6005
|
const headers = new Headers;
|
|
4243
|
-
headers.set("Location",
|
|
6006
|
+
headers.set("Location", url2);
|
|
4244
6007
|
return new Response(null, {
|
|
4245
6008
|
status,
|
|
4246
6009
|
headers
|
|
@@ -4250,8 +6013,8 @@ function registerRedirectMethods(RouterClass) {
|
|
|
4250
6013
|
configurable: true
|
|
4251
6014
|
},
|
|
4252
6015
|
permanentRedirect: {
|
|
4253
|
-
value(
|
|
4254
|
-
return this.redirect(
|
|
6016
|
+
value(url2) {
|
|
6017
|
+
return this.redirect(url2, 301);
|
|
4255
6018
|
},
|
|
4256
6019
|
writable: true,
|
|
4257
6020
|
configurable: true
|
|
@@ -5249,8 +7012,8 @@ class RouteCompiler {
|
|
|
5249
7012
|
}
|
|
5250
7013
|
const pattern = this.convertToURLPattern(route.path, route.constraints && !Array.isArray(route.constraints) ? route.constraints : undefined);
|
|
5251
7014
|
const urlPatternAdapter = {
|
|
5252
|
-
exec: (
|
|
5253
|
-
const pathname =
|
|
7015
|
+
exec: (url2) => {
|
|
7016
|
+
const pathname = url2.pathname;
|
|
5254
7017
|
const result = pattern.exec(pathname);
|
|
5255
7018
|
if (!result)
|
|
5256
7019
|
return null;
|
|
@@ -5621,8 +7384,8 @@ function registerOptimizedRouteMatching(RouterClass) {
|
|
|
5621
7384
|
},
|
|
5622
7385
|
fallbackMatchRoute: {
|
|
5623
7386
|
value(path, method, domain) {
|
|
5624
|
-
const
|
|
5625
|
-
const cacheKey = `${domain || ""}:${method}:${
|
|
7387
|
+
const url2 = new URL(path, "http://localhost");
|
|
7388
|
+
const cacheKey = `${domain || ""}:${method}:${url2.pathname}`;
|
|
5626
7389
|
if (this.routeCache.has(cacheKey)) {
|
|
5627
7390
|
return this.routeCache.get(cacheKey);
|
|
5628
7391
|
}
|
|
@@ -5630,7 +7393,7 @@ function registerOptimizedRouteMatching(RouterClass) {
|
|
|
5630
7393
|
this.routeCache.clear();
|
|
5631
7394
|
}
|
|
5632
7395
|
if (this.staticRoutes.has(method)) {
|
|
5633
|
-
const staticRoute = this.staticRoutes.get(method).get(
|
|
7396
|
+
const staticRoute = this.staticRoutes.get(method).get(url2.pathname);
|
|
5634
7397
|
if (staticRoute && (!domain || !staticRoute.domain || staticRoute.domain === domain)) {
|
|
5635
7398
|
const result = {
|
|
5636
7399
|
route: staticRoute,
|
|
@@ -5643,7 +7406,7 @@ function registerOptimizedRouteMatching(RouterClass) {
|
|
|
5643
7406
|
const potentialRoutes = domain && this.domains[domain] ? this.domains[domain] : this.routes;
|
|
5644
7407
|
const methodRoutes = potentialRoutes.filter((route) => route.method === method);
|
|
5645
7408
|
for (const route of methodRoutes) {
|
|
5646
|
-
if (route.path ===
|
|
7409
|
+
if (route.path === url2.pathname) {
|
|
5647
7410
|
const result = {
|
|
5648
7411
|
route,
|
|
5649
7412
|
params: {}
|
|
@@ -5654,7 +7417,7 @@ function registerOptimizedRouteMatching(RouterClass) {
|
|
|
5654
7417
|
}
|
|
5655
7418
|
for (const route of methodRoutes) {
|
|
5656
7419
|
if (route.pattern) {
|
|
5657
|
-
const match = route.pattern.exec(
|
|
7420
|
+
const match = route.pattern.exec(url2);
|
|
5658
7421
|
if (match) {
|
|
5659
7422
|
const result = {
|
|
5660
7423
|
route,
|
|
@@ -5672,10 +7435,10 @@ function registerOptimizedRouteMatching(RouterClass) {
|
|
|
5672
7435
|
continue;
|
|
5673
7436
|
}
|
|
5674
7437
|
const basePath = route.path.slice(0, -1);
|
|
5675
|
-
if (
|
|
7438
|
+
if (url2.pathname.startsWith(basePath)) {
|
|
5676
7439
|
const result = {
|
|
5677
7440
|
route,
|
|
5678
|
-
params: { wildcard:
|
|
7441
|
+
params: { wildcard: url2.pathname.slice(basePath.length) }
|
|
5679
7442
|
};
|
|
5680
7443
|
this.routeCache.set(cacheKey, result);
|
|
5681
7444
|
return result;
|
|
@@ -5797,10 +7560,10 @@ function registerRouteBuilding(RouterClass) {
|
|
|
5797
7560
|
} catch {}
|
|
5798
7561
|
}
|
|
5799
7562
|
bunRoutes[path] = async (req) => {
|
|
5800
|
-
const
|
|
7563
|
+
const url2 = new URL(req.url);
|
|
5801
7564
|
const params = {};
|
|
5802
7565
|
const constraintsRecord = route.constraints && !Array.isArray(route.constraints) ? route.constraints : undefined;
|
|
5803
|
-
matchPath(path,
|
|
7566
|
+
matchPath(path, url2.pathname, params, constraintsRecord);
|
|
5804
7567
|
const enhancedReq = this.enhanceRequest(req, params);
|
|
5805
7568
|
const routeHandlerMiddleware = async (req2, _next) => await this.resolveHandler(route.handler, req2);
|
|
5806
7569
|
const middlewareStack = [...this.globalMiddleware, ...route.middleware, routeHandlerMiddleware];
|
|
@@ -5812,10 +7575,10 @@ function registerRouteBuilding(RouterClass) {
|
|
|
5812
7575
|
for (const route of routes) {
|
|
5813
7576
|
const method = route.method;
|
|
5814
7577
|
methodHandlers[method] = async (req) => {
|
|
5815
|
-
const
|
|
7578
|
+
const url2 = new URL(req.url);
|
|
5816
7579
|
const params = {};
|
|
5817
7580
|
const constraintsRecord = route.constraints && !Array.isArray(route.constraints) ? route.constraints : undefined;
|
|
5818
|
-
matchPath(path,
|
|
7581
|
+
matchPath(path, url2.pathname, params, constraintsRecord);
|
|
5819
7582
|
const enhancedReq = this.enhanceRequest(req, params);
|
|
5820
7583
|
const routeHandlerMiddleware = async (req2, _next) => await this.resolveHandler(route.handler, req2);
|
|
5821
7584
|
const middlewareStack = [...this.globalMiddleware, ...route.middleware, routeHandlerMiddleware];
|
|
@@ -5909,13 +7672,13 @@ function registerRouteMatching(RouterClass) {
|
|
|
5909
7672
|
Object.defineProperties(RouterClass.prototype, {
|
|
5910
7673
|
matchRoute: {
|
|
5911
7674
|
value(path, method, domain) {
|
|
5912
|
-
const
|
|
5913
|
-
const cacheKey = `${domain || ""}:${method}:${
|
|
7675
|
+
const url2 = new URL(path, "http://localhost");
|
|
7676
|
+
const cacheKey = `${domain || ""}:${method}:${url2.pathname}`;
|
|
5914
7677
|
if (this.routeCache.has(cacheKey)) {
|
|
5915
7678
|
return this.routeCache.get(cacheKey);
|
|
5916
7679
|
}
|
|
5917
7680
|
if (this.staticRoutes.has(method)) {
|
|
5918
|
-
const staticRoute = this.staticRoutes.get(method).get(
|
|
7681
|
+
const staticRoute = this.staticRoutes.get(method).get(url2.pathname);
|
|
5919
7682
|
if (staticRoute && (!domain || !staticRoute.domain || staticRoute.domain === domain)) {
|
|
5920
7683
|
const result = {
|
|
5921
7684
|
route: staticRoute,
|
|
@@ -5928,7 +7691,7 @@ function registerRouteMatching(RouterClass) {
|
|
|
5928
7691
|
const potentialRoutes = domain && this.domains[domain] ? this.domains[domain] : this.routes;
|
|
5929
7692
|
const methodRoutes = potentialRoutes.filter((route) => route.method === method);
|
|
5930
7693
|
for (const route of methodRoutes) {
|
|
5931
|
-
if (route.path ===
|
|
7694
|
+
if (route.path === url2.pathname) {
|
|
5932
7695
|
const result = {
|
|
5933
7696
|
route,
|
|
5934
7697
|
params: {}
|
|
@@ -5939,7 +7702,7 @@ function registerRouteMatching(RouterClass) {
|
|
|
5939
7702
|
}
|
|
5940
7703
|
for (const route of methodRoutes) {
|
|
5941
7704
|
if (route.pattern) {
|
|
5942
|
-
const match = route.pattern.exec(
|
|
7705
|
+
const match = route.pattern.exec(url2);
|
|
5943
7706
|
if (match) {
|
|
5944
7707
|
const result = {
|
|
5945
7708
|
route,
|
|
@@ -5954,11 +7717,11 @@ function registerRouteMatching(RouterClass) {
|
|
|
5954
7717
|
const wildcardRoutes = this.domains[domain].filter((route) => route.method === method && route.path.endsWith("*"));
|
|
5955
7718
|
for (const route of wildcardRoutes) {
|
|
5956
7719
|
const basePath = route.path.slice(0, -1);
|
|
5957
|
-
if (
|
|
7720
|
+
if (url2.pathname.startsWith(basePath)) {
|
|
5958
7721
|
const result = {
|
|
5959
7722
|
route,
|
|
5960
7723
|
params: {
|
|
5961
|
-
wildcard:
|
|
7724
|
+
wildcard: url2.pathname.slice(basePath.length)
|
|
5962
7725
|
}
|
|
5963
7726
|
};
|
|
5964
7727
|
this.routeCache.set(cacheKey, result);
|
|
@@ -5969,11 +7732,11 @@ function registerRouteMatching(RouterClass) {
|
|
|
5969
7732
|
const globalWildcardRoutes = this.routes.filter((route) => route.method === method && route.path.endsWith("*") && (!domain || !route.domain));
|
|
5970
7733
|
for (const route of globalWildcardRoutes) {
|
|
5971
7734
|
const basePath = route.path.slice(0, -1);
|
|
5972
|
-
if (
|
|
7735
|
+
if (url2.pathname.startsWith(basePath)) {
|
|
5973
7736
|
const result = {
|
|
5974
7737
|
route,
|
|
5975
7738
|
params: {
|
|
5976
|
-
wildcard:
|
|
7739
|
+
wildcard: url2.pathname.slice(basePath.length)
|
|
5977
7740
|
}
|
|
5978
7741
|
};
|
|
5979
7742
|
this.routeCache.set(cacheKey, result);
|
|
@@ -6074,9 +7837,9 @@ function registerRouteMatching(RouterClass) {
|
|
|
6074
7837
|
const routePath = lastRoute.path;
|
|
6075
7838
|
const constraintsRecord = lastRoute.constraints && !Array.isArray(lastRoute.constraints) ? lastRoute.constraints : undefined;
|
|
6076
7839
|
lastRoute.pattern = {
|
|
6077
|
-
exec: (
|
|
7840
|
+
exec: (url2) => {
|
|
6078
7841
|
const params2 = {};
|
|
6079
|
-
const isMatch = matchPath(routePath,
|
|
7842
|
+
const isMatch = matchPath(routePath, url2.pathname, params2, constraintsRecord);
|
|
6080
7843
|
if (!isMatch) {
|
|
6081
7844
|
return null;
|
|
6082
7845
|
}
|
|
@@ -6327,10 +8090,10 @@ class Router {
|
|
|
6327
8090
|
route.constraints = constraints;
|
|
6328
8091
|
}
|
|
6329
8092
|
route.pattern = {
|
|
6330
|
-
exec: (
|
|
8093
|
+
exec: (url2) => {
|
|
6331
8094
|
const params = {};
|
|
6332
8095
|
const constraintsRecord = route.constraints && !Array.isArray(route.constraints) ? route.constraints : undefined;
|
|
6333
|
-
const isMatch = matchPath(routePath,
|
|
8096
|
+
const isMatch = matchPath(routePath, url2.pathname, params, constraintsRecord);
|
|
6334
8097
|
if (!isMatch) {
|
|
6335
8098
|
return null;
|
|
6336
8099
|
}
|
|
@@ -6411,27 +8174,27 @@ class Router {
|
|
|
6411
8174
|
if (!route) {
|
|
6412
8175
|
throw new Error(`Route with name "${name}" not found`);
|
|
6413
8176
|
}
|
|
6414
|
-
let
|
|
8177
|
+
let url2 = route.path;
|
|
6415
8178
|
for (const [param, value] of Object.entries(params)) {
|
|
6416
|
-
|
|
6417
|
-
|
|
8179
|
+
url2 = url2.replace(`{${param}}`, encodeURIComponent(value));
|
|
8180
|
+
url2 = url2.replace(`{${param}?}`, encodeURIComponent(value));
|
|
6418
8181
|
}
|
|
6419
|
-
return
|
|
8182
|
+
return url2;
|
|
6420
8183
|
}
|
|
6421
8184
|
onError(handler) {
|
|
6422
8185
|
this.errorHandler = handler;
|
|
6423
8186
|
return this;
|
|
6424
8187
|
}
|
|
6425
|
-
redirect(
|
|
8188
|
+
redirect(url2, status = 302) {
|
|
6426
8189
|
const headers = new Headers;
|
|
6427
|
-
headers.set("Location",
|
|
8190
|
+
headers.set("Location", url2);
|
|
6428
8191
|
return new Response(null, {
|
|
6429
8192
|
status,
|
|
6430
8193
|
headers
|
|
6431
8194
|
});
|
|
6432
8195
|
}
|
|
6433
|
-
permanentRedirect(
|
|
6434
|
-
return this.redirect(
|
|
8196
|
+
permanentRedirect(url2) {
|
|
8197
|
+
return this.redirect(url2, 301);
|
|
6435
8198
|
}
|
|
6436
8199
|
redirectRoute(from, to, status = 302) {
|
|
6437
8200
|
this.get(from, (_req) => {
|
|
@@ -6458,13 +8221,13 @@ class Router {
|
|
|
6458
8221
|
}
|
|
6459
8222
|
async handleRequest(req) {
|
|
6460
8223
|
try {
|
|
6461
|
-
const
|
|
8224
|
+
const url2 = new URL(req.url);
|
|
6462
8225
|
if (this.config.verbose) {
|
|
6463
|
-
console.log(`${req.method} ${
|
|
8226
|
+
console.log(`${req.method} ${url2.pathname}`);
|
|
6464
8227
|
}
|
|
6465
8228
|
if (req.method === "OPTIONS") {
|
|
6466
|
-
const hostname2 =
|
|
6467
|
-
const optionsMatch = this.matchRoute(
|
|
8229
|
+
const hostname2 = url2.hostname || req.headers.get("host")?.split(":")[0] || "localhost";
|
|
8230
|
+
const optionsMatch = this.matchRoute(url2.pathname, "OPTIONS", hostname2);
|
|
6468
8231
|
if (!optionsMatch) {
|
|
6469
8232
|
return new Response(null, {
|
|
6470
8233
|
status: 204,
|
|
@@ -6477,8 +8240,8 @@ class Router {
|
|
|
6477
8240
|
});
|
|
6478
8241
|
}
|
|
6479
8242
|
}
|
|
6480
|
-
const hostname =
|
|
6481
|
-
const match = this.matchRoute(
|
|
8243
|
+
const hostname = url2.hostname || req.headers.get("host")?.split(":")[0] || "localhost";
|
|
8244
|
+
const match = this.matchRoute(url2.pathname, req.method, hostname);
|
|
6482
8245
|
const enhancedReq = this.enhanceRequest(req, match?.params || {});
|
|
6483
8246
|
if (match) {
|
|
6484
8247
|
enhancedReq.route = match.route;
|
|
@@ -6496,12 +8259,12 @@ class Router {
|
|
|
6496
8259
|
}
|
|
6497
8260
|
return new Response("No response from middleware chain", { status: 500 });
|
|
6498
8261
|
}
|
|
6499
|
-
const allowedMethods = this.getAllowedMethods(
|
|
8262
|
+
const allowedMethods = this.getAllowedMethods(url2.pathname, hostname);
|
|
6500
8263
|
if (allowedMethods.length > 0) {
|
|
6501
8264
|
const methodNotAllowedHandler = async (_req, _next) => {
|
|
6502
8265
|
return new Response(JSON.stringify({
|
|
6503
8266
|
error: "Method Not Allowed",
|
|
6504
|
-
path:
|
|
8267
|
+
path: url2.pathname,
|
|
6505
8268
|
method: req.method,
|
|
6506
8269
|
allowed: allowedMethods
|
|
6507
8270
|
}), {
|
|
@@ -6528,7 +8291,7 @@ class Router {
|
|
|
6528
8291
|
}
|
|
6529
8292
|
return new Response(JSON.stringify({
|
|
6530
8293
|
error: "Not Found",
|
|
6531
|
-
path:
|
|
8294
|
+
path: url2.pathname,
|
|
6532
8295
|
method: req.method
|
|
6533
8296
|
}), {
|
|
6534
8297
|
status: 404,
|
|
@@ -6547,7 +8310,7 @@ class Router {
|
|
|
6547
8310
|
}
|
|
6548
8311
|
return new Response(JSON.stringify({
|
|
6549
8312
|
error: "Not Found",
|
|
6550
|
-
path:
|
|
8313
|
+
path: url2.pathname,
|
|
6551
8314
|
method: req.method
|
|
6552
8315
|
}), {
|
|
6553
8316
|
status: 404,
|
|
@@ -6590,15 +8353,15 @@ class Router {
|
|
|
6590
8353
|
}
|
|
6591
8354
|
}
|
|
6592
8355
|
getAllowedMethods(path, domain) {
|
|
6593
|
-
const
|
|
6594
|
-
const cacheKey = `${domain || ""}:${
|
|
8356
|
+
const url2 = new URL(path, "http://localhost");
|
|
8357
|
+
const cacheKey = `${domain || ""}:${url2.pathname}`;
|
|
6595
8358
|
const cached = this._allowedMethodsCache.get(cacheKey);
|
|
6596
8359
|
if (cached) {
|
|
6597
8360
|
return cached;
|
|
6598
8361
|
}
|
|
6599
8362
|
const methods = new Set;
|
|
6600
8363
|
for (const [method, routesByPath] of this.staticRoutes) {
|
|
6601
|
-
const staticRoute = routesByPath.get(
|
|
8364
|
+
const staticRoute = routesByPath.get(url2.pathname);
|
|
6602
8365
|
if (staticRoute && (!domain || !staticRoute.domain || staticRoute.domain === domain)) {
|
|
6603
8366
|
methods.add(method);
|
|
6604
8367
|
}
|
|
@@ -6608,15 +8371,15 @@ class Router {
|
|
|
6608
8371
|
if (methods.has(route.method)) {
|
|
6609
8372
|
continue;
|
|
6610
8373
|
}
|
|
6611
|
-
if (route.path ===
|
|
8374
|
+
if (route.path === url2.pathname) {
|
|
6612
8375
|
methods.add(route.method);
|
|
6613
8376
|
continue;
|
|
6614
8377
|
}
|
|
6615
|
-
if (route.pattern && route.pattern.exec(
|
|
8378
|
+
if (route.pattern && route.pattern.exec(url2)) {
|
|
6616
8379
|
methods.add(route.method);
|
|
6617
8380
|
continue;
|
|
6618
8381
|
}
|
|
6619
|
-
if (route.path.endsWith("*") &&
|
|
8382
|
+
if (route.path.endsWith("*") && url2.pathname.startsWith(route.path.slice(0, -1))) {
|
|
6620
8383
|
methods.add(route.method);
|
|
6621
8384
|
}
|
|
6622
8385
|
}
|
|
@@ -6631,13 +8394,13 @@ class Router {
|
|
|
6631
8394
|
return result;
|
|
6632
8395
|
}
|
|
6633
8396
|
matchRoute(path, method, domain) {
|
|
6634
|
-
const
|
|
6635
|
-
const cacheKey = `${domain || ""}:${method}:${
|
|
8397
|
+
const url2 = new URL(path, "http://localhost");
|
|
8398
|
+
const cacheKey = `${domain || ""}:${method}:${url2.pathname}`;
|
|
6636
8399
|
if (this.routeCache.has(cacheKey)) {
|
|
6637
8400
|
return this.routeCache.get(cacheKey);
|
|
6638
8401
|
}
|
|
6639
8402
|
if (this.staticRoutes.has(method)) {
|
|
6640
|
-
const staticRoute = this.staticRoutes.get(method).get(
|
|
8403
|
+
const staticRoute = this.staticRoutes.get(method).get(url2.pathname);
|
|
6641
8404
|
if (staticRoute && (!domain || !staticRoute.domain || staticRoute.domain === domain)) {
|
|
6642
8405
|
const result = {
|
|
6643
8406
|
route: staticRoute,
|
|
@@ -6650,7 +8413,7 @@ class Router {
|
|
|
6650
8413
|
const potentialRoutes = domain && this.domains[domain] ? this.domains[domain] : this.routes;
|
|
6651
8414
|
const methodRoutes = potentialRoutes.filter((route) => route.method === method);
|
|
6652
8415
|
for (const route of methodRoutes) {
|
|
6653
|
-
if (route.path ===
|
|
8416
|
+
if (route.path === url2.pathname) {
|
|
6654
8417
|
const result = {
|
|
6655
8418
|
route,
|
|
6656
8419
|
params: {}
|
|
@@ -6661,7 +8424,7 @@ class Router {
|
|
|
6661
8424
|
}
|
|
6662
8425
|
for (const route of methodRoutes) {
|
|
6663
8426
|
if (route.pattern) {
|
|
6664
|
-
const match = route.pattern.exec(
|
|
8427
|
+
const match = route.pattern.exec(url2);
|
|
6665
8428
|
if (match) {
|
|
6666
8429
|
const result = {
|
|
6667
8430
|
route,
|
|
@@ -6676,11 +8439,11 @@ class Router {
|
|
|
6676
8439
|
const wildcardRoutes = this.domains[domain].filter((route) => route.method === method && route.path.endsWith("*"));
|
|
6677
8440
|
for (const route of wildcardRoutes) {
|
|
6678
8441
|
const basePath = route.path.slice(0, -1);
|
|
6679
|
-
if (
|
|
8442
|
+
if (url2.pathname.startsWith(basePath)) {
|
|
6680
8443
|
const result = {
|
|
6681
8444
|
route,
|
|
6682
8445
|
params: {
|
|
6683
|
-
wildcard:
|
|
8446
|
+
wildcard: url2.pathname.slice(basePath.length)
|
|
6684
8447
|
}
|
|
6685
8448
|
};
|
|
6686
8449
|
this.routeCache.set(cacheKey, result);
|
|
@@ -6691,11 +8454,11 @@ class Router {
|
|
|
6691
8454
|
const globalWildcardRoutes = this.routes.filter((route) => route.method === method && route.path.endsWith("*") && (!domain || !route.domain));
|
|
6692
8455
|
for (const route of globalWildcardRoutes) {
|
|
6693
8456
|
const basePath = route.path.slice(0, -1);
|
|
6694
|
-
if (
|
|
8457
|
+
if (url2.pathname.startsWith(basePath)) {
|
|
6695
8458
|
const result = {
|
|
6696
8459
|
route,
|
|
6697
8460
|
params: {
|
|
6698
|
-
wildcard:
|
|
8461
|
+
wildcard: url2.pathname.slice(basePath.length)
|
|
6699
8462
|
}
|
|
6700
8463
|
};
|
|
6701
8464
|
this.routeCache.set(cacheKey, result);
|
|
@@ -6724,9 +8487,9 @@ class Router {
|
|
|
6724
8487
|
}
|
|
6725
8488
|
return parsedCookies;
|
|
6726
8489
|
};
|
|
6727
|
-
const
|
|
8490
|
+
const url2 = new URL(req.url);
|
|
6728
8491
|
const query = {};
|
|
6729
|
-
|
|
8492
|
+
url2.searchParams.forEach((value, key) => {
|
|
6730
8493
|
query[key] = value;
|
|
6731
8494
|
});
|
|
6732
8495
|
const enhancedReq = {
|
|
@@ -7103,7 +8866,7 @@ class Router {
|
|
|
7103
8866
|
}
|
|
7104
8867
|
return this;
|
|
7105
8868
|
}
|
|
7106
|
-
async executeMiddleware(middleware2,
|
|
8869
|
+
async executeMiddleware(middleware2, request2, handler) {
|
|
7107
8870
|
if (middleware2.length === 0) {
|
|
7108
8871
|
return handler();
|
|
7109
8872
|
}
|
|
@@ -7113,7 +8876,7 @@ class Router {
|
|
|
7113
8876
|
return handler();
|
|
7114
8877
|
}
|
|
7115
8878
|
const mw = middleware2[currentIndex++];
|
|
7116
|
-
const result = await mw(
|
|
8879
|
+
const result = await mw(request2, next);
|
|
7117
8880
|
return result || new Response("No response from middleware", { status: 500 });
|
|
7118
8881
|
};
|
|
7119
8882
|
return next();
|
|
@@ -7275,16 +9038,16 @@ class RequestWithMacros {
|
|
|
7275
9038
|
static macroAccessor(name, getter) {
|
|
7276
9039
|
requestMacroRegistry.register(name, getter, true);
|
|
7277
9040
|
}
|
|
7278
|
-
static applyMacros(
|
|
9041
|
+
static applyMacros(request2) {
|
|
7279
9042
|
const epoch = requestMacroRegistry.getEpoch();
|
|
7280
9043
|
if (epoch !== macroProtoEpoch) {
|
|
7281
9044
|
macroProtoCache = new WeakMap;
|
|
7282
9045
|
macroProtoEpoch = epoch;
|
|
7283
9046
|
}
|
|
7284
|
-
let base = Object.getPrototypeOf(
|
|
9047
|
+
let base = Object.getPrototypeOf(request2) ?? Object.prototype;
|
|
7285
9048
|
if (base[MACRO_PROTO]) {
|
|
7286
9049
|
if (macroProtoCache.get(base[MACRO_BASE]) === base) {
|
|
7287
|
-
return
|
|
9050
|
+
return request2;
|
|
7288
9051
|
}
|
|
7289
9052
|
base = base[MACRO_BASE];
|
|
7290
9053
|
}
|
|
@@ -7313,8 +9076,8 @@ class RequestWithMacros {
|
|
|
7313
9076
|
Object.defineProperty(proto, MACRO_BASE, { value: base });
|
|
7314
9077
|
macroProtoCache.set(base, proto);
|
|
7315
9078
|
}
|
|
7316
|
-
Object.setPrototypeOf(
|
|
7317
|
-
return
|
|
9079
|
+
Object.setPrototypeOf(request2, proto);
|
|
9080
|
+
return request2;
|
|
7318
9081
|
}
|
|
7319
9082
|
static hasMacro(name) {
|
|
7320
9083
|
return requestMacroRegistry.has(name);
|
|
@@ -7508,8 +9271,8 @@ var BuiltInRequestMacros = {
|
|
|
7508
9271
|
return this.url;
|
|
7509
9272
|
},
|
|
7510
9273
|
root() {
|
|
7511
|
-
const
|
|
7512
|
-
return `${
|
|
9274
|
+
const url2 = getParsedURL(this);
|
|
9275
|
+
return `${url2.protocol}//${url2.host}`;
|
|
7513
9276
|
},
|
|
7514
9277
|
is(pattern) {
|
|
7515
9278
|
const path = this.path();
|
|
@@ -7528,11 +9291,11 @@ var BuiltInRequestMacros = {
|
|
|
7528
9291
|
return this.routeName || null;
|
|
7529
9292
|
},
|
|
7530
9293
|
fingerprint() {
|
|
7531
|
-
const
|
|
9294
|
+
const url2 = getParsedURL(this);
|
|
7532
9295
|
const data = {
|
|
7533
9296
|
method: this.method,
|
|
7534
|
-
path:
|
|
7535
|
-
query:
|
|
9297
|
+
path: url2.pathname,
|
|
9298
|
+
query: url2.search,
|
|
7536
9299
|
ip: BuiltInRequestMacros.ip.call(this)
|
|
7537
9300
|
};
|
|
7538
9301
|
return btoa(JSON.stringify(data));
|
|
@@ -7583,36 +9346,6 @@ function registerBuiltInRequestMacros() {
|
|
|
7583
9346
|
}
|
|
7584
9347
|
registerBuiltInRequestMacros();
|
|
7585
9348
|
|
|
7586
|
-
// src/request/context.ts
|
|
7587
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
7588
|
-
var storage = new AsyncLocalStorage;
|
|
7589
|
-
var contextEnabled = false;
|
|
7590
|
-
var syncCurrent = null;
|
|
7591
|
-
function runWithRequest(initial, fn) {
|
|
7592
|
-
if (!contextEnabled) {
|
|
7593
|
-
syncCurrent = initial;
|
|
7594
|
-
const result = fn();
|
|
7595
|
-
if (result instanceof Promise) {
|
|
7596
|
-
return result.finally(() => {
|
|
7597
|
-
syncCurrent = null;
|
|
7598
|
-
});
|
|
7599
|
-
}
|
|
7600
|
-
syncCurrent = null;
|
|
7601
|
-
return result;
|
|
7602
|
-
}
|
|
7603
|
-
return storage.run({ current: initial }, fn);
|
|
7604
|
-
}
|
|
7605
|
-
function setCurrentRequest(req) {
|
|
7606
|
-
const store = storage.getStore();
|
|
7607
|
-
if (store) {
|
|
7608
|
-
store.current = req;
|
|
7609
|
-
return;
|
|
7610
|
-
}
|
|
7611
|
-
if (syncCurrent !== null) {
|
|
7612
|
-
syncCurrent = req;
|
|
7613
|
-
}
|
|
7614
|
-
}
|
|
7615
|
-
|
|
7616
9349
|
// src/router/server.ts
|
|
7617
9350
|
if (!RequestWithMacros.hasMacro("getParam")) {
|
|
7618
9351
|
RequestWithMacros.macro("getParam", function(name, defaultValue) {
|
|
@@ -7898,10 +9631,10 @@ function registerServerHandling(RouterClass) {
|
|
|
7898
9631
|
handleRequestImpl: {
|
|
7899
9632
|
async value(req) {
|
|
7900
9633
|
try {
|
|
7901
|
-
const
|
|
7902
|
-
req._parsedURL =
|
|
7903
|
-
const hostname =
|
|
7904
|
-
const match = this.matchRoute(
|
|
9634
|
+
const url2 = new URL(req.url);
|
|
9635
|
+
req._parsedURL = url2;
|
|
9636
|
+
const hostname = url2.hostname || req.headers.get("host")?.split(":")[0] || "localhost";
|
|
9637
|
+
const match = this.matchRoute(url2.pathname, req.method, hostname);
|
|
7905
9638
|
if (req.method === "OPTIONS" && !match) {
|
|
7906
9639
|
const origin = req.headers.get("origin");
|
|
7907
9640
|
const preflightHeaders = {
|
|
@@ -7921,7 +9654,7 @@ function registerServerHandling(RouterClass) {
|
|
|
7921
9654
|
}
|
|
7922
9655
|
const enhancedReq = this.enhanceRequest(req, {});
|
|
7923
9656
|
setCurrentRequest(enhancedReq);
|
|
7924
|
-
const allowedMethods = this.getAllowedMethods(
|
|
9657
|
+
const allowedMethods = this.getAllowedMethods(url2.pathname, hostname);
|
|
7925
9658
|
const corsHeaders = {
|
|
7926
9659
|
"Content-Type": "application/json",
|
|
7927
9660
|
"Access-Control-Allow-Origin": "*",
|
|
@@ -7933,7 +9666,7 @@ function registerServerHandling(RouterClass) {
|
|
|
7933
9666
|
return new Response(JSON.stringify({
|
|
7934
9667
|
success: false,
|
|
7935
9668
|
message: "Method Not Allowed",
|
|
7936
|
-
path:
|
|
9669
|
+
path: url2.pathname,
|
|
7937
9670
|
method: req.method,
|
|
7938
9671
|
allowed: allowedMethods
|
|
7939
9672
|
}), {
|
|
@@ -7957,7 +9690,7 @@ function registerServerHandling(RouterClass) {
|
|
|
7957
9690
|
return new Response(JSON.stringify({
|
|
7958
9691
|
success: false,
|
|
7959
9692
|
message: "Not Found",
|
|
7960
|
-
path:
|
|
9693
|
+
path: url2.pathname,
|
|
7961
9694
|
method: req.method
|
|
7962
9695
|
}), { status: 404, headers: corsHeaders });
|
|
7963
9696
|
};
|
|
@@ -8252,34 +9985,34 @@ function registerWebSocketHandling(RouterClass) {
|
|
|
8252
9985
|
configurable: true
|
|
8253
9986
|
},
|
|
8254
9987
|
upgrade: {
|
|
8255
|
-
value(
|
|
9988
|
+
value(request2, options) {
|
|
8256
9989
|
if (!this.serverInstance) {
|
|
8257
9990
|
throw new Error("Server not started, cannot upgrade to WebSocket");
|
|
8258
9991
|
}
|
|
8259
9992
|
if (!this.wsConfig) {
|
|
8260
9993
|
throw new Error("WebSocket configuration not set, use router.websocket() to configure");
|
|
8261
9994
|
}
|
|
8262
|
-
return this.serverInstance.upgrade(
|
|
9995
|
+
return this.serverInstance.upgrade(request2, options);
|
|
8263
9996
|
},
|
|
8264
9997
|
writable: true,
|
|
8265
9998
|
configurable: true
|
|
8266
9999
|
},
|
|
8267
10000
|
timeout: {
|
|
8268
|
-
value(
|
|
10001
|
+
value(request2, seconds) {
|
|
8269
10002
|
if (!this.serverInstance) {
|
|
8270
10003
|
throw new Error("Server not started, cannot set timeout");
|
|
8271
10004
|
}
|
|
8272
|
-
this.serverInstance.timeout(
|
|
10005
|
+
this.serverInstance.timeout(request2, seconds);
|
|
8273
10006
|
},
|
|
8274
10007
|
writable: true,
|
|
8275
10008
|
configurable: true
|
|
8276
10009
|
},
|
|
8277
10010
|
requestIP: {
|
|
8278
|
-
value(
|
|
10011
|
+
value(request2) {
|
|
8279
10012
|
if (!this.serverInstance) {
|
|
8280
10013
|
throw new Error("Server not started, cannot get request IP");
|
|
8281
10014
|
}
|
|
8282
|
-
return this.serverInstance.requestIP(
|
|
10015
|
+
return this.serverInstance.requestIP(request2);
|
|
8283
10016
|
},
|
|
8284
10017
|
writable: true,
|
|
8285
10018
|
configurable: true
|
|
@@ -8287,7 +10020,7 @@ function registerWebSocketHandling(RouterClass) {
|
|
|
8287
10020
|
});
|
|
8288
10021
|
}
|
|
8289
10022
|
// src/response/macros.ts
|
|
8290
|
-
import
|
|
10023
|
+
import process4 from "process";
|
|
8291
10024
|
|
|
8292
10025
|
class ResponseMacroRegistry {
|
|
8293
10026
|
macros = new Map;
|
|
@@ -8468,16 +10201,16 @@ var BuiltInResponseMacros = {
|
|
|
8468
10201
|
}
|
|
8469
10202
|
});
|
|
8470
10203
|
},
|
|
8471
|
-
redirect: (
|
|
10204
|
+
redirect: (url2, status = 302) => {
|
|
8472
10205
|
return new Response(null, {
|
|
8473
10206
|
status,
|
|
8474
10207
|
headers: {
|
|
8475
|
-
Location:
|
|
10208
|
+
Location: url2
|
|
8476
10209
|
}
|
|
8477
10210
|
});
|
|
8478
10211
|
},
|
|
8479
|
-
permanentRedirect: (
|
|
8480
|
-
return BuiltInResponseMacros.redirect(
|
|
10212
|
+
permanentRedirect: (url2) => {
|
|
10213
|
+
return BuiltInResponseMacros.redirect(url2, 301);
|
|
8481
10214
|
},
|
|
8482
10215
|
download: (data, filename, contentType = "application/octet-stream") => {
|
|
8483
10216
|
return new Response(data, {
|
|
@@ -8548,7 +10281,7 @@ var BuiltInResponseMacros = {
|
|
|
8548
10281
|
status,
|
|
8549
10282
|
timestamp: new Date().toISOString(),
|
|
8550
10283
|
checks,
|
|
8551
|
-
uptime:
|
|
10284
|
+
uptime: process4.uptime?.() || 0
|
|
8552
10285
|
};
|
|
8553
10286
|
return new Response(JSON.stringify(response2), {
|
|
8554
10287
|
status: statusCode,
|
|
@@ -8612,8 +10345,8 @@ var BuiltInRules = {
|
|
|
8612
10345
|
name: "url",
|
|
8613
10346
|
validate: (value) => {
|
|
8614
10347
|
try {
|
|
8615
|
-
const
|
|
8616
|
-
return Boolean(
|
|
10348
|
+
const url2 = new URL(value);
|
|
10349
|
+
return Boolean(url2);
|
|
8617
10350
|
} catch {
|
|
8618
10351
|
return false;
|
|
8619
10352
|
}
|
|
@@ -9272,6 +11005,119 @@ registerRouteBuilding(Router);
|
|
|
9272
11005
|
registerModelBinding(Router);
|
|
9273
11006
|
registerFileBasedRouting(Router);
|
|
9274
11007
|
registerApiRoutes(Router);
|
|
11008
|
+
// src/router/middleware-groups.ts
|
|
11009
|
+
class MiddlewareGroupRegistry {
|
|
11010
|
+
groups = new Map;
|
|
11011
|
+
globalMiddleware = [];
|
|
11012
|
+
middlewarePriority = new Map;
|
|
11013
|
+
define(name, middleware2) {
|
|
11014
|
+
this.groups.set(name, [...middleware2]);
|
|
11015
|
+
return this;
|
|
11016
|
+
}
|
|
11017
|
+
extend(name, middleware2) {
|
|
11018
|
+
const existing = this.groups.get(name) || [];
|
|
11019
|
+
this.groups.set(name, [...existing, ...middleware2]);
|
|
11020
|
+
return this;
|
|
11021
|
+
}
|
|
11022
|
+
prepend(name, middleware2) {
|
|
11023
|
+
const existing = this.groups.get(name) || [];
|
|
11024
|
+
this.groups.set(name, [...middleware2, ...existing]);
|
|
11025
|
+
return this;
|
|
11026
|
+
}
|
|
11027
|
+
get(name) {
|
|
11028
|
+
return this.groups.get(name) || [];
|
|
11029
|
+
}
|
|
11030
|
+
has(name) {
|
|
11031
|
+
return this.groups.has(name);
|
|
11032
|
+
}
|
|
11033
|
+
remove(name) {
|
|
11034
|
+
this.groups.delete(name);
|
|
11035
|
+
return this;
|
|
11036
|
+
}
|
|
11037
|
+
resolve(middleware2) {
|
|
11038
|
+
if (typeof middleware2 === "function") {
|
|
11039
|
+
return [middleware2];
|
|
11040
|
+
}
|
|
11041
|
+
if (this.groups.has(middleware2)) {
|
|
11042
|
+
return this.get(middleware2);
|
|
11043
|
+
}
|
|
11044
|
+
return [middleware2];
|
|
11045
|
+
}
|
|
11046
|
+
resolveAll(middleware2) {
|
|
11047
|
+
const resolved = [];
|
|
11048
|
+
for (const mw of middleware2) {
|
|
11049
|
+
resolved.push(...this.resolve(mw));
|
|
11050
|
+
}
|
|
11051
|
+
return resolved;
|
|
11052
|
+
}
|
|
11053
|
+
pushGlobal(...middleware2) {
|
|
11054
|
+
this.globalMiddleware.push(...middleware2);
|
|
11055
|
+
return this;
|
|
11056
|
+
}
|
|
11057
|
+
prependGlobal(...middleware2) {
|
|
11058
|
+
this.globalMiddleware.unshift(...middleware2);
|
|
11059
|
+
return this;
|
|
11060
|
+
}
|
|
11061
|
+
removeGlobal(middleware2) {
|
|
11062
|
+
this.globalMiddleware = this.globalMiddleware.filter((mw) => typeof mw === "string" ? mw !== middleware2 : true);
|
|
11063
|
+
return this;
|
|
11064
|
+
}
|
|
11065
|
+
getGlobal() {
|
|
11066
|
+
return [...this.globalMiddleware];
|
|
11067
|
+
}
|
|
11068
|
+
setPriority(name, priority) {
|
|
11069
|
+
this.middlewarePriority.set(name, priority);
|
|
11070
|
+
return this;
|
|
11071
|
+
}
|
|
11072
|
+
sortByPriority(middleware2) {
|
|
11073
|
+
return [...middleware2].sort((a, b) => {
|
|
11074
|
+
const nameA = typeof a === "string" ? a.split(":")[0] : "";
|
|
11075
|
+
const nameB = typeof b === "string" ? b.split(":")[0] : "";
|
|
11076
|
+
const priorityA = this.middlewarePriority.get(nameA) ?? 100;
|
|
11077
|
+
const priorityB = this.middlewarePriority.get(nameB) ?? 100;
|
|
11078
|
+
return priorityA - priorityB;
|
|
11079
|
+
});
|
|
11080
|
+
}
|
|
11081
|
+
filter(middleware2, options) {
|
|
11082
|
+
if (options.only && options.only.length > 0) {
|
|
11083
|
+
return middleware2.filter((mw) => {
|
|
11084
|
+
if (typeof mw === "string") {
|
|
11085
|
+
const name = mw.split(":")[0];
|
|
11086
|
+
return options.only.includes(name);
|
|
11087
|
+
}
|
|
11088
|
+
return false;
|
|
11089
|
+
});
|
|
11090
|
+
}
|
|
11091
|
+
if (options.except && options.except.length > 0) {
|
|
11092
|
+
return middleware2.filter((mw) => {
|
|
11093
|
+
if (typeof mw === "string") {
|
|
11094
|
+
const name = mw.split(":")[0];
|
|
11095
|
+
return !options.except.includes(name);
|
|
11096
|
+
}
|
|
11097
|
+
return true;
|
|
11098
|
+
});
|
|
11099
|
+
}
|
|
11100
|
+
return middleware2;
|
|
11101
|
+
}
|
|
11102
|
+
flush() {
|
|
11103
|
+
this.groups.clear();
|
|
11104
|
+
this.globalMiddleware = [];
|
|
11105
|
+
this.middlewarePriority.clear();
|
|
11106
|
+
}
|
|
11107
|
+
}
|
|
11108
|
+
var middlewareGroups = new MiddlewareGroupRegistry;
|
|
11109
|
+
middlewareGroups.define("web", ["session", "csrf", "cors"]);
|
|
11110
|
+
middlewareGroups.define("api", ["throttle:60,1", "cors"]);
|
|
11111
|
+
middlewareGroups.setPriority("session", 10);
|
|
11112
|
+
middlewareGroups.setPriority("cors", 20);
|
|
11113
|
+
middlewareGroups.setPriority("csrf", 30);
|
|
11114
|
+
middlewareGroups.setPriority("auth", 40);
|
|
11115
|
+
middlewareGroups.setPriority("throttle", 50);
|
|
11116
|
+
middlewareGroups.setPriority("role", 60);
|
|
11117
|
+
middlewareGroups.setPriority("permission", 60);
|
|
11118
|
+
middlewareGroups.setPriority("can", 70);
|
|
11119
|
+
middlewareGroups.setPriority("abilities", 70);
|
|
11120
|
+
middlewareGroups.setPriority("verified", 80);
|
|
9275
11121
|
// src/testing/auth-testing.ts
|
|
9276
11122
|
import { mock } from "bun:test";
|
|
9277
11123
|
|
|
@@ -9490,8 +11336,8 @@ function createCookieAccessor2(cookies) {
|
|
|
9490
11336
|
|
|
9491
11337
|
class AuthTester {
|
|
9492
11338
|
request;
|
|
9493
|
-
constructor(
|
|
9494
|
-
this.request =
|
|
11339
|
+
constructor(request2) {
|
|
11340
|
+
this.request = request2 || createMockRequest();
|
|
9495
11341
|
}
|
|
9496
11342
|
as(user) {
|
|
9497
11343
|
this.request.user = user;
|
|
@@ -9719,16 +11565,16 @@ var authMocks = {
|
|
|
9719
11565
|
})
|
|
9720
11566
|
};
|
|
9721
11567
|
var authHelpers = {
|
|
9722
|
-
authenticatedRequest: (user,
|
|
9723
|
-
return new AuthTester(
|
|
11568
|
+
authenticatedRequest: (user, request2) => {
|
|
11569
|
+
return new AuthTester(request2).as(user).getRequest();
|
|
9724
11570
|
},
|
|
9725
|
-
jwtRequest: (payload = {},
|
|
11571
|
+
jwtRequest: (payload = {}, request2) => {
|
|
9726
11572
|
const jwt = new JWTTester;
|
|
9727
11573
|
const token = jwt.generateToken(payload);
|
|
9728
|
-
return new AuthTester(
|
|
11574
|
+
return new AuthTester(request2).withJWT(token).getRequest();
|
|
9729
11575
|
},
|
|
9730
|
-
sessionRequest: (sessionData = {},
|
|
9731
|
-
return new AuthTester(
|
|
11576
|
+
sessionRequest: (sessionData = {}, request2) => {
|
|
11577
|
+
return new AuthTester(request2).withSession(sessionData).getRequest();
|
|
9732
11578
|
},
|
|
9733
11579
|
verifyJWTStructure: (token) => {
|
|
9734
11580
|
const parts = token.split(".");
|
|
@@ -9743,8 +11589,8 @@ var authHelpers = {
|
|
|
9743
11589
|
}
|
|
9744
11590
|
}
|
|
9745
11591
|
};
|
|
9746
|
-
function createAuthTester(
|
|
9747
|
-
return new AuthTester(
|
|
11592
|
+
function createAuthTester(request2) {
|
|
11593
|
+
return new AuthTester(request2);
|
|
9748
11594
|
}
|
|
9749
11595
|
function createJWTTester(options) {
|
|
9750
11596
|
return new JWTTester(options);
|
|
@@ -9752,14 +11598,13 @@ function createJWTTester(options) {
|
|
|
9752
11598
|
function createSessionTester(options) {
|
|
9753
11599
|
return new SessionTester(options);
|
|
9754
11600
|
}
|
|
9755
|
-
|
|
9756
11601
|
// src/testing/file-upload-testing.ts
|
|
9757
11602
|
import { mock as mock2 } from "bun:test";
|
|
9758
11603
|
class FileUploadTester {
|
|
9759
11604
|
request;
|
|
9760
11605
|
files;
|
|
9761
|
-
constructor(
|
|
9762
|
-
this.request =
|
|
11606
|
+
constructor(request2) {
|
|
11607
|
+
this.request = request2 || createMockRequest();
|
|
9763
11608
|
this.files = [];
|
|
9764
11609
|
}
|
|
9765
11610
|
addFile(testFile) {
|
|
@@ -9991,8 +11836,8 @@ var fileUploadScenarios = {
|
|
|
9991
11836
|
}
|
|
9992
11837
|
};
|
|
9993
11838
|
var fileUploadAssertions = {
|
|
9994
|
-
hasFiles: (
|
|
9995
|
-
const files =
|
|
11839
|
+
hasFiles: (request2, count) => {
|
|
11840
|
+
const files = request2.files || [];
|
|
9996
11841
|
if (count !== undefined && files.length !== count) {
|
|
9997
11842
|
throw new Error(`Expected ${count} files, but got ${files.length}`);
|
|
9998
11843
|
}
|
|
@@ -10000,8 +11845,8 @@ var fileUploadAssertions = {
|
|
|
10000
11845
|
throw new Error("Expected request to have files");
|
|
10001
11846
|
}
|
|
10002
11847
|
},
|
|
10003
|
-
hasFileWithField: (
|
|
10004
|
-
const files =
|
|
11848
|
+
hasFileWithField: (request2, fieldName) => {
|
|
11849
|
+
const files = request2.files || [];
|
|
10005
11850
|
const hasField = files.some((file) => file.fieldName === fieldName);
|
|
10006
11851
|
if (!hasField) {
|
|
10007
11852
|
throw new Error(`Expected file with field name '${fieldName}'`);
|
|
@@ -10025,8 +11870,8 @@ var fileUploadAssertions = {
|
|
|
10025
11870
|
}
|
|
10026
11871
|
}
|
|
10027
11872
|
};
|
|
10028
|
-
function createFileUploadTester(
|
|
10029
|
-
return new FileUploadTester(
|
|
11873
|
+
function createFileUploadTester(request2) {
|
|
11874
|
+
return new FileUploadTester(request2);
|
|
10030
11875
|
}
|
|
10031
11876
|
var testFiles = {
|
|
10032
11877
|
image: (fieldName = "image", filename = "test.jpg") => ({
|
|
@@ -10060,7 +11905,6 @@ var testFiles = {
|
|
|
10060
11905
|
mimetype: "application/x-executable"
|
|
10061
11906
|
})
|
|
10062
11907
|
};
|
|
10063
|
-
|
|
10064
11908
|
// src/testing/middleware-testing.ts
|
|
10065
11909
|
import { mock as mock3, spyOn } from "bun:test";
|
|
10066
11910
|
class MiddlewareTester {
|
|
@@ -10071,15 +11915,15 @@ class MiddlewareTester {
|
|
|
10071
11915
|
this.context = this.createContext();
|
|
10072
11916
|
}
|
|
10073
11917
|
createContext() {
|
|
10074
|
-
const
|
|
11918
|
+
const request2 = createMockRequest();
|
|
10075
11919
|
const next = mock3(() => Promise.resolve(new Response("Next called", { status: 200 })));
|
|
10076
11920
|
return {
|
|
10077
|
-
request,
|
|
11921
|
+
request: request2,
|
|
10078
11922
|
next
|
|
10079
11923
|
};
|
|
10080
11924
|
}
|
|
10081
|
-
withRequest(
|
|
10082
|
-
this.context.request =
|
|
11925
|
+
withRequest(request2) {
|
|
11926
|
+
this.context.request = request2;
|
|
10083
11927
|
return this;
|
|
10084
11928
|
}
|
|
10085
11929
|
withNext(nextFn) {
|
|
@@ -10177,8 +12021,8 @@ class MiddlewareChainTester {
|
|
|
10177
12021
|
this.request = createMockRequest();
|
|
10178
12022
|
this.finalHandler = mock3(async () => new Response("Final handler", { status: 200 }));
|
|
10179
12023
|
}
|
|
10180
|
-
withRequest(
|
|
10181
|
-
this.request =
|
|
12024
|
+
withRequest(request2) {
|
|
12025
|
+
this.request = request2;
|
|
10182
12026
|
return this;
|
|
10183
12027
|
}
|
|
10184
12028
|
withFinalHandler(handler) {
|
|
@@ -10218,11 +12062,11 @@ function createMiddlewareSpy() {
|
|
|
10218
12062
|
}
|
|
10219
12063
|
async function benchmarkMiddleware(middleware2, iterations = 1000) {
|
|
10220
12064
|
const times = [];
|
|
10221
|
-
const
|
|
12065
|
+
const request2 = createMockRequest();
|
|
10222
12066
|
const next = mock3(async () => new Response("OK"));
|
|
10223
12067
|
for (let i = 0;i < iterations; i++) {
|
|
10224
12068
|
const start = performance.now();
|
|
10225
|
-
await middleware2(
|
|
12069
|
+
await middleware2(request2, next);
|
|
10226
12070
|
const end = performance.now();
|
|
10227
12071
|
times.push(end - start);
|
|
10228
12072
|
}
|
|
@@ -10237,7 +12081,6 @@ async function benchmarkMiddleware(middleware2, iterations = 1000) {
|
|
|
10237
12081
|
totalTime
|
|
10238
12082
|
};
|
|
10239
12083
|
}
|
|
10240
|
-
|
|
10241
12084
|
// src/testing/model-binding-testing.ts
|
|
10242
12085
|
import { mock as mock4 } from "bun:test";
|
|
10243
12086
|
class GenericMockModel {
|
|
@@ -10321,8 +12164,8 @@ class ModelBindingTester {
|
|
|
10321
12164
|
this.options = options;
|
|
10322
12165
|
this.request = createMockRequest();
|
|
10323
12166
|
}
|
|
10324
|
-
withRequest(
|
|
10325
|
-
this.request =
|
|
12167
|
+
withRequest(request2) {
|
|
12168
|
+
this.request = request2;
|
|
10326
12169
|
return this;
|
|
10327
12170
|
}
|
|
10328
12171
|
withParams(params) {
|
|
@@ -10460,18 +12303,18 @@ var modelMocks = {
|
|
|
10460
12303
|
};
|
|
10461
12304
|
var constraintHelpers = {
|
|
10462
12305
|
belongsTo: (userIdField = "userId") => {
|
|
10463
|
-
const fn = async (model,
|
|
10464
|
-
if (!
|
|
12306
|
+
const fn = async (model, request2) => {
|
|
12307
|
+
if (!request2.user)
|
|
10465
12308
|
return false;
|
|
10466
|
-
return model[userIdField] ===
|
|
12309
|
+
return model[userIdField] === request2.user.id;
|
|
10467
12310
|
};
|
|
10468
12311
|
return mock4(fn);
|
|
10469
12312
|
},
|
|
10470
12313
|
belongsToUser: () => {
|
|
10471
|
-
const fn = async (model,
|
|
10472
|
-
if (!
|
|
12314
|
+
const fn = async (model, request2) => {
|
|
12315
|
+
if (!request2.user)
|
|
10473
12316
|
return false;
|
|
10474
|
-
return model.userId ===
|
|
12317
|
+
return model.userId === request2.user.id;
|
|
10475
12318
|
};
|
|
10476
12319
|
return mock4(fn);
|
|
10477
12320
|
},
|
|
@@ -10491,9 +12334,9 @@ var constraintHelpers = {
|
|
|
10491
12334
|
return mock4(constraintFn);
|
|
10492
12335
|
},
|
|
10493
12336
|
combine: (...constraints) => {
|
|
10494
|
-
const fn = async (model,
|
|
12337
|
+
const fn = async (model, request2) => {
|
|
10495
12338
|
for (const constraint of constraints) {
|
|
10496
|
-
if (!await constraint(model,
|
|
12339
|
+
if (!await constraint(model, request2)) {
|
|
10497
12340
|
return false;
|
|
10498
12341
|
}
|
|
10499
12342
|
}
|
|
@@ -10550,24 +12393,24 @@ class ModelResolverTester {
|
|
|
10550
12393
|
}
|
|
10551
12394
|
var routeModelHelpers = {
|
|
10552
12395
|
testRoute: async (path, params, modelBindings) => {
|
|
10553
|
-
const
|
|
10554
|
-
|
|
12396
|
+
const request2 = createMockRequest("GET", path);
|
|
12397
|
+
request2.params = params;
|
|
10555
12398
|
for (const [key, model] of Object.entries(modelBindings)) {
|
|
10556
|
-
|
|
10557
|
-
|
|
12399
|
+
request2.context = request2.context || {};
|
|
12400
|
+
request2.context[key] = model;
|
|
10558
12401
|
}
|
|
10559
|
-
return
|
|
12402
|
+
return request2;
|
|
10560
12403
|
},
|
|
10561
12404
|
createHandler: (expectedModels) => {
|
|
10562
|
-
const fn = async (
|
|
10563
|
-
const context =
|
|
12405
|
+
const fn = async (request2) => {
|
|
12406
|
+
const context = request2.context || {};
|
|
10564
12407
|
for (const modelKey of expectedModels) {
|
|
10565
12408
|
if (!context[modelKey]) {
|
|
10566
12409
|
return new Response(`Model ${modelKey} not found`, { status: 404 });
|
|
10567
12410
|
}
|
|
10568
12411
|
}
|
|
10569
12412
|
return new Response(JSON.stringify({
|
|
10570
|
-
params:
|
|
12413
|
+
params: request2.params,
|
|
10571
12414
|
models: Object.fromEntries(expectedModels.map((key) => [key, context[key]]))
|
|
10572
12415
|
}), {
|
|
10573
12416
|
headers: { "Content-Type": "application/json" }
|
|
@@ -10576,16 +12419,16 @@ var routeModelHelpers = {
|
|
|
10576
12419
|
return mock4(fn);
|
|
10577
12420
|
},
|
|
10578
12421
|
testBindingMiddleware: (bindings) => {
|
|
10579
|
-
const fn = async (
|
|
10580
|
-
|
|
12422
|
+
const fn = async (request2, next) => {
|
|
12423
|
+
request2.context = request2.context || {};
|
|
10581
12424
|
for (const [key, resolver] of Object.entries(bindings)) {
|
|
10582
|
-
const paramValue =
|
|
12425
|
+
const paramValue = request2.params[key];
|
|
10583
12426
|
if (paramValue) {
|
|
10584
12427
|
try {
|
|
10585
12428
|
if (typeof resolver === "function") {
|
|
10586
|
-
|
|
12429
|
+
request2.context[key] = await resolver(paramValue);
|
|
10587
12430
|
} else {
|
|
10588
|
-
|
|
12431
|
+
request2.context[key] = resolver;
|
|
10589
12432
|
}
|
|
10590
12433
|
} catch (error) {
|
|
10591
12434
|
console.error(`Error resolving model ${key}:`, error);
|
|
@@ -10620,10 +12463,9 @@ var scopedBindingHelpers = {
|
|
|
10620
12463
|
return requiredPermissions.every((permission) => user.permissions.includes(permission));
|
|
10621
12464
|
}
|
|
10622
12465
|
};
|
|
10623
|
-
|
|
10624
12466
|
// src/testing/performance-testing.ts
|
|
10625
12467
|
import { mock as mock5 } from "bun:test";
|
|
10626
|
-
import
|
|
12468
|
+
import process5 from "process";
|
|
10627
12469
|
class PerformanceTester {
|
|
10628
12470
|
metrics = [];
|
|
10629
12471
|
startTime = 0;
|
|
@@ -10742,10 +12584,10 @@ class LoadTester {
|
|
|
10742
12584
|
var benchmarkTests = {
|
|
10743
12585
|
routeHandler: async (handler, iterations = 1000) => {
|
|
10744
12586
|
const times = [];
|
|
10745
|
-
const
|
|
12587
|
+
const request2 = createMockRequest();
|
|
10746
12588
|
for (let i = 0;i < iterations; i++) {
|
|
10747
12589
|
const start = performance.now();
|
|
10748
|
-
await handler(
|
|
12590
|
+
await handler(request2);
|
|
10749
12591
|
const end = performance.now();
|
|
10750
12592
|
times.push(end - start);
|
|
10751
12593
|
}
|
|
@@ -10764,11 +12606,11 @@ var benchmarkTests = {
|
|
|
10764
12606
|
},
|
|
10765
12607
|
middleware: async (middleware2, iterations = 1000) => {
|
|
10766
12608
|
const times = [];
|
|
10767
|
-
const
|
|
12609
|
+
const request2 = createMockRequest();
|
|
10768
12610
|
const next = mock5(async () => new Response("OK"));
|
|
10769
12611
|
for (let i = 0;i < iterations; i++) {
|
|
10770
12612
|
const start = performance.now();
|
|
10771
|
-
await middleware2(
|
|
12613
|
+
await middleware2(request2, next);
|
|
10772
12614
|
const end = performance.now();
|
|
10773
12615
|
times.push(end - start);
|
|
10774
12616
|
}
|
|
@@ -10841,11 +12683,11 @@ var memoryTests = {
|
|
|
10841
12683
|
if (globalThis.gc) {
|
|
10842
12684
|
globalThis.gc();
|
|
10843
12685
|
}
|
|
10844
|
-
const initialHeap =
|
|
12686
|
+
const initialHeap = process5.memoryUsage().heapUsed;
|
|
10845
12687
|
for (let i = 0;i < iterations; i++) {
|
|
10846
12688
|
const result = await fn();
|
|
10847
12689
|
results.push(result);
|
|
10848
|
-
const memUsage =
|
|
12690
|
+
const memUsage = process5.memoryUsage();
|
|
10849
12691
|
heapUsages.push(memUsage.heapUsed);
|
|
10850
12692
|
if (i % 10 === 0 && globalThis.gc) {
|
|
10851
12693
|
globalThis.gc();
|
|
@@ -10854,7 +12696,7 @@ var memoryTests = {
|
|
|
10854
12696
|
if (globalThis.gc) {
|
|
10855
12697
|
globalThis.gc();
|
|
10856
12698
|
}
|
|
10857
|
-
const finalHeap =
|
|
12699
|
+
const finalHeap = process5.memoryUsage().heapUsed;
|
|
10858
12700
|
const averageHeapUsed = heapUsages.reduce((sum, usage) => sum + usage, 0) / heapUsages.length;
|
|
10859
12701
|
const maxHeapUsed = Math.max(...heapUsages);
|
|
10860
12702
|
const memoryLeakDetected = finalHeap - initialHeap > 1024 * 1024;
|
|
@@ -10868,17 +12710,17 @@ var memoryTests = {
|
|
|
10868
12710
|
testRouteHandlerMemoryLeaks: async (handler, iterations = 1000) => {
|
|
10869
12711
|
if (globalThis.gc)
|
|
10870
12712
|
globalThis.gc();
|
|
10871
|
-
const initialMemory =
|
|
12713
|
+
const initialMemory = process5.memoryUsage().heapUsed;
|
|
10872
12714
|
for (let i = 0;i < iterations; i++) {
|
|
10873
|
-
const
|
|
10874
|
-
await handler(
|
|
12715
|
+
const request2 = createMockRequest();
|
|
12716
|
+
await handler(request2);
|
|
10875
12717
|
if (i % 100 === 0 && globalThis.gc) {
|
|
10876
12718
|
globalThis.gc();
|
|
10877
12719
|
}
|
|
10878
12720
|
}
|
|
10879
12721
|
if (globalThis.gc)
|
|
10880
12722
|
globalThis.gc();
|
|
10881
|
-
const finalMemory =
|
|
12723
|
+
const finalMemory = process5.memoryUsage().heapUsed;
|
|
10882
12724
|
const memoryGrowth = finalMemory - initialMemory;
|
|
10883
12725
|
const memoryLeakDetected = memoryGrowth > 5 * 1024 * 1024;
|
|
10884
12726
|
return {
|
|
@@ -10970,7 +12812,7 @@ function createLoadTester(client, config2) {
|
|
|
10970
12812
|
}
|
|
10971
12813
|
var performanceUtils = {
|
|
10972
12814
|
getCurrentMemoryUsage: () => {
|
|
10973
|
-
const usage =
|
|
12815
|
+
const usage = process5.memoryUsage();
|
|
10974
12816
|
return {
|
|
10975
12817
|
heapUsed: usage.heapUsed,
|
|
10976
12818
|
heapTotal: usage.heapTotal,
|
|
@@ -10979,7 +12821,7 @@ var performanceUtils = {
|
|
|
10979
12821
|
};
|
|
10980
12822
|
},
|
|
10981
12823
|
getCPUUsage: () => {
|
|
10982
|
-
const usage =
|
|
12824
|
+
const usage = process5.cpuUsage();
|
|
10983
12825
|
return {
|
|
10984
12826
|
user: usage.user,
|
|
10985
12827
|
system: usage.system
|
|
@@ -10994,7 +12836,6 @@ var performanceUtils = {
|
|
|
10994
12836
|
return JSON.stringify(results, null, 2);
|
|
10995
12837
|
}
|
|
10996
12838
|
};
|
|
10997
|
-
|
|
10998
12839
|
// src/testing/test-response.ts
|
|
10999
12840
|
class TestResponseWrapper {
|
|
11000
12841
|
status;
|
|
@@ -11115,14 +12956,14 @@ class TestResponseWrapper {
|
|
|
11115
12956
|
}
|
|
11116
12957
|
return this;
|
|
11117
12958
|
}
|
|
11118
|
-
expectRedirect(
|
|
12959
|
+
expectRedirect(url2) {
|
|
11119
12960
|
if (this.status < 300 || this.status >= 400) {
|
|
11120
12961
|
throw new Error(`Expected redirect status (3xx), got ${this.status}`);
|
|
11121
12962
|
}
|
|
11122
|
-
if (
|
|
12963
|
+
if (url2) {
|
|
11123
12964
|
const location = this.headers.location;
|
|
11124
|
-
if (location !==
|
|
11125
|
-
throw new Error(`Expected redirect to '${
|
|
12965
|
+
if (location !== url2) {
|
|
12966
|
+
throw new Error(`Expected redirect to '${url2}', got '${location}'`);
|
|
11126
12967
|
}
|
|
11127
12968
|
}
|
|
11128
12969
|
return this;
|
|
@@ -11265,24 +13106,24 @@ class TestClient {
|
|
|
11265
13106
|
async request(method, path, options = {}) {
|
|
11266
13107
|
const headers = { ...this.defaultHeaders, ...options.headers };
|
|
11267
13108
|
const requestOptions = { ...options, headers };
|
|
11268
|
-
const
|
|
13109
|
+
const request2 = new TestRequestBuilder(method, path, requestOptions).build();
|
|
11269
13110
|
try {
|
|
11270
|
-
const response2 = await this.handleRequest(
|
|
13111
|
+
const response2 = await this.handleRequest(request2);
|
|
11271
13112
|
return await createTestResponse(response2);
|
|
11272
13113
|
} catch (error) {
|
|
11273
13114
|
throw new Error(`Test request failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
11274
13115
|
}
|
|
11275
13116
|
}
|
|
11276
|
-
async handleRequest(
|
|
11277
|
-
const
|
|
11278
|
-
const matchResult = this.findMatchingRoute(
|
|
13117
|
+
async handleRequest(request2) {
|
|
13118
|
+
const url2 = new URL(request2.url);
|
|
13119
|
+
const matchResult = this.findMatchingRoute(request2.method, url2.pathname);
|
|
11279
13120
|
if (!matchResult) {
|
|
11280
13121
|
return new Response("Not Found", { status: 404 });
|
|
11281
13122
|
}
|
|
11282
13123
|
const { route, params } = matchResult;
|
|
11283
|
-
|
|
13124
|
+
request2.params = { ...request2.params, ...params };
|
|
11284
13125
|
try {
|
|
11285
|
-
const response2 = await this.executeMiddlewareChain(
|
|
13126
|
+
const response2 = await this.executeMiddlewareChain(request2, route);
|
|
11286
13127
|
return response2 || new Response("No response", { status: 500 });
|
|
11287
13128
|
} catch (error) {
|
|
11288
13129
|
return new Response(JSON.stringify({ error: error instanceof Error ? error.message : "Internal Server Error" }), {
|
|
@@ -11322,28 +13163,28 @@ class TestClient {
|
|
|
11322
13163
|
}
|
|
11323
13164
|
return params;
|
|
11324
13165
|
}
|
|
11325
|
-
async executeMiddlewareChain(
|
|
13166
|
+
async executeMiddlewareChain(request2, route) {
|
|
11326
13167
|
const middlewares = [...this.router.globalMiddleware, ...route.middleware];
|
|
11327
13168
|
let index = 0;
|
|
11328
13169
|
const next = async () => {
|
|
11329
13170
|
if (index < middlewares.length) {
|
|
11330
13171
|
const middleware2 = middlewares[index++];
|
|
11331
|
-
return await middleware2(
|
|
13172
|
+
return await middleware2(request2, next);
|
|
11332
13173
|
} else {
|
|
11333
|
-
return await this.executeHandler(route.handler,
|
|
13174
|
+
return await this.executeHandler(route.handler, request2);
|
|
11334
13175
|
}
|
|
11335
13176
|
};
|
|
11336
13177
|
const result = await next();
|
|
11337
13178
|
return result || new Response("No response", { status: 500 });
|
|
11338
13179
|
}
|
|
11339
|
-
async executeHandler(handler,
|
|
13180
|
+
async executeHandler(handler, request2) {
|
|
11340
13181
|
if (typeof handler === "function") {
|
|
11341
|
-
return await handler(
|
|
13182
|
+
return await handler(request2);
|
|
11342
13183
|
} else if (typeof handler === "string") {
|
|
11343
13184
|
throw new TypeError(`String handlers not supported in test environment: ${handler}`);
|
|
11344
13185
|
} else if (typeof handler === "object" && handler.handle) {
|
|
11345
13186
|
const instance = new handler;
|
|
11346
|
-
return await instance.handle(
|
|
13187
|
+
return await instance.handle(request2);
|
|
11347
13188
|
}
|
|
11348
13189
|
throw new Error("Invalid handler type");
|
|
11349
13190
|
}
|
|
@@ -11384,7 +13225,6 @@ var testClient = {
|
|
|
11384
13225
|
withAuth: (token, type = "Bearer") => (router2) => createTestClient(router2).auth(token, type),
|
|
11385
13226
|
withHeaders: (headers) => (router2) => createTestClient(router2).withHeaders(headers)
|
|
11386
13227
|
};
|
|
11387
|
-
|
|
11388
13228
|
// src/testing/websocket-testing.ts
|
|
11389
13229
|
import { mock as mock6 } from "bun:test";
|
|
11390
13230
|
|
|
@@ -11739,13 +13579,13 @@ export {
|
|
|
11739
13579
|
spyOnMiddleware,
|
|
11740
13580
|
singleFileUpload,
|
|
11741
13581
|
signRequest,
|
|
11742
|
-
|
|
13582
|
+
setCurrentRequest,
|
|
11743
13583
|
setContainer,
|
|
11744
13584
|
session,
|
|
11745
13585
|
securitySuite,
|
|
11746
13586
|
security,
|
|
11747
13587
|
scopedBindingHelpers,
|
|
11748
|
-
|
|
13588
|
+
runWithRequest,
|
|
11749
13589
|
rule,
|
|
11750
13590
|
router,
|
|
11751
13591
|
routeModelHelpers,
|
|
@@ -11758,7 +13598,7 @@ export {
|
|
|
11758
13598
|
requestSigning,
|
|
11759
13599
|
requestId,
|
|
11760
13600
|
request,
|
|
11761
|
-
|
|
13601
|
+
registerNamedRoute,
|
|
11762
13602
|
rateLimitRegistry,
|
|
11763
13603
|
rateLimit,
|
|
11764
13604
|
processHtmlTemplate,
|
|
@@ -11788,7 +13628,7 @@ export {
|
|
|
11788
13628
|
globalValidator,
|
|
11789
13629
|
globalMiddlewarePipeline,
|
|
11790
13630
|
getNamedRoutes,
|
|
11791
|
-
|
|
13631
|
+
getNamedRoutePath,
|
|
11792
13632
|
getCurrentRequest,
|
|
11793
13633
|
getContainer,
|
|
11794
13634
|
getConfig,
|
|
@@ -11919,7 +13759,7 @@ export {
|
|
|
11919
13759
|
FileUpload,
|
|
11920
13760
|
FileSessionStore,
|
|
11921
13761
|
FileSecurity,
|
|
11922
|
-
Errors,
|
|
13762
|
+
exports_errors as Errors,
|
|
11923
13763
|
EnvironmentManager,
|
|
11924
13764
|
EnhancementPresets,
|
|
11925
13765
|
DomainMatcher,
|
|
@@ -11939,6 +13779,6 @@ export {
|
|
|
11939
13779
|
BuiltInRules,
|
|
11940
13780
|
BindingBuilder,
|
|
11941
13781
|
AuthTester,
|
|
11942
|
-
|
|
13782
|
+
auth_default as Auth,
|
|
11943
13783
|
ApiKeyManager
|
|
11944
13784
|
};
|