@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,356 @@
1
+ /**
2
+ * Tests for Bounded Queue
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from "vitest";
6
+ import { BoundedQueue, QueueOverflowError, type OverflowStrategy } from "./bounded-queue";
7
+
8
+ describe("BoundedQueue", () => {
9
+ describe("constructor", () => {
10
+ it("should create a queue with specified max size", () => {
11
+ const queue = new BoundedQueue<number>(10);
12
+ expect(queue.getMaxSize()).toBe(10);
13
+ expect(queue.size()).toBe(0);
14
+ expect(queue.isEmpty()).toBe(true);
15
+ });
16
+
17
+ it("should use drop-oldest as default strategy", () => {
18
+ const queue = new BoundedQueue<number>(10);
19
+ expect(queue.getStrategy()).toBe("drop-oldest");
20
+ });
21
+
22
+ it("should accept custom strategy", () => {
23
+ const queue = new BoundedQueue<number>(10, "drop-newest");
24
+ expect(queue.getStrategy()).toBe("drop-newest");
25
+ });
26
+
27
+ it("should throw error if maxSize is less than 1", () => {
28
+ expect(() => new BoundedQueue<number>(0)).toThrow("maxSize must be at least 1");
29
+ expect(() => new BoundedQueue<number>(-1)).toThrow("maxSize must be at least 1");
30
+ });
31
+ });
32
+
33
+ describe("basic operations", () => {
34
+ let queue: BoundedQueue<string>;
35
+
36
+ beforeEach(() => {
37
+ queue = new BoundedQueue<string>(5);
38
+ });
39
+
40
+ it("should push items to the queue", () => {
41
+ expect(queue.push("a")).toBe(true);
42
+ expect(queue.push("b")).toBe(true);
43
+ expect(queue.size()).toBe(2);
44
+ });
45
+
46
+ it("should shift items from the queue (FIFO)", () => {
47
+ queue.push("a");
48
+ queue.push("b");
49
+ queue.push("c");
50
+
51
+ expect(queue.shift()).toBe("a");
52
+ expect(queue.shift()).toBe("b");
53
+ expect(queue.shift()).toBe("c");
54
+ expect(queue.size()).toBe(0);
55
+ });
56
+
57
+ it("should return undefined when shifting from empty queue", () => {
58
+ expect(queue.shift()).toBeUndefined();
59
+ });
60
+
61
+ it("should peek at front item without removing", () => {
62
+ queue.push("a");
63
+ queue.push("b");
64
+
65
+ expect(queue.peek()).toBe("a");
66
+ expect(queue.size()).toBe(2); // Size unchanged
67
+ expect(queue.peek()).toBe("a"); // Still the same item
68
+ });
69
+
70
+ it("should return undefined when peeking at empty queue", () => {
71
+ expect(queue.peek()).toBeUndefined();
72
+ });
73
+
74
+ it("should clear all items", () => {
75
+ queue.push("a");
76
+ queue.push("b");
77
+ queue.push("c");
78
+ expect(queue.size()).toBe(3);
79
+
80
+ queue.clear();
81
+ expect(queue.size()).toBe(0);
82
+ expect(queue.isEmpty()).toBe(true);
83
+ });
84
+
85
+ it("should detect when queue is full", () => {
86
+ expect(queue.isFull()).toBe(false);
87
+
88
+ for (let i = 0; i < 5; i++) {
89
+ queue.push(`item-${i}`);
90
+ }
91
+
92
+ expect(queue.isFull()).toBe(true);
93
+ });
94
+
95
+ it("should detect when queue is empty", () => {
96
+ expect(queue.isEmpty()).toBe(true);
97
+ queue.push("a");
98
+ expect(queue.isEmpty()).toBe(false);
99
+ queue.shift();
100
+ expect(queue.isEmpty()).toBe(true);
101
+ });
102
+
103
+ it("should convert to array", () => {
104
+ queue.push("a");
105
+ queue.push("b");
106
+ queue.push("c");
107
+
108
+ const arr = queue.toArray();
109
+ expect(arr).toEqual(["a", "b", "c"]);
110
+
111
+ // Should be a copy (not affect original)
112
+ arr.push("d");
113
+ expect(queue.size()).toBe(3);
114
+ });
115
+ });
116
+
117
+ describe("drop-oldest strategy", () => {
118
+ let queue: BoundedQueue<number>;
119
+
120
+ beforeEach(() => {
121
+ queue = new BoundedQueue<number>(3, "drop-oldest");
122
+ });
123
+
124
+ it("should drop oldest item when queue is full", () => {
125
+ queue.push(1);
126
+ queue.push(2);
127
+ queue.push(3);
128
+
129
+ // Queue: [1, 2, 3]
130
+ expect(queue.isFull()).toBe(true);
131
+
132
+ // Push 4 should drop 1
133
+ const result = queue.push(4);
134
+
135
+ expect(result).toBe(true);
136
+ expect(queue.size()).toBe(3);
137
+ expect(queue.toArray()).toEqual([2, 3, 4]);
138
+ });
139
+
140
+ it("should continue dropping oldest on subsequent pushes", () => {
141
+ queue.push(1);
142
+ queue.push(2);
143
+ queue.push(3);
144
+ queue.push(4); // Drops 1
145
+ queue.push(5); // Drops 2
146
+ queue.push(6); // Drops 3
147
+
148
+ expect(queue.toArray()).toEqual([4, 5, 6]);
149
+ });
150
+
151
+ it("should maintain FIFO order after dropping", () => {
152
+ queue.push(1);
153
+ queue.push(2);
154
+ queue.push(3);
155
+ queue.push(4); // Drops 1
156
+
157
+ expect(queue.shift()).toBe(2);
158
+ expect(queue.shift()).toBe(3);
159
+ expect(queue.shift()).toBe(4);
160
+ });
161
+ });
162
+
163
+ describe("drop-newest strategy", () => {
164
+ let queue: BoundedQueue<number>;
165
+
166
+ beforeEach(() => {
167
+ queue = new BoundedQueue<number>(3, "drop-newest");
168
+ });
169
+
170
+ it("should reject new item when queue is full", () => {
171
+ queue.push(1);
172
+ queue.push(2);
173
+ queue.push(3);
174
+
175
+ // Queue: [1, 2, 3]
176
+ expect(queue.isFull()).toBe(true);
177
+
178
+ // Try to push 4
179
+ const result = queue.push(4);
180
+
181
+ expect(result).toBe(false); // Rejected
182
+ expect(queue.size()).toBe(3);
183
+ expect(queue.toArray()).toEqual([1, 2, 3]); // Original items preserved
184
+ });
185
+
186
+ it("should preserve original items on multiple reject attempts", () => {
187
+ queue.push(1);
188
+ queue.push(2);
189
+ queue.push(3);
190
+
191
+ queue.push(4); // Rejected
192
+ queue.push(5); // Rejected
193
+ queue.push(6); // Rejected
194
+
195
+ expect(queue.toArray()).toEqual([1, 2, 3]);
196
+ });
197
+
198
+ it("should allow push after items are removed", () => {
199
+ queue.push(1);
200
+ queue.push(2);
201
+ queue.push(3);
202
+
203
+ queue.shift(); // Remove 1
204
+
205
+ const result = queue.push(4);
206
+ expect(result).toBe(true);
207
+ expect(queue.toArray()).toEqual([2, 3, 4]);
208
+ });
209
+ });
210
+
211
+ describe("reject strategy", () => {
212
+ let queue: BoundedQueue<number>;
213
+
214
+ beforeEach(() => {
215
+ queue = new BoundedQueue<number>(3, "reject");
216
+ });
217
+
218
+ it("should throw QueueOverflowError when queue is full", () => {
219
+ queue.push(1);
220
+ queue.push(2);
221
+ queue.push(3);
222
+
223
+ expect(() => queue.push(4)).toThrow(QueueOverflowError);
224
+ expect(() => queue.push(4)).toThrow(/Queue is full/);
225
+ });
226
+
227
+ it("should not modify queue when overflow error is thrown", () => {
228
+ queue.push(1);
229
+ queue.push(2);
230
+ queue.push(3);
231
+
232
+ try {
233
+ queue.push(4);
234
+ } catch (error) {
235
+ // Expected
236
+ }
237
+
238
+ expect(queue.toArray()).toEqual([1, 2, 3]);
239
+ });
240
+
241
+ it("should allow push after handling overflow error and removing items", () => {
242
+ queue.push(1);
243
+ queue.push(2);
244
+ queue.push(3);
245
+
246
+ try {
247
+ queue.push(4);
248
+ } catch (error) {
249
+ queue.shift(); // Remove oldest
250
+ }
251
+
252
+ const result = queue.push(4);
253
+ expect(result).toBe(true);
254
+ expect(queue.toArray()).toEqual([2, 3, 4]);
255
+ });
256
+ });
257
+
258
+ describe("type safety", () => {
259
+ it("should work with different types", () => {
260
+ interface Task {
261
+ id: number;
262
+ name: string;
263
+ }
264
+
265
+ const queue = new BoundedQueue<Task>(3);
266
+
267
+ queue.push({ id: 1, name: "task1" });
268
+ queue.push({ id: 2, name: "task2" });
269
+
270
+ const task = queue.shift();
271
+ expect(task?.id).toBe(1);
272
+ expect(task?.name).toBe("task1");
273
+ });
274
+
275
+ it("should work with complex objects", () => {
276
+ interface WebhookPayload {
277
+ url: string;
278
+ data: any;
279
+ attempts: number;
280
+ }
281
+
282
+ const queue = new BoundedQueue<WebhookPayload>(10);
283
+
284
+ queue.push({
285
+ url: "https://example.com",
286
+ data: { test: true },
287
+ attempts: 0
288
+ });
289
+
290
+ const payload = queue.peek();
291
+ expect(payload?.url).toBe("https://example.com");
292
+ expect(payload?.data.test).toBe(true);
293
+ });
294
+ });
295
+
296
+ describe("edge cases", () => {
297
+ it("should handle queue of size 1", () => {
298
+ const queue = new BoundedQueue<string>(1, "drop-oldest");
299
+
300
+ queue.push("a");
301
+ expect(queue.size()).toBe(1);
302
+
303
+ queue.push("b");
304
+ expect(queue.size()).toBe(1);
305
+ expect(queue.toArray()).toEqual(["b"]);
306
+ });
307
+
308
+ it("should handle rapid push and shift operations", () => {
309
+ const queue = new BoundedQueue<number>(5);
310
+
311
+ for (let i = 0; i < 100; i++) {
312
+ queue.push(i);
313
+ if (i % 2 === 0) {
314
+ queue.shift();
315
+ }
316
+ }
317
+
318
+ expect(queue.size()).toBeLessThanOrEqual(5);
319
+ });
320
+
321
+ it("should handle clearing and refilling", () => {
322
+ const queue = new BoundedQueue<number>(3);
323
+
324
+ queue.push(1);
325
+ queue.push(2);
326
+ queue.push(3);
327
+ queue.clear();
328
+
329
+ expect(queue.isEmpty()).toBe(true);
330
+
331
+ queue.push(4);
332
+ queue.push(5);
333
+
334
+ expect(queue.toArray()).toEqual([4, 5]);
335
+ });
336
+ });
337
+ });
338
+
339
+ describe("QueueOverflowError", () => {
340
+ it("should be instanceof Error", () => {
341
+ const error = new QueueOverflowError("Test");
342
+ expect(error).toBeInstanceOf(Error);
343
+ expect(error).toBeInstanceOf(QueueOverflowError);
344
+ });
345
+
346
+ it("should have correct name", () => {
347
+ const error = new QueueOverflowError("Test");
348
+ expect(error.name).toBe("QueueOverflowError");
349
+ });
350
+
351
+ it("should preserve error message", () => {
352
+ const message = "Custom overflow message";
353
+ const error = new QueueOverflowError(message);
354
+ expect(error.message).toBe(message);
355
+ });
356
+ });
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Bounded Queue with Overflow Strategies
3
+ * Prevents unbounded memory growth by enforcing a maximum size
4
+ *
5
+ * This queue prevents the OOM (Out of Memory) issues that can occur
6
+ * with unbounded queues when producers outpace consumers (e.g., webhook
7
+ * delivery failures causing queue buildup).
8
+ */
9
+
10
+ /**
11
+ * Strategy for handling queue overflow when at max capacity
12
+ */
13
+ export type OverflowStrategy =
14
+ | 'drop-oldest' // Remove oldest item to make room (FIFO eviction)
15
+ | 'drop-newest' // Reject new item, keep existing (preserve old data)
16
+ | 'reject'; // Throw error, let caller handle (fail-fast)
17
+
18
+ /**
19
+ * A queue with a maximum size limit and configurable overflow behavior
20
+ *
21
+ * @template T The type of items stored in the queue
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * // Create queue that drops oldest items when full
26
+ * const queue = new BoundedQueue<Message>(1000, 'drop-oldest');
27
+ *
28
+ * queue.push(message1); // returns true
29
+ * queue.push(message2); // returns true
30
+ *
31
+ * const msg = queue.shift(); // Remove from front
32
+ * const peek = queue.peek(); // Look at front without removing
33
+ *
34
+ * if (queue.isFull()) {
35
+ * console.log('Queue at capacity');
36
+ * }
37
+ * ```
38
+ */
39
+ export class BoundedQueue<T> {
40
+ private queue: T[] = [];
41
+
42
+ /**
43
+ * Creates a new bounded queue
44
+ *
45
+ * @param maxSize Maximum number of items the queue can hold
46
+ * @param strategy How to handle overflow when queue is full
47
+ * @throws {Error} If maxSize is less than 1
48
+ */
49
+ constructor(
50
+ private readonly maxSize: number,
51
+ private readonly strategy: OverflowStrategy = 'drop-oldest'
52
+ ) {
53
+ if (maxSize < 1) {
54
+ throw new Error('BoundedQueue maxSize must be at least 1');
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Add an item to the end of the queue
60
+ *
61
+ * Behavior when queue is full depends on strategy:
62
+ * - 'drop-oldest': Removes oldest item, adds new item, returns true
63
+ * - 'drop-newest': Rejects new item, returns false
64
+ * - 'reject': Throws QueueOverflowError
65
+ *
66
+ * @param item Item to add to the queue
67
+ * @returns true if item was added, false if rejected (drop-newest strategy only)
68
+ * @throws {QueueOverflowError} If strategy is 'reject' and queue is full
69
+ */
70
+ public push(item: T): boolean {
71
+ if (this.queue.length >= this.maxSize) {
72
+ return this.handleOverflow(item);
73
+ }
74
+
75
+ this.queue.push(item);
76
+ return true;
77
+ }
78
+
79
+ /**
80
+ * Remove and return the item at the front of the queue
81
+ *
82
+ * @returns The item at the front, or undefined if queue is empty
83
+ */
84
+ public shift(): T | undefined {
85
+ return this.queue.shift();
86
+ }
87
+
88
+ /**
89
+ * Look at the item at the front of the queue without removing it
90
+ *
91
+ * @returns The item at the front, or undefined if queue is empty
92
+ */
93
+ public peek(): T | undefined {
94
+ return this.queue[0];
95
+ }
96
+
97
+ /**
98
+ * Remove all items from the queue
99
+ */
100
+ public clear(): void {
101
+ this.queue = [];
102
+ }
103
+
104
+ /**
105
+ * Get the current number of items in the queue
106
+ *
107
+ * @returns Number of items currently in the queue
108
+ */
109
+ public size(): number {
110
+ return this.queue.length;
111
+ }
112
+
113
+ /**
114
+ * Check if the queue is at maximum capacity
115
+ *
116
+ * @returns true if queue is full, false otherwise
117
+ */
118
+ public isFull(): boolean {
119
+ return this.queue.length >= this.maxSize;
120
+ }
121
+
122
+ /**
123
+ * Check if the queue is empty
124
+ *
125
+ * @returns true if queue has no items, false otherwise
126
+ */
127
+ public isEmpty(): boolean {
128
+ return this.queue.length === 0;
129
+ }
130
+
131
+ /**
132
+ * Get the maximum capacity of the queue
133
+ *
134
+ * @returns The maximum number of items the queue can hold
135
+ */
136
+ public getMaxSize(): number {
137
+ return this.maxSize;
138
+ }
139
+
140
+ /**
141
+ * Get the current overflow strategy
142
+ *
143
+ * @returns The configured overflow strategy
144
+ */
145
+ public getStrategy(): OverflowStrategy {
146
+ return this.strategy;
147
+ }
148
+
149
+ /**
150
+ * Get all items in the queue (for inspection/debugging)
151
+ * Returns a copy to prevent external modification
152
+ *
153
+ * @returns Array of all items in the queue (oldest to newest)
154
+ */
155
+ public toArray(): T[] {
156
+ return [...this.queue];
157
+ }
158
+
159
+ /**
160
+ * Handle queue overflow based on configured strategy
161
+ *
162
+ * @param item The item attempting to be added
163
+ * @returns true if item was added, false if rejected
164
+ * @throws {QueueOverflowError} If strategy is 'reject'
165
+ */
166
+ private handleOverflow(item: T): boolean {
167
+ switch (this.strategy) {
168
+ case 'drop-oldest':
169
+ // Remove oldest item and add new one
170
+ this.queue.shift();
171
+ this.queue.push(item);
172
+ return true;
173
+
174
+ case 'drop-newest':
175
+ // Reject the new item
176
+ return false;
177
+
178
+ case 'reject':
179
+ // Throw error - let caller handle
180
+ throw new QueueOverflowError(
181
+ `Queue is full (max size: ${this.maxSize}). Cannot add more items.`
182
+ );
183
+
184
+ default:
185
+ // TypeScript exhaustiveness check
186
+ const _exhaustive: never = this.strategy;
187
+ throw new Error(`Unknown overflow strategy: ${_exhaustive}`);
188
+ }
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Error thrown when attempting to add to a full queue with 'reject' strategy
194
+ */
195
+ export class QueueOverflowError extends Error {
196
+ constructor(message: string) {
197
+ super(message);
198
+ this.name = 'QueueOverflowError';
199
+
200
+ // Maintains proper stack trace for where error was thrown (V8 only)
201
+ if (Error.captureStackTrace) {
202
+ Error.captureStackTrace(this, QueueOverflowError);
203
+ }
204
+ }
205
+ }