@stacksjs/bun-router 0.0.14 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +261 -584
- package/bin/cli.ts +9 -0
- package/dist/auth.d.ts +134 -0
- package/dist/cache/lru-cache.d.ts +139 -0
- package/dist/cache/middleware-memoization.d.ts +151 -0
- package/dist/cache/route-cache-warmer.d.ts +161 -0
- package/dist/cache/sqlite-cache.d.ts +209 -0
- package/dist/cache/streaming-cache.d.ts +132 -0
- package/dist/chunk-j0e7z7hd.js +18053 -0
- package/dist/cli/colors.d.ts +15 -0
- package/dist/cli/index.d.ts +10 -0
- package/dist/cli/middleware.d.ts +31 -0
- package/dist/cli/openapi.d.ts +17 -0
- package/dist/cli/router.d.ts +15 -0
- package/dist/cli/routes.d.ts +29 -0
- package/dist/cli/utils.d.ts +74 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3360 -0
- package/dist/config.d.ts +7 -0
- package/dist/container/container.d.ts +273 -0
- package/dist/container/contextual-binding.d.ts +240 -0
- package/dist/container/decorators.d.ts +141 -0
- package/dist/container/service-provider.d.ts +285 -0
- package/dist/development/hot-reload.d.ts +187 -0
- package/dist/development/index.d.ts +167 -0
- package/dist/development/performance-profiler.d.ts +217 -0
- package/dist/development/route-debugger.d.ts +154 -0
- package/dist/development/route-inspector.d.ts +211 -0
- package/dist/development/typescript-utilities.d.ts +209 -0
- package/dist/docs.d.ts +11 -0
- package/dist/errors/circuit-breaker.d.ts +195 -0
- package/dist/errors/error-handler.d.ts +88 -0
- package/dist/errors/error-reporting.d.ts +144 -0
- package/dist/errors/exceptions.d.ts +184 -0
- package/dist/errors/graceful-degradation.d.ts +154 -0
- package/dist/errors/index.d.ts +9 -0
- package/dist/errors/router-errors.d.ts +110 -0
- package/dist/file-serving/static-files.d.ts +143 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13968 -0
- package/dist/middleware/auth.d.ts +63 -0
- package/dist/middleware/content_security_policy.d.ts +53 -0
- package/dist/middleware/cors.d.ts +5 -0
- package/dist/middleware/csrf.d.ts +7 -0
- package/dist/middleware/ddos_protection.d.ts +39 -0
- package/dist/middleware/file_security.d.ts +26 -0
- package/dist/middleware/file_upload.d.ts +38 -0
- package/dist/middleware/helmet.d.ts +54 -0
- package/dist/middleware/index.d.ts +44 -0
- package/dist/middleware/input_validation.d.ts +45 -0
- package/dist/middleware/json_body.d.ts +4 -0
- package/dist/middleware/performance_alerting.d.ts +87 -0
- package/dist/middleware/performance_dashboard.d.ts +87 -0
- package/dist/middleware/performance_monitor.d.ts +209 -0
- package/dist/middleware/pipeline.d.ts +131 -0
- package/dist/middleware/rate_limit.d.ts +36 -0
- package/dist/middleware/request_id.d.ts +4 -0
- package/dist/middleware/request_signing.d.ts +153 -0
- package/dist/middleware/request_tracer.d.ts +72 -0
- package/dist/middleware/response_cache.d.ts +97 -0
- package/dist/middleware/security.d.ts +69 -0
- package/dist/middleware/security_suite.d.ts +46 -0
- package/dist/middleware/session.d.ts +23 -0
- package/dist/model-binding/index.d.ts +2 -0
- package/dist/model-binding/model-middleware.d.ts +118 -0
- package/dist/model-binding/model-registry.d.ts +164 -0
- package/dist/model-binding.d.ts +185 -0
- package/dist/model-resolver-factory.d.ts +31 -0
- package/dist/observability/correlation.d.ts +187 -0
- package/dist/observability/health-checks.d.ts +186 -0
- package/dist/observability/index.d.ts +60 -0
- package/dist/observability/integration.d.ts +147 -0
- package/dist/observability/metrics.d.ts +184 -0
- package/dist/observability/tracing.d.ts +187 -0
- package/dist/optimization/bun-utilities.d.ts +224 -0
- package/dist/query-builder-integration.d.ts +36 -0
- package/dist/request/context.d.ts +34 -0
- package/dist/request/enhanced-request.d.ts +213 -0
- package/dist/request/macros.d.ts +300 -0
- package/dist/response/macros.d.ts +256 -0
- package/dist/response/response-factory.d.ts +133 -0
- package/dist/router/api-routes.d.ts +5 -0
- package/dist/router/file-based-routing.d.ts +118 -0
- package/dist/router/file-streaming.d.ts +5 -0
- package/dist/router/fluent-router.d.ts +315 -0
- package/dist/router/fluent-routing.d.ts +271 -0
- package/dist/router/group-organization.d.ts +5 -0
- package/dist/router/handler-resolver.d.ts +24 -0
- package/dist/router/http-methods.d.ts +10 -0
- package/dist/router/index.d.ts +114 -0
- package/dist/router/middleware-groups.d.ts +94 -0
- package/dist/router/middleware-integration.d.ts +112 -0
- package/dist/router/middleware.d.ts +5 -0
- package/dist/router/model-binding.d.ts +5 -0
- package/dist/router/optimized-route-matching.d.ts +6 -0
- package/dist/router/route-building.d.ts +5 -0
- package/dist/router/route-compiler.d.ts +153 -0
- package/dist/router/route-matching.d.ts +5 -0
- package/dist/router/route-trie.d.ts +114 -0
- package/dist/router/router.d.ts +317 -0
- package/dist/router/server.d.ts +5 -0
- package/dist/router/validation-integration.d.ts +170 -0
- package/dist/router/view-rendering.d.ts +5 -0
- package/dist/router/websocket.d.ts +5 -0
- package/dist/routing/route-caching.d.ts +129 -0
- package/dist/routing/route-throttling.d.ts +150 -0
- package/dist/routing/subdomain-routing.d.ts +206 -0
- package/dist/session/database-store.d.ts +51 -0
- package/dist/session/file-store.d.ts +22 -0
- package/dist/session/index.d.ts +106 -0
- package/dist/session/memory-store.d.ts +21 -0
- package/dist/session/redis-store.d.ts +31 -0
- package/dist/streaming/index.d.ts +3 -0
- package/dist/streaming/sse-handler.d.ts +138 -0
- package/dist/streaming/stream-handler.d.ts +114 -0
- package/dist/testing/auth-testing.d.ts +156 -0
- package/dist/testing/file-upload-testing.d.ts +190 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/middleware-testing.d.ts +138 -0
- package/dist/testing/model-binding-testing.d.ts +186 -0
- package/dist/testing/performance-testing.d.ts +235 -0
- package/dist/testing/test-client.d.ts +117 -0
- package/dist/testing/test-request.d.ts +85 -0
- package/dist/testing/test-response.d.ts +90 -0
- package/dist/testing/types.d.ts +207 -0
- package/dist/testing/websocket-testing.d.ts +227 -0
- package/dist/types/controller-types.d.ts +208 -0
- package/dist/types/core.d.ts +543 -0
- package/dist/types/middleware-types.d.ts +227 -0
- package/dist/types/request-response-augmentation.d.ts +261 -0
- package/dist/types/route-inference.d.ts +168 -0
- package/dist/types.d.ts +1794 -0
- package/dist/url.d.ts +56 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/query-preservation.d.ts +48 -0
- package/dist/utils.d.ts +69 -0
- package/dist/validation/validator.d.ts +140 -0
- package/dist/websocket/clustering.d.ts +185 -0
- package/package.json +45 -44
- package/src/auth.ts +469 -0
- package/src/cache/lru-cache.ts +457 -0
- package/src/cache/middleware-memoization.ts +531 -0
- package/src/cache/route-cache-warmer.ts +486 -0
- package/src/cache/sqlite-cache.ts +783 -0
- package/src/cache/streaming-cache.ts +572 -0
- package/src/cli/colors.ts +29 -0
- package/src/cli/index.ts +287 -0
- package/src/cli/middleware.ts +291 -0
- package/src/cli/openapi.ts +407 -0
- package/src/cli/router.ts +188 -0
- package/src/cli/routes.ts +265 -0
- package/src/cli/utils.ts +531 -0
- package/src/cli.ts +5 -0
- package/src/config.ts +322 -0
- package/src/container/container.ts +740 -0
- package/src/container/contextual-binding.ts +603 -0
- package/src/container/decorators.ts +359 -0
- package/src/container/service-provider.ts +596 -0
- package/src/development/hot-reload.ts +673 -0
- package/src/development/index.ts +499 -0
- package/src/development/performance-profiler.ts +717 -0
- package/src/development/route-debugger.ts +527 -0
- package/src/development/route-inspector.ts +749 -0
- package/src/development/typescript-utilities.ts +682 -0
- package/src/docs.ts +397 -0
- package/src/errors/circuit-breaker.ts +732 -0
- package/src/errors/error-handler.ts +569 -0
- package/src/errors/error-reporting.ts +672 -0
- package/src/errors/exceptions.ts +536 -0
- package/src/errors/graceful-degradation.ts +621 -0
- package/src/errors/index.ts +21 -0
- package/src/errors/router-errors.ts +632 -0
- package/src/file-serving/static-files.ts +581 -0
- package/src/index.ts +14 -0
- package/src/middleware/auth.ts +220 -0
- package/src/middleware/content_security_policy.ts +215 -0
- package/src/middleware/cors.ts +74 -0
- package/src/middleware/csrf.ts +108 -0
- package/src/middleware/ddos_protection.ts +255 -0
- package/src/middleware/file_security.ts +191 -0
- package/src/middleware/file_upload.ts +275 -0
- package/src/middleware/helmet.ts +268 -0
- package/src/middleware/index.ts +117 -0
- package/src/middleware/input_validation.ts +449 -0
- package/src/middleware/json_body.ts +37 -0
- package/src/middleware/performance_alerting.ts +538 -0
- package/src/middleware/performance_dashboard.ts +661 -0
- package/src/middleware/performance_monitor.ts +943 -0
- package/src/middleware/pipeline.ts +489 -0
- package/src/middleware/rate_limit.ts +245 -0
- package/src/middleware/request_id.ts +36 -0
- package/src/middleware/request_signing.ts +636 -0
- package/src/middleware/request_tracer.ts +636 -0
- package/src/middleware/response_cache.ts +743 -0
- package/src/middleware/security.ts +482 -0
- package/src/middleware/security_suite.ts +257 -0
- package/src/middleware/session.ts +91 -0
- package/src/model-binding/index.ts +18 -0
- package/src/model-binding/model-middleware.ts +425 -0
- package/src/model-binding/model-registry.ts +550 -0
- package/src/model-binding.ts +370 -0
- package/src/model-resolver-factory.ts +106 -0
- package/src/observability/correlation.ts +691 -0
- package/src/observability/health-checks.ts +729 -0
- package/src/observability/index.ts +184 -0
- package/src/observability/integration.ts +548 -0
- package/src/observability/metrics.ts +753 -0
- package/src/observability/tracing.ts +638 -0
- package/src/optimization/bun-utilities.ts +778 -0
- package/src/query-builder-integration.ts +137 -0
- package/src/request/context.ts +62 -0
- package/src/request/enhanced-request.ts +857 -0
- package/src/request/macros.ts +688 -0
- package/src/response/macros.ts +665 -0
- package/src/response/response-factory.ts +596 -0
- package/src/router/api-routes.ts +243 -0
- package/src/router/file-based-routing.ts +690 -0
- package/src/router/file-streaming.ts +383 -0
- package/src/router/fluent-router.ts +927 -0
- package/src/router/fluent-routing.ts +797 -0
- package/src/router/group-organization.ts +213 -0
- package/src/router/handler-resolver.ts +291 -0
- package/src/router/http-methods.ts +377 -0
- package/src/router/index.ts +187 -0
- package/src/router/middleware-groups.ts +222 -0
- package/src/router/middleware-integration.ts +399 -0
- package/src/router/middleware.ts +231 -0
- package/src/router/model-binding.ts +215 -0
- package/src/router/optimized-route-matching.ts +253 -0
- package/src/router/route-building.ts +221 -0
- package/src/router/route-compiler.ts +702 -0
- package/src/router/route-matching.ts +349 -0
- package/src/router/route-trie.ts +464 -0
- package/src/router/router.ts +1604 -0
- package/src/router/server.ts +462 -0
- package/src/router/validation-integration.ts +443 -0
- package/src/router/view-rendering.ts +233 -0
- package/src/router/websocket.ts +100 -0
- package/src/routing/route-caching.ts +402 -0
- package/src/routing/route-throttling.ts +469 -0
- package/src/routing/subdomain-routing.ts +492 -0
- package/src/session/database-store.ts +109 -0
- package/src/session/file-store.ts +148 -0
- package/src/session/index.ts +244 -0
- package/src/session/memory-store.ts +88 -0
- package/src/session/redis-store.ts +93 -0
- package/src/streaming/index.ts +17 -0
- package/src/streaming/sse-handler.ts +482 -0
- package/src/streaming/stream-handler.ts +552 -0
- package/src/testing/auth-testing.ts +446 -0
- package/src/testing/file-upload-testing.ts +543 -0
- package/src/testing/index.ts +10 -0
- package/src/testing/middleware-testing.ts +322 -0
- package/src/testing/model-binding-testing.ts +645 -0
- package/src/testing/performance-testing.ts +738 -0
- package/src/testing/test-client.ts +310 -0
- package/src/testing/test-request.ts +315 -0
- package/src/testing/test-response.ts +332 -0
- package/src/testing/types.ts +224 -0
- package/src/testing/websocket-testing.ts +590 -0
- package/src/types/controller-types.ts +385 -0
- package/src/types/core.ts +683 -0
- package/src/types/middleware-types.ts +419 -0
- package/src/types/request-response-augmentation.ts +489 -0
- package/src/types/route-inference.ts +357 -0
- package/src/types.ts +2067 -0
- package/src/url.ts +126 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/query-preservation.ts +201 -0
- package/src/utils.ts +327 -0
- package/src/validation/validator.ts +685 -0
- package/src/websocket/clustering.ts +762 -0
- package/CHANGELOG.md +0 -770
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Error Handling - Circuit Breaker Pattern
|
|
3
|
+
*
|
|
4
|
+
* Prevents cascading failures by monitoring external service health
|
|
5
|
+
* and temporarily blocking requests when services are failing
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ErrorContext } from './exceptions'
|
|
9
|
+
import { CircuitBreakerOpenException, ExternalServiceException, TimeoutException } from './exceptions'
|
|
10
|
+
|
|
11
|
+
export interface CircuitBreakerConfig {
|
|
12
|
+
name: string
|
|
13
|
+
failureThreshold: number
|
|
14
|
+
recoveryTimeout: number
|
|
15
|
+
timeout: number
|
|
16
|
+
monitoringPeriod: number
|
|
17
|
+
minimumRequests: number
|
|
18
|
+
errorThresholdPercentage: number
|
|
19
|
+
halfOpenMaxCalls: number
|
|
20
|
+
resetTimeout: number
|
|
21
|
+
onStateChange?: (state: CircuitBreakerState, name: string) => void
|
|
22
|
+
onFailure?: (error: Error, name: string) => void
|
|
23
|
+
onSuccess?: (name: string) => void
|
|
24
|
+
shouldTripOnError?: (error: Error) => boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type CircuitBreakerState = 'CLOSED' | 'OPEN' | 'HALF_OPEN'
|
|
28
|
+
|
|
29
|
+
export interface CircuitBreakerMetrics {
|
|
30
|
+
totalRequests: number
|
|
31
|
+
successfulRequests: number
|
|
32
|
+
failedRequests: number
|
|
33
|
+
timeouts: number
|
|
34
|
+
averageResponseTime: number
|
|
35
|
+
lastFailureTime?: Date
|
|
36
|
+
lastSuccessTime?: Date
|
|
37
|
+
state: CircuitBreakerState
|
|
38
|
+
nextAttemptTime?: Date
|
|
39
|
+
consecutiveFailures: number
|
|
40
|
+
consecutiveSuccesses: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CircuitBreakerCall<T> {
|
|
44
|
+
execute: () => Promise<T>
|
|
45
|
+
fallback?: () => Promise<T>
|
|
46
|
+
timeout?: number
|
|
47
|
+
retries?: number
|
|
48
|
+
context?: ErrorContext
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Circuit breaker implementation
|
|
53
|
+
*/
|
|
54
|
+
export class CircuitBreaker {
|
|
55
|
+
private config: CircuitBreakerConfig
|
|
56
|
+
private state: CircuitBreakerState = 'CLOSED'
|
|
57
|
+
private metrics: CircuitBreakerMetrics
|
|
58
|
+
private nextAttemptTime?: Date
|
|
59
|
+
private halfOpenCallCount = 0
|
|
60
|
+
private recentRequests: Array<{ timestamp: Date, success: boolean, responseTime: number }> = []
|
|
61
|
+
|
|
62
|
+
constructor(config: CircuitBreakerConfig) {
|
|
63
|
+
const defaults = {
|
|
64
|
+
failureThreshold: 5,
|
|
65
|
+
recoveryTimeout: 60000,
|
|
66
|
+
timeout: 30000,
|
|
67
|
+
monitoringPeriod: 60000,
|
|
68
|
+
minimumRequests: 10,
|
|
69
|
+
errorThresholdPercentage: 50,
|
|
70
|
+
halfOpenMaxCalls: 3,
|
|
71
|
+
resetTimeout: 60000,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.config = {
|
|
75
|
+
...defaults,
|
|
76
|
+
...config,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.metrics = {
|
|
80
|
+
totalRequests: 0,
|
|
81
|
+
successfulRequests: 0,
|
|
82
|
+
failedRequests: 0,
|
|
83
|
+
timeouts: 0,
|
|
84
|
+
averageResponseTime: 0,
|
|
85
|
+
state: this.state,
|
|
86
|
+
consecutiveFailures: 0,
|
|
87
|
+
consecutiveSuccesses: 0,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Clean up old requests periodically
|
|
91
|
+
setInterval(() => this.cleanupOldRequests(), this.config.monitoringPeriod / 4)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Execute a function with circuit breaker protection
|
|
96
|
+
*/
|
|
97
|
+
async execute<T>(call: CircuitBreakerCall<T>): Promise<T> {
|
|
98
|
+
// Check if circuit is open
|
|
99
|
+
if (this.state === 'OPEN') {
|
|
100
|
+
if (this.nextAttemptTime && new Date() < this.nextAttemptTime) {
|
|
101
|
+
// Circuit is still open, reject immediately
|
|
102
|
+
const error = new CircuitBreakerOpenException(
|
|
103
|
+
this.config.name,
|
|
104
|
+
this.nextAttemptTime,
|
|
105
|
+
call.context,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if (call.fallback) {
|
|
109
|
+
try {
|
|
110
|
+
return await call.fallback()
|
|
111
|
+
}
|
|
112
|
+
catch (fallbackError) {
|
|
113
|
+
console.error(fallbackError)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
throw error
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Time to try half-open
|
|
121
|
+
this.transitionToHalfOpen()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check half-open call limit
|
|
126
|
+
if (this.state === 'HALF_OPEN' && this.halfOpenCallCount >= this.config.halfOpenMaxCalls) {
|
|
127
|
+
const error = new CircuitBreakerOpenException(
|
|
128
|
+
this.config.name,
|
|
129
|
+
this.nextAttemptTime || new Date(Date.now() + this.config.recoveryTimeout),
|
|
130
|
+
call.context,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if (call.fallback) {
|
|
134
|
+
try {
|
|
135
|
+
return await call.fallback()
|
|
136
|
+
}
|
|
137
|
+
catch (fallbackError) {
|
|
138
|
+
console.error(fallbackError)
|
|
139
|
+
|
|
140
|
+
throw error
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
throw error
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const startTime = Date.now()
|
|
148
|
+
let result: T
|
|
149
|
+
let error: Error | null = null
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// Increment call count for half-open state
|
|
153
|
+
if (this.state === 'HALF_OPEN') {
|
|
154
|
+
this.halfOpenCallCount++
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Execute with timeout
|
|
158
|
+
const timeout = call.timeout || this.config.timeout
|
|
159
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
160
|
+
setTimeout(() => {
|
|
161
|
+
reject(new TimeoutException(`Circuit breaker timeout for ${this.config.name}`, timeout, call.context))
|
|
162
|
+
}, timeout)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
result = await Promise.race([call.execute(), timeoutPromise])
|
|
166
|
+
|
|
167
|
+
// Success
|
|
168
|
+
this.onSuccess(Date.now() - startTime)
|
|
169
|
+
return result
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
error = err as Error
|
|
173
|
+
this.onFailure(error, Date.now() - startTime)
|
|
174
|
+
|
|
175
|
+
// Try fallback if available
|
|
176
|
+
if (call.fallback) {
|
|
177
|
+
try {
|
|
178
|
+
return await call.fallback()
|
|
179
|
+
}
|
|
180
|
+
catch (fallbackError) {
|
|
181
|
+
console.error(fallbackError)
|
|
182
|
+
throw error
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
throw error
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get current circuit breaker state
|
|
193
|
+
*/
|
|
194
|
+
getState(): CircuitBreakerState {
|
|
195
|
+
return this.state
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get circuit breaker metrics
|
|
200
|
+
*/
|
|
201
|
+
getMetrics(): CircuitBreakerMetrics {
|
|
202
|
+
return {
|
|
203
|
+
...this.metrics,
|
|
204
|
+
state: this.state,
|
|
205
|
+
nextAttemptTime: this.nextAttemptTime,
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Force circuit breaker to open state
|
|
211
|
+
*/
|
|
212
|
+
forceOpen(): void {
|
|
213
|
+
this.transitionToOpen()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Force circuit breaker to closed state
|
|
218
|
+
*/
|
|
219
|
+
forceClosed(): void {
|
|
220
|
+
this.transitionToClosed()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Reset circuit breaker metrics
|
|
225
|
+
*/
|
|
226
|
+
reset(): void {
|
|
227
|
+
this.metrics = {
|
|
228
|
+
totalRequests: 0,
|
|
229
|
+
successfulRequests: 0,
|
|
230
|
+
failedRequests: 0,
|
|
231
|
+
timeouts: 0,
|
|
232
|
+
averageResponseTime: 0,
|
|
233
|
+
state: this.state,
|
|
234
|
+
consecutiveFailures: 0,
|
|
235
|
+
consecutiveSuccesses: 0,
|
|
236
|
+
}
|
|
237
|
+
this.recentRequests = []
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private onSuccess(responseTime: number): void {
|
|
241
|
+
this.metrics.totalRequests++
|
|
242
|
+
this.metrics.successfulRequests++
|
|
243
|
+
this.metrics.consecutiveSuccesses++
|
|
244
|
+
this.metrics.consecutiveFailures = 0
|
|
245
|
+
this.metrics.lastSuccessTime = new Date()
|
|
246
|
+
|
|
247
|
+
// Update average response time
|
|
248
|
+
this.updateAverageResponseTime(responseTime)
|
|
249
|
+
|
|
250
|
+
// Add to recent requests
|
|
251
|
+
this.recentRequests.push({
|
|
252
|
+
timestamp: new Date(),
|
|
253
|
+
success: true,
|
|
254
|
+
responseTime,
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// Handle state transitions
|
|
258
|
+
if (this.state === 'HALF_OPEN') {
|
|
259
|
+
if (this.metrics.consecutiveSuccesses >= this.config.halfOpenMaxCalls) {
|
|
260
|
+
this.transitionToClosed()
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (this.config.onSuccess) {
|
|
265
|
+
this.config.onSuccess(this.config.name)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private onFailure(error: Error, responseTime: number): void {
|
|
270
|
+
this.metrics.totalRequests++
|
|
271
|
+
this.metrics.failedRequests++
|
|
272
|
+
this.metrics.consecutiveFailures++
|
|
273
|
+
this.metrics.consecutiveSuccesses = 0
|
|
274
|
+
this.metrics.lastFailureTime = new Date()
|
|
275
|
+
|
|
276
|
+
if (error instanceof TimeoutException) {
|
|
277
|
+
this.metrics.timeouts++
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Update average response time
|
|
281
|
+
this.updateAverageResponseTime(responseTime)
|
|
282
|
+
|
|
283
|
+
// Add to recent requests
|
|
284
|
+
this.recentRequests.push({
|
|
285
|
+
timestamp: new Date(),
|
|
286
|
+
success: false,
|
|
287
|
+
responseTime,
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
// Check if we should trip the circuit breaker
|
|
291
|
+
if (this.shouldTrip(error)) {
|
|
292
|
+
if (this.state === 'CLOSED') {
|
|
293
|
+
this.transitionToOpen()
|
|
294
|
+
}
|
|
295
|
+
else if (this.state === 'HALF_OPEN') {
|
|
296
|
+
this.transitionToOpen()
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (this.config.onFailure) {
|
|
301
|
+
this.config.onFailure(error, this.config.name)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private shouldTrip(error: Error): boolean {
|
|
306
|
+
// Custom error check
|
|
307
|
+
if (this.config.shouldTripOnError && !this.config.shouldTripOnError(error)) {
|
|
308
|
+
return false
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Check consecutive failures threshold
|
|
312
|
+
if (this.metrics.consecutiveFailures >= this.config.failureThreshold) {
|
|
313
|
+
return true
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Check error rate over monitoring period
|
|
317
|
+
const recentRequests = this.getRecentRequests()
|
|
318
|
+
if (recentRequests.length >= this.config.minimumRequests) {
|
|
319
|
+
const failedRequests = recentRequests.filter(r => !r.success).length
|
|
320
|
+
const errorRate = (failedRequests / recentRequests.length) * 100
|
|
321
|
+
|
|
322
|
+
if (errorRate >= this.config.errorThresholdPercentage) {
|
|
323
|
+
return true
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return false
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private transitionToClosed(): void {
|
|
331
|
+
this.state = 'CLOSED'
|
|
332
|
+
this.nextAttemptTime = undefined
|
|
333
|
+
this.halfOpenCallCount = 0
|
|
334
|
+
this.metrics.state = this.state
|
|
335
|
+
|
|
336
|
+
if (this.config.onStateChange) {
|
|
337
|
+
this.config.onStateChange(this.state, this.config.name)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private transitionToOpen(): void {
|
|
342
|
+
this.state = 'OPEN'
|
|
343
|
+
this.nextAttemptTime = new Date(Date.now() + this.config.recoveryTimeout)
|
|
344
|
+
this.halfOpenCallCount = 0
|
|
345
|
+
this.metrics.state = this.state
|
|
346
|
+
this.metrics.nextAttemptTime = this.nextAttemptTime
|
|
347
|
+
|
|
348
|
+
if (this.config.onStateChange) {
|
|
349
|
+
this.config.onStateChange(this.state, this.config.name)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private transitionToHalfOpen(): void {
|
|
354
|
+
this.state = 'HALF_OPEN'
|
|
355
|
+
this.halfOpenCallCount = 0
|
|
356
|
+
this.metrics.state = this.state
|
|
357
|
+
|
|
358
|
+
if (this.config.onStateChange) {
|
|
359
|
+
this.config.onStateChange(this.state, this.config.name)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private updateAverageResponseTime(responseTime: number): void {
|
|
364
|
+
if (this.metrics.totalRequests === 1) {
|
|
365
|
+
this.metrics.averageResponseTime = responseTime
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
// Exponential moving average
|
|
369
|
+
const alpha = 0.1
|
|
370
|
+
this.metrics.averageResponseTime
|
|
371
|
+
= (alpha * responseTime) + ((1 - alpha) * this.metrics.averageResponseTime)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private getRecentRequests(): Array<{ timestamp: Date, success: boolean, responseTime: number }> {
|
|
376
|
+
const cutoff = new Date(Date.now() - this.config.monitoringPeriod)
|
|
377
|
+
return this.recentRequests.filter(r => r.timestamp > cutoff)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private cleanupOldRequests(): void {
|
|
381
|
+
const cutoff = new Date(Date.now() - this.config.monitoringPeriod)
|
|
382
|
+
this.recentRequests = this.recentRequests.filter(r => r.timestamp > cutoff)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Circuit breaker registry for managing multiple circuit breakers
|
|
388
|
+
*/
|
|
389
|
+
export class CircuitBreakerRegistry {
|
|
390
|
+
private breakers = new Map<string, CircuitBreaker>()
|
|
391
|
+
private configs = new Map<string, CircuitBreakerConfig>()
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Register a new circuit breaker
|
|
395
|
+
*/
|
|
396
|
+
register(config: CircuitBreakerConfig): CircuitBreaker {
|
|
397
|
+
const breaker = new CircuitBreaker(config)
|
|
398
|
+
this.breakers.set(config.name, breaker)
|
|
399
|
+
this.configs.set(config.name, config)
|
|
400
|
+
return breaker
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Get circuit breaker by name
|
|
405
|
+
*/
|
|
406
|
+
get(name: string): CircuitBreaker | undefined {
|
|
407
|
+
return this.breakers.get(name)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Execute function with circuit breaker protection
|
|
412
|
+
*/
|
|
413
|
+
async execute<T>(name: string, call: CircuitBreakerCall<T>): Promise<T> {
|
|
414
|
+
const breaker = this.breakers.get(name)
|
|
415
|
+
if (!breaker) {
|
|
416
|
+
throw new Error(`Circuit breaker '${name}' not found`)
|
|
417
|
+
}
|
|
418
|
+
return breaker.execute(call)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Get all circuit breaker metrics
|
|
423
|
+
*/
|
|
424
|
+
getAllMetrics(): Record<string, CircuitBreakerMetrics> {
|
|
425
|
+
const metrics: Record<string, CircuitBreakerMetrics> = {}
|
|
426
|
+
for (const [name, breaker] of this.breakers) {
|
|
427
|
+
metrics[name] = breaker.getMetrics()
|
|
428
|
+
}
|
|
429
|
+
return metrics
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Get circuit breakers by state
|
|
434
|
+
*/
|
|
435
|
+
getBreakersByState(state: CircuitBreakerState): string[] {
|
|
436
|
+
const names: string[] = []
|
|
437
|
+
for (const [name, breaker] of this.breakers) {
|
|
438
|
+
if (breaker.getState() === state) {
|
|
439
|
+
names.push(name)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return names
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Force all circuit breakers to closed state
|
|
447
|
+
*/
|
|
448
|
+
forceAllClosed(): void {
|
|
449
|
+
for (const breaker of this.breakers.values()) {
|
|
450
|
+
breaker.forceClosed()
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Reset all circuit breaker metrics
|
|
456
|
+
*/
|
|
457
|
+
resetAll(): void {
|
|
458
|
+
for (const breaker of this.breakers.values()) {
|
|
459
|
+
breaker.reset()
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Remove circuit breaker
|
|
465
|
+
*/
|
|
466
|
+
remove(name: string): boolean {
|
|
467
|
+
this.configs.delete(name)
|
|
468
|
+
return this.breakers.delete(name)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Get all registered circuit breaker names
|
|
473
|
+
*/
|
|
474
|
+
getNames(): string[] {
|
|
475
|
+
return Array.from(this.breakers.keys())
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Decorator for circuit breaker protection
|
|
481
|
+
*/
|
|
482
|
+
export function circuitBreaker(config: CircuitBreakerConfig): <T extends (...args: any[]) => Promise<any>>(
|
|
483
|
+
target: any,
|
|
484
|
+
propertyName: string,
|
|
485
|
+
descriptor: TypedPropertyDescriptor<T>,
|
|
486
|
+
) => TypedPropertyDescriptor<T> {
|
|
487
|
+
const breaker = new CircuitBreaker(config)
|
|
488
|
+
|
|
489
|
+
return function <T extends (...args: any[]) => Promise<any>>(
|
|
490
|
+
target: any,
|
|
491
|
+
propertyName: string,
|
|
492
|
+
descriptor: TypedPropertyDescriptor<T>,
|
|
493
|
+
): TypedPropertyDescriptor<T> {
|
|
494
|
+
const method = descriptor.value!
|
|
495
|
+
|
|
496
|
+
descriptor.value = async function (this: any, ...args: any[]) {
|
|
497
|
+
return breaker.execute({
|
|
498
|
+
execute: () => method.apply(this, args),
|
|
499
|
+
context: {
|
|
500
|
+
method: propertyName,
|
|
501
|
+
timestamp: new Date(),
|
|
502
|
+
},
|
|
503
|
+
})
|
|
504
|
+
} as T
|
|
505
|
+
|
|
506
|
+
return descriptor
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* HTTP client with circuit breaker protection
|
|
512
|
+
*/
|
|
513
|
+
export class CircuitBreakerHttpClient {
|
|
514
|
+
private registry: CircuitBreakerRegistry
|
|
515
|
+
|
|
516
|
+
constructor(registry?: CircuitBreakerRegistry) {
|
|
517
|
+
this.registry = registry || new CircuitBreakerRegistry()
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Make HTTP request with circuit breaker protection
|
|
522
|
+
*/
|
|
523
|
+
async fetch(
|
|
524
|
+
url: string,
|
|
525
|
+
options: RequestInit & {
|
|
526
|
+
circuitBreaker?: string | CircuitBreakerConfig
|
|
527
|
+
fallback?: () => Promise<Response>
|
|
528
|
+
context?: ErrorContext
|
|
529
|
+
} = {},
|
|
530
|
+
): Promise<Response> {
|
|
531
|
+
const { circuitBreaker, fallback, context, ...fetchOptions } = options
|
|
532
|
+
|
|
533
|
+
// Determine circuit breaker to use
|
|
534
|
+
let breakerName: string
|
|
535
|
+
if (typeof circuitBreaker === 'string') {
|
|
536
|
+
breakerName = circuitBreaker
|
|
537
|
+
}
|
|
538
|
+
else if (circuitBreaker) {
|
|
539
|
+
breakerName = circuitBreaker.name
|
|
540
|
+
if (!this.registry.get(breakerName)) {
|
|
541
|
+
this.registry.register(circuitBreaker)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
// Create default circuit breaker based on hostname
|
|
546
|
+
const hostname = new URL(url).hostname
|
|
547
|
+
breakerName = `http-${hostname}`
|
|
548
|
+
if (!this.registry.get(breakerName)) {
|
|
549
|
+
this.registry.register({
|
|
550
|
+
name: breakerName,
|
|
551
|
+
failureThreshold: 5,
|
|
552
|
+
recoveryTimeout: 60000,
|
|
553
|
+
timeout: 30000,
|
|
554
|
+
monitoringPeriod: 60000,
|
|
555
|
+
minimumRequests: 10,
|
|
556
|
+
errorThresholdPercentage: 50,
|
|
557
|
+
halfOpenMaxCalls: 3,
|
|
558
|
+
resetTimeout: 60000,
|
|
559
|
+
})
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return this.registry.execute(breakerName, {
|
|
564
|
+
execute: async () => {
|
|
565
|
+
const response = await fetch(url, fetchOptions)
|
|
566
|
+
|
|
567
|
+
// Consider 5xx status codes as failures
|
|
568
|
+
if (response.status >= 500) {
|
|
569
|
+
throw new ExternalServiceException(
|
|
570
|
+
`HTTP ${response.status}: ${response.statusText}`,
|
|
571
|
+
breakerName,
|
|
572
|
+
url,
|
|
573
|
+
response.status,
|
|
574
|
+
undefined,
|
|
575
|
+
context,
|
|
576
|
+
)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return response
|
|
580
|
+
},
|
|
581
|
+
fallback,
|
|
582
|
+
context,
|
|
583
|
+
})
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Get circuit breaker registry
|
|
588
|
+
*/
|
|
589
|
+
getRegistry(): CircuitBreakerRegistry {
|
|
590
|
+
return this.registry
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Middleware for circuit breaker protection
|
|
596
|
+
*/
|
|
597
|
+
export function createCircuitBreakerMiddleware(registry: CircuitBreakerRegistry) {
|
|
598
|
+
return async (req: any, next: () => Promise<Response>): Promise<Response> => {
|
|
599
|
+
const serviceName = req.context?.serviceName as string
|
|
600
|
+
if (!serviceName) {
|
|
601
|
+
return next()
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const breaker = registry.get(serviceName)
|
|
605
|
+
if (!breaker) {
|
|
606
|
+
return next()
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return breaker.execute({
|
|
610
|
+
execute: next,
|
|
611
|
+
context: {
|
|
612
|
+
requestId: req.requestId,
|
|
613
|
+
route: req.route,
|
|
614
|
+
method: req.method,
|
|
615
|
+
url: req.url,
|
|
616
|
+
},
|
|
617
|
+
})
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Factory functions for common circuit breaker configurations
|
|
623
|
+
*/
|
|
624
|
+
export const CircuitBreakerPresets = {
|
|
625
|
+
/**
|
|
626
|
+
* Fast-failing circuit breaker for critical services
|
|
627
|
+
*/
|
|
628
|
+
critical: (name: string): CircuitBreakerConfig => ({
|
|
629
|
+
name,
|
|
630
|
+
failureThreshold: 3,
|
|
631
|
+
recoveryTimeout: 30000,
|
|
632
|
+
timeout: 5000,
|
|
633
|
+
monitoringPeriod: 30000,
|
|
634
|
+
minimumRequests: 5,
|
|
635
|
+
errorThresholdPercentage: 30,
|
|
636
|
+
halfOpenMaxCalls: 2,
|
|
637
|
+
resetTimeout: 30000,
|
|
638
|
+
}),
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Standard circuit breaker for regular services
|
|
642
|
+
*/
|
|
643
|
+
standard: (name: string): CircuitBreakerConfig => ({
|
|
644
|
+
name,
|
|
645
|
+
failureThreshold: 5,
|
|
646
|
+
recoveryTimeout: 60000,
|
|
647
|
+
timeout: 30000,
|
|
648
|
+
monitoringPeriod: 60000,
|
|
649
|
+
minimumRequests: 10,
|
|
650
|
+
errorThresholdPercentage: 50,
|
|
651
|
+
halfOpenMaxCalls: 3,
|
|
652
|
+
resetTimeout: 60000,
|
|
653
|
+
}),
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Tolerant circuit breaker for non-critical services
|
|
657
|
+
*/
|
|
658
|
+
tolerant: (name: string): CircuitBreakerConfig => ({
|
|
659
|
+
name,
|
|
660
|
+
failureThreshold: 10,
|
|
661
|
+
recoveryTimeout: 120000,
|
|
662
|
+
timeout: 60000,
|
|
663
|
+
monitoringPeriod: 120000,
|
|
664
|
+
minimumRequests: 20,
|
|
665
|
+
errorThresholdPercentage: 70,
|
|
666
|
+
halfOpenMaxCalls: 5,
|
|
667
|
+
resetTimeout: 120000,
|
|
668
|
+
}),
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Database circuit breaker with longer timeouts
|
|
672
|
+
*/
|
|
673
|
+
database: (name: string): CircuitBreakerConfig => ({
|
|
674
|
+
name,
|
|
675
|
+
failureThreshold: 3,
|
|
676
|
+
recoveryTimeout: 60000,
|
|
677
|
+
timeout: 10000,
|
|
678
|
+
monitoringPeriod: 60000,
|
|
679
|
+
minimumRequests: 5,
|
|
680
|
+
errorThresholdPercentage: 40,
|
|
681
|
+
halfOpenMaxCalls: 2,
|
|
682
|
+
resetTimeout: 60000,
|
|
683
|
+
shouldTripOnError: (error) => {
|
|
684
|
+
// Don't trip on validation errors or client errors
|
|
685
|
+
return !(error.message.includes('validation') || error.message.includes('constraint'))
|
|
686
|
+
},
|
|
687
|
+
}),
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* External API circuit breaker
|
|
691
|
+
*/
|
|
692
|
+
externalApi: (name: string): CircuitBreakerConfig => ({
|
|
693
|
+
name,
|
|
694
|
+
failureThreshold: 5,
|
|
695
|
+
recoveryTimeout: 90000,
|
|
696
|
+
timeout: 15000,
|
|
697
|
+
monitoringPeriod: 90000,
|
|
698
|
+
minimumRequests: 8,
|
|
699
|
+
errorThresholdPercentage: 60,
|
|
700
|
+
halfOpenMaxCalls: 3,
|
|
701
|
+
resetTimeout: 90000,
|
|
702
|
+
shouldTripOnError: (error) => {
|
|
703
|
+
// Don't trip on 4xx client errors, only 5xx server errors
|
|
704
|
+
if (error instanceof ExternalServiceException) {
|
|
705
|
+
return error.statusCode >= 500
|
|
706
|
+
}
|
|
707
|
+
return true
|
|
708
|
+
},
|
|
709
|
+
}),
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Global circuit breaker registry instance
|
|
714
|
+
*/
|
|
715
|
+
export const globalCircuitBreakerRegistry: CircuitBreakerRegistry = new CircuitBreakerRegistry()
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Convenience function to create and register a circuit breaker
|
|
719
|
+
*/
|
|
720
|
+
export function createCircuitBreaker(config: CircuitBreakerConfig): CircuitBreaker {
|
|
721
|
+
return globalCircuitBreakerRegistry.register(config)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Convenience function to execute with circuit breaker protection
|
|
726
|
+
*/
|
|
727
|
+
export async function withCircuitBreaker<T>(
|
|
728
|
+
name: string,
|
|
729
|
+
call: CircuitBreakerCall<T>,
|
|
730
|
+
): Promise<T> {
|
|
731
|
+
return globalCircuitBreakerRegistry.execute(name, call)
|
|
732
|
+
}
|