@teneo-protocol/sdk 1.0.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.
Files changed (281) hide show
  1. package/.dockerignore +14 -0
  2. package/.env.test.example +14 -0
  3. package/.eslintrc.json +26 -0
  4. package/.github/workflows/claude-code-review.yml +78 -0
  5. package/.github/workflows/claude-reviewer.yml +64 -0
  6. package/.github/workflows/publish-npm.yml +38 -0
  7. package/.github/workflows/push-to-main.yml +23 -0
  8. package/.node-version +1 -0
  9. package/.prettierrc +11 -0
  10. package/Dockerfile +25 -0
  11. package/LICENCE +661 -0
  12. package/README.md +709 -0
  13. package/dist/constants.d.ts +42 -0
  14. package/dist/constants.d.ts.map +1 -0
  15. package/dist/constants.js +45 -0
  16. package/dist/constants.js.map +1 -0
  17. package/dist/core/websocket-client.d.ts +261 -0
  18. package/dist/core/websocket-client.d.ts.map +1 -0
  19. package/dist/core/websocket-client.js +875 -0
  20. package/dist/core/websocket-client.js.map +1 -0
  21. package/dist/formatters/response-formatter.d.ts +354 -0
  22. package/dist/formatters/response-formatter.d.ts.map +1 -0
  23. package/dist/formatters/response-formatter.js +575 -0
  24. package/dist/formatters/response-formatter.js.map +1 -0
  25. package/dist/handlers/message-handler-registry.d.ts +155 -0
  26. package/dist/handlers/message-handler-registry.d.ts.map +1 -0
  27. package/dist/handlers/message-handler-registry.js +216 -0
  28. package/dist/handlers/message-handler-registry.js.map +1 -0
  29. package/dist/handlers/message-handlers/agent-selected-handler.d.ts +112 -0
  30. package/dist/handlers/message-handlers/agent-selected-handler.d.ts.map +1 -0
  31. package/dist/handlers/message-handlers/agent-selected-handler.js +40 -0
  32. package/dist/handlers/message-handlers/agent-selected-handler.js.map +1 -0
  33. package/dist/handlers/message-handlers/agents-list-handler.d.ts +14 -0
  34. package/dist/handlers/message-handlers/agents-list-handler.d.ts.map +1 -0
  35. package/dist/handlers/message-handlers/agents-list-handler.js +25 -0
  36. package/dist/handlers/message-handlers/agents-list-handler.js.map +1 -0
  37. package/dist/handlers/message-handlers/auth-error-handler.d.ts +71 -0
  38. package/dist/handlers/message-handlers/auth-error-handler.d.ts.map +1 -0
  39. package/dist/handlers/message-handlers/auth-error-handler.js +30 -0
  40. package/dist/handlers/message-handlers/auth-error-handler.js.map +1 -0
  41. package/dist/handlers/message-handlers/auth-message-handler.d.ts +18 -0
  42. package/dist/handlers/message-handlers/auth-message-handler.d.ts.map +1 -0
  43. package/dist/handlers/message-handlers/auth-message-handler.js +60 -0
  44. package/dist/handlers/message-handlers/auth-message-handler.js.map +1 -0
  45. package/dist/handlers/message-handlers/auth-required-handler.d.ts +76 -0
  46. package/dist/handlers/message-handlers/auth-required-handler.d.ts.map +1 -0
  47. package/dist/handlers/message-handlers/auth-required-handler.js +23 -0
  48. package/dist/handlers/message-handlers/auth-required-handler.js.map +1 -0
  49. package/dist/handlers/message-handlers/auth-success-handler.d.ts +18 -0
  50. package/dist/handlers/message-handlers/auth-success-handler.d.ts.map +1 -0
  51. package/dist/handlers/message-handlers/auth-success-handler.js +51 -0
  52. package/dist/handlers/message-handlers/auth-success-handler.js.map +1 -0
  53. package/dist/handlers/message-handlers/base-handler.d.ts +55 -0
  54. package/dist/handlers/message-handlers/base-handler.d.ts.map +1 -0
  55. package/dist/handlers/message-handlers/base-handler.js +83 -0
  56. package/dist/handlers/message-handlers/base-handler.js.map +1 -0
  57. package/dist/handlers/message-handlers/challenge-handler.d.ts +73 -0
  58. package/dist/handlers/message-handlers/challenge-handler.d.ts.map +1 -0
  59. package/dist/handlers/message-handlers/challenge-handler.js +47 -0
  60. package/dist/handlers/message-handlers/challenge-handler.js.map +1 -0
  61. package/dist/handlers/message-handlers/error-message-handler.d.ts +76 -0
  62. package/dist/handlers/message-handlers/error-message-handler.d.ts.map +1 -0
  63. package/dist/handlers/message-handlers/error-message-handler.js +29 -0
  64. package/dist/handlers/message-handlers/error-message-handler.js.map +1 -0
  65. package/dist/handlers/message-handlers/index.d.ts +28 -0
  66. package/dist/handlers/message-handlers/index.d.ts.map +1 -0
  67. package/dist/handlers/message-handlers/index.js +100 -0
  68. package/dist/handlers/message-handlers/index.js.map +1 -0
  69. package/dist/handlers/message-handlers/list-rooms-response-handler.d.ts +122 -0
  70. package/dist/handlers/message-handlers/list-rooms-response-handler.d.ts.map +1 -0
  71. package/dist/handlers/message-handlers/list-rooms-response-handler.js +30 -0
  72. package/dist/handlers/message-handlers/list-rooms-response-handler.js.map +1 -0
  73. package/dist/handlers/message-handlers/ping-pong-handler.d.ts +104 -0
  74. package/dist/handlers/message-handlers/ping-pong-handler.d.ts.map +1 -0
  75. package/dist/handlers/message-handlers/ping-pong-handler.js +36 -0
  76. package/dist/handlers/message-handlers/ping-pong-handler.js.map +1 -0
  77. package/dist/handlers/message-handlers/regular-message-handler.d.ts +56 -0
  78. package/dist/handlers/message-handlers/regular-message-handler.d.ts.map +1 -0
  79. package/dist/handlers/message-handlers/regular-message-handler.js +59 -0
  80. package/dist/handlers/message-handlers/regular-message-handler.js.map +1 -0
  81. package/dist/handlers/message-handlers/subscribe-response-handler.d.ts +81 -0
  82. package/dist/handlers/message-handlers/subscribe-response-handler.d.ts.map +1 -0
  83. package/dist/handlers/message-handlers/subscribe-response-handler.js +48 -0
  84. package/dist/handlers/message-handlers/subscribe-response-handler.js.map +1 -0
  85. package/dist/handlers/message-handlers/task-response-handler.d.ts +14 -0
  86. package/dist/handlers/message-handlers/task-response-handler.d.ts.map +1 -0
  87. package/dist/handlers/message-handlers/task-response-handler.js +44 -0
  88. package/dist/handlers/message-handlers/task-response-handler.js.map +1 -0
  89. package/dist/handlers/message-handlers/types.d.ts +51 -0
  90. package/dist/handlers/message-handlers/types.d.ts.map +1 -0
  91. package/dist/handlers/message-handlers/types.js +7 -0
  92. package/dist/handlers/message-handlers/types.js.map +1 -0
  93. package/dist/handlers/message-handlers/unsubscribe-response-handler.d.ts +81 -0
  94. package/dist/handlers/message-handlers/unsubscribe-response-handler.d.ts.map +1 -0
  95. package/dist/handlers/message-handlers/unsubscribe-response-handler.js +48 -0
  96. package/dist/handlers/message-handlers/unsubscribe-response-handler.js.map +1 -0
  97. package/dist/handlers/webhook-handler.d.ts +202 -0
  98. package/dist/handlers/webhook-handler.d.ts.map +1 -0
  99. package/dist/handlers/webhook-handler.js +511 -0
  100. package/dist/handlers/webhook-handler.js.map +1 -0
  101. package/dist/index.d.ts +71 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +217 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/managers/agent-registry.d.ts +173 -0
  106. package/dist/managers/agent-registry.d.ts.map +1 -0
  107. package/dist/managers/agent-registry.js +310 -0
  108. package/dist/managers/agent-registry.js.map +1 -0
  109. package/dist/managers/connection-manager.d.ts +134 -0
  110. package/dist/managers/connection-manager.d.ts.map +1 -0
  111. package/dist/managers/connection-manager.js +176 -0
  112. package/dist/managers/connection-manager.js.map +1 -0
  113. package/dist/managers/index.d.ts +9 -0
  114. package/dist/managers/index.d.ts.map +1 -0
  115. package/dist/managers/index.js +16 -0
  116. package/dist/managers/index.js.map +1 -0
  117. package/dist/managers/message-router.d.ts +112 -0
  118. package/dist/managers/message-router.d.ts.map +1 -0
  119. package/dist/managers/message-router.js +260 -0
  120. package/dist/managers/message-router.js.map +1 -0
  121. package/dist/managers/room-manager.d.ts +165 -0
  122. package/dist/managers/room-manager.d.ts.map +1 -0
  123. package/dist/managers/room-manager.js +227 -0
  124. package/dist/managers/room-manager.js.map +1 -0
  125. package/dist/teneo-sdk.d.ts +703 -0
  126. package/dist/teneo-sdk.d.ts.map +1 -0
  127. package/dist/teneo-sdk.js +907 -0
  128. package/dist/teneo-sdk.js.map +1 -0
  129. package/dist/types/config.d.ts +1047 -0
  130. package/dist/types/config.d.ts.map +1 -0
  131. package/dist/types/config.js +720 -0
  132. package/dist/types/config.js.map +1 -0
  133. package/dist/types/error-codes.d.ts +29 -0
  134. package/dist/types/error-codes.d.ts.map +1 -0
  135. package/dist/types/error-codes.js +41 -0
  136. package/dist/types/error-codes.js.map +1 -0
  137. package/dist/types/events.d.ts +616 -0
  138. package/dist/types/events.d.ts.map +1 -0
  139. package/dist/types/events.js +261 -0
  140. package/dist/types/events.js.map +1 -0
  141. package/dist/types/health.d.ts +40 -0
  142. package/dist/types/health.d.ts.map +1 -0
  143. package/dist/types/health.js +6 -0
  144. package/dist/types/health.js.map +1 -0
  145. package/dist/types/index.d.ts +10 -0
  146. package/dist/types/index.d.ts.map +1 -0
  147. package/dist/types/index.js +123 -0
  148. package/dist/types/index.js.map +1 -0
  149. package/dist/types/messages.d.ts +3734 -0
  150. package/dist/types/messages.d.ts.map +1 -0
  151. package/dist/types/messages.js +482 -0
  152. package/dist/types/messages.js.map +1 -0
  153. package/dist/types/validation.d.ts +81 -0
  154. package/dist/types/validation.d.ts.map +1 -0
  155. package/dist/types/validation.js +115 -0
  156. package/dist/types/validation.js.map +1 -0
  157. package/dist/utils/bounded-queue.d.ts +127 -0
  158. package/dist/utils/bounded-queue.d.ts.map +1 -0
  159. package/dist/utils/bounded-queue.js +181 -0
  160. package/dist/utils/bounded-queue.js.map +1 -0
  161. package/dist/utils/circuit-breaker.d.ts +141 -0
  162. package/dist/utils/circuit-breaker.d.ts.map +1 -0
  163. package/dist/utils/circuit-breaker.js +215 -0
  164. package/dist/utils/circuit-breaker.js.map +1 -0
  165. package/dist/utils/deduplication-cache.d.ts +110 -0
  166. package/dist/utils/deduplication-cache.d.ts.map +1 -0
  167. package/dist/utils/deduplication-cache.js +177 -0
  168. package/dist/utils/deduplication-cache.js.map +1 -0
  169. package/dist/utils/event-waiter.d.ts +101 -0
  170. package/dist/utils/event-waiter.d.ts.map +1 -0
  171. package/dist/utils/event-waiter.js +118 -0
  172. package/dist/utils/event-waiter.js.map +1 -0
  173. package/dist/utils/index.d.ts +51 -0
  174. package/dist/utils/index.d.ts.map +1 -0
  175. package/dist/utils/index.js +72 -0
  176. package/dist/utils/index.js.map +1 -0
  177. package/dist/utils/logger.d.ts +22 -0
  178. package/dist/utils/logger.d.ts.map +1 -0
  179. package/dist/utils/logger.js +91 -0
  180. package/dist/utils/logger.js.map +1 -0
  181. package/dist/utils/rate-limiter.d.ts +122 -0
  182. package/dist/utils/rate-limiter.d.ts.map +1 -0
  183. package/dist/utils/rate-limiter.js +190 -0
  184. package/dist/utils/rate-limiter.js.map +1 -0
  185. package/dist/utils/retry-policy.d.ts +191 -0
  186. package/dist/utils/retry-policy.d.ts.map +1 -0
  187. package/dist/utils/retry-policy.js +225 -0
  188. package/dist/utils/retry-policy.js.map +1 -0
  189. package/dist/utils/secure-private-key.d.ts +113 -0
  190. package/dist/utils/secure-private-key.d.ts.map +1 -0
  191. package/dist/utils/secure-private-key.js +188 -0
  192. package/dist/utils/secure-private-key.js.map +1 -0
  193. package/dist/utils/signature-verifier.d.ts +143 -0
  194. package/dist/utils/signature-verifier.d.ts.map +1 -0
  195. package/dist/utils/signature-verifier.js +238 -0
  196. package/dist/utils/signature-verifier.js.map +1 -0
  197. package/dist/utils/ssrf-validator.d.ts +36 -0
  198. package/dist/utils/ssrf-validator.d.ts.map +1 -0
  199. package/dist/utils/ssrf-validator.js +195 -0
  200. package/dist/utils/ssrf-validator.js.map +1 -0
  201. package/examples/.env.example +17 -0
  202. package/examples/basic-usage.ts +211 -0
  203. package/examples/production-dashboard/.env.example +153 -0
  204. package/examples/production-dashboard/package.json +39 -0
  205. package/examples/production-dashboard/public/dashboard.html +642 -0
  206. package/examples/production-dashboard/server.ts +753 -0
  207. package/examples/webhook-integration.ts +239 -0
  208. package/examples/x-influencer-battle-redesign.html +1065 -0
  209. package/examples/x-influencer-battle-server.ts +217 -0
  210. package/examples/x-influencer-battle.html +787 -0
  211. package/package.json +65 -0
  212. package/src/constants.ts +43 -0
  213. package/src/core/websocket-client.test.ts +512 -0
  214. package/src/core/websocket-client.ts +1056 -0
  215. package/src/formatters/response-formatter.test.ts +571 -0
  216. package/src/formatters/response-formatter.ts +677 -0
  217. package/src/handlers/message-handler-registry.ts +239 -0
  218. package/src/handlers/message-handlers/agent-selected-handler.ts +40 -0
  219. package/src/handlers/message-handlers/agents-list-handler.ts +26 -0
  220. package/src/handlers/message-handlers/auth-error-handler.ts +31 -0
  221. package/src/handlers/message-handlers/auth-message-handler.ts +66 -0
  222. package/src/handlers/message-handlers/auth-required-handler.ts +23 -0
  223. package/src/handlers/message-handlers/auth-success-handler.ts +57 -0
  224. package/src/handlers/message-handlers/base-handler.ts +101 -0
  225. package/src/handlers/message-handlers/challenge-handler.ts +57 -0
  226. package/src/handlers/message-handlers/error-message-handler.ts +27 -0
  227. package/src/handlers/message-handlers/index.ts +77 -0
  228. package/src/handlers/message-handlers/list-rooms-response-handler.ts +28 -0
  229. package/src/handlers/message-handlers/ping-pong-handler.ts +30 -0
  230. package/src/handlers/message-handlers/regular-message-handler.ts +65 -0
  231. package/src/handlers/message-handlers/subscribe-response-handler.ts +47 -0
  232. package/src/handlers/message-handlers/task-response-handler.ts +45 -0
  233. package/src/handlers/message-handlers/types.ts +77 -0
  234. package/src/handlers/message-handlers/unsubscribe-response-handler.ts +47 -0
  235. package/src/handlers/webhook-handler.test.ts +789 -0
  236. package/src/handlers/webhook-handler.ts +576 -0
  237. package/src/index.ts +269 -0
  238. package/src/managers/agent-registry.test.ts +466 -0
  239. package/src/managers/agent-registry.ts +347 -0
  240. package/src/managers/connection-manager.ts +195 -0
  241. package/src/managers/index.ts +9 -0
  242. package/src/managers/message-router.ts +349 -0
  243. package/src/managers/room-manager.ts +248 -0
  244. package/src/teneo-sdk.ts +1022 -0
  245. package/src/types/config.test.ts +325 -0
  246. package/src/types/config.ts +799 -0
  247. package/src/types/error-codes.ts +44 -0
  248. package/src/types/events.test.ts +302 -0
  249. package/src/types/events.ts +382 -0
  250. package/src/types/health.ts +46 -0
  251. package/src/types/index.ts +199 -0
  252. package/src/types/messages.test.ts +660 -0
  253. package/src/types/messages.ts +570 -0
  254. package/src/types/validation.ts +123 -0
  255. package/src/utils/bounded-queue.test.ts +356 -0
  256. package/src/utils/bounded-queue.ts +205 -0
  257. package/src/utils/circuit-breaker.test.ts +394 -0
  258. package/src/utils/circuit-breaker.ts +262 -0
  259. package/src/utils/deduplication-cache.test.ts +380 -0
  260. package/src/utils/deduplication-cache.ts +198 -0
  261. package/src/utils/event-waiter.test.ts +381 -0
  262. package/src/utils/event-waiter.ts +172 -0
  263. package/src/utils/index.ts +74 -0
  264. package/src/utils/logger.ts +87 -0
  265. package/src/utils/rate-limiter.test.ts +341 -0
  266. package/src/utils/rate-limiter.ts +211 -0
  267. package/src/utils/retry-policy.test.ts +558 -0
  268. package/src/utils/retry-policy.ts +272 -0
  269. package/src/utils/secure-private-key.test.ts +356 -0
  270. package/src/utils/secure-private-key.ts +205 -0
  271. package/src/utils/signature-verifier.test.ts +464 -0
  272. package/src/utils/signature-verifier.ts +298 -0
  273. package/src/utils/ssrf-validator.test.ts +372 -0
  274. package/src/utils/ssrf-validator.ts +224 -0
  275. package/tests/integration/real-server.test.ts +740 -0
  276. package/tests/integration/websocket.test.ts +381 -0
  277. package/tests/integration-setup.ts +16 -0
  278. package/tests/setup.ts +34 -0
  279. package/tsconfig.json +32 -0
  280. package/vitest.config.ts +42 -0
  281. package/vitest.integration.config.ts +23 -0
@@ -0,0 +1,394 @@
1
+ /**
2
+ * Tests for Circuit Breaker
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, vi } from "vitest";
6
+ import { CircuitBreaker, CircuitBreakerError, type CircuitState } from "./circuit-breaker";
7
+
8
+ describe("CircuitBreaker", () => {
9
+ describe("constructor", () => {
10
+ it("should create circuit breaker with default options", () => {
11
+ const breaker = new CircuitBreaker();
12
+ const config = breaker.getConfig();
13
+
14
+ expect(config.failureThreshold).toBe(5);
15
+ expect(config.successThreshold).toBe(2);
16
+ expect(config.timeout).toBe(60000);
17
+ expect(config.windowSize).toBe(60000);
18
+ });
19
+
20
+ it("should create circuit breaker with custom options", () => {
21
+ const breaker = new CircuitBreaker({
22
+ failureThreshold: 3,
23
+ successThreshold: 1,
24
+ timeout: 30000,
25
+ windowSize: 30000
26
+ });
27
+ const config = breaker.getConfig();
28
+
29
+ expect(config.failureThreshold).toBe(3);
30
+ expect(config.successThreshold).toBe(1);
31
+ expect(config.timeout).toBe(30000);
32
+ expect(config.windowSize).toBe(30000);
33
+ });
34
+
35
+ it("should throw error if failureThreshold is less than 1", () => {
36
+ expect(() => new CircuitBreaker({ failureThreshold: 0 })).toThrow(
37
+ "failureThreshold must be at least 1"
38
+ );
39
+ });
40
+
41
+ it("should throw error if successThreshold is less than 1", () => {
42
+ expect(() => new CircuitBreaker({ successThreshold: 0 })).toThrow(
43
+ "successThreshold must be at least 1"
44
+ );
45
+ });
46
+ });
47
+
48
+ describe("CLOSED state", () => {
49
+ let breaker: CircuitBreaker;
50
+
51
+ beforeEach(() => {
52
+ breaker = new CircuitBreaker({ failureThreshold: 3 });
53
+ });
54
+
55
+ it("should start in CLOSED state", () => {
56
+ const state = breaker.getState();
57
+ expect(state.state).toBe("CLOSED");
58
+ expect(state.failureCount).toBe(0);
59
+ });
60
+
61
+ it("should execute operations successfully in CLOSED state", async () => {
62
+ const operation = vi.fn(async () => "success");
63
+
64
+ const result = await breaker.execute(operation);
65
+
66
+ expect(result).toBe("success");
67
+ expect(operation).toHaveBeenCalledTimes(1);
68
+ });
69
+
70
+ it("should track failures in CLOSED state", async () => {
71
+ const operation = vi.fn(async () => {
72
+ throw new Error("operation failed");
73
+ });
74
+
75
+ await expect(breaker.execute(operation)).rejects.toThrow("operation failed");
76
+
77
+ const state = breaker.getState();
78
+ expect(state.failureCount).toBe(1);
79
+ expect(state.state).toBe("CLOSED");
80
+ });
81
+
82
+ it("should open circuit after reaching failure threshold", async () => {
83
+ const operation = vi.fn(async () => {
84
+ throw new Error("operation failed");
85
+ });
86
+
87
+ // Fail 3 times (threshold = 3)
88
+ for (let i = 0; i < 3; i++) {
89
+ await expect(breaker.execute(operation)).rejects.toThrow("operation failed");
90
+ }
91
+
92
+ const state = breaker.getState();
93
+ expect(state.state).toBe("OPEN");
94
+ expect(state.failureCount).toBe(3);
95
+ });
96
+
97
+ it("should only count failures within the time window", async () => {
98
+ const breaker = new CircuitBreaker({
99
+ failureThreshold: 3,
100
+ windowSize: 100 // 100ms window
101
+ });
102
+
103
+ const operation = vi.fn(async () => {
104
+ throw new Error("failed");
105
+ });
106
+
107
+ // First failure
108
+ await expect(breaker.execute(operation)).rejects.toThrow("failed");
109
+ expect(breaker.getState().failureCount).toBe(1);
110
+
111
+ // Wait for window to expire
112
+ await new Promise((resolve) => setTimeout(resolve, 150));
113
+
114
+ // Second failure (first should be outside window now)
115
+ await expect(breaker.execute(operation)).rejects.toThrow("failed");
116
+ expect(breaker.getState().failureCount).toBe(1);
117
+ expect(breaker.getState().state).toBe("CLOSED");
118
+ });
119
+ });
120
+
121
+ describe("OPEN state", () => {
122
+ let breaker: CircuitBreaker;
123
+
124
+ beforeEach(async () => {
125
+ breaker = new CircuitBreaker({
126
+ failureThreshold: 2,
127
+ timeout: 1000
128
+ });
129
+
130
+ // Trigger circuit to open
131
+ const failOp = vi.fn(async () => {
132
+ throw new Error("fail");
133
+ });
134
+ for (let i = 0; i < 2; i++) {
135
+ await expect(breaker.execute(failOp)).rejects.toThrow("fail");
136
+ }
137
+ });
138
+
139
+ it("should be in OPEN state after threshold failures", () => {
140
+ const state = breaker.getState();
141
+ expect(state.state).toBe("OPEN");
142
+ });
143
+
144
+ it("should fail fast with CircuitBreakerError in OPEN state", async () => {
145
+ const operation = vi.fn(async () => "success");
146
+
147
+ await expect(breaker.execute(operation)).rejects.toThrow(CircuitBreakerError);
148
+ await expect(breaker.execute(operation)).rejects.toThrow("Circuit breaker is OPEN");
149
+
150
+ // Operation should not be called
151
+ expect(operation).not.toHaveBeenCalled();
152
+ });
153
+
154
+ it("should transition to HALF_OPEN after timeout", async () => {
155
+ expect(breaker.getState().state).toBe("OPEN");
156
+
157
+ // Wait for timeout
158
+ await new Promise((resolve) => setTimeout(resolve, 1100));
159
+
160
+ // Try operation - should be in HALF_OPEN now
161
+ const operation = vi.fn(async () => "success");
162
+ await breaker.execute(operation);
163
+
164
+ expect(operation).toHaveBeenCalled();
165
+ });
166
+
167
+ it("should set nextAttemptTime when opening", () => {
168
+ const state = breaker.getState();
169
+ expect(state.nextAttemptTime).toBeDefined();
170
+ expect(state.nextAttemptTime! > Date.now()).toBe(true);
171
+ });
172
+ });
173
+
174
+ describe("HALF_OPEN state", () => {
175
+ let breaker: CircuitBreaker;
176
+
177
+ beforeEach(async () => {
178
+ breaker = new CircuitBreaker({
179
+ failureThreshold: 2,
180
+ successThreshold: 2,
181
+ timeout: 100 // Short timeout for testing
182
+ });
183
+
184
+ // Open the circuit
185
+ const failOp = vi.fn(async () => {
186
+ throw new Error("fail");
187
+ });
188
+ for (let i = 0; i < 2; i++) {
189
+ await expect(breaker.execute(failOp)).rejects.toThrow("fail");
190
+ }
191
+
192
+ // Wait for timeout to move to HALF_OPEN
193
+ await new Promise((resolve) => setTimeout(resolve, 150));
194
+ });
195
+
196
+ it("should allow operations in HALF_OPEN state", async () => {
197
+ const operation = vi.fn(async () => "success");
198
+
199
+ const result = await breaker.execute(operation);
200
+
201
+ expect(result).toBe("success");
202
+ expect(operation).toHaveBeenCalled();
203
+ });
204
+
205
+ it("should close circuit after enough successes", async () => {
206
+ const operation = vi.fn(async () => "success");
207
+
208
+ // Need 2 successes (successThreshold = 2)
209
+ await breaker.execute(operation);
210
+ expect(breaker.getState().state).toBe("HALF_OPEN");
211
+
212
+ await breaker.execute(operation);
213
+ expect(breaker.getState().state).toBe("CLOSED");
214
+ expect(breaker.getState().failureCount).toBe(0);
215
+ });
216
+
217
+ it("should reopen circuit on failure in HALF_OPEN", async () => {
218
+ const operation = vi.fn(async () => {
219
+ throw new Error("failed again");
220
+ });
221
+
222
+ await expect(breaker.execute(operation)).rejects.toThrow("failed again");
223
+
224
+ const state = breaker.getState();
225
+ expect(state.state).toBe("OPEN");
226
+ expect(state.nextAttemptTime).toBeDefined();
227
+ });
228
+
229
+ it("should reset success count when reopening", async () => {
230
+ const successOp = vi.fn(async () => "success");
231
+ const failOp = vi.fn(async () => {
232
+ throw new Error("fail");
233
+ });
234
+
235
+ // One success
236
+ await breaker.execute(successOp);
237
+ expect(breaker.getState().successCount).toBe(1);
238
+
239
+ // Then fail - should reopen and reset success count
240
+ await expect(breaker.execute(failOp)).rejects.toThrow("fail");
241
+ expect(breaker.getState().state).toBe("OPEN");
242
+ expect(breaker.getState().successCount).toBe(0);
243
+ });
244
+ });
245
+
246
+ describe("reset", () => {
247
+ it("should reset circuit to CLOSED state", async () => {
248
+ const breaker = new CircuitBreaker({ failureThreshold: 1 });
249
+
250
+ // Open circuit
251
+ const failOp = vi.fn(async () => {
252
+ throw new Error("fail");
253
+ });
254
+ await expect(breaker.execute(failOp)).rejects.toThrow("fail");
255
+
256
+ expect(breaker.getState().state).toBe("OPEN");
257
+
258
+ // Reset
259
+ breaker.reset();
260
+
261
+ const state = breaker.getState();
262
+ expect(state.state).toBe("CLOSED");
263
+ expect(state.failureCount).toBe(0);
264
+ expect(state.successCount).toBe(0);
265
+ expect(state.lastFailureTime).toBeUndefined();
266
+ expect(state.nextAttemptTime).toBeUndefined();
267
+ });
268
+
269
+ it("should allow operations after reset", async () => {
270
+ const breaker = new CircuitBreaker({ failureThreshold: 1 });
271
+
272
+ // Open circuit
273
+ await expect(
274
+ breaker.execute(async () => {
275
+ throw new Error("fail");
276
+ })
277
+ ).rejects.toThrow("fail");
278
+
279
+ breaker.reset();
280
+
281
+ // Should work now
282
+ const result = await breaker.execute(async () => "success");
283
+ expect(result).toBe("success");
284
+ });
285
+ });
286
+
287
+ describe("getState", () => {
288
+ it("should return current state and metrics", async () => {
289
+ const breaker = new CircuitBreaker();
290
+
291
+ const state1 = breaker.getState();
292
+ expect(state1).toMatchObject({
293
+ state: "CLOSED",
294
+ failureCount: 0,
295
+ successCount: 0
296
+ });
297
+
298
+ // Cause a failure
299
+ await expect(
300
+ breaker.execute(async () => {
301
+ throw new Error("fail");
302
+ })
303
+ ).rejects.toThrow("fail");
304
+
305
+ const state2 = breaker.getState();
306
+ expect(state2.failureCount).toBe(1);
307
+ expect(state2.lastFailureTime).toBeDefined();
308
+ });
309
+ });
310
+
311
+ describe("edge cases", () => {
312
+ it("should handle rapid failures", async () => {
313
+ const breaker = new CircuitBreaker({ failureThreshold: 10 });
314
+
315
+ const operation = vi.fn(async () => {
316
+ throw new Error("fail");
317
+ });
318
+
319
+ // Rapid failures
320
+ const promises = Array(10)
321
+ .fill(0)
322
+ .map(() => breaker.execute(operation).catch(() => {}));
323
+
324
+ await Promise.all(promises);
325
+
326
+ expect(breaker.getState().state).toBe("OPEN");
327
+ });
328
+
329
+ it("should handle mixed success/failure in CLOSED", async () => {
330
+ const breaker = new CircuitBreaker({ failureThreshold: 3 });
331
+
332
+ let shouldFail = true;
333
+ const operation = vi.fn(async () => {
334
+ if (shouldFail) {
335
+ throw new Error("fail");
336
+ }
337
+ return "success";
338
+ });
339
+
340
+ // Fail
341
+ await expect(breaker.execute(operation)).rejects.toThrow("fail");
342
+ expect(breaker.getState().failureCount).toBe(1);
343
+
344
+ // Succeed
345
+ shouldFail = false;
346
+ await breaker.execute(operation);
347
+ expect(breaker.getState().failureCount).toBe(0);
348
+
349
+ // Should still be CLOSED
350
+ expect(breaker.getState().state).toBe("CLOSED");
351
+ });
352
+
353
+ it("should handle synchronous errors", async () => {
354
+ const breaker = new CircuitBreaker({ failureThreshold: 1 });
355
+
356
+ await expect(
357
+ breaker.execute(async () => {
358
+ throw new Error("sync error");
359
+ })
360
+ ).rejects.toThrow("sync error");
361
+
362
+ expect(breaker.getState().state).toBe("OPEN");
363
+ });
364
+
365
+ it("should handle operations that return undefined", async () => {
366
+ const breaker = new CircuitBreaker();
367
+
368
+ const result = await breaker.execute(async () => undefined);
369
+
370
+ expect(result).toBeUndefined();
371
+ expect(breaker.getState().state).toBe("CLOSED");
372
+ });
373
+ });
374
+
375
+ describe("CircuitBreakerError", () => {
376
+ it("should be instanceof Error", () => {
377
+ const error = new CircuitBreakerError("Test", "OPEN");
378
+ expect(error).toBeInstanceOf(Error);
379
+ expect(error).toBeInstanceOf(CircuitBreakerError);
380
+ });
381
+
382
+ it("should have correct name and state", () => {
383
+ const error = new CircuitBreakerError("Test", "OPEN");
384
+ expect(error.name).toBe("CircuitBreakerError");
385
+ expect(error.state).toBe("OPEN");
386
+ });
387
+
388
+ it("should preserve error message", () => {
389
+ const message = "Custom message";
390
+ const error = new CircuitBreakerError(message, "OPEN");
391
+ expect(error.message).toBe(message);
392
+ });
393
+ });
394
+ });
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Circuit Breaker Pattern Implementation
3
+ * Provides fault tolerance by preventing cascading failures
4
+ *
5
+ * States:
6
+ * - CLOSED: Normal operation, requests pass through
7
+ * - OPEN: Failure threshold exceeded, requests fail fast
8
+ * - HALF_OPEN: Testing if service recovered, limited requests allowed
9
+ */
10
+
11
+ /**
12
+ * Circuit breaker states
13
+ */
14
+ export type CircuitState = "CLOSED" | "OPEN" | "HALF_OPEN";
15
+
16
+ /**
17
+ * Circuit breaker configuration options
18
+ */
19
+ export interface CircuitBreakerOptions {
20
+ /** Number of failures before opening circuit (default: 5) */
21
+ failureThreshold?: number;
22
+ /** Success count needed to close from half-open (default: 2) */
23
+ successThreshold?: number;
24
+ /** Time in ms to wait before trying half-open (default: 60000) */
25
+ timeout?: number;
26
+ /** Window size in ms for tracking failures (default: 60000) */
27
+ windowSize?: number;
28
+ }
29
+
30
+ /**
31
+ * Error thrown when circuit is open
32
+ */
33
+ export class CircuitBreakerError extends Error {
34
+ constructor(message: string, public readonly state: CircuitState) {
35
+ super(message);
36
+ this.name = "CircuitBreakerError";
37
+
38
+ if (Error.captureStackTrace) {
39
+ Error.captureStackTrace(this, CircuitBreakerError);
40
+ }
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Circuit breaker for fault tolerance
46
+ * Prevents cascading failures by failing fast when errors are detected
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * const breaker = new CircuitBreaker({
51
+ * failureThreshold: 5,
52
+ * timeout: 60000
53
+ * });
54
+ *
55
+ * try {
56
+ * await breaker.execute(async () => {
57
+ * return await fetch('https://api.example.com/data');
58
+ * });
59
+ * } catch (error) {
60
+ * if (error instanceof CircuitBreakerError) {
61
+ * console.log('Circuit is open, failing fast');
62
+ * }
63
+ * }
64
+ * ```
65
+ */
66
+ export class CircuitBreaker {
67
+ private state: CircuitState = "CLOSED";
68
+ private failureCount = 0;
69
+ private successCount = 0;
70
+ private lastFailureTime?: number;
71
+ private nextAttemptTime?: number;
72
+ private readonly failures: number[] = []; // Timestamps of failures
73
+
74
+ private readonly failureThreshold: number;
75
+ private readonly successThreshold: number;
76
+ private readonly timeout: number;
77
+ private readonly windowSize: number;
78
+
79
+ /**
80
+ * Creates a new circuit breaker
81
+ *
82
+ * @param options - Configuration options
83
+ * @throws {Error} If thresholds are less than 1
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * const breaker = new CircuitBreaker({
88
+ * failureThreshold: 5, // Open after 5 failures
89
+ * successThreshold: 2, // Close after 2 successes
90
+ * timeout: 60000, // Wait 60s before trying again
91
+ * windowSize: 60000 // Track failures in 60s window
92
+ * });
93
+ * ```
94
+ */
95
+ constructor(options: CircuitBreakerOptions = {}) {
96
+ this.failureThreshold = options.failureThreshold ?? 5;
97
+ this.successThreshold = options.successThreshold ?? 2;
98
+ this.timeout = options.timeout ?? 60000;
99
+ this.windowSize = options.windowSize ?? 60000;
100
+
101
+ if (this.failureThreshold < 1) {
102
+ throw new Error("CircuitBreaker failureThreshold must be at least 1");
103
+ }
104
+ if (this.successThreshold < 1) {
105
+ throw new Error("CircuitBreaker successThreshold must be at least 1");
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Execute an operation through the circuit breaker
111
+ *
112
+ * @template T - Return type of the operation
113
+ * @param operation - Async operation to execute
114
+ * @returns Promise resolving to operation result
115
+ * @throws {CircuitBreakerError} If circuit is open
116
+ * @throws Operation error if operation fails
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * const result = await breaker.execute(async () => {
121
+ * const response = await fetch(url);
122
+ * return await response.json();
123
+ * });
124
+ * ```
125
+ */
126
+ public async execute<T>(operation: () => Promise<T>): Promise<T> {
127
+ // Check if we should try to close the circuit
128
+ this.tryTransitionToHalfOpen();
129
+
130
+ // If circuit is open, fail fast
131
+ if (this.state === "OPEN") {
132
+ throw new CircuitBreakerError("Circuit breaker is OPEN", this.state);
133
+ }
134
+
135
+ try {
136
+ const result = await operation();
137
+ this.onSuccess();
138
+ return result;
139
+ } catch (error) {
140
+ this.onFailure();
141
+ throw error;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Get current circuit breaker state and metrics
147
+ *
148
+ * @returns State object with status and counters
149
+ */
150
+ public getState(): {
151
+ state: CircuitState;
152
+ failureCount: number;
153
+ successCount: number;
154
+ lastFailureTime?: number;
155
+ nextAttemptTime?: number;
156
+ } {
157
+ return {
158
+ state: this.state,
159
+ failureCount: this.failureCount,
160
+ successCount: this.successCount,
161
+ lastFailureTime: this.lastFailureTime,
162
+ nextAttemptTime: this.nextAttemptTime
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Get circuit breaker configuration
168
+ */
169
+ public getConfig(): {
170
+ failureThreshold: number;
171
+ successThreshold: number;
172
+ timeout: number;
173
+ windowSize: number;
174
+ } {
175
+ return {
176
+ failureThreshold: this.failureThreshold,
177
+ successThreshold: this.successThreshold,
178
+ timeout: this.timeout,
179
+ windowSize: this.windowSize
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Manually reset the circuit breaker to CLOSED state
185
+ * Useful for recovery scenarios or testing
186
+ */
187
+ public reset(): void {
188
+ this.state = "CLOSED";
189
+ this.failureCount = 0;
190
+ this.successCount = 0;
191
+ this.lastFailureTime = undefined;
192
+ this.nextAttemptTime = undefined;
193
+ this.failures.length = 0;
194
+ }
195
+
196
+ /**
197
+ * Try to transition from OPEN to HALF_OPEN if timeout has passed
198
+ */
199
+ private tryTransitionToHalfOpen(): void {
200
+ if (this.state === "OPEN" && this.nextAttemptTime) {
201
+ if (Date.now() >= this.nextAttemptTime) {
202
+ this.state = "HALF_OPEN";
203
+ this.successCount = 0;
204
+ }
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Handle successful operation
210
+ */
211
+ private onSuccess(): void {
212
+ if (this.state === "HALF_OPEN") {
213
+ this.successCount++;
214
+ if (this.successCount >= this.successThreshold) {
215
+ // Close the circuit - service has recovered
216
+ this.state = "CLOSED";
217
+ this.failureCount = 0;
218
+ this.successCount = 0;
219
+ this.failures.length = 0;
220
+ this.lastFailureTime = undefined;
221
+ this.nextAttemptTime = undefined;
222
+ }
223
+ } else if (this.state === "CLOSED") {
224
+ // Reset failure count on success in CLOSED state
225
+ this.failureCount = Math.max(0, this.failureCount - 1);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Handle failed operation
231
+ */
232
+ private onFailure(): void {
233
+ const now = Date.now();
234
+ this.lastFailureTime = now;
235
+
236
+ if (this.state === "HALF_OPEN") {
237
+ // Failed while testing - reopen circuit
238
+ this.state = "OPEN";
239
+ this.nextAttemptTime = now + this.timeout;
240
+ this.successCount = 0;
241
+ return;
242
+ }
243
+
244
+ // Track failure with timestamp
245
+ this.failures.push(now);
246
+
247
+ // Remove failures outside the window
248
+ const windowStart = now - this.windowSize;
249
+ while (this.failures.length > 0 && this.failures[0]! < windowStart) {
250
+ this.failures.shift();
251
+ }
252
+
253
+ // Count failures in current window
254
+ this.failureCount = this.failures.length;
255
+
256
+ // Open circuit if threshold exceeded
257
+ if (this.failureCount >= this.failureThreshold) {
258
+ this.state = "OPEN";
259
+ this.nextAttemptTime = now + this.timeout;
260
+ }
261
+ }
262
+ }