@da440dil/cbr 0.0.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
@@ -1,138 +1,76 @@
1
1
  # @da440dil/cbr
2
2
 
3
- ![GitHub Actions](https://github.com/da440dil/js-cbr/actions/workflows/ci.yml/badge.svg)
3
+ [![CI](https://github.com/da440dil/js-cbr/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/da440dil/js-cbr/actions/workflows/ci.yml)
4
4
  [![Coverage Status](https://coveralls.io/repos/github/da440dil/js-cbr/badge.svg?branch=main)](https://coveralls.io/github/da440dil/js-cbr?branch=main)
5
5
 
6
6
  [Circuit breaker](https://martinfowler.com/bliki/CircuitBreaker.html).
7
7
 
8
- [Example](./examples/decorator.ts) decorator ([typescript](https://www.typescriptlang.org/) [version](https://github.com/microsoft/TypeScript/releases/tag/v5.0.2) >= 5):
8
+ [Example](https://github.com/da440dil/js-cbr/blob/main/examples/decorator.ts):
9
9
  ```typescript
10
- import { promisify } from 'util';
11
- import http from 'http';
12
- import { CircuitBreaker } from '@da440dil/cbr';
10
+ import http from 'node:http';
11
+ import { once } from 'node:events';
12
+ import { setTimeout } from 'node:timers/promises';
13
+ import { fixedWindow, Breakable } from '@da440dil/cbr';
13
14
 
14
- // Create the circuit breaker which switches from "Closed" to "Open" state on first error,
15
- // from "Open" to "HalfOpen" state after 100ms, from "HalfOpen" to "Closed" state on first success.
16
- const breaker = new CircuitBreaker({ errorThreshold: 1, resetTimeout: 100, successThreshold: 1 });
15
+ // Create circuit which uses "Fixed Window" algorithm to store counters for 10s,
16
+ // switches from "Closed" to "Open" state on first error, from "Open" to "HalfOpen" state after 100ms,
17
+ // from "HalfOpen" to "Closed" state on first success.
18
+ const circuit = fixedWindow({ windowSize: 10000, resetTimeout: 100 });
17
19
 
18
- class Client {
19
- private url = 'http://localhost:3000';
20
- // Decorate the class method with circuit breaker logic.
21
- @CircuitBreaker.Breakable(breaker)
22
- public get(): Promise<string> {
23
- return new Promise((resolve, reject) => {
24
- const req = http.get(this.url, (res) => {
25
- if (res.statusCode !== 200) {
26
- res.resume();
27
- return reject(new Error(`Failed with statusCode ${String(res.statusCode)}`));
28
- }
29
- let data = '';
30
- res.on('data', (chunk) => { data += chunk; });
31
- res.on('end', () => { resolve(data); });
32
- });
33
- req.on('error', reject);
34
- });
35
- }
36
- }
37
- const client = new Client();
38
- const get = async () => {
39
- try {
40
- const data = await client.get();
41
- console.log(`DATA: ${data}`);
42
- } catch (err) {
43
- console.log(err instanceof Error ? `ERROR: ${(err).message}` : err);
44
- }
45
- };
46
-
47
- let x = 0;
48
- const server = http.createServer((_, res) => {
49
- const statusCode = x > 2 || x % 2 ? 418 : 200; // { 0 => 200, 1 => 418, 2 => 200, ... => 418 }
50
- res.writeHead(statusCode).end(`{ x: ${x} }`);
51
- x++;
20
+ circuit.on('state', (state) => {
21
+ console.log(`STATE: ${state}`);
52
22
  });
53
- server.listen(3000);
54
-
55
- async function main() {
56
- for (let i = 0; i < 3; i++) {
57
- await get();
58
- }
59
- await promisify(setTimeout)(100);
60
- await get();
61
- // Output:
62
- // DATA: { x: 0 }
63
- // ERROR: Failed with statusCode 418
64
- // ERROR: Circuit broken
65
- // DATA: { x: 2 }
66
- }
67
-
68
- main().then(() => { process.exit(0); }).catch((err) => { console.error(err); process.exit(1); });
69
- ```
70
- ```
71
- npm run file examples/decorator.ts
72
- ```
73
-
74
- [Example](./examples/function.ts) function:
75
- ```typescript
76
- import { promisify } from 'util';
77
- import http from 'http';
78
- import { CircuitBreaker } from '@da440dil/cbr';
79
23
 
80
24
  class Client {
81
- // Create the circuit breaker which switches from "Closed" to "Open" state on first error,
82
- // from "Open" to "HalfOpen" state after 100ms, from "HalfOpen" to "Closed" state on first success.
83
- private breaker = new CircuitBreaker({ errorThreshold: 1, resetTimeout: 100, successThreshold: 1 });
84
- private url = 'http://localhost:3000';
85
- public get(): Promise<string> {
86
- // Execute the function with circuit breaker logic.
87
- return this.breaker.exec(() => {
88
- return new Promise((resolve, reject) => {
89
- const req = http.get(this.url, (res) => {
90
- if (res.statusCode !== 200) {
91
- res.resume();
92
- return reject(new Error(`Failed with statusCode ${String(res.statusCode)}`));
93
- }
94
- let data = '';
95
- res.on('data', (chunk) => { data += chunk; });
96
- res.on('end', () => { resolve(data); });
97
- });
98
- req.on('error', reject);
99
- });
100
- });
25
+ // Decorate class method with circuit breaker logic.
26
+ @Breakable(circuit)
27
+ public async get(): Promise<string> {
28
+ const res = await fetch('http://localhost:3000');
29
+ if (res.status !== 200) {
30
+ throw new Error(`Failed with status ${res.status}`);
31
+ }
32
+ return res.text();
101
33
  }
102
34
  }
103
- const client = new Client();
104
- const get = async () => {
105
- try {
106
- const data = await client.get();
107
- console.log(`DATA: ${data}`);
108
- } catch (err) {
109
- console.log(err instanceof Error ? `ERROR: ${(err).message}` : err);
110
- }
111
- };
112
-
113
- let x = 0;
114
- const server = http.createServer((_, res) => {
115
- const statusCode = x > 2 || x % 2 ? 418 : 200; // { 0 => 200, 1 => 418, 2 => 200, ... => 418 }
116
- res.writeHead(statusCode).end(`{ x: ${x} }`);
117
- x++;
118
- });
119
- server.listen(3000);
120
35
 
121
36
  async function main() {
37
+ let x = 0;
38
+ const server = http.createServer((_, res) => {
39
+ // { 0 => 200, 1 => 418, 2 => 200, ... => 418 }
40
+ res.writeHead(x > 2 || x % 2 ? 418 : 200).end(`{ x: ${x++} }`);
41
+ });
42
+ server.listen(3000);
43
+ await once(server, 'listening');
44
+
45
+ const client = new Client();
122
46
  for (let i = 0; i < 3; i++) {
123
- await get();
47
+ try {
48
+ const data = await client.get();
49
+ console.log(`DATA: ${data}`);
50
+ } catch (err) {
51
+ console.log(`ERROR: ${err}`);
52
+ }
124
53
  }
125
- await promisify(setTimeout)(100);
126
- await get();
54
+ console.log(`TIMEOUT ${circuit.resetTimeout}ms`);
55
+ await setTimeout(circuit.resetTimeout);
56
+ const data = await client.get();
57
+ console.log(`DATA: ${data}`);
127
58
  // Output:
128
59
  // DATA: { x: 0 }
129
- // ERROR: Failed with statusCode 418
130
- // ERROR: Circuit broken
60
+ // STATE: 1
61
+ // ERROR: Error: Failed with status 418
62
+ // ERROR: CircuitError: Circuit broken
63
+ // TIMEOUT 100ms
64
+ // STATE: 2
65
+ // STATE: 0
131
66
  // DATA: { x: 2 }
67
+
68
+ server.close();
69
+ await once(server, 'close');
132
70
  }
133
71
 
134
- main().then(() => { process.exit(0); }).catch((err) => { console.error(err); process.exit(1); });
72
+ main().catch(console.error);
135
73
  ```
136
74
  ```
137
- npm run file examples/function.ts
75
+ npm run file examples/decorator.ts
138
76
  ```
@@ -0,0 +1,3 @@
1
+ import { Circuit } from './Circuit';
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>, _ctx: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Promise<Return>>) => (this: This, ...args: Args) => Promise<Return>;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Breakable = void 0;
4
+ const Breaker_1 = require("./Breaker");
5
+ /** Decorate a class method with circuit breaker logic. */
6
+ const Breakable = (circuit) => {
7
+ const breaker = new Breaker_1.Breaker(circuit);
8
+ return (fn,
9
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
10
+ _ctx) => {
11
+ return function breakable(...args) {
12
+ return breaker.exec(() => fn.apply(this, args));
13
+ };
14
+ };
15
+ };
16
+ exports.Breakable = Breakable;
@@ -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
+ }
@@ -0,0 +1,87 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { ICounter } from './ICounter';
3
+ import { CircuitState } from './CircuitState';
4
+ export declare class Circuit extends EventEmitter<{
5
+ state: [CircuitState];
6
+ }> {
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
+ private counter;
17
+ private threshold;
18
+ private expireAt;
19
+ private timer?;
20
+ constructor({ state, errorThreshold, volumeThreshold, resetTimeout, successThreshold, counter }: Omit<CircuitOptions, 'windowSize'> & {
21
+ counter: ICounter;
22
+ });
23
+ /**
24
+ * Increments request counter if circuit state is "HalfOpen" and number of requests is not greater than `successThreshold`.
25
+ *
26
+ * Returns `false` if circuit is "broken": circuit state is "Open" or "HalfOpen" +
27
+ * number of requests is greater than `successThreshold`.
28
+ */
29
+ request(): boolean;
30
+ /** Increments success request counter if circuit state is "Closed" or "HalfOpen". */
31
+ success(): void;
32
+ /** Increments error counter if circuit state is "Closed". */
33
+ error(): void;
34
+ private open;
35
+ private setState;
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;
50
+ }
51
+ export type CircuitOptions = {
52
+ /** Initial circuit state. Default: "Closed". */
53
+ state?: CircuitState;
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;
59
+ /**
60
+ * The number of errors within specified `windowSize`,
61
+ * upon reaching which the circuit state switches from "Closed" to "Open". Default: 1.
62
+ *
63
+ * If the value is less than 1, then this is the error threshold in percent:
64
+ * calculated based on the ratio of the number of errors to the total number of requests.
65
+ */
66
+ errorThreshold?: number;
67
+ /**
68
+ * The minimum number of requests within specified `windowSize`,
69
+ * upon reaching which the circuit state switches from "Closed" to "Open".
70
+ * Default: 1.
71
+ *
72
+ * It doesn't matter how many errors there were,
73
+ * until a certain number of requests were made the circuit state will not switch from "Closed" to "Open".
74
+ */
75
+ volumeThreshold?: number;
76
+ /**
77
+ * The period of time in milliseconds, when passed the circuit state switches from "Open" to "HalfOpen".
78
+ * Default: 30 seconds.
79
+ */
80
+ resetTimeout?: number;
81
+ /**
82
+ * The number of success requests within specified `windowSize`,
83
+ * upon reaching which the circuit state switches from "HalfOpen" to "Closed".
84
+ * Default: 1.
85
+ */
86
+ successThreshold?: number;
87
+ };
package/lib/Circuit.js ADDED
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Circuit = void 0;
4
+ const node_events_1 = require("node:events");
5
+ const CounterFixed_1 = require("./CounterFixed");
6
+ const CounterSliding_1 = require("./CounterSliding");
7
+ const CircuitState_1 = require("./CircuitState");
8
+ const Threshold_1 = require("./Threshold");
9
+ const request = 'r';
10
+ const success = 's';
11
+ const error = 'e';
12
+ class Circuit extends node_events_1.EventEmitter {
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 });
18
+ }
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 });
24
+ }
25
+ constructor({ state = CircuitState_1.CircuitState.Closed, errorThreshold = 1, volumeThreshold = 1, resetTimeout = 30000, successThreshold = 1, counter }) {
26
+ super();
27
+ this.expireAt = 0;
28
+ this.circuitState = state;
29
+ this.errorThreshold = errorThreshold;
30
+ this.volumeThreshold = volumeThreshold;
31
+ this.resetTimeout = resetTimeout;
32
+ this.successThreshold = successThreshold;
33
+ this.counter = counter;
34
+ this.threshold = (0, Threshold_1.threshold)(errorThreshold);
35
+ if (state === CircuitState_1.CircuitState.Open) {
36
+ this.open();
37
+ }
38
+ }
39
+ /**
40
+ * Increments request counter if circuit state is "HalfOpen" and number of requests is not greater than `successThreshold`.
41
+ *
42
+ * Returns `false` if circuit is "broken": circuit state is "Open" or "HalfOpen" +
43
+ * number of requests is greater than `successThreshold`.
44
+ */
45
+ request() {
46
+ if (this.circuitState === CircuitState_1.CircuitState.Open) {
47
+ return false;
48
+ }
49
+ else if (this.circuitState === CircuitState_1.CircuitState.HalfOpen) {
50
+ if (this.counter.get(request) >= this.successThreshold) {
51
+ return false;
52
+ }
53
+ this.counter.incr(request);
54
+ }
55
+ return true;
56
+ }
57
+ /** Increments success request counter if circuit state is "Closed" or "HalfOpen". */
58
+ success() {
59
+ if (this.circuitState !== CircuitState_1.CircuitState.Open) {
60
+ this.counter.incr(success);
61
+ if (this.circuitState === CircuitState_1.CircuitState.HalfOpen) {
62
+ if (this.counter.get(success) >= this.successThreshold) {
63
+ this.setState(CircuitState_1.CircuitState.Closed);
64
+ }
65
+ }
66
+ }
67
+ }
68
+ /** Increments error counter if circuit state is "Closed". */
69
+ error() {
70
+ if (this.circuitState === CircuitState_1.CircuitState.Closed) {
71
+ this.counter.incr(error);
72
+ if (this.threshold(this.errorThreshold, this.volumeThreshold, this.counter.get(success), this.counter.get(error))) {
73
+ this.open();
74
+ }
75
+ }
76
+ else if (this.circuitState === CircuitState_1.CircuitState.HalfOpen) {
77
+ this.open();
78
+ }
79
+ }
80
+ open() {
81
+ this.setState(CircuitState_1.CircuitState.Open);
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
+ }
90
+ }
91
+ setState(state) {
92
+ this.circuitState = state;
93
+ this.counter.reset();
94
+ this.emit('state', this.circuitState);
95
+ }
96
+ /** Destroys circuit: stops internal timers. */
97
+ destroy() {
98
+ this.counter.stop();
99
+ clearTimeout(this.timer);
100
+ this.expireAt = 0;
101
+ this.timer = undefined;
102
+ }
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;
127
+ }
128
+ }
129
+ exports.Circuit = Circuit;
@@ -1,4 +1,9 @@
1
- /** Service unavailable: either the circuit state is "Open", or the circuit state is "HalfOpen" and the number of requests exceeds the limit. */
2
- export declare const CircuitBroken = "Circuit broken";
1
+ /**
2
+ * Service unavailable: either the circuit state is "Open",
3
+ * or the circuit state is "HalfOpen" and the number of requests exceeds the limit.
4
+ */
3
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);
4
9
  }
@@ -1,10 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CircuitError = exports.CircuitBroken = void 0;
4
- /** Service unavailable: either the circuit state is "Open", or the circuit state is "HalfOpen" and the number of requests exceeds the limit. */
5
- exports.CircuitBroken = 'Circuit broken';
3
+ exports.CircuitError = void 0;
4
+ /**
5
+ * Service unavailable: either the circuit state is "Open",
6
+ * or the circuit state is "HalfOpen" and the number of requests exceeds the limit.
7
+ */
6
8
  class CircuitError extends Error {
9
+ constructor(message, ttl = 0) {
10
+ super(message);
11
+ this.ttl = ttl;
12
+ }
7
13
  }
8
14
  exports.CircuitError = CircuitError;
9
15
  CircuitError.prototype.name = 'CircuitError';
10
- CircuitError.prototype.message = exports.CircuitBroken;
@@ -1,10 +1,10 @@
1
1
  /** Circuit state: "Closed", "Open", "HalfOpen". */
2
2
  export declare const CircuitState: {
3
- /** Requests are allowed. Switches to "Open" state if errors threshold is reached. */
3
+ /** Requests are allowed. Switches to "Open" state if `errorThreshold` is reached. */
4
4
  readonly Closed: 0;
5
- /** Requests are disallowed. Switches to "HalfOpen" state after reset timeout passed. */
5
+ /** Requests are disallowed. Switches to "HalfOpen" state after `resetTimeout` passed. */
6
6
  readonly Open: 1;
7
- /** Limited number of requests are allowed. Switches to "Closed" state if success threshold is reached. */
7
+ /** Limited number of requests are allowed. Switches to "Closed" state if `successThreshold` is reached. */
8
8
  readonly HalfOpen: 2;
9
9
  };
10
10
  export type CircuitState = typeof CircuitState[keyof typeof CircuitState];
@@ -3,10 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CircuitState = void 0;
4
4
  /** Circuit state: "Closed", "Open", "HalfOpen". */
5
5
  exports.CircuitState = {
6
- /** Requests are allowed. Switches to "Open" state if errors threshold is reached. */
6
+ /** Requests are allowed. Switches to "Open" state if `errorThreshold` is reached. */
7
7
  Closed: 0,
8
- /** Requests are disallowed. Switches to "HalfOpen" state after reset timeout passed. */
8
+ /** Requests are disallowed. Switches to "HalfOpen" state after `resetTimeout` passed. */
9
9
  Open: 1,
10
- /** Limited number of requests are allowed. Switches to "Closed" state if success threshold is reached. */
10
+ /** Limited number of requests are allowed. Switches to "Closed" state if `successThreshold` is reached. */
11
11
  HalfOpen: 2
12
12
  };
@@ -0,0 +1,13 @@
1
+ import { ICounter } from './ICounter';
2
+ /** "Fixed Window" algorithm. */
3
+ export declare class CounterFixed implements ICounter {
4
+ private data;
5
+ private size;
6
+ private timer?;
7
+ constructor(size: number);
8
+ start(): void;
9
+ stop(): void;
10
+ incr(key: string): void;
11
+ get(key: string): number;
12
+ reset(): void;
13
+ }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CounterFixed = void 0;
4
+ /** "Fixed Window" algorithm. */
5
+ class CounterFixed {
6
+ constructor(size) {
7
+ this.data = {};
8
+ this.size = size;
9
+ }
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;
24
+ }
25
+ incr(key) {
26
+ const v = this.data[key] || 0;
27
+ this.data[key] = v + 1;
28
+ }
29
+ get(key) {
30
+ return this.data[key] || 0;
31
+ }
32
+ reset() {
33
+ this.data = {};
34
+ }
35
+ }
36
+ exports.CounterFixed = CounterFixed;
@@ -0,0 +1,15 @@
1
+ import { ICounter } from './ICounter';
2
+ /** "Sliding Window" algorithm. */
3
+ export declare class CounterSliding implements ICounter {
4
+ private prevData;
5
+ private currData;
6
+ private size;
7
+ private time;
8
+ private timer?;
9
+ constructor(size: number);
10
+ start(): void;
11
+ stop(): void;
12
+ incr(key: string): void;
13
+ get(key: string): number;
14
+ reset(): void;
15
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CounterSliding = void 0;
4
+ /** "Sliding Window" algorithm. */
5
+ class CounterSliding {
6
+ constructor(size) {
7
+ this.prevData = {};
8
+ this.currData = {};
9
+ this.time = 0;
10
+ this.size = size;
11
+ }
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;
29
+ }
30
+ incr(key) {
31
+ const v = this.currData[key] || 0;
32
+ this.currData[key] = v + 1;
33
+ }
34
+ get(key) {
35
+ const prev = this.prevData[key] || 0;
36
+ let curr = this.currData[key] || 0;
37
+ if (prev) {
38
+ const rest = this.size - (Date.now() - this.time);
39
+ if (rest > 0) {
40
+ curr += Math.floor(prev * (rest / this.size));
41
+ }
42
+ }
43
+ return curr;
44
+ }
45
+ reset() {
46
+ this.prevData = {};
47
+ this.currData = {};
48
+ }
49
+ }
50
+ exports.CounterSliding = CounterSliding;
@@ -0,0 +1,12 @@
1
+ export interface ICounter {
2
+ /** Start timer. */
3
+ start(): void;
4
+ /** Stop timer. */
5
+ stop(): void;
6
+ /** Increment key value. */
7
+ incr(key: string): void;
8
+ /** Get key value. */
9
+ get(key: string): number;
10
+ /** Reset all values to zero. */
11
+ reset(): void;
12
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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,2 +1,13 @@
1
- export { CircuitBreaker } from './CircuitBreaker';
2
- export { CircuitError, CircuitBroken } from './CircuitError';
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;
10
+ export { CircuitState } from './CircuitState';
11
+ export { CircuitError } from './CircuitError';
12
+ export { Breaker, type BreakerOptions, type ExecFunction } from './Breaker';
13
+ export { Breakable } from './Breakable';
package/lib/index.js CHANGED
@@ -1,8 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CircuitBroken = exports.CircuitError = exports.CircuitBreaker = void 0;
4
- var CircuitBreaker_1 = require("./CircuitBreaker");
5
- Object.defineProperty(exports, "CircuitBreaker", { enumerable: true, get: function () { return CircuitBreaker_1.CircuitBreaker; } });
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 };
8
+ var CircuitState_1 = require("./CircuitState");
9
+ Object.defineProperty(exports, "CircuitState", { enumerable: true, get: function () { return CircuitState_1.CircuitState; } });
6
10
  var CircuitError_1 = require("./CircuitError");
7
11
  Object.defineProperty(exports, "CircuitError", { enumerable: true, get: function () { return CircuitError_1.CircuitError; } });
8
- Object.defineProperty(exports, "CircuitBroken", { enumerable: true, get: function () { return CircuitError_1.CircuitBroken; } });
12
+ var Breaker_1 = require("./Breaker");
13
+ Object.defineProperty(exports, "Breaker", { enumerable: true, get: function () { return Breaker_1.Breaker; } });
14
+ var Breakable_1 = require("./Breakable");
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.0.0",
3
+ "version": "0.2.0",
4
4
  "description": "Circuit breaker",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -20,16 +20,16 @@
20
20
  "author": "Anatoly Demidovich",
21
21
  "license": "MIT",
22
22
  "engines": {
23
- "node": ">=14"
23
+ "node": ">=18"
24
24
  },
25
25
  "devDependencies": {
26
- "@types/jest": "^29.5.0",
27
- "@typescript-eslint/eslint-plugin": "^5.57.0",
28
- "@typescript-eslint/parser": "^5.57.0",
29
- "eslint": "^8.37.0",
30
- "jest": "^29.5.0",
31
- "ts-jest": "^29.1.0",
32
- "ts-node": "^10.9.1",
33
- "typescript": "^5.0.4"
26
+ "@eslint/js": "^9.31.0",
27
+ "@types/jest": "^30.0.0",
28
+ "eslint": "^9.31.0",
29
+ "jest": "^30.0.4",
30
+ "ts-jest": "^29.4.0",
31
+ "ts-node": "^10.9.2",
32
+ "typescript": "^5.8.3",
33
+ "typescript-eslint": "^8.37.0"
34
34
  }
35
35
  }
@@ -1,30 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [ main ]
6
- pull_request:
7
- branches: [ main ]
8
-
9
- jobs:
10
- test:
11
-
12
- runs-on: ubuntu-latest
13
-
14
- strategy:
15
- matrix:
16
- node-version: [14.x, 16.x, 18.x, 20.x]
17
-
18
- steps:
19
- - uses: actions/checkout@v3
20
- - name: Use Node.js ${{ matrix.node-version }}
21
- uses: actions/setup-node@v3
22
- with:
23
- node-version: ${{ matrix.node-version }}
24
- - run: npm install
25
- - run: npm run test:coverage
26
-
27
- - name: Coveralls
28
- uses: coverallsapp/github-action@master
29
- with:
30
- github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -1,32 +0,0 @@
1
- /// <reference types="node" />
2
- import { EventEmitter } from 'events';
3
- export declare class CircuitBreaker extends EventEmitter {
4
- /** Decorate a class method with circuit breaker logic. */
5
- static Breakable<This, Args extends unknown[], Return>(breaker: CircuitBreaker): (fn: (this: This, ...args: Args) => Promise<Return>, _: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Promise<Return>>) => (this: This, ...args: Args) => Promise<Return>;
6
- private state;
7
- private requestCount;
8
- private failureCount;
9
- private successCount;
10
- private expiresAt;
11
- private errorThreshold;
12
- private resetTimeout;
13
- private successThreshold;
14
- constructor({ errorThreshold, resetTimeout, successThreshold, isBreakable }: {
15
- /** The number of consecutive errors, if reached the circuit state switches from "Closed" to "Open". By default equals 1. */
16
- errorThreshold?: number;
17
- /** The period of time, when passed the circuit state switches from "Open" to "HalfOpen". */
18
- resetTimeout: number;
19
- /** The number of consecutive successes, if reached the circuit state switches from "HalfOpen" to "Closed". By default equals 1. */
20
- successThreshold?: number;
21
- /** Error checking function. Answers the question: should the error be taken into account or not. By default, all errors are taken into account. */
22
- isBreakable?: (err: unknown) => boolean;
23
- });
24
- /** Execute a function with circuit breaker logic. */
25
- exec<T>(fn: () => Promise<T>): Promise<T>;
26
- private broken;
27
- private success;
28
- private failure;
29
- private setExpiresAt;
30
- private setState;
31
- private isBreakable;
32
- }
@@ -1,100 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CircuitBreaker = void 0;
4
- const events_1 = require("events");
5
- const CircuitState_1 = require("./CircuitState");
6
- const CircuitError_1 = require("./CircuitError");
7
- class CircuitBreaker extends events_1.EventEmitter {
8
- /** Decorate a class method with circuit breaker logic. */
9
- static Breakable(breaker) {
10
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
11
- return (fn, _) => {
12
- return function breakable(...args) {
13
- return breaker.exec(() => fn.apply(this, args));
14
- };
15
- };
16
- }
17
- constructor({ errorThreshold = 1, resetTimeout, successThreshold = 1, isBreakable }) {
18
- super();
19
- this.state = CircuitState_1.CircuitState.Closed;
20
- this.requestCount = 0;
21
- this.failureCount = 0;
22
- this.successCount = 0;
23
- this.expiresAt = 0;
24
- this.errorThreshold = errorThreshold;
25
- this.resetTimeout = resetTimeout;
26
- this.successThreshold = successThreshold;
27
- if (isBreakable) {
28
- this.isBreakable = isBreakable;
29
- }
30
- }
31
- /** Execute a function with circuit breaker logic. */
32
- async exec(fn) {
33
- if (this.broken()) {
34
- throw new CircuitError_1.CircuitError();
35
- }
36
- try {
37
- const v = await fn();
38
- this.success();
39
- return v;
40
- }
41
- catch (err) {
42
- if (this.isBreakable(err)) {
43
- this.failure();
44
- }
45
- throw err;
46
- }
47
- }
48
- broken() {
49
- if (this.state === CircuitState_1.CircuitState.Open) {
50
- if (this.expiresAt < Date.now()) {
51
- this.setState(CircuitState_1.CircuitState.HalfOpen);
52
- this.expiresAt = 0;
53
- }
54
- else {
55
- return true;
56
- }
57
- }
58
- else if (this.state === CircuitState_1.CircuitState.HalfOpen && this.requestCount >= this.successThreshold) {
59
- return true;
60
- }
61
- this.requestCount++;
62
- return false;
63
- }
64
- success() {
65
- if (this.state !== CircuitState_1.CircuitState.Open) {
66
- this.successCount++;
67
- if (this.state === CircuitState_1.CircuitState.HalfOpen && this.successCount >= this.successThreshold) {
68
- this.setState(CircuitState_1.CircuitState.Closed);
69
- }
70
- }
71
- }
72
- failure() {
73
- if (this.state === CircuitState_1.CircuitState.Closed) {
74
- this.failureCount++;
75
- if (this.failureCount >= this.errorThreshold) {
76
- this.setState(CircuitState_1.CircuitState.Open);
77
- this.setExpiresAt();
78
- }
79
- }
80
- else if (this.state === CircuitState_1.CircuitState.HalfOpen) {
81
- this.setState(CircuitState_1.CircuitState.Open);
82
- this.setExpiresAt();
83
- }
84
- }
85
- setExpiresAt() {
86
- this.expiresAt = Date.now() + this.resetTimeout;
87
- }
88
- setState(state) {
89
- this.state = state;
90
- this.requestCount = 0;
91
- this.failureCount = 0;
92
- this.successCount = 0;
93
- this.emit('state', this.state);
94
- }
95
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
96
- isBreakable(_) {
97
- return true;
98
- }
99
- }
100
- exports.CircuitBreaker = CircuitBreaker;