@hazeljs/resilience 0.2.0-beta.41
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/LICENSE +192 -0
- package/README.md +95 -0
- package/dist/__tests__/bulkhead-timeout.test.d.ts +2 -0
- package/dist/__tests__/bulkhead-timeout.test.d.ts.map +1 -0
- package/dist/__tests__/bulkhead-timeout.test.js +74 -0
- package/dist/__tests__/circuit-breaker.test.d.ts +2 -0
- package/dist/__tests__/circuit-breaker.test.d.ts.map +1 -0
- package/dist/__tests__/circuit-breaker.test.js +160 -0
- package/dist/__tests__/decorators.test.d.ts +2 -0
- package/dist/__tests__/decorators.test.d.ts.map +1 -0
- package/dist/__tests__/decorators.test.js +288 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.js +50 -0
- package/dist/__tests__/metrics.test.d.ts +2 -0
- package/dist/__tests__/metrics.test.d.ts.map +1 -0
- package/dist/__tests__/metrics.test.js +83 -0
- package/dist/__tests__/rate-limiter.test.d.ts +2 -0
- package/dist/__tests__/rate-limiter.test.d.ts.map +1 -0
- package/dist/__tests__/rate-limiter.test.js +143 -0
- package/dist/__tests__/retry-policy.test.d.ts +2 -0
- package/dist/__tests__/retry-policy.test.d.ts.map +1 -0
- package/dist/__tests__/retry-policy.test.js +84 -0
- package/dist/__tests__/sliding-window.test.d.ts +2 -0
- package/dist/__tests__/sliding-window.test.d.ts.map +1 -0
- package/dist/__tests__/sliding-window.test.js +93 -0
- package/dist/bulkhead/bulkhead.d.ts +34 -0
- package/dist/bulkhead/bulkhead.d.ts.map +1 -0
- package/dist/bulkhead/bulkhead.js +97 -0
- package/dist/circuit-breaker/circuit-breaker-registry.d.ts +38 -0
- package/dist/circuit-breaker/circuit-breaker-registry.d.ts.map +1 -0
- package/dist/circuit-breaker/circuit-breaker-registry.js +61 -0
- package/dist/circuit-breaker/circuit-breaker.d.ts +51 -0
- package/dist/circuit-breaker/circuit-breaker.d.ts.map +1 -0
- package/dist/circuit-breaker/circuit-breaker.js +182 -0
- package/dist/circuit-breaker/sliding-window.d.ts +49 -0
- package/dist/circuit-breaker/sliding-window.d.ts.map +1 -0
- package/dist/circuit-breaker/sliding-window.js +89 -0
- package/dist/decorators/index.d.ts +51 -0
- package/dist/decorators/index.d.ts.map +1 -0
- package/dist/decorators/index.js +133 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/metrics/metrics-collector.d.ts +69 -0
- package/dist/metrics/metrics-collector.d.ts.map +1 -0
- package/dist/metrics/metrics-collector.js +180 -0
- package/dist/rate-limiter/rate-limiter.d.ts +72 -0
- package/dist/rate-limiter/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter/rate-limiter.js +147 -0
- package/dist/retry/retry-policy.d.ts +19 -0
- package/dist/retry/retry-policy.d.ts.map +1 -0
- package/dist/retry/retry-policy.js +87 -0
- package/dist/timeout/timeout.d.ts +23 -0
- package/dist/timeout/timeout.d.ts.map +1 -0
- package/dist/timeout/timeout.js +55 -0
- package/dist/types/index.d.ts +135 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +61 -0
- package/package.json +63 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/rate-limiter/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,iBAAiB,EAAkB,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAElF;;;GAGG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAS;gBAEf,QAAQ,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM;IAOzD;;OAEG;IACH,UAAU,IAAI,OAAO;IASrB;;OAEG;IACH,eAAe,IAAI,MAAM;IAOzB,OAAO,CAAC,MAAM;CAMf;AAED;;;GAGG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,WAAW,CAAS;gBAEhB,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;IAOzC;;OAEG;IACH,UAAU,IAAI,OAAO;IAYrB;;OAEG;IACH,eAAe,IAAI,MAAM;IAOzB,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,KAAK;CAQd;AAED;;GAEG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAA4C;IAC3D,OAAO,CAAC,QAAQ,CAAsB;gBAE1B,MAAM,EAAE,iBAAiB;IAWrC;;OAEG;IACG,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAQlD;;OAEG;IACH,UAAU,IAAI,OAAO;IAIrB;;OAEG;IACH,eAAe,IAAI,MAAM;IAIzB;;OAEG;IACH,WAAW,IAAI,mBAAmB;CAGnC"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Rate Limiter
|
|
4
|
+
* Token bucket and sliding window implementations for rate limiting.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.RateLimiter = exports.SlidingWindowLimiter = exports.TokenBucketLimiter = void 0;
|
|
8
|
+
const types_1 = require("../types");
|
|
9
|
+
/**
|
|
10
|
+
* Token Bucket Rate Limiter
|
|
11
|
+
* Allows bursts up to the bucket capacity and refills at a steady rate.
|
|
12
|
+
*/
|
|
13
|
+
class TokenBucketLimiter {
|
|
14
|
+
constructor(capacity, refillRatePerSecond) {
|
|
15
|
+
this.capacity = capacity;
|
|
16
|
+
this.tokens = capacity;
|
|
17
|
+
this.refillRate = refillRatePerSecond / 1000;
|
|
18
|
+
this.lastRefill = Date.now();
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Try to consume a token. Returns true if allowed.
|
|
22
|
+
*/
|
|
23
|
+
tryAcquire() {
|
|
24
|
+
this.refill();
|
|
25
|
+
if (this.tokens >= 1) {
|
|
26
|
+
this.tokens -= 1;
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get time in ms until the next token is available
|
|
33
|
+
*/
|
|
34
|
+
getRetryAfterMs() {
|
|
35
|
+
this.refill();
|
|
36
|
+
if (this.tokens >= 1)
|
|
37
|
+
return 0;
|
|
38
|
+
const needed = 1 - this.tokens;
|
|
39
|
+
return Math.ceil(needed / this.refillRate);
|
|
40
|
+
}
|
|
41
|
+
refill() {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
const elapsed = now - this.lastRefill;
|
|
44
|
+
this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate);
|
|
45
|
+
this.lastRefill = now;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.TokenBucketLimiter = TokenBucketLimiter;
|
|
49
|
+
/**
|
|
50
|
+
* Sliding Window Counter Rate Limiter
|
|
51
|
+
* Tracks request counts in small sub-windows for more accurate rate limiting.
|
|
52
|
+
*/
|
|
53
|
+
class SlidingWindowLimiter {
|
|
54
|
+
constructor(max, windowMs) {
|
|
55
|
+
this.windows = new Map();
|
|
56
|
+
this.max = max;
|
|
57
|
+
this.windowMs = windowMs;
|
|
58
|
+
// Use 10 sub-windows for granularity
|
|
59
|
+
this.subWindowMs = Math.max(1, Math.floor(windowMs / 10));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Try to record a request. Returns true if within limit.
|
|
63
|
+
*/
|
|
64
|
+
tryAcquire() {
|
|
65
|
+
this.evict();
|
|
66
|
+
const count = this.getCurrentCount();
|
|
67
|
+
if (count >= this.max) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const subKey = this.getCurrentSubKey();
|
|
71
|
+
this.windows.set(subKey, (this.windows.get(subKey) || 0) + 1);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get time in ms until a slot opens up
|
|
76
|
+
*/
|
|
77
|
+
getRetryAfterMs() {
|
|
78
|
+
this.evict();
|
|
79
|
+
if (this.getCurrentCount() < this.max)
|
|
80
|
+
return 0;
|
|
81
|
+
// Earliest sub-window will expire after subWindowMs
|
|
82
|
+
return this.subWindowMs;
|
|
83
|
+
}
|
|
84
|
+
getCurrentCount() {
|
|
85
|
+
let total = 0;
|
|
86
|
+
for (const count of this.windows.values()) {
|
|
87
|
+
total += count;
|
|
88
|
+
}
|
|
89
|
+
return total;
|
|
90
|
+
}
|
|
91
|
+
getCurrentSubKey() {
|
|
92
|
+
return Math.floor(Date.now() / this.subWindowMs);
|
|
93
|
+
}
|
|
94
|
+
evict() {
|
|
95
|
+
const cutoffKey = Math.floor((Date.now() - this.windowMs) / this.subWindowMs);
|
|
96
|
+
for (const key of this.windows.keys()) {
|
|
97
|
+
if (key <= cutoffKey) {
|
|
98
|
+
this.windows.delete(key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.SlidingWindowLimiter = SlidingWindowLimiter;
|
|
104
|
+
/**
|
|
105
|
+
* Unified RateLimiter that wraps the configured strategy
|
|
106
|
+
*/
|
|
107
|
+
class RateLimiter {
|
|
108
|
+
constructor(config) {
|
|
109
|
+
this.strategy = config.strategy;
|
|
110
|
+
if (config.strategy === 'token-bucket') {
|
|
111
|
+
const refillRate = config.refillRate ?? config.max / (config.window / 1000);
|
|
112
|
+
this.limiter = new TokenBucketLimiter(config.max, refillRate);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
this.limiter = new SlidingWindowLimiter(config.max, config.window);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Execute a function within rate limit constraints
|
|
120
|
+
*/
|
|
121
|
+
async execute(fn) {
|
|
122
|
+
if (!this.limiter.tryAcquire()) {
|
|
123
|
+
const retryAfter = this.limiter.getRetryAfterMs();
|
|
124
|
+
throw new types_1.RateLimitError(`Rate limit exceeded. Retry after ${retryAfter}ms`, retryAfter);
|
|
125
|
+
}
|
|
126
|
+
return fn();
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Try to acquire permission (consumes a token/slot)
|
|
130
|
+
*/
|
|
131
|
+
tryAcquire() {
|
|
132
|
+
return this.limiter.tryAcquire();
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get time in ms until the next request is allowed
|
|
136
|
+
*/
|
|
137
|
+
getRetryAfterMs() {
|
|
138
|
+
return this.limiter.getRetryAfterMs();
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Get the strategy in use
|
|
142
|
+
*/
|
|
143
|
+
getStrategy() {
|
|
144
|
+
return this.strategy;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
exports.RateLimiter = RateLimiter;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry Policy
|
|
3
|
+
* Configurable retry with exponential backoff, jitter, and retryable-error predicates.
|
|
4
|
+
*/
|
|
5
|
+
import { RetryConfig } from '../types';
|
|
6
|
+
export declare class RetryPolicy {
|
|
7
|
+
private config;
|
|
8
|
+
constructor(config?: Partial<RetryConfig>);
|
|
9
|
+
/**
|
|
10
|
+
* Execute a function with retry logic
|
|
11
|
+
*/
|
|
12
|
+
execute<T>(fn: () => Promise<T>): Promise<T>;
|
|
13
|
+
/**
|
|
14
|
+
* Get the configured max attempts
|
|
15
|
+
*/
|
|
16
|
+
getMaxAttempts(): number;
|
|
17
|
+
private sleep;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=retry-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry-policy.d.ts","sourceRoot":"","sources":["../../src/retry/retry-policy.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAwC,MAAM,UAAU,CAAC;AAoD7E,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAwB;gBAE1B,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM;IAI7C;;OAEG;IACG,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IA2ClD;;OAEG;IACH,cAAc,IAAI,MAAM;IAIxB,OAAO,CAAC,KAAK;CAGd"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Retry Policy
|
|
4
|
+
* Configurable retry with exponential backoff, jitter, and retryable-error predicates.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.RetryPolicy = void 0;
|
|
8
|
+
const types_1 = require("../types");
|
|
9
|
+
const DEFAULT_CONFIG = {
|
|
10
|
+
maxAttempts: 3,
|
|
11
|
+
backoff: 'exponential',
|
|
12
|
+
baseDelay: 1000,
|
|
13
|
+
maxDelay: 30000,
|
|
14
|
+
jitter: true,
|
|
15
|
+
retryPredicate: undefined,
|
|
16
|
+
onRetry: undefined,
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Calculate delay based on backoff strategy
|
|
20
|
+
*/
|
|
21
|
+
function calculateDelay(strategy, attempt, baseDelay, maxDelay, jitter) {
|
|
22
|
+
let delay;
|
|
23
|
+
switch (strategy) {
|
|
24
|
+
case 'fixed':
|
|
25
|
+
delay = baseDelay;
|
|
26
|
+
break;
|
|
27
|
+
case 'linear':
|
|
28
|
+
delay = baseDelay * attempt;
|
|
29
|
+
break;
|
|
30
|
+
case 'exponential':
|
|
31
|
+
delay = baseDelay * Math.pow(2, attempt - 1);
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
delay = baseDelay;
|
|
35
|
+
}
|
|
36
|
+
// Cap at max delay
|
|
37
|
+
delay = Math.min(delay, maxDelay);
|
|
38
|
+
// Add jitter (random value between 0 and delay)
|
|
39
|
+
if (jitter) {
|
|
40
|
+
delay = delay * (0.5 + Math.random() * 0.5);
|
|
41
|
+
}
|
|
42
|
+
return Math.floor(delay);
|
|
43
|
+
}
|
|
44
|
+
class RetryPolicy {
|
|
45
|
+
constructor(config = {}) {
|
|
46
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Execute a function with retry logic
|
|
50
|
+
*/
|
|
51
|
+
async execute(fn) {
|
|
52
|
+
let lastError;
|
|
53
|
+
// Attempt 0 is the initial call, attempts 1..maxAttempts are retries
|
|
54
|
+
for (let attempt = 0; attempt <= this.config.maxAttempts; attempt++) {
|
|
55
|
+
try {
|
|
56
|
+
return await fn();
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
lastError = error;
|
|
60
|
+
// Check if this error is retryable
|
|
61
|
+
if (this.config.retryPredicate && !this.config.retryPredicate(error)) {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
// If we've exhausted all retries, throw
|
|
65
|
+
if (attempt >= this.config.maxAttempts) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
// Notify retry listener
|
|
69
|
+
this.config.onRetry?.(error, attempt + 1);
|
|
70
|
+
// Wait before next attempt
|
|
71
|
+
const delay = calculateDelay(this.config.backoff, attempt + 1, this.config.baseDelay, this.config.maxDelay, this.config.jitter);
|
|
72
|
+
await this.sleep(delay);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
throw new types_1.RetryExhaustedError(`Retry exhausted after ${this.config.maxAttempts} attempts`, this.config.maxAttempts, lastError);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get the configured max attempts
|
|
79
|
+
*/
|
|
80
|
+
getMaxAttempts() {
|
|
81
|
+
return this.config.maxAttempts;
|
|
82
|
+
}
|
|
83
|
+
sleep(ms) {
|
|
84
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.RetryPolicy = RetryPolicy;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeout
|
|
3
|
+
* Promise-based timeout wrapper with cancellation support.
|
|
4
|
+
*/
|
|
5
|
+
import { TimeoutConfig } from '../types';
|
|
6
|
+
export declare class Timeout {
|
|
7
|
+
private durationMs;
|
|
8
|
+
private message;
|
|
9
|
+
constructor(config: TimeoutConfig | number);
|
|
10
|
+
/**
|
|
11
|
+
* Execute a function with a timeout
|
|
12
|
+
*/
|
|
13
|
+
execute<T>(fn: () => Promise<T>): Promise<T>;
|
|
14
|
+
/**
|
|
15
|
+
* Get the configured duration
|
|
16
|
+
*/
|
|
17
|
+
getDuration(): number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Convenience function to wrap a promise with a timeout
|
|
21
|
+
*/
|
|
22
|
+
export declare function withTimeout<T>(fn: () => Promise<T>, durationMs: number, message?: string): Promise<T>;
|
|
23
|
+
//# sourceMappingURL=timeout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeout.d.ts","sourceRoot":"","sources":["../../src/timeout/timeout.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,aAAa,EAAgB,MAAM,UAAU,CAAC;AAEvD,qBAAa,OAAO;IAClB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,OAAO,CAAS;gBAEZ,MAAM,EAAE,aAAa,GAAG,MAAM;IAU1C;;OAEG;IACG,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAmBlD;;OAEG;IACH,WAAW,IAAI,MAAM;CAGtB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,CAAC,EACjC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,CAAC,CAAC,CAGZ"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Timeout
|
|
4
|
+
* Promise-based timeout wrapper with cancellation support.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.Timeout = void 0;
|
|
8
|
+
exports.withTimeout = withTimeout;
|
|
9
|
+
const types_1 = require("../types");
|
|
10
|
+
class Timeout {
|
|
11
|
+
constructor(config) {
|
|
12
|
+
if (typeof config === 'number') {
|
|
13
|
+
this.durationMs = config;
|
|
14
|
+
this.message = 'Operation timed out';
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
this.durationMs = config.duration;
|
|
18
|
+
this.message = config.message || 'Operation timed out';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Execute a function with a timeout
|
|
23
|
+
*/
|
|
24
|
+
async execute(fn) {
|
|
25
|
+
let timeoutId;
|
|
26
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
27
|
+
timeoutId = setTimeout(() => {
|
|
28
|
+
reject(new types_1.TimeoutError(`${this.message} (after ${this.durationMs}ms)`));
|
|
29
|
+
}, this.durationMs);
|
|
30
|
+
});
|
|
31
|
+
try {
|
|
32
|
+
const result = await Promise.race([fn(), timeoutPromise]);
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
if (timeoutId) {
|
|
37
|
+
clearTimeout(timeoutId);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get the configured duration
|
|
43
|
+
*/
|
|
44
|
+
getDuration() {
|
|
45
|
+
return this.durationMs;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.Timeout = Timeout;
|
|
49
|
+
/**
|
|
50
|
+
* Convenience function to wrap a promise with a timeout
|
|
51
|
+
*/
|
|
52
|
+
async function withTimeout(fn, durationMs, message) {
|
|
53
|
+
const timeout = new Timeout({ duration: durationMs, message });
|
|
54
|
+
return timeout.execute(fn);
|
|
55
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hazeljs/resilience - Type Definitions
|
|
3
|
+
*/
|
|
4
|
+
export declare enum CircuitState {
|
|
5
|
+
CLOSED = "CLOSED",
|
|
6
|
+
OPEN = "OPEN",
|
|
7
|
+
HALF_OPEN = "HALF_OPEN"
|
|
8
|
+
}
|
|
9
|
+
export interface SlidingWindowConfig {
|
|
10
|
+
/** 'count' uses last N calls; 'time' uses a rolling time window */
|
|
11
|
+
type: 'count' | 'time';
|
|
12
|
+
/** For 'count': number of calls. For 'time': window duration in ms */
|
|
13
|
+
size: number;
|
|
14
|
+
}
|
|
15
|
+
export interface CircuitBreakerConfig {
|
|
16
|
+
/** Number of failures before opening the circuit */
|
|
17
|
+
failureThreshold: number;
|
|
18
|
+
/** Number of successes in HALF_OPEN before closing */
|
|
19
|
+
successThreshold: number;
|
|
20
|
+
/** Max time a single call may take before being considered failed (ms) */
|
|
21
|
+
timeout: number;
|
|
22
|
+
/** Time to wait in OPEN before transitioning to HALF_OPEN (ms) */
|
|
23
|
+
resetTimeout: number;
|
|
24
|
+
/** Sliding window configuration for failure rate calculation */
|
|
25
|
+
slidingWindow?: SlidingWindowConfig;
|
|
26
|
+
/** Custom predicate to determine if an error should count as a failure */
|
|
27
|
+
failurePredicate?: (error: unknown) => boolean;
|
|
28
|
+
/** Name of the fallback method on the same class */
|
|
29
|
+
fallback?: string;
|
|
30
|
+
/** Callback when state changes */
|
|
31
|
+
onStateChange?: (from: CircuitState, to: CircuitState) => void;
|
|
32
|
+
}
|
|
33
|
+
export interface CircuitBreakerMetrics {
|
|
34
|
+
totalRequests: number;
|
|
35
|
+
successCount: number;
|
|
36
|
+
failureCount: number;
|
|
37
|
+
failureRate: number;
|
|
38
|
+
state: CircuitState;
|
|
39
|
+
lastFailureTime?: number;
|
|
40
|
+
lastSuccessTime?: number;
|
|
41
|
+
averageResponseTime: number;
|
|
42
|
+
p99ResponseTime: number;
|
|
43
|
+
}
|
|
44
|
+
export type BackoffStrategy = 'fixed' | 'exponential' | 'linear';
|
|
45
|
+
export interface RetryConfig {
|
|
46
|
+
/** Maximum number of retry attempts (not counting the initial call) */
|
|
47
|
+
maxAttempts: number;
|
|
48
|
+
/** Backoff strategy between retries */
|
|
49
|
+
backoff: BackoffStrategy;
|
|
50
|
+
/** Base delay in ms */
|
|
51
|
+
baseDelay: number;
|
|
52
|
+
/** Maximum delay between retries in ms */
|
|
53
|
+
maxDelay?: number;
|
|
54
|
+
/** Whether to add jitter to delay to avoid thundering herd */
|
|
55
|
+
jitter?: boolean;
|
|
56
|
+
/** Custom predicate to decide if an error is retryable */
|
|
57
|
+
retryPredicate?: (error: unknown) => boolean;
|
|
58
|
+
/** Called before each retry attempt */
|
|
59
|
+
onRetry?: (error: unknown, attempt: number) => void;
|
|
60
|
+
}
|
|
61
|
+
export interface TimeoutConfig {
|
|
62
|
+
/** Timeout duration in ms */
|
|
63
|
+
duration: number;
|
|
64
|
+
/** Custom error message */
|
|
65
|
+
message?: string;
|
|
66
|
+
}
|
|
67
|
+
export interface BulkheadConfig {
|
|
68
|
+
/** Maximum concurrent executions */
|
|
69
|
+
maxConcurrent: number;
|
|
70
|
+
/** Maximum number of calls waiting in queue */
|
|
71
|
+
maxQueue: number;
|
|
72
|
+
/** Time a call can wait in the queue before being rejected (ms) */
|
|
73
|
+
queueTimeout?: number;
|
|
74
|
+
}
|
|
75
|
+
export interface BulkheadMetrics {
|
|
76
|
+
activeCalls: number;
|
|
77
|
+
queueLength: number;
|
|
78
|
+
maxConcurrent: number;
|
|
79
|
+
maxQueue: number;
|
|
80
|
+
rejectedCount: number;
|
|
81
|
+
}
|
|
82
|
+
export type RateLimiterStrategy = 'token-bucket' | 'sliding-window';
|
|
83
|
+
export interface RateLimiterConfig {
|
|
84
|
+
/** Strategy to use */
|
|
85
|
+
strategy: RateLimiterStrategy;
|
|
86
|
+
/** Maximum number of requests allowed in the window */
|
|
87
|
+
max: number;
|
|
88
|
+
/** Time window in ms (e.g. 60000 for 1 minute) */
|
|
89
|
+
window: number;
|
|
90
|
+
/** For token-bucket: refill rate (tokens per second). Defaults to max/window. */
|
|
91
|
+
refillRate?: number;
|
|
92
|
+
}
|
|
93
|
+
export interface MetricsSnapshot {
|
|
94
|
+
totalCalls: number;
|
|
95
|
+
successCalls: number;
|
|
96
|
+
failureCalls: number;
|
|
97
|
+
failureRate: number;
|
|
98
|
+
averageResponseTime: number;
|
|
99
|
+
p50ResponseTime: number;
|
|
100
|
+
p95ResponseTime: number;
|
|
101
|
+
p99ResponseTime: number;
|
|
102
|
+
minResponseTime: number;
|
|
103
|
+
maxResponseTime: number;
|
|
104
|
+
lastCallTime?: number;
|
|
105
|
+
}
|
|
106
|
+
export interface MetricsEntry {
|
|
107
|
+
timestamp: number;
|
|
108
|
+
duration: number;
|
|
109
|
+
success: boolean;
|
|
110
|
+
error?: string;
|
|
111
|
+
}
|
|
112
|
+
export declare class ResilienceError extends Error {
|
|
113
|
+
readonly code: string;
|
|
114
|
+
constructor(message: string, code: string);
|
|
115
|
+
}
|
|
116
|
+
export declare class CircuitBreakerError extends ResilienceError {
|
|
117
|
+
readonly state: CircuitState;
|
|
118
|
+
constructor(message: string, state: CircuitState);
|
|
119
|
+
}
|
|
120
|
+
export declare class TimeoutError extends ResilienceError {
|
|
121
|
+
constructor(message?: string);
|
|
122
|
+
}
|
|
123
|
+
export declare class BulkheadError extends ResilienceError {
|
|
124
|
+
constructor(message?: string);
|
|
125
|
+
}
|
|
126
|
+
export declare class RateLimitError extends ResilienceError {
|
|
127
|
+
readonly retryAfterMs?: number | undefined;
|
|
128
|
+
constructor(message?: string, retryAfterMs?: number | undefined);
|
|
129
|
+
}
|
|
130
|
+
export declare class RetryExhaustedError extends ResilienceError {
|
|
131
|
+
readonly attempts: number;
|
|
132
|
+
readonly lastError: unknown;
|
|
133
|
+
constructor(message: string, attempts: number, lastError: unknown);
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @hazeljs/resilience - Type Definitions
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RetryExhaustedError = exports.RateLimitError = exports.BulkheadError = exports.TimeoutError = exports.CircuitBreakerError = exports.ResilienceError = exports.CircuitState = void 0;
|
|
7
|
+
// ─── Circuit Breaker ───
|
|
8
|
+
var CircuitState;
|
|
9
|
+
(function (CircuitState) {
|
|
10
|
+
CircuitState["CLOSED"] = "CLOSED";
|
|
11
|
+
CircuitState["OPEN"] = "OPEN";
|
|
12
|
+
CircuitState["HALF_OPEN"] = "HALF_OPEN";
|
|
13
|
+
})(CircuitState || (exports.CircuitState = CircuitState = {}));
|
|
14
|
+
// ─── Common ───
|
|
15
|
+
class ResilienceError extends Error {
|
|
16
|
+
constructor(message, code) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.name = 'ResilienceError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.ResilienceError = ResilienceError;
|
|
23
|
+
class CircuitBreakerError extends ResilienceError {
|
|
24
|
+
constructor(message, state) {
|
|
25
|
+
super(message, 'CIRCUIT_OPEN');
|
|
26
|
+
this.state = state;
|
|
27
|
+
this.name = 'CircuitBreakerError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.CircuitBreakerError = CircuitBreakerError;
|
|
31
|
+
class TimeoutError extends ResilienceError {
|
|
32
|
+
constructor(message = 'Operation timed out') {
|
|
33
|
+
super(message, 'TIMEOUT');
|
|
34
|
+
this.name = 'TimeoutError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.TimeoutError = TimeoutError;
|
|
38
|
+
class BulkheadError extends ResilienceError {
|
|
39
|
+
constructor(message = 'Bulkhead capacity exceeded') {
|
|
40
|
+
super(message, 'BULKHEAD_FULL');
|
|
41
|
+
this.name = 'BulkheadError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.BulkheadError = BulkheadError;
|
|
45
|
+
class RateLimitError extends ResilienceError {
|
|
46
|
+
constructor(message = 'Rate limit exceeded', retryAfterMs) {
|
|
47
|
+
super(message, 'RATE_LIMITED');
|
|
48
|
+
this.retryAfterMs = retryAfterMs;
|
|
49
|
+
this.name = 'RateLimitError';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.RateLimitError = RateLimitError;
|
|
53
|
+
class RetryExhaustedError extends ResilienceError {
|
|
54
|
+
constructor(message, attempts, lastError) {
|
|
55
|
+
super(message, 'RETRY_EXHAUSTED');
|
|
56
|
+
this.attempts = attempts;
|
|
57
|
+
this.lastError = lastError;
|
|
58
|
+
this.name = 'RetryExhaustedError';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.RetryExhaustedError = RetryExhaustedError;
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hazeljs/resilience",
|
|
3
|
+
"version": "0.2.0-beta.41",
|
|
4
|
+
"description": "Fault-tolerance and resilience patterns for HazelJS - Circuit Breaker, Retry, Bulkhead, Timeout, Rate Limiter",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"test": "jest --coverage --passWithNoTests",
|
|
13
|
+
"test:ci": "jest --coverage --coverageReporters=text --coverageReporters=lcov --coverageReporters=clover --no-coverage-threshold",
|
|
14
|
+
"test:watch": "jest --watch",
|
|
15
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
16
|
+
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
|
17
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
18
|
+
"clean": "rm -rf dist"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@hazeljs/core": "^0.2.0-beta.41",
|
|
22
|
+
"reflect-metadata": "^0.2.2"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/jest": "^29.5.14",
|
|
26
|
+
"@types/node": "^20.17.50",
|
|
27
|
+
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
|
28
|
+
"@typescript-eslint/parser": "^8.18.2",
|
|
29
|
+
"eslint": "^8.56.0",
|
|
30
|
+
"eslint-config-prettier": "^9.1.0",
|
|
31
|
+
"eslint-plugin-prettier": "^5.1.3",
|
|
32
|
+
"jest": "^29.7.0",
|
|
33
|
+
"prettier": "^3.2.5",
|
|
34
|
+
"ts-jest": "^29.1.2",
|
|
35
|
+
"typescript": "^5.3.3"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/hazel-js/hazeljs.git",
|
|
43
|
+
"directory": "packages/resilience"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"hazeljs",
|
|
47
|
+
"resilience",
|
|
48
|
+
"circuit-breaker",
|
|
49
|
+
"retry",
|
|
50
|
+
"bulkhead",
|
|
51
|
+
"timeout",
|
|
52
|
+
"rate-limiter",
|
|
53
|
+
"fault-tolerance",
|
|
54
|
+
"microservices"
|
|
55
|
+
],
|
|
56
|
+
"author": "Muhammad Arslan <marslan@hazeljs.com>",
|
|
57
|
+
"license": "MIT",
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/hazeljs/hazel-js/issues"
|
|
60
|
+
},
|
|
61
|
+
"homepage": "https://hazeljs.com",
|
|
62
|
+
"gitHead": "b50699eaa4847a8cfe9c346d7e4464b4af7f3120"
|
|
63
|
+
}
|