@da440dil/cbr 0.1.0 → 0.2.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/README.md CHANGED
@@ -5,19 +5,17 @@
5
5
 
6
6
  [Circuit breaker](https://martinfowler.com/bliki/CircuitBreaker.html).
7
7
 
8
- [Example](./examples/decorator.ts):
8
+ [Example](https://github.com/da440dil/js-cbr/blob/main/examples/decorator.ts):
9
9
  ```typescript
10
10
  import http from 'node:http';
11
11
  import { once } from 'node:events';
12
12
  import { setTimeout } from 'node:timers/promises';
13
- import { Circuit, Breakable } from '@da440dil/cbr';
13
+ import { fixedWindow, Breakable } from '@da440dil/cbr';
14
14
 
15
15
  // Create circuit which uses "Fixed Window" algorithm to store counters for 10s,
16
16
  // switches from "Closed" to "Open" state on first error, from "Open" to "HalfOpen" state after 100ms,
17
17
  // from "HalfOpen" to "Closed" state on first success.
18
- const circuit = Circuit.fixed({
19
- windowSize: 10000, errorThreshold: 1, resetTimeout: 100, successThreshold: 1
20
- });
18
+ const circuit = fixedWindow({ windowSize: 10000, resetTimeout: 100 });
21
19
 
22
20
  circuit.on('state', (state) => {
23
21
  console.log(`STATE: ${state}`);
@@ -53,7 +51,8 @@ async function main() {
53
51
  console.log(`ERROR: ${err}`);
54
52
  }
55
53
  }
56
- await setTimeout(100);
54
+ console.log(`TIMEOUT ${circuit.resetTimeout}ms`);
55
+ await setTimeout(circuit.resetTimeout);
57
56
  const data = await client.get();
58
57
  console.log(`DATA: ${data}`);
59
58
  // Output:
@@ -61,6 +60,7 @@ async function main() {
61
60
  // STATE: 1
62
61
  // ERROR: Error: Failed with status 418
63
62
  // ERROR: CircuitError: Circuit broken
63
+ // TIMEOUT 100ms
64
64
  // STATE: 2
65
65
  // STATE: 0
66
66
  // DATA: { x: 2 }
@@ -1,3 +1,3 @@
1
1
  import { Circuit } from './Circuit';
2
2
  /** Decorate a class method with circuit breaker logic. */
3
- export declare const Breakable: <This, Args extends unknown[], Return>(circuit: Circuit) => (fn: (this: This, ...args: Args) => Promise<Return>, _: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Promise<Return>>) => (this: This, ...args: Args) => Promise<Return>;
3
+ export declare const Breakable: <This, Args extends unknown[], Return>(circuit: Circuit) => (fn: (this: This, ...args: Args) => Promise<Return>, _ctx: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Promise<Return>>) => (this: This, ...args: Args) => Promise<Return>;
package/lib/Breakable.js CHANGED
@@ -1,13 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Breakable = void 0;
4
- const CircuitBreaker_1 = require("./CircuitBreaker");
4
+ const Breaker_1 = require("./Breaker");
5
5
  /** Decorate a class method with circuit breaker logic. */
6
6
  const Breakable = (circuit) => {
7
- const breaker = new CircuitBreaker_1.CircuitBreaker(circuit);
7
+ const breaker = new Breaker_1.Breaker(circuit);
8
8
  return (fn,
9
9
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
10
- _) => {
10
+ _ctx) => {
11
11
  return function breakable(...args) {
12
12
  return breaker.exec(() => fn.apply(this, args));
13
13
  };
@@ -0,0 +1,20 @@
1
+ import { Circuit } from './Circuit';
2
+ export declare class Breaker {
3
+ private circuit;
4
+ private timeout;
5
+ private run;
6
+ constructor(circuit: Circuit, { timeout, isBreakable }?: BreakerOptions);
7
+ /** Execute a function with circuit breaker logic. */
8
+ exec<T>(fn: ExecFunction<T>, signal?: AbortSignal): Promise<T>;
9
+ protected isBreakable(_err: unknown): boolean;
10
+ }
11
+ export type BreakerOptions = {
12
+ /** Maximum time in milliseconds to execute a function before timing out. Default: no timeout = 0. */
13
+ timeout?: number;
14
+ /**
15
+ * Error checking function. Answers the question: should the error be taken into account or not.
16
+ * Default: all errors are taken into account.
17
+ */
18
+ isBreakable?: (err: unknown) => boolean;
19
+ };
20
+ export type ExecFunction<T> = (signal?: AbortSignal) => Promise<T>;
package/lib/Breaker.js ADDED
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Breaker = void 0;
4
+ const CircuitState_1 = require("./CircuitState");
5
+ const CircuitError_1 = require("./CircuitError");
6
+ class Breaker {
7
+ constructor(circuit, { timeout, isBreakable } = {}) {
8
+ this.timeout = 0;
9
+ this.run = run;
10
+ this.circuit = circuit;
11
+ if (timeout) {
12
+ this.timeout = timeout;
13
+ this.run = withTimeout(timeout);
14
+ }
15
+ if (isBreakable) {
16
+ this.isBreakable = isBreakable;
17
+ }
18
+ }
19
+ /** Execute a function with circuit breaker logic. */
20
+ async exec(fn, signal) {
21
+ if (!this.circuit.request()) {
22
+ if (this.circuit.state() === CircuitState_1.CircuitState.Open) {
23
+ throw new CircuitError_1.CircuitError('Circuit broken', this.circuit.ttl());
24
+ }
25
+ throw new CircuitError_1.CircuitError('Request rate limit exceeded', this.timeout);
26
+ }
27
+ try {
28
+ const v = signal ? await withSignal(fn, signal) : await this.run(fn);
29
+ this.circuit.success();
30
+ return v;
31
+ }
32
+ catch (err) {
33
+ if (this.isBreakable(err)) {
34
+ this.circuit.error();
35
+ }
36
+ throw err;
37
+ }
38
+ }
39
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
40
+ isBreakable(_err) {
41
+ return true;
42
+ }
43
+ }
44
+ exports.Breaker = Breaker;
45
+ async function withSignal(fn, signal) {
46
+ const v = await fn(signal);
47
+ signal.throwIfAborted();
48
+ return v;
49
+ }
50
+ function run(fn) {
51
+ return fn();
52
+ }
53
+ function withTimeout(timeout) {
54
+ return async function withSignal(fn) {
55
+ const controller = new AbortController();
56
+ let timer;
57
+ const timeoutPromise = new Promise((resolve) => {
58
+ timer = setTimeout(() => {
59
+ controller.abort(new CircuitError_1.CircuitError('Request timeout exceeded'));
60
+ resolve();
61
+ }, timeout);
62
+ });
63
+ try {
64
+ const v = await Promise.race([fn(controller.signal), timeoutPromise]);
65
+ controller.signal.throwIfAborted();
66
+ return v;
67
+ }
68
+ finally {
69
+ clearTimeout(timer);
70
+ }
71
+ };
72
+ }
package/lib/Circuit.d.ts CHANGED
@@ -1,21 +1,23 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { ICounter } from './ICounter';
3
3
  import { CircuitState } from './CircuitState';
4
- import { CircuitStats } from './CircuitStats';
5
4
  export declare class Circuit extends EventEmitter<{
6
5
  state: [CircuitState];
7
6
  }> {
8
- static fixed({ windowSize, ...rest }: CircuitParams): Circuit;
9
- static sliding({ windowSize, ...rest }: CircuitParams): Circuit;
10
- private state;
11
- expiry: number;
12
- private errorThreshold;
13
- private volumeThreshold;
14
- private resetTimeout;
15
- private successThreshold;
7
+ /** Create circuit which uses "Fixed Window" algorithm to store counters. */
8
+ static fixedWindow({ windowSize, ...rest }?: CircuitOptions): Circuit;
9
+ /** Create circuit which uses "Sliding Window" algorithm to store counters. */
10
+ static slidingWindow({ windowSize, ...rest }?: CircuitOptions): Circuit;
11
+ private circuitState;
12
+ readonly errorThreshold: number;
13
+ readonly volumeThreshold: number;
14
+ readonly resetTimeout: number;
15
+ readonly successThreshold: number;
16
16
  private counter;
17
17
  private threshold;
18
- constructor({ state, errorThreshold, volumeThreshold, resetTimeout, successThreshold, counter }: Omit<CircuitParams, 'windowSize'> & {
18
+ private expireAt;
19
+ private timer?;
20
+ constructor({ state, errorThreshold, volumeThreshold, resetTimeout, successThreshold, counter }: Omit<CircuitOptions, 'windowSize'> & {
19
21
  counter: ICounter;
20
22
  });
21
23
  /**
@@ -31,22 +33,32 @@ export declare class Circuit extends EventEmitter<{
31
33
  error(): void;
32
34
  private open;
33
35
  private setState;
34
- /** Current stats: state and counter values. */
35
- stats(): CircuitStats;
36
- /**
37
- * Time in seconds during which the circuit state will not switch:
38
- * `Cache-Control` header `max-age` value or `Retry-After` header value.
39
- */
40
- maxAge(): number;
36
+ /** Destroys circuit: stops internal timers. */
37
+ destroy(): void;
38
+ /** Circuit state: "Closed", "Open", "HalfOpen". */
39
+ state(): CircuitState;
40
+ /** Request counter. Increments at `Circuit.request()` method if circuit state is "HalfOpen". */
41
+ requestCount(): number;
42
+ /** Success request counter. Increments at `Circuit.success()` method if circuit state is "Closed" or "HalfOpen". */
43
+ successCount(): number;
44
+ /** Error counter. Increments at `Circuit.error()` method if circuit state is "Closed". */
45
+ errorCount(): number;
46
+ /** Timestamp upon reaching which the circuit state will switch from "Open" to "HalfOpen". */
47
+ expiry(): number;
48
+ /** Time in milliseconds during which the circuit state will not switch from "Open" to "HalfOpen". */
49
+ ttl(): number;
41
50
  }
42
- export type CircuitParams = {
43
- /** Initial circuit state. Default "Closed". */
51
+ export type CircuitOptions = {
52
+ /** Initial circuit state. Default: "Closed". */
44
53
  state?: CircuitState;
45
- /** The size of the counter window for the total number of requests, successful requests, and errors, in milliseconds. */
46
- windowSize: number;
54
+ /**
55
+ * The size of the counter window for the total number of requests, successful requests, and errors,
56
+ * in milliseconds. Default: 30 seconds.
57
+ */
58
+ windowSize?: number;
47
59
  /**
48
60
  * The number of errors within specified `windowSize`,
49
- * upon reaching which the circuit state switches from "Closed" to "Open". Default 1.
61
+ * upon reaching which the circuit state switches from "Closed" to "Open". Default: 1.
50
62
  *
51
63
  * If the value is less than 1, then this is the error threshold in percent:
52
64
  * calculated based on the ratio of the number of errors to the total number of requests.
@@ -55,7 +67,7 @@ export type CircuitParams = {
55
67
  /**
56
68
  * The minimum number of requests within specified `windowSize`,
57
69
  * upon reaching which the circuit state switches from "Closed" to "Open".
58
- * Default 1.
70
+ * Default: 1.
59
71
  *
60
72
  * It doesn't matter how many errors there were,
61
73
  * until a certain number of requests were made the circuit state will not switch from "Closed" to "Open".
@@ -63,12 +75,13 @@ export type CircuitParams = {
63
75
  volumeThreshold?: number;
64
76
  /**
65
77
  * The period of time in milliseconds, when passed the circuit state switches from "Open" to "HalfOpen".
78
+ * Default: 30 seconds.
66
79
  */
67
- resetTimeout: number;
80
+ resetTimeout?: number;
68
81
  /**
69
82
  * The number of success requests within specified `windowSize`,
70
83
  * upon reaching which the circuit state switches from "HalfOpen" to "Closed".
71
- * Default 1.
84
+ * Default: 1.
72
85
  */
73
86
  successThreshold?: number;
74
87
  };
package/lib/Circuit.js CHANGED
@@ -5,23 +5,36 @@ const node_events_1 = require("node:events");
5
5
  const CounterFixed_1 = require("./CounterFixed");
6
6
  const CounterSliding_1 = require("./CounterSliding");
7
7
  const CircuitState_1 = require("./CircuitState");
8
+ const Threshold_1 = require("./Threshold");
9
+ const request = 'r';
10
+ const success = 's';
11
+ const error = 'e';
8
12
  class Circuit extends node_events_1.EventEmitter {
9
- static fixed({ windowSize, ...rest }) {
10
- return new Circuit({ ...rest, counter: new CounterFixed_1.CounterFixed(windowSize) });
13
+ /** Create circuit which uses "Fixed Window" algorithm to store counters. */
14
+ static fixedWindow({ windowSize = 30000, ...rest } = {}) {
15
+ const counter = new CounterFixed_1.CounterFixed(windowSize);
16
+ counter.start();
17
+ return new Circuit({ ...rest, counter });
11
18
  }
12
- static sliding({ windowSize, ...rest }) {
13
- return new Circuit({ ...rest, counter: new CounterSliding_1.CounterSliding(windowSize) });
19
+ /** Create circuit which uses "Sliding Window" algorithm to store counters. */
20
+ static slidingWindow({ windowSize = 30000, ...rest } = {}) {
21
+ const counter = new CounterSliding_1.CounterSliding(windowSize);
22
+ counter.start();
23
+ return new Circuit({ ...rest, counter });
14
24
  }
15
- constructor({ state = CircuitState_1.CircuitState.Closed, errorThreshold = 1, volumeThreshold = 1, resetTimeout, successThreshold = 1, counter }) {
25
+ constructor({ state = CircuitState_1.CircuitState.Closed, errorThreshold = 1, volumeThreshold = 1, resetTimeout = 30000, successThreshold = 1, counter }) {
16
26
  super();
17
- this.expiry = 0;
18
- this.state = state;
27
+ this.expireAt = 0;
28
+ this.circuitState = state;
19
29
  this.errorThreshold = errorThreshold;
20
30
  this.volumeThreshold = volumeThreshold;
21
31
  this.resetTimeout = resetTimeout;
22
32
  this.successThreshold = successThreshold;
23
33
  this.counter = counter;
24
- this.threshold = errorThreshold < 1 && errorThreshold % 1 ? thresholdPercent : threshold;
34
+ this.threshold = (0, Threshold_1.threshold)(errorThreshold);
35
+ if (state === CircuitState_1.CircuitState.Open) {
36
+ this.open();
37
+ }
25
38
  }
26
39
  /**
27
40
  * Increments request counter if circuit state is "HalfOpen" and number of requests is not greater than `successThreshold`.
@@ -30,29 +43,22 @@ class Circuit extends node_events_1.EventEmitter {
30
43
  * number of requests is greater than `successThreshold`.
31
44
  */
32
45
  request() {
33
- if (this.state === CircuitState_1.CircuitState.Open) {
34
- if (this.expiry > Date.now()) {
35
- return false;
36
- }
37
- this.setState(CircuitState_1.CircuitState.HalfOpen);
38
- this.expiry = 0;
39
- this.counter.count(request);
46
+ if (this.circuitState === CircuitState_1.CircuitState.Open) {
47
+ return false;
40
48
  }
41
- else if (this.state === CircuitState_1.CircuitState.HalfOpen) {
42
- this.counter.tidy();
49
+ else if (this.circuitState === CircuitState_1.CircuitState.HalfOpen) {
43
50
  if (this.counter.get(request) >= this.successThreshold) {
44
51
  return false;
45
52
  }
46
- this.counter.count(request);
53
+ this.counter.incr(request);
47
54
  }
48
55
  return true;
49
56
  }
50
57
  /** Increments success request counter if circuit state is "Closed" or "HalfOpen". */
51
58
  success() {
52
- if (this.state !== CircuitState_1.CircuitState.Open) {
53
- this.counter.tidy();
54
- this.counter.count(success);
55
- if (this.state === CircuitState_1.CircuitState.HalfOpen) {
59
+ if (this.circuitState !== CircuitState_1.CircuitState.Open) {
60
+ this.counter.incr(success);
61
+ if (this.circuitState === CircuitState_1.CircuitState.HalfOpen) {
56
62
  if (this.counter.get(success) >= this.successThreshold) {
57
63
  this.setState(CircuitState_1.CircuitState.Closed);
58
64
  }
@@ -61,54 +67,63 @@ class Circuit extends node_events_1.EventEmitter {
61
67
  }
62
68
  /** Increments error counter if circuit state is "Closed". */
63
69
  error() {
64
- if (this.state === CircuitState_1.CircuitState.Closed) {
65
- this.counter.tidy();
66
- this.counter.count(error);
70
+ if (this.circuitState === CircuitState_1.CircuitState.Closed) {
71
+ this.counter.incr(error);
67
72
  if (this.threshold(this.errorThreshold, this.volumeThreshold, this.counter.get(success), this.counter.get(error))) {
68
73
  this.open();
69
74
  }
70
75
  }
71
- else if (this.state === CircuitState_1.CircuitState.HalfOpen) {
76
+ else if (this.circuitState === CircuitState_1.CircuitState.HalfOpen) {
72
77
  this.open();
73
78
  }
74
79
  }
75
80
  open() {
76
81
  this.setState(CircuitState_1.CircuitState.Open);
77
- this.expiry = Date.now() + this.resetTimeout;
82
+ if (!this.timer) {
83
+ this.expireAt = Date.now() + this.resetTimeout;
84
+ this.timer = setTimeout(() => {
85
+ this.expireAt = 0;
86
+ this.timer = undefined;
87
+ this.setState(CircuitState_1.CircuitState.HalfOpen);
88
+ }, this.resetTimeout);
89
+ }
78
90
  }
79
91
  setState(state) {
80
- this.state = state;
92
+ this.circuitState = state;
81
93
  this.counter.reset();
82
- this.emit('state', this.state);
94
+ this.emit('state', this.circuitState);
83
95
  }
84
- /** Current stats: state and counter values. */
85
- stats() {
86
- this.counter.tidy();
87
- return {
88
- state: this.state,
89
- requestCount: this.counter.get(request),
90
- successCount: this.counter.get(success),
91
- errorCount: this.counter.get(error)
92
- };
96
+ /** Destroys circuit: stops internal timers. */
97
+ destroy() {
98
+ this.counter.stop();
99
+ clearTimeout(this.timer);
100
+ this.expireAt = 0;
101
+ this.timer = undefined;
93
102
  }
94
- /**
95
- * Time in seconds during which the circuit state will not switch:
96
- * `Cache-Control` header `max-age` value or `Retry-After` header value.
97
- */
98
- maxAge() {
99
- const diff = this.expiry - Date.now();
100
- return diff > 0 ? Math.ceil(diff / 1000) : 0;
103
+ /** Circuit state: "Closed", "Open", "HalfOpen". */
104
+ state() {
105
+ return this.circuitState;
106
+ }
107
+ /** Request counter. Increments at `Circuit.request()` method if circuit state is "HalfOpen". */
108
+ requestCount() {
109
+ return this.counter.get(request);
110
+ }
111
+ /** Success request counter. Increments at `Circuit.success()` method if circuit state is "Closed" or "HalfOpen". */
112
+ successCount() {
113
+ return this.counter.get(success);
114
+ }
115
+ /** Error counter. Increments at `Circuit.error()` method if circuit state is "Closed". */
116
+ errorCount() {
117
+ return this.counter.get(error);
118
+ }
119
+ /** Timestamp upon reaching which the circuit state will switch from "Open" to "HalfOpen". */
120
+ expiry() {
121
+ return this.expireAt;
122
+ }
123
+ /** Time in milliseconds during which the circuit state will not switch from "Open" to "HalfOpen". */
124
+ ttl() {
125
+ const diff = this.expireAt - Date.now();
126
+ return diff > 0 ? diff : 0;
101
127
  }
102
128
  }
103
129
  exports.Circuit = Circuit;
104
- const request = 'r';
105
- const success = 's';
106
- const error = 'e';
107
- const threshold = (errorThreshold, volumeThreshold, successCount, errorCount) => {
108
- const total = successCount + errorCount;
109
- return total >= volumeThreshold && errorCount >= errorThreshold;
110
- };
111
- const thresholdPercent = (errorThreshold, volumeThreshold, successCount, errorCount) => {
112
- const total = successCount + errorCount;
113
- return total >= volumeThreshold && errorCount / total >= errorThreshold;
114
- };
@@ -3,4 +3,7 @@
3
3
  * or the circuit state is "HalfOpen" and the number of requests exceeds the limit.
4
4
  */
5
5
  export declare class CircuitError extends Error {
6
+ /** Time in milliseconds during which the circuit state will not switch from "Open" to "HalfOpen". */
7
+ ttl: number;
8
+ constructor(message: string, ttl?: number);
6
9
  }
@@ -6,7 +6,10 @@ exports.CircuitError = void 0;
6
6
  * or the circuit state is "HalfOpen" and the number of requests exceeds the limit.
7
7
  */
8
8
  class CircuitError extends Error {
9
+ constructor(message, ttl = 0) {
10
+ super(message);
11
+ this.ttl = ttl;
12
+ }
9
13
  }
10
14
  exports.CircuitError = CircuitError;
11
15
  CircuitError.prototype.name = 'CircuitError';
12
- CircuitError.prototype.message = 'Circuit broken';
@@ -3,11 +3,11 @@ import { ICounter } from './ICounter';
3
3
  export declare class CounterFixed implements ICounter {
4
4
  private data;
5
5
  private size;
6
- private key;
6
+ private timer?;
7
7
  constructor(size: number);
8
- private currKey;
9
- count(key: string): void;
8
+ start(): void;
9
+ stop(): void;
10
+ incr(key: string): void;
10
11
  get(key: string): number;
11
- tidy(): void;
12
12
  reset(): void;
13
13
  }
@@ -6,25 +6,29 @@ class CounterFixed {
6
6
  constructor(size) {
7
7
  this.data = {};
8
8
  this.size = size;
9
- this.key = this.currKey(Date.now());
10
9
  }
11
- currKey(now) {
12
- return now - now % this.size;
10
+ start() {
11
+ if (!this.timer) {
12
+ this.timer = setTimeout(() => {
13
+ this.data = {};
14
+ if (this.timer) {
15
+ this.timer.refresh();
16
+ }
17
+ }, this.size);
18
+ this.timer.unref();
19
+ }
20
+ }
21
+ stop() {
22
+ clearTimeout(this.timer);
23
+ this.timer = undefined;
13
24
  }
14
- count(key) {
25
+ incr(key) {
15
26
  const v = this.data[key] || 0;
16
27
  this.data[key] = v + 1;
17
28
  }
18
29
  get(key) {
19
30
  return this.data[key] || 0;
20
31
  }
21
- tidy() {
22
- const currKey = this.currKey(Date.now());
23
- if (currKey > this.key) {
24
- this.key = currKey;
25
- this.data = {};
26
- }
27
- }
28
32
  reset() {
29
33
  this.data = {};
30
34
  }
@@ -4,11 +4,12 @@ export declare class CounterSliding implements ICounter {
4
4
  private prevData;
5
5
  private currData;
6
6
  private size;
7
- private key;
7
+ private time;
8
+ private timer?;
8
9
  constructor(size: number);
9
- private currKey;
10
- count(key: string): void;
10
+ start(): void;
11
+ stop(): void;
12
+ incr(key: string): void;
11
13
  get(key: string): number;
12
- tidy(): void;
13
14
  reset(): void;
14
15
  }
@@ -6,13 +6,28 @@ class CounterSliding {
6
6
  constructor(size) {
7
7
  this.prevData = {};
8
8
  this.currData = {};
9
+ this.time = 0;
9
10
  this.size = size;
10
- this.key = this.currKey(Date.now());
11
11
  }
12
- currKey(now) {
13
- return now - now % this.size;
12
+ start() {
13
+ if (!this.timer) {
14
+ this.time = Date.now();
15
+ this.timer = setTimeout(() => {
16
+ this.prevData = this.currData;
17
+ this.currData = {};
18
+ if (this.timer) {
19
+ this.time = Date.now();
20
+ this.timer.refresh();
21
+ }
22
+ }, this.size);
23
+ this.timer.unref();
24
+ }
25
+ }
26
+ stop() {
27
+ clearTimeout(this.timer);
28
+ this.timer = undefined;
14
29
  }
15
- count(key) {
30
+ incr(key) {
16
31
  const v = this.currData[key] || 0;
17
32
  this.currData[key] = v + 1;
18
33
  }
@@ -20,26 +35,12 @@ class CounterSliding {
20
35
  const prev = this.prevData[key] || 0;
21
36
  let curr = this.currData[key] || 0;
22
37
  if (prev) {
23
- const now = Date.now();
24
- const currKey = this.currKey(now);
25
- const remainder = this.size - (now - currKey);
26
- curr += Math.floor(prev * (remainder / this.size));
27
- }
28
- return curr;
29
- }
30
- tidy() {
31
- const currKey = this.currKey(Date.now());
32
- const prevKey = currKey - this.size;
33
- if (prevKey >= this.key) {
34
- if (prevKey === this.key) {
35
- this.prevData = this.currData;
38
+ const rest = this.size - (Date.now() - this.time);
39
+ if (rest > 0) {
40
+ curr += Math.floor(prev * (rest / this.size));
36
41
  }
37
- else {
38
- this.prevData = {};
39
- }
40
- this.currData = {};
41
- this.key = currKey;
42
42
  }
43
+ return curr;
43
44
  }
44
45
  reset() {
45
46
  this.prevData = {};
package/lib/ICounter.d.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  export interface ICounter {
2
+ /** Start timer. */
3
+ start(): void;
4
+ /** Stop timer. */
5
+ stop(): void;
2
6
  /** Increment key value. */
3
- count(key: string): void;
7
+ incr(key: string): void;
4
8
  /** Get key value. */
5
9
  get(key: string): number;
6
- /** Actualize all values on current time. */
7
- tidy(): void;
8
10
  /** Reset all values to zero. */
9
11
  reset(): void;
10
12
  }
@@ -0,0 +1,2 @@
1
+ export type Threshold = (errorThreshold: number, volumeThreshold: number, successCount: number, errorCount: number) => boolean;
2
+ export declare const threshold: (errorThreshold: number) => Threshold;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.threshold = void 0;
4
+ const threshold = (errorThreshold) => {
5
+ return errorThreshold < 1 && errorThreshold % 1 ? percentThreshold : integerThreshold;
6
+ };
7
+ exports.threshold = threshold;
8
+ const integerThreshold = (errorThreshold, volumeThreshold, successCount, errorCount) => {
9
+ const total = successCount + errorCount;
10
+ return total >= volumeThreshold && errorCount >= errorThreshold;
11
+ };
12
+ const percentThreshold = (errorThreshold, volumeThreshold, successCount, errorCount) => {
13
+ const total = successCount + errorCount;
14
+ return total >= volumeThreshold && errorCount / total >= errorThreshold;
15
+ };
package/lib/index.d.ts CHANGED
@@ -1,6 +1,13 @@
1
- export { Circuit, CircuitParams } from './Circuit';
1
+ import { Circuit, CircuitOptions } from './Circuit';
2
+ export type { Circuit, CircuitOptions };
3
+ export declare const fixedWindow: typeof Circuit.fixedWindow;
4
+ export declare const slidingWindow: typeof Circuit.slidingWindow;
5
+ declare const _default: {
6
+ fixedWindow: typeof Circuit.fixedWindow;
7
+ slidingWindow: typeof Circuit.slidingWindow;
8
+ };
9
+ export default _default;
2
10
  export { CircuitState } from './CircuitState';
3
- export { CircuitStats } from './CircuitStats';
4
11
  export { CircuitError } from './CircuitError';
5
- export { CircuitBreaker } from './CircuitBreaker';
12
+ export { Breaker, type BreakerOptions, type ExecFunction } from './Breaker';
6
13
  export { Breakable } from './Breakable';
package/lib/index.js CHANGED
@@ -1,13 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Breakable = exports.CircuitBreaker = exports.CircuitError = exports.CircuitState = exports.Circuit = void 0;
4
- var Circuit_1 = require("./Circuit");
5
- Object.defineProperty(exports, "Circuit", { enumerable: true, get: function () { return Circuit_1.Circuit; } });
3
+ exports.Breakable = exports.Breaker = exports.CircuitError = exports.CircuitState = exports.slidingWindow = exports.fixedWindow = void 0;
4
+ const Circuit_1 = require("./Circuit");
5
+ exports.fixedWindow = Circuit_1.Circuit.fixedWindow;
6
+ exports.slidingWindow = Circuit_1.Circuit.slidingWindow;
7
+ exports.default = { fixedWindow: exports.fixedWindow, slidingWindow: exports.slidingWindow };
6
8
  var CircuitState_1 = require("./CircuitState");
7
9
  Object.defineProperty(exports, "CircuitState", { enumerable: true, get: function () { return CircuitState_1.CircuitState; } });
8
10
  var CircuitError_1 = require("./CircuitError");
9
11
  Object.defineProperty(exports, "CircuitError", { enumerable: true, get: function () { return CircuitError_1.CircuitError; } });
10
- var CircuitBreaker_1 = require("./CircuitBreaker");
11
- Object.defineProperty(exports, "CircuitBreaker", { enumerable: true, get: function () { return CircuitBreaker_1.CircuitBreaker; } });
12
+ var Breaker_1 = require("./Breaker");
13
+ Object.defineProperty(exports, "Breaker", { enumerable: true, get: function () { return Breaker_1.Breaker; } });
12
14
  var Breakable_1 = require("./Breakable");
13
15
  Object.defineProperty(exports, "Breakable", { enumerable: true, get: function () { return Breakable_1.Breakable; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@da440dil/cbr",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Circuit breaker",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -1,12 +0,0 @@
1
- import { Circuit } from './Circuit';
2
- export declare class CircuitBreaker {
3
- private circuit;
4
- constructor(circuit: Circuit);
5
- /** Execute a function with circuit breaker logic. */
6
- exec<T>(fn: () => Promise<T>): Promise<T>;
7
- /**
8
- * Error checking function. Answers the question: should the error be taken into account or not.
9
- * Default: all errors are taken into account.
10
- */
11
- protected isBreakable(_: unknown): boolean;
12
- }
@@ -1,35 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CircuitBreaker = void 0;
4
- const CircuitError_1 = require("./CircuitError");
5
- class CircuitBreaker {
6
- constructor(circuit) {
7
- this.circuit = circuit;
8
- }
9
- /** Execute a function with circuit breaker logic. */
10
- async exec(fn) {
11
- if (!this.circuit.request()) {
12
- throw new CircuitError_1.CircuitError();
13
- }
14
- try {
15
- const v = await fn();
16
- this.circuit.success();
17
- return v;
18
- }
19
- catch (err) {
20
- if (this.isBreakable(err)) {
21
- this.circuit.error();
22
- }
23
- throw err;
24
- }
25
- }
26
- /**
27
- * Error checking function. Answers the question: should the error be taken into account or not.
28
- * Default: all errors are taken into account.
29
- */
30
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
31
- isBreakable(_) {
32
- return true;
33
- }
34
- }
35
- exports.CircuitBreaker = CircuitBreaker;
@@ -1,12 +0,0 @@
1
- import { CircuitState } from './CircuitState';
2
- /** Current stats: state and counter values. */
3
- export type CircuitStats = {
4
- /** Circuit state: "Closed", "Open", "HalfOpen". */
5
- state: CircuitState;
6
- /** Request counter. Increments at `Circuit.request()` method if circuit state is "HalfOpen". */
7
- requestCount: number;
8
- /** Success request counter. Increments at `Circuit.success()` method if circuit state is "Closed" or "HalfOpen". */
9
- successCount: number;
10
- /** Error counter. Increments at `Circuit.error()` method if circuit state is "Closed". */
11
- errorCount: number;
12
- };
@@ -1,2 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });