redeye-breaker 0.1.0
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 +21 -0
- package/README.md +203 -0
- package/dist/circuit-breaker.d.ts +211 -0
- package/dist/circuit-breaker.d.ts.map +1 -0
- package/dist/circuit-breaker.js +577 -0
- package/dist/circuit-breaker.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/store.d.ts +55 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +3 -0
- package/dist/store.js.map +1 -0
- package/dist/stores/redis-store.d.ts +40 -0
- package/dist/stores/redis-store.d.ts.map +1 -0
- package/dist/stores/redis-store.js +131 -0
- package/dist/stores/redis-store.js.map +1 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +20 -0
- package/dist/types.js.map +1 -0
- package/package.json +69 -0
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CircuitBreakerState } from './types';
|
|
2
|
+
export interface Store {
|
|
3
|
+
get<T>(key: string): Promise<T | null>;
|
|
4
|
+
set<T>(key: string, value: T, ttlSeconds: number): Promise<void>;
|
|
5
|
+
del(key: string): Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Atomically increments the stored failure count for `key` and decides
|
|
8
|
+
* `isOpen` in a single round trip, so concurrent callers across instances
|
|
9
|
+
* can't race and lose an increment the way a plain get-then-set would.
|
|
10
|
+
*
|
|
11
|
+
* If omitted, the breaker falls back to a non-atomic get-then-set, which
|
|
12
|
+
* has a narrow race window under concurrent failures (see README).
|
|
13
|
+
*/
|
|
14
|
+
recordFailureAtomic?(key: string, opts: {
|
|
15
|
+
ttlSeconds: number;
|
|
16
|
+
failureThreshold: number;
|
|
17
|
+
now: number;
|
|
18
|
+
}): Promise<CircuitBreakerState>;
|
|
19
|
+
/**
|
|
20
|
+
* Atomically claims a short-lived, exclusive "trial" slot for `key`
|
|
21
|
+
* (conceptually a SET-if-not-exists). Returns `true` if this call won the
|
|
22
|
+
* claim. Used to implement half-open: only the winner gets to make the
|
|
23
|
+
* trial call while a breaker is transitioning from open back to closed;
|
|
24
|
+
* everyone else stays blocked until the trial resolves.
|
|
25
|
+
*
|
|
26
|
+
* If omitted, the breaker falls back to letting every caller through
|
|
27
|
+
* once the reset window elapses (a thundering herd on recovery — see
|
|
28
|
+
* README).
|
|
29
|
+
*/
|
|
30
|
+
claimTrial?(key: string, ttlSeconds: number): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* `strategy: 'errorRate'` only. Atomically folds one call's outcome into
|
|
33
|
+
* an EWMA failure-rate estimate and decides `isOpen` in a single round
|
|
34
|
+
* trip, the same atomicity guarantee `recordFailureAtomic` provides for
|
|
35
|
+
* the `'consecutive'` strategy. Called on every closed-phase call
|
|
36
|
+
* (success or failure), not just failures.
|
|
37
|
+
*
|
|
38
|
+
* `openedNow` distinguishes "this call is what tripped the breaker" from
|
|
39
|
+
* "the breaker was already open," so the caller can fire `onStateChange`
|
|
40
|
+
* exactly once per transition.
|
|
41
|
+
*
|
|
42
|
+
* If omitted, the breaker falls back to a non-atomic get-then-set.
|
|
43
|
+
*/
|
|
44
|
+
recordOutcomeAtomic?(key: string, opts: {
|
|
45
|
+
success: boolean;
|
|
46
|
+
decay: number;
|
|
47
|
+
minimumCalls: number;
|
|
48
|
+
errorRateThreshold: number;
|
|
49
|
+
ttlSeconds: number;
|
|
50
|
+
now: number;
|
|
51
|
+
}): Promise<CircuitBreakerState & {
|
|
52
|
+
openedNow: boolean;
|
|
53
|
+
}>;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,WAAW,KAAK;IACpB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACvC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhC;;;;;;;OAOG;IACH,mBAAmB,CAAC,CAClB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAClE,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAEhC;;;;;;;;;;OAUG;IACH,UAAU,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE/D;;;;;;;;;;;;OAYG;IACH,mBAAmB,CAAC,CAClB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,kBAAkB,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAC3H,OAAO,CAAC,mBAAmB,GAAG;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC1D"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Redis } from 'ioredis';
|
|
2
|
+
import { Store } from '../store';
|
|
3
|
+
import { CircuitBreakerState } from '../types';
|
|
4
|
+
/**
|
|
5
|
+
* Redis-backed Store implementation. Import this from
|
|
6
|
+
* `redeye/redis-store` so the core package has no hard dependency on
|
|
7
|
+
* `ioredis` — only consumers who use RedisStore need it installed.
|
|
8
|
+
*
|
|
9
|
+
* Implements all three optional atomic capabilities (`recordFailureAtomic`
|
|
10
|
+
* and `recordOutcomeAtomic` via Lua scripts, `claimTrial` via `SET ... NX`),
|
|
11
|
+
* so breakers backed by RedisStore get exact counting and real single-trial
|
|
12
|
+
* half-open behavior under both strategies instead of best-effort fallbacks.
|
|
13
|
+
*/
|
|
14
|
+
export declare class RedisStore implements Store {
|
|
15
|
+
private readonly redis;
|
|
16
|
+
private readonly prefix;
|
|
17
|
+
constructor(redis: Redis, options?: {
|
|
18
|
+
keyPrefix?: string;
|
|
19
|
+
});
|
|
20
|
+
get<T>(key: string): Promise<T | null>;
|
|
21
|
+
set<T>(key: string, value: T, ttlSeconds: number): Promise<void>;
|
|
22
|
+
del(key: string): Promise<void>;
|
|
23
|
+
recordFailureAtomic(key: string, opts: {
|
|
24
|
+
ttlSeconds: number;
|
|
25
|
+
failureThreshold: number;
|
|
26
|
+
now: number;
|
|
27
|
+
}): Promise<CircuitBreakerState>;
|
|
28
|
+
recordOutcomeAtomic(key: string, opts: {
|
|
29
|
+
success: boolean;
|
|
30
|
+
decay: number;
|
|
31
|
+
minimumCalls: number;
|
|
32
|
+
errorRateThreshold: number;
|
|
33
|
+
ttlSeconds: number;
|
|
34
|
+
now: number;
|
|
35
|
+
}): Promise<CircuitBreakerState & {
|
|
36
|
+
openedNow: boolean;
|
|
37
|
+
}>;
|
|
38
|
+
claimTrial(key: string, ttlSeconds: number): Promise<boolean>;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=redis-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-store.d.ts","sourceRoot":"","sources":["../../src/stores/redis-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AA0F/C;;;;;;;;;GASG;AACH,qBAAa,UAAW,YAAW,KAAK;IAG1B,OAAO,CAAC,QAAQ,CAAC,KAAK;IAFlC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEH,KAAK,EAAE,KAAK,EAAE,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO;IAIzE,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAMtC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/B,mBAAmB,CACvB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAClE,OAAO,CAAC,mBAAmB,CAAC;IAYzB,mBAAmB,CACvB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,kBAAkB,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAC3H,OAAO,CAAC,mBAAmB,GAAG;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;IAelD,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAIpE"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RedisStore = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Atomically increments the stored failure count and decides `isOpen` in one
|
|
6
|
+
* round trip: GET -> decode -> increment -> compute isOpen -> SET, all inside
|
|
7
|
+
* Redis's single-threaded script execution, so two instances failing at the
|
|
8
|
+
* same instant cannot race and lose an increment. Used by the `'consecutive'`
|
|
9
|
+
* strategy.
|
|
10
|
+
*/
|
|
11
|
+
const RECORD_FAILURE_SCRIPT = `
|
|
12
|
+
local raw = redis.call('GET', KEYS[1])
|
|
13
|
+
local failures = 0
|
|
14
|
+
local openCount = 0
|
|
15
|
+
local errorRate = 0
|
|
16
|
+
local sampleCount = 0
|
|
17
|
+
if raw then
|
|
18
|
+
local ok, decoded = pcall(cjson.decode, raw)
|
|
19
|
+
if ok and decoded then
|
|
20
|
+
failures = tonumber(decoded.failures) or 0
|
|
21
|
+
openCount = tonumber(decoded.openCount) or 0
|
|
22
|
+
errorRate = tonumber(decoded.errorRate) or 0
|
|
23
|
+
sampleCount = tonumber(decoded.sampleCount) or 0
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
failures = failures + 1
|
|
27
|
+
local threshold = tonumber(ARGV[2])
|
|
28
|
+
local state = {
|
|
29
|
+
failures = failures,
|
|
30
|
+
lastFailure = tonumber(ARGV[3]),
|
|
31
|
+
isOpen = failures >= threshold,
|
|
32
|
+
openCount = openCount,
|
|
33
|
+
errorRate = errorRate,
|
|
34
|
+
sampleCount = sampleCount
|
|
35
|
+
}
|
|
36
|
+
redis.call('SET', KEYS[1], cjson.encode(state), 'EX', tonumber(ARGV[1]))
|
|
37
|
+
return cjson.encode(state)
|
|
38
|
+
`;
|
|
39
|
+
/**
|
|
40
|
+
* Atomically folds one call's outcome into an EWMA failure-rate estimate and
|
|
41
|
+
* decides `isOpen` in one round trip. Used by the `'errorRate'` strategy;
|
|
42
|
+
* called on every closed-phase call, not just failures. Also reports
|
|
43
|
+
* whether this specific call is what tripped the breaker (`openedNow`), so
|
|
44
|
+
* the caller can fire `onStateChange` exactly once per transition without a
|
|
45
|
+
* separate read.
|
|
46
|
+
*/
|
|
47
|
+
const RECORD_OUTCOME_SCRIPT = `
|
|
48
|
+
local raw = redis.call('GET', KEYS[1])
|
|
49
|
+
local failures = 0
|
|
50
|
+
local openCount = 0
|
|
51
|
+
local errorRate = 0
|
|
52
|
+
local sampleCount = 0
|
|
53
|
+
local wasOpen = false
|
|
54
|
+
if raw then
|
|
55
|
+
local ok, decoded = pcall(cjson.decode, raw)
|
|
56
|
+
if ok and decoded then
|
|
57
|
+
failures = tonumber(decoded.failures) or 0
|
|
58
|
+
openCount = tonumber(decoded.openCount) or 0
|
|
59
|
+
errorRate = tonumber(decoded.errorRate) or 0
|
|
60
|
+
sampleCount = tonumber(decoded.sampleCount) or 0
|
|
61
|
+
wasOpen = decoded.isOpen or false
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
local decay = tonumber(ARGV[1])
|
|
65
|
+
local isFailure = tonumber(ARGV[2])
|
|
66
|
+
local minimumCalls = tonumber(ARGV[3])
|
|
67
|
+
local threshold = tonumber(ARGV[4])
|
|
68
|
+
local ttlSeconds = tonumber(ARGV[5])
|
|
69
|
+
local now = tonumber(ARGV[6])
|
|
70
|
+
|
|
71
|
+
errorRate = errorRate * decay + isFailure * (1 - decay)
|
|
72
|
+
sampleCount = sampleCount + 1
|
|
73
|
+
local isOpen = (sampleCount >= minimumCalls) and (errorRate >= threshold)
|
|
74
|
+
|
|
75
|
+
local state = {
|
|
76
|
+
failures = failures,
|
|
77
|
+
lastFailure = now,
|
|
78
|
+
isOpen = isOpen,
|
|
79
|
+
openCount = openCount,
|
|
80
|
+
errorRate = errorRate,
|
|
81
|
+
sampleCount = sampleCount
|
|
82
|
+
}
|
|
83
|
+
redis.call('SET', KEYS[1], cjson.encode(state), 'EX', ttlSeconds)
|
|
84
|
+
|
|
85
|
+
local result = {}
|
|
86
|
+
for k, v in pairs(state) do result[k] = v end
|
|
87
|
+
result.openedNow = isOpen and not wasOpen
|
|
88
|
+
return cjson.encode(result)
|
|
89
|
+
`;
|
|
90
|
+
/**
|
|
91
|
+
* Redis-backed Store implementation. Import this from
|
|
92
|
+
* `redeye/redis-store` so the core package has no hard dependency on
|
|
93
|
+
* `ioredis` — only consumers who use RedisStore need it installed.
|
|
94
|
+
*
|
|
95
|
+
* Implements all three optional atomic capabilities (`recordFailureAtomic`
|
|
96
|
+
* and `recordOutcomeAtomic` via Lua scripts, `claimTrial` via `SET ... NX`),
|
|
97
|
+
* so breakers backed by RedisStore get exact counting and real single-trial
|
|
98
|
+
* half-open behavior under both strategies instead of best-effort fallbacks.
|
|
99
|
+
*/
|
|
100
|
+
class RedisStore {
|
|
101
|
+
constructor(redis, options = {}) {
|
|
102
|
+
this.redis = redis;
|
|
103
|
+
this.prefix = options.keyPrefix ?? '';
|
|
104
|
+
}
|
|
105
|
+
async get(key) {
|
|
106
|
+
const raw = await this.redis.get(this.prefix + key);
|
|
107
|
+
if (raw === null)
|
|
108
|
+
return null;
|
|
109
|
+
return JSON.parse(raw);
|
|
110
|
+
}
|
|
111
|
+
async set(key, value, ttlSeconds) {
|
|
112
|
+
await this.redis.set(this.prefix + key, JSON.stringify(value), 'EX', Math.max(1, Math.ceil(ttlSeconds)));
|
|
113
|
+
}
|
|
114
|
+
async del(key) {
|
|
115
|
+
await this.redis.del(this.prefix + key);
|
|
116
|
+
}
|
|
117
|
+
async recordFailureAtomic(key, opts) {
|
|
118
|
+
const raw = (await this.redis.eval(RECORD_FAILURE_SCRIPT, 1, this.prefix + key, Math.max(1, Math.ceil(opts.ttlSeconds)), opts.failureThreshold, opts.now));
|
|
119
|
+
return JSON.parse(raw);
|
|
120
|
+
}
|
|
121
|
+
async recordOutcomeAtomic(key, opts) {
|
|
122
|
+
const raw = (await this.redis.eval(RECORD_OUTCOME_SCRIPT, 1, this.prefix + key, opts.decay, opts.success ? 0 : 1, opts.minimumCalls, opts.errorRateThreshold, Math.max(1, Math.ceil(opts.ttlSeconds)), opts.now));
|
|
123
|
+
return JSON.parse(raw);
|
|
124
|
+
}
|
|
125
|
+
async claimTrial(key, ttlSeconds) {
|
|
126
|
+
const result = await this.redis.set(this.prefix + key, '1', 'EX', Math.max(1, Math.ceil(ttlSeconds)), 'NX');
|
|
127
|
+
return result === 'OK';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
exports.RedisStore = RedisStore;
|
|
131
|
+
//# sourceMappingURL=redis-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-store.js","sourceRoot":"","sources":["../../src/stores/redis-store.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface CircuitBreakerState {
|
|
2
|
+
failures: number;
|
|
3
|
+
lastFailure: number;
|
|
4
|
+
isOpen: boolean;
|
|
5
|
+
/** Number of consecutive times a half-open trial has failed since the breaker last fully closed. Drives backoff growth. */
|
|
6
|
+
openCount: number;
|
|
7
|
+
/** `errorRate` strategy only: current EWMA estimate of the failure rate, 0-1. */
|
|
8
|
+
errorRate: number;
|
|
9
|
+
/** `errorRate` strategy only: number of calls folded into `errorRate` since the breaker last fully closed. */
|
|
10
|
+
sampleCount: number;
|
|
11
|
+
}
|
|
12
|
+
export interface CircuitMetrics {
|
|
13
|
+
totalCalls: number;
|
|
14
|
+
totalSuccesses: number;
|
|
15
|
+
totalFailures: number;
|
|
16
|
+
/** Calls rejected without invoking the wrapped function because the breaker was open. */
|
|
17
|
+
totalRejections: number;
|
|
18
|
+
/** Distributed mode only: store read/write failures encountered. */
|
|
19
|
+
totalStoreErrors: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const CLOSED_STATE: CircuitBreakerState;
|
|
22
|
+
export declare const emptyMetrics: () => CircuitMetrics;
|
|
23
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,2HAA2H;IAC3H,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,SAAS,EAAE,MAAM,CAAC;IAClB,8GAA8G;IAC9G,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,yFAAyF;IACzF,eAAe,EAAE,MAAM,CAAC;IACxB,oEAAoE;IACpE,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,eAAO,MAAM,YAAY,EAAE,mBAO1B,CAAC;AAEF,eAAO,MAAM,YAAY,QAAO,cAM9B,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.emptyMetrics = exports.CLOSED_STATE = void 0;
|
|
4
|
+
exports.CLOSED_STATE = {
|
|
5
|
+
failures: 0,
|
|
6
|
+
lastFailure: 0,
|
|
7
|
+
isOpen: false,
|
|
8
|
+
openCount: 0,
|
|
9
|
+
errorRate: 0,
|
|
10
|
+
sampleCount: 0,
|
|
11
|
+
};
|
|
12
|
+
const emptyMetrics = () => ({
|
|
13
|
+
totalCalls: 0,
|
|
14
|
+
totalSuccesses: 0,
|
|
15
|
+
totalFailures: 0,
|
|
16
|
+
totalRejections: 0,
|
|
17
|
+
totalStoreErrors: 0,
|
|
18
|
+
});
|
|
19
|
+
exports.emptyMetrics = emptyMetrics;
|
|
20
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;;AAsBa,QAAA,YAAY,GAAwB;IAC/C,QAAQ,EAAE,CAAC;IACX,WAAW,EAAE,CAAC;IACd,MAAM,EAAE,KAAK;IACb,SAAS,EAAE,CAAC;IACZ,SAAS,EAAE,CAAC;IACZ,WAAW,EAAE,CAAC;CACf,CAAC;AAEK,MAAM,YAAY,GAAG,GAAmB,EAAE,CAAC,CAAC;IACjD,UAAU,EAAE,CAAC;IACb,cAAc,EAAE,CAAC;IACjB,aAAa,EAAE,CAAC;IAChB,eAAe,EAAE,CAAC;IAClB,gBAAgB,EAAE,CAAC;CACpB,CAAC,CAAC;AANU,QAAA,YAAY,gBAMtB"}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "redeye-breaker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A circuit breaker for Node.js with an optional Redis-backed distributed mode, so breaker state can be shared across multiple app instances instead of being tracked per-process.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./redis-store": {
|
|
16
|
+
"types": "./dist/stores/redis-store.d.ts",
|
|
17
|
+
"default": "./dist/stores/redis-store.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"typesVersions": {
|
|
21
|
+
"*": {
|
|
22
|
+
"redis-store": ["dist/stores/redis-store.d.ts"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.json",
|
|
27
|
+
"test": "jest",
|
|
28
|
+
"test:integration": "jest --config jest.integration.config.js",
|
|
29
|
+
"lint": "tsc --noEmit",
|
|
30
|
+
"prepublishOnly": "npm run lint && npm run test && npm run build"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"circuit-breaker",
|
|
34
|
+
"resilience",
|
|
35
|
+
"redis",
|
|
36
|
+
"distributed-systems",
|
|
37
|
+
"fault-tolerance",
|
|
38
|
+
"microservices"
|
|
39
|
+
],
|
|
40
|
+
"author": "Laurels Echichinwo",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/laurells/redeye.git"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/laurells/redeye/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/laurells/redeye#readme",
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"ioredis": ">=5.0.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"ioredis": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/jest": "^29.5.12",
|
|
60
|
+
"@types/node": "^20.11.0",
|
|
61
|
+
"ioredis": "^5.4.1",
|
|
62
|
+
"jest": "^29.7.0",
|
|
63
|
+
"ts-jest": "^29.1.2",
|
|
64
|
+
"typescript": "^5.4.5"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=18"
|
|
68
|
+
}
|
|
69
|
+
}
|