@node-yalc/resilience 0.0.0-stage → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/resilience.d.ts +26 -0
- package/dist/resilience.js +98 -0
- package/dist/resilience.js.map +1 -0
- package/package.json +7 -6
- package/src/index.ts +1 -0
- package/src/resilience.ts +178 -0
- package/README.md +0 -3
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './resilience';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../resilience/src/index.ts"],"names":[],"mappings":";;;AAAA,uDAA6B"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
|
|
2
|
+
export declare class CircuitBreaker {
|
|
3
|
+
private failureThreshold;
|
|
4
|
+
private recoveryTimeMs;
|
|
5
|
+
private state;
|
|
6
|
+
private failureCount;
|
|
7
|
+
private lastFailureTime;
|
|
8
|
+
constructor(failureThreshold?: number, recoveryTimeMs?: number);
|
|
9
|
+
execute<T>(fn: () => Promise<T>, fallback?: () => T): Promise<T>;
|
|
10
|
+
private recordFailure;
|
|
11
|
+
reset(): void;
|
|
12
|
+
getState(): CircuitState;
|
|
13
|
+
}
|
|
14
|
+
export declare class RateLimiter {
|
|
15
|
+
private capacity;
|
|
16
|
+
private refillRatePerSec;
|
|
17
|
+
private tokens;
|
|
18
|
+
private lastRefillTimestamp;
|
|
19
|
+
constructor(capacity?: number, refillRatePerSec?: number);
|
|
20
|
+
allowRequest(tokensRequested?: number): boolean;
|
|
21
|
+
private refill;
|
|
22
|
+
}
|
|
23
|
+
export declare class Singleflight {
|
|
24
|
+
private inFlightCalls;
|
|
25
|
+
do<T>(key: string, fn: () => Promise<T>): Promise<T>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Singleflight = exports.RateLimiter = exports.CircuitBreaker = void 0;
|
|
4
|
+
class CircuitBreaker {
|
|
5
|
+
constructor(failureThreshold = 5, recoveryTimeMs = 10000) {
|
|
6
|
+
this.state = 'CLOSED';
|
|
7
|
+
this.failureCount = 0;
|
|
8
|
+
this.lastFailureTime = 0;
|
|
9
|
+
this.failureThreshold = failureThreshold;
|
|
10
|
+
this.recoveryTimeMs = recoveryTimeMs;
|
|
11
|
+
}
|
|
12
|
+
async execute(fn, fallback) {
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
if (this.state === 'OPEN') {
|
|
15
|
+
if (now - this.lastFailureTime > this.recoveryTimeMs) {
|
|
16
|
+
this.state = 'HALF_OPEN';
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
if (fallback)
|
|
20
|
+
return fallback();
|
|
21
|
+
throw new Error('CircuitBreaker is OPEN. Execution blocked.');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const result = await fn();
|
|
26
|
+
if (this.state === 'HALF_OPEN') {
|
|
27
|
+
this.reset();
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
this.recordFailure();
|
|
33
|
+
if (fallback)
|
|
34
|
+
return fallback();
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
recordFailure() {
|
|
39
|
+
this.failureCount++;
|
|
40
|
+
this.lastFailureTime = Date.now();
|
|
41
|
+
if (this.failureCount >= this.failureThreshold) {
|
|
42
|
+
this.state = 'OPEN';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
reset() {
|
|
46
|
+
this.state = 'CLOSED';
|
|
47
|
+
this.failureCount = 0;
|
|
48
|
+
}
|
|
49
|
+
getState() {
|
|
50
|
+
return this.state;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.CircuitBreaker = CircuitBreaker;
|
|
54
|
+
class RateLimiter {
|
|
55
|
+
constructor(capacity = 100, refillRatePerSec = 10) {
|
|
56
|
+
this.capacity = capacity;
|
|
57
|
+
this.refillRatePerSec = refillRatePerSec;
|
|
58
|
+
this.tokens = capacity;
|
|
59
|
+
this.lastRefillTimestamp = Date.now();
|
|
60
|
+
}
|
|
61
|
+
allowRequest(tokensRequested = 1) {
|
|
62
|
+
this.refill();
|
|
63
|
+
if (this.tokens >= tokensRequested) {
|
|
64
|
+
this.tokens -= tokensRequested;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
refill() {
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
const elapsedSecs = (now - this.lastRefillTimestamp) / 1000;
|
|
72
|
+
this.tokens = Math.min(this.capacity, this.tokens + elapsedSecs * this.refillRatePerSec);
|
|
73
|
+
this.lastRefillTimestamp = now;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.RateLimiter = RateLimiter;
|
|
77
|
+
class Singleflight {
|
|
78
|
+
constructor() {
|
|
79
|
+
this.inFlightCalls = new Map();
|
|
80
|
+
}
|
|
81
|
+
async do(key, fn) {
|
|
82
|
+
if (this.inFlightCalls.has(key)) {
|
|
83
|
+
return this.inFlightCalls.get(key);
|
|
84
|
+
}
|
|
85
|
+
const promise = (async () => {
|
|
86
|
+
try {
|
|
87
|
+
return await fn();
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
this.inFlightCalls.delete(key);
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
this.inFlightCalls.set(key, promise);
|
|
94
|
+
return promise;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.Singleflight = Singleflight;
|
|
98
|
+
//# sourceMappingURL=resilience.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resilience.js","sourceRoot":"","sources":["../../../resilience/src/resilience.ts"],"names":[],"mappings":";;;AAaA,MAAa,cAAc;IAazB,YAAY,mBAA2B,CAAC,EAAE,iBAAyB,KAAK;QAVhE,UAAK,GAAiB,QAAQ,CAAC;QAC/B,iBAAY,GAAW,CAAC,CAAC;QACzB,oBAAe,GAAW,CAAC,CAAC;QASlC,IAAI,CAAC,gBAAgB,GAAG,gBAAgB,CAAC;QACzC,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;IACvC,CAAC;IAWM,KAAK,CAAC,OAAO,CAAI,EAAoB,EAAE,QAAkB;QAC9D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAEvB,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM,EAAE,CAAC;YAC1B,IAAI,GAAG,GAAG,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;gBACrD,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC;YAC3B,CAAC;iBAAM,CAAC;gBACN,IAAI,QAAQ;oBAAE,OAAO,QAAQ,EAAE,CAAC;gBAChC,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;YAChE,CAAC;QACH,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,EAAE,EAAE,CAAC;YAC1B,IAAI,IAAI,CAAC,KAAK,KAAK,WAAW,EAAE,CAAC;gBAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,aAAa,EAAE,CAAC;YACrB,IAAI,QAAQ;gBAAE,OAAO,QAAQ,EAAE,CAAC;YAChC,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAMO,aAAa;QACnB,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAClC,IAAI,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC/C,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC;QACtB,CAAC;IACH,CAAC;IAKM,KAAK;QACV,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC;QACtB,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;IACxB,CAAC;IAMM,QAAQ;QACb,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;CACF;AA/ED,wCA+EC;AAMD,MAAa,WAAW;IAYtB,YAAY,WAAmB,GAAG,EAAE,mBAA2B,EAAE;QAC/D,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,gBAAgB,GAAG,gBAAgB,CAAC;QACzC,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC;QACvB,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACxC,CAAC;IAQM,YAAY,CAAC,kBAA0B,CAAC;QAC7C,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,IAAI,IAAI,CAAC,MAAM,IAAI,eAAe,EAAE,CAAC;YACnC,IAAI,CAAC,MAAM,IAAI,eAAe,CAAC;YAC/B,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAMO,MAAM;QACZ,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,WAAW,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,mBAAmB,CAAC,GAAG,IAAI,CAAC;QAC5D,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,GAAG,WAAW,GAAG,IAAI,CAAC,gBAAgB,CAAC,CAAC;QACzF,IAAI,CAAC,mBAAmB,GAAG,GAAG,CAAC;IACjC,CAAC;CACF;AA5CD,kCA4CC;AAQD,MAAa,YAAY;IAAzB;QACU,kBAAa,GAA8B,IAAI,GAAG,EAAE,CAAC;IA0B/D,CAAC;IAhBQ,KAAK,CAAC,EAAE,CAAI,GAAW,EAAE,EAAoB;QAClD,IAAI,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAChC,OAAO,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAe,CAAC;QACnD,CAAC;QAED,MAAM,OAAO,GAAG,CAAC,KAAK,IAAI,EAAE;YAC1B,IAAI,CAAC;gBACH,OAAO,MAAM,EAAE,EAAE,CAAC;YACpB,CAAC;oBAAS,CAAC;gBACT,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACjC,CAAC;QACH,CAAC,CAAC,EAAE,CAAC;QAEL,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACrC,OAAO,OAAO,CAAC;IACjB,CAAC;CACF;AA3BD,oCA2BC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@node-yalc/resilience",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"type": "module"
|
|
7
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './resilience';
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents the current state of a Circuit Breaker.
|
|
3
|
+
* - `CLOSED`: Operations are permitted (normal operation).
|
|
4
|
+
* - `OPEN`: Operations are blocked because the failure threshold was exceeded.
|
|
5
|
+
* - `HALF_OPEN`: Circuit is testing the underlying service to see if it recovered.
|
|
6
|
+
*/
|
|
7
|
+
export type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Enterprise Circuit Breaker pattern.
|
|
11
|
+
* Prevents catastrophic cascading failures by temporarily blocking execution of
|
|
12
|
+
* a degraded operation, giving the underlying service time to recover.
|
|
13
|
+
*/
|
|
14
|
+
export class CircuitBreaker {
|
|
15
|
+
private failureThreshold: number;
|
|
16
|
+
private recoveryTimeMs: number;
|
|
17
|
+
private state: CircuitState = 'CLOSED';
|
|
18
|
+
private failureCount: number = 0;
|
|
19
|
+
private lastFailureTime: number = 0;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Initializes the Circuit Breaker.
|
|
23
|
+
*
|
|
24
|
+
* @param failureThreshold The number of consecutive failures before the circuit trips (opens).
|
|
25
|
+
* @param recoveryTimeMs The duration in milliseconds to remain OPEN before shifting to HALF_OPEN.
|
|
26
|
+
*/
|
|
27
|
+
constructor(failureThreshold: number = 5, recoveryTimeMs: number = 10000) {
|
|
28
|
+
this.failureThreshold = failureThreshold;
|
|
29
|
+
this.recoveryTimeMs = recoveryTimeMs;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Executes a potentially failing asynchronous function through the circuit breaker.
|
|
34
|
+
*
|
|
35
|
+
* @template T The expected return type of the wrapped function.
|
|
36
|
+
* @param {() => Promise<T>} fn The asynchronous operation to wrap.
|
|
37
|
+
* @param {() => T} [fallback] Optional synchronous fallback logic if the circuit is OPEN or fails.
|
|
38
|
+
* @returns {Promise<T>} The successful result of `fn` or `fallback`.
|
|
39
|
+
* @throws {Error} If execution fails and no fallback is provided, or if the circuit is OPEN.
|
|
40
|
+
*/
|
|
41
|
+
public async execute<T>(fn: () => Promise<T>, fallback?: () => T): Promise<T> {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
|
|
44
|
+
if (this.state === 'OPEN') {
|
|
45
|
+
if (now - this.lastFailureTime > this.recoveryTimeMs) {
|
|
46
|
+
this.state = 'HALF_OPEN';
|
|
47
|
+
} else {
|
|
48
|
+
if (fallback) return fallback();
|
|
49
|
+
throw new Error('CircuitBreaker is OPEN. Execution blocked.');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const result = await fn();
|
|
55
|
+
if (this.state === 'HALF_OPEN') {
|
|
56
|
+
this.reset();
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
} catch (err) {
|
|
60
|
+
this.recordFailure();
|
|
61
|
+
if (fallback) return fallback();
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Internal routine to increment failure metrics and trip the circuit if needed.
|
|
68
|
+
* @private
|
|
69
|
+
*/
|
|
70
|
+
private recordFailure(): void {
|
|
71
|
+
this.failureCount++;
|
|
72
|
+
this.lastFailureTime = Date.now();
|
|
73
|
+
if (this.failureCount >= this.failureThreshold) {
|
|
74
|
+
this.state = 'OPEN';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Manually resets the circuit breaker back to its baseline CLOSED state.
|
|
80
|
+
*/
|
|
81
|
+
public reset(): void {
|
|
82
|
+
this.state = 'CLOSED';
|
|
83
|
+
this.failureCount = 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Returns the current operational state of the circuit.
|
|
88
|
+
* @returns {CircuitState}
|
|
89
|
+
*/
|
|
90
|
+
public getState(): CircuitState {
|
|
91
|
+
return this.state;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Enterprise Token Bucket Rate Limiter.
|
|
97
|
+
* Used to throttle API usage or heavy background jobs, preventing resource exhaustion.
|
|
98
|
+
*/
|
|
99
|
+
export class RateLimiter {
|
|
100
|
+
private capacity: number;
|
|
101
|
+
private refillRatePerSec: number;
|
|
102
|
+
private tokens: number;
|
|
103
|
+
private lastRefillTimestamp: number;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Initializes the Rate Limiter.
|
|
107
|
+
*
|
|
108
|
+
* @param capacity The maximum number of tokens the bucket can hold.
|
|
109
|
+
* @param refillRatePerSec The number of tokens added back to the bucket every second.
|
|
110
|
+
*/
|
|
111
|
+
constructor(capacity: number = 100, refillRatePerSec: number = 10) {
|
|
112
|
+
this.capacity = capacity;
|
|
113
|
+
this.refillRatePerSec = refillRatePerSec;
|
|
114
|
+
this.tokens = capacity;
|
|
115
|
+
this.lastRefillTimestamp = Date.now();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Attempts to consume the specified number of tokens from the bucket.
|
|
120
|
+
*
|
|
121
|
+
* @param tokensRequested The number of tokens needed for the operation (default 1).
|
|
122
|
+
* @returns {boolean} True if the tokens were successfully consumed; false if rate limited.
|
|
123
|
+
*/
|
|
124
|
+
public allowRequest(tokensRequested: number = 1): boolean {
|
|
125
|
+
this.refill();
|
|
126
|
+
if (this.tokens >= tokensRequested) {
|
|
127
|
+
this.tokens -= tokensRequested;
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Internal routine that mathematically calculates and adds tokens based on elapsed time.
|
|
135
|
+
* @private
|
|
136
|
+
*/
|
|
137
|
+
private refill(): void {
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
const elapsedSecs = (now - this.lastRefillTimestamp) / 1000;
|
|
140
|
+
this.tokens = Math.min(this.capacity, this.tokens + elapsedSecs * this.refillRatePerSec);
|
|
141
|
+
this.lastRefillTimestamp = now;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Enterprise Singleflight (Promise Coalescing) Pattern.
|
|
147
|
+
* Prevents redundant concurrent executions of an identical heavy operation.
|
|
148
|
+
* If 10 requests ask for the same data simultaneously, only 1 function execution occurs
|
|
149
|
+
* and all 10 await the single shared promise.
|
|
150
|
+
*/
|
|
151
|
+
export class Singleflight {
|
|
152
|
+
private inFlightCalls: Map<string, Promise<any>> = new Map();
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Executes a deduplicated function call keyed by a unique string.
|
|
156
|
+
*
|
|
157
|
+
* @template T The expected return type.
|
|
158
|
+
* @param key A unique identifier representing the operation (e.g., 'getUser_123').
|
|
159
|
+
* @param fn The heavy asynchronous function to execute if no flight is already active.
|
|
160
|
+
* @returns {Promise<T>} The result of the operation.
|
|
161
|
+
*/
|
|
162
|
+
public async do<T>(key: string, fn: () => Promise<T>): Promise<T> {
|
|
163
|
+
if (this.inFlightCalls.has(key)) {
|
|
164
|
+
return this.inFlightCalls.get(key) as Promise<T>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const promise = (async () => {
|
|
168
|
+
try {
|
|
169
|
+
return await fn();
|
|
170
|
+
} finally {
|
|
171
|
+
this.inFlightCalls.delete(key);
|
|
172
|
+
}
|
|
173
|
+
})();
|
|
174
|
+
|
|
175
|
+
this.inFlightCalls.set(key, promise);
|
|
176
|
+
return promise;
|
|
177
|
+
}
|
|
178
|
+
}
|
package/README.md
DELETED