@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,341 @@
1
+ /**
2
+ * Tests for Token Bucket Rate Limiter
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, vi } from "vitest";
6
+ import { TokenBucketRateLimiter, RateLimitError } from "./rate-limiter";
7
+
8
+ describe("TokenBucketRateLimiter", () => {
9
+ describe("constructor", () => {
10
+ it("should create a rate limiter with valid parameters", () => {
11
+ const limiter = new TokenBucketRateLimiter(10, 20);
12
+ const config = limiter.getConfig();
13
+
14
+ expect(config.tokensPerSecond).toBe(10);
15
+ expect(config.maxBurst).toBe(20);
16
+ expect(limiter.getAvailableTokens()).toBe(20); // Starts full
17
+ });
18
+
19
+ it("should throw error if tokensPerSecond is less than 1", () => {
20
+ expect(() => new TokenBucketRateLimiter(0, 10)).toThrow(
21
+ "tokensPerSecond must be at least 1"
22
+ );
23
+ expect(() => new TokenBucketRateLimiter(-1, 10)).toThrow(
24
+ "tokensPerSecond must be at least 1"
25
+ );
26
+ });
27
+
28
+ it("should throw error if maxBurst is less than 1", () => {
29
+ expect(() => new TokenBucketRateLimiter(10, 0)).toThrow(
30
+ "maxBurst must be at least 1"
31
+ );
32
+ expect(() => new TokenBucketRateLimiter(10, -1)).toThrow(
33
+ "maxBurst must be at least 1"
34
+ );
35
+ });
36
+ });
37
+
38
+ describe("tryConsume", () => {
39
+ it("should consume tokens successfully when available", () => {
40
+ const limiter = new TokenBucketRateLimiter(10, 5);
41
+
42
+ expect(limiter.tryConsume()).toBe(true);
43
+ expect(limiter.getAvailableTokens()).toBeCloseTo(4, 0);
44
+
45
+ expect(limiter.tryConsume()).toBe(true);
46
+ expect(limiter.getAvailableTokens()).toBeCloseTo(3, 0);
47
+ });
48
+
49
+ it("should return false when no tokens available", () => {
50
+ const limiter = new TokenBucketRateLimiter(10, 2);
51
+
52
+ // Consume all tokens
53
+ expect(limiter.tryConsume()).toBe(true);
54
+ expect(limiter.tryConsume()).toBe(true);
55
+
56
+ // Should fail now
57
+ expect(limiter.tryConsume()).toBe(false);
58
+ expect(limiter.tryConsume()).toBe(false);
59
+ });
60
+
61
+ it("should allow burst up to maxBurst", () => {
62
+ const limiter = new TokenBucketRateLimiter(10, 5);
63
+
64
+ // Should allow 5 rapid operations
65
+ for (let i = 0; i < 5; i++) {
66
+ expect(limiter.tryConsume()).toBe(true);
67
+ }
68
+
69
+ // 6th should fail
70
+ expect(limiter.tryConsume()).toBe(false);
71
+ });
72
+ });
73
+
74
+ describe("token refill", () => {
75
+ it("should refill tokens over time", async () => {
76
+ // 10 tokens/sec = 1 token per 100ms
77
+ const limiter = new TokenBucketRateLimiter(10, 5);
78
+
79
+ // Consume all tokens
80
+ for (let i = 0; i < 5; i++) {
81
+ limiter.tryConsume();
82
+ }
83
+ expect(limiter.getAvailableTokens()).toBeCloseTo(0, 0);
84
+
85
+ // Wait 250ms (should refill ~2.5 tokens)
86
+ await new Promise((resolve) => setTimeout(resolve, 250));
87
+
88
+ const tokens = limiter.getAvailableTokens();
89
+ expect(tokens).toBeGreaterThan(2);
90
+ expect(tokens).toBeLessThan(3);
91
+ });
92
+
93
+ it("should cap refill at maxBurst", async () => {
94
+ const limiter = new TokenBucketRateLimiter(100, 5); // Fast refill
95
+
96
+ // Wait long enough to refill way past maxBurst
97
+ await new Promise((resolve) => setTimeout(resolve, 200));
98
+
99
+ expect(limiter.getAvailableTokens()).toBe(5); // Capped at maxBurst
100
+ });
101
+
102
+ it("should refill continuously at specified rate", async () => {
103
+ // 20 tokens/sec = 1 token per 50ms
104
+ const limiter = new TokenBucketRateLimiter(20, 10);
105
+
106
+ // Consume all
107
+ for (let i = 0; i < 10; i++) {
108
+ limiter.tryConsume();
109
+ }
110
+
111
+ // Wait and check refill multiple times
112
+ await new Promise((resolve) => setTimeout(resolve, 100)); // ~2 tokens
113
+ expect(limiter.getAvailableTokens()).toBeGreaterThan(1.5);
114
+ expect(limiter.getAvailableTokens()).toBeLessThan(2.5);
115
+
116
+ await new Promise((resolve) => setTimeout(resolve, 100)); // ~4 tokens total
117
+ expect(limiter.getAvailableTokens()).toBeGreaterThan(3.5);
118
+ expect(limiter.getAvailableTokens()).toBeLessThan(4.5);
119
+ });
120
+ });
121
+
122
+ describe("consume (blocking)", () => {
123
+ it("should consume immediately when token available", async () => {
124
+ const limiter = new TokenBucketRateLimiter(10, 5);
125
+
126
+ const start = Date.now();
127
+ await limiter.consume();
128
+ const elapsed = Date.now() - start;
129
+
130
+ expect(elapsed).toBeLessThan(50); // Should be immediate
131
+ expect(limiter.getAvailableTokens()).toBeCloseTo(4, 0);
132
+ });
133
+
134
+ it("should wait for token when none available", async () => {
135
+ // 10 tokens/sec = 100ms per token
136
+ const limiter = new TokenBucketRateLimiter(10, 2);
137
+
138
+ // Consume all
139
+ limiter.tryConsume();
140
+ limiter.tryConsume();
141
+
142
+ const start = Date.now();
143
+ await limiter.consume(); // Should wait ~100ms for refill
144
+ const elapsed = Date.now() - start;
145
+
146
+ expect(elapsed).toBeGreaterThan(80); // Allow some timing variance
147
+ expect(elapsed).toBeLessThan(200);
148
+ });
149
+
150
+ it("should throw RateLimitError when timeout exceeded", async () => {
151
+ const limiter = new TokenBucketRateLimiter(10, 1);
152
+
153
+ // Consume token
154
+ limiter.tryConsume();
155
+
156
+ // Try to consume with very short timeout (token needs 100ms to refill)
157
+ await expect(limiter.consume(10)).rejects.toThrow(RateLimitError);
158
+
159
+ // Reset and try again to verify error message
160
+ limiter.reset();
161
+ limiter.tryConsume();
162
+ await expect(limiter.consume(10)).rejects.toThrow(
163
+ /Rate limit timeout/
164
+ );
165
+ });
166
+
167
+ it("should succeed within timeout if token becomes available", async () => {
168
+ // 10 tokens/sec = 100ms per token
169
+ const limiter = new TokenBucketRateLimiter(10, 1);
170
+
171
+ limiter.tryConsume();
172
+
173
+ // Wait with generous timeout
174
+ await expect(limiter.consume(500)).resolves.toBeUndefined();
175
+ });
176
+ });
177
+
178
+ describe("reset", () => {
179
+ it("should reset tokens to full capacity", () => {
180
+ const limiter = new TokenBucketRateLimiter(10, 5);
181
+
182
+ // Consume some tokens
183
+ limiter.tryConsume();
184
+ limiter.tryConsume();
185
+ limiter.tryConsume();
186
+ expect(limiter.getAvailableTokens()).toBeCloseTo(2, 0);
187
+
188
+ // Reset
189
+ limiter.reset();
190
+ expect(limiter.getAvailableTokens()).toBe(5);
191
+ });
192
+
193
+ it("should reset refill timer", async () => {
194
+ const limiter = new TokenBucketRateLimiter(10, 5);
195
+
196
+ // Consume all
197
+ for (let i = 0; i < 5; i++) {
198
+ limiter.tryConsume();
199
+ }
200
+
201
+ // Wait a bit
202
+ await new Promise((resolve) => setTimeout(resolve, 50));
203
+
204
+ // Reset should clear any pending refill
205
+ limiter.reset();
206
+ expect(limiter.getAvailableTokens()).toBe(5);
207
+ });
208
+ });
209
+
210
+ describe("getAvailableTokens", () => {
211
+ it("should return current token count", () => {
212
+ const limiter = new TokenBucketRateLimiter(10, 10);
213
+
214
+ expect(limiter.getAvailableTokens()).toBe(10);
215
+
216
+ limiter.tryConsume();
217
+ expect(limiter.getAvailableTokens()).toBeCloseTo(9, 0);
218
+
219
+ limiter.tryConsume();
220
+ expect(limiter.getAvailableTokens()).toBeCloseTo(8, 0);
221
+ });
222
+
223
+ it("should trigger refill before returning", async () => {
224
+ const limiter = new TokenBucketRateLimiter(10, 5);
225
+
226
+ // Consume all
227
+ for (let i = 0; i < 5; i++) {
228
+ limiter.tryConsume();
229
+ }
230
+
231
+ // Wait for refill
232
+ await new Promise((resolve) => setTimeout(resolve, 150));
233
+
234
+ // getAvailableTokens should show refilled amount
235
+ const tokens = limiter.getAvailableTokens();
236
+ expect(tokens).toBeGreaterThan(1);
237
+ });
238
+ });
239
+
240
+ describe("getConfig", () => {
241
+ it("should return configuration", () => {
242
+ const limiter = new TokenBucketRateLimiter(15, 30);
243
+ const config = limiter.getConfig();
244
+
245
+ expect(config).toEqual({
246
+ tokensPerSecond: 15,
247
+ maxBurst: 30
248
+ });
249
+ });
250
+ });
251
+
252
+ describe("edge cases", () => {
253
+ it("should handle very high rates", () => {
254
+ const limiter = new TokenBucketRateLimiter(1000, 1000);
255
+
256
+ // Should allow 1000 rapid operations
257
+ for (let i = 0; i < 1000; i++) {
258
+ expect(limiter.tryConsume()).toBe(true);
259
+ }
260
+
261
+ expect(limiter.tryConsume()).toBe(false);
262
+ });
263
+
264
+ it("should handle rate of 1 token per second", async () => {
265
+ const limiter = new TokenBucketRateLimiter(1, 2);
266
+
267
+ limiter.tryConsume();
268
+ limiter.tryConsume();
269
+
270
+ // Should take ~1 second for next token
271
+ const start = Date.now();
272
+ await limiter.consume();
273
+ const elapsed = Date.now() - start;
274
+
275
+ expect(elapsed).toBeGreaterThan(900);
276
+ expect(elapsed).toBeLessThan(1200);
277
+ });
278
+
279
+ it("should handle burst = 1 (no burst)", () => {
280
+ const limiter = new TokenBucketRateLimiter(10, 1);
281
+
282
+ expect(limiter.tryConsume()).toBe(true);
283
+ expect(limiter.tryConsume()).toBe(false); // Immediate rate limit
284
+ });
285
+
286
+ it("should be reusable after rate limit", async () => {
287
+ const limiter = new TokenBucketRateLimiter(10, 2);
288
+
289
+ // Hit rate limit
290
+ limiter.tryConsume();
291
+ limiter.tryConsume();
292
+ expect(limiter.tryConsume()).toBe(false);
293
+
294
+ // Wait for refill
295
+ await new Promise((resolve) => setTimeout(resolve, 150));
296
+
297
+ // Should work again
298
+ expect(limiter.tryConsume()).toBe(true);
299
+ });
300
+ });
301
+
302
+ describe("concurrent operations", () => {
303
+ it("should handle multiple consume calls correctly", async () => {
304
+ const limiter = new TokenBucketRateLimiter(10, 3);
305
+
306
+ // Consume all tokens
307
+ limiter.tryConsume();
308
+ limiter.tryConsume();
309
+ limiter.tryConsume();
310
+
311
+ // Start multiple blocking consume calls
312
+ const promises = [
313
+ limiter.consume(1000),
314
+ limiter.consume(1000),
315
+ limiter.consume(1000)
316
+ ];
317
+
318
+ // All should eventually succeed as tokens refill
319
+ await expect(Promise.all(promises)).resolves.toBeDefined();
320
+ });
321
+ });
322
+ });
323
+
324
+ describe("RateLimitError", () => {
325
+ it("should be instanceof Error", () => {
326
+ const error = new RateLimitError("Test");
327
+ expect(error).toBeInstanceOf(Error);
328
+ expect(error).toBeInstanceOf(RateLimitError);
329
+ });
330
+
331
+ it("should have correct name", () => {
332
+ const error = new RateLimitError("Test");
333
+ expect(error.name).toBe("RateLimitError");
334
+ });
335
+
336
+ it("should preserve error message", () => {
337
+ const message = "Custom rate limit message";
338
+ const error = new RateLimitError(message);
339
+ expect(error.message).toBe(message);
340
+ });
341
+ });
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Token Bucket Rate Limiter
3
+ * Prevents message flooding and ensures fair resource usage
4
+ *
5
+ * Uses the token bucket algorithm: tokens are added at a constant rate,
6
+ * and each operation consumes one token. When no tokens are available,
7
+ * operations must wait until tokens are replenished.
8
+ */
9
+
10
+ /**
11
+ * Error thrown when rate limit is exceeded with tryConsume()
12
+ */
13
+ export class RateLimitError extends Error {
14
+ constructor(message: string) {
15
+ super(message);
16
+ this.name = 'RateLimitError';
17
+
18
+ if (Error.captureStackTrace) {
19
+ Error.captureStackTrace(this, RateLimitError);
20
+ }
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Token bucket rate limiter for controlling operation frequency
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * // Allow 10 messages per second with burst of 20
30
+ * const limiter = new TokenBucketRateLimiter(10, 20);
31
+ *
32
+ * // Non-blocking check
33
+ * if (limiter.tryConsume()) {
34
+ * await sendMessage(msg);
35
+ * } else {
36
+ * console.log('Rate limit exceeded');
37
+ * }
38
+ *
39
+ * // Blocking wait (auto-waits for token)
40
+ * await limiter.consume();
41
+ * await sendMessage(msg);
42
+ * ```
43
+ */
44
+ export class TokenBucketRateLimiter {
45
+ private tokens: number;
46
+ private lastRefill: number;
47
+ private readonly refillInterval: number; // ms per token
48
+
49
+ /**
50
+ * Creates a new token bucket rate limiter
51
+ *
52
+ * @param tokensPerSecond - Rate at which tokens are added (operations per second)
53
+ * @param maxBurst - Maximum tokens that can accumulate (burst capacity)
54
+ * @throws {Error} If tokensPerSecond or maxBurst is less than 1
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * // 10 ops/sec, burst up to 20
59
+ * const limiter = new TokenBucketRateLimiter(10, 20);
60
+ * ```
61
+ */
62
+ constructor(
63
+ private readonly tokensPerSecond: number,
64
+ private readonly maxBurst: number
65
+ ) {
66
+ if (tokensPerSecond < 1) {
67
+ throw new Error('TokenBucketRateLimiter tokensPerSecond must be at least 1');
68
+ }
69
+ if (maxBurst < 1) {
70
+ throw new Error('TokenBucketRateLimiter maxBurst must be at least 1');
71
+ }
72
+
73
+ this.tokens = maxBurst; // Start with full bucket
74
+ this.lastRefill = Date.now();
75
+ this.refillInterval = 1000 / tokensPerSecond; // ms between tokens
76
+ }
77
+
78
+ /**
79
+ * Try to consume one token without blocking
80
+ *
81
+ * @returns true if token was consumed, false if no tokens available
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * if (limiter.tryConsume()) {
86
+ * // Proceed with operation
87
+ * } else {
88
+ * // Rate limited - handle accordingly
89
+ * }
90
+ * ```
91
+ */
92
+ public tryConsume(): boolean {
93
+ this.refill();
94
+
95
+ if (this.tokens >= 1) {
96
+ this.tokens -= 1;
97
+ return true;
98
+ }
99
+
100
+ return false;
101
+ }
102
+
103
+ /**
104
+ * Consume one token, waiting if necessary until a token is available
105
+ *
106
+ * @param timeout - Optional max wait time in ms (default: no timeout)
107
+ * @returns Promise that resolves when token is consumed
108
+ * @throws {RateLimitError} If timeout is exceeded
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * // Wait indefinitely for token
113
+ * await limiter.consume();
114
+ *
115
+ * // Wait max 5 seconds
116
+ * try {
117
+ * await limiter.consume(5000);
118
+ * } catch (error) {
119
+ * console.log('Timeout waiting for rate limit');
120
+ * }
121
+ * ```
122
+ */
123
+ public async consume(timeout?: number): Promise<void> {
124
+ const startTime = Date.now();
125
+
126
+ while (true) {
127
+ if (this.tryConsume()) {
128
+ return;
129
+ }
130
+
131
+ // Check timeout
132
+ const elapsed = Date.now() - startTime;
133
+ if (timeout !== undefined && elapsed >= timeout) {
134
+ throw new RateLimitError(
135
+ `Rate limit timeout: no token available after ${timeout}ms`
136
+ );
137
+ }
138
+
139
+ // Calculate wait time until next token
140
+ // If timeout is specified, don't wait longer than remaining timeout
141
+ const baseWaitTime = Math.min(this.refillInterval, 100);
142
+ const waitTime = timeout !== undefined
143
+ ? Math.min(baseWaitTime, timeout - elapsed)
144
+ : baseWaitTime;
145
+
146
+ // If waitTime is very small or negative, check timeout immediately
147
+ if (waitTime <= 0) {
148
+ continue;
149
+ }
150
+
151
+ await this.sleep(waitTime);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Get the number of tokens currently available
157
+ * Refills tokens before returning count
158
+ *
159
+ * @returns Number of tokens available (may be fractional before consumption)
160
+ */
161
+ public getAvailableTokens(): number {
162
+ this.refill();
163
+ return this.tokens;
164
+ }
165
+
166
+ /**
167
+ * Get rate limiter configuration
168
+ *
169
+ * @returns Configuration object with rate and burst capacity
170
+ */
171
+ public getConfig(): { tokensPerSecond: number; maxBurst: number } {
172
+ return {
173
+ tokensPerSecond: this.tokensPerSecond,
174
+ maxBurst: this.maxBurst
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Reset the rate limiter to full capacity
180
+ * Useful for testing or manual reset scenarios
181
+ */
182
+ public reset(): void {
183
+ this.tokens = this.maxBurst;
184
+ this.lastRefill = Date.now();
185
+ }
186
+
187
+ /**
188
+ * Refill tokens based on elapsed time since last refill
189
+ * Called automatically before token consumption
190
+ */
191
+ private refill(): void {
192
+ const now = Date.now();
193
+ const elapsed = now - this.lastRefill;
194
+
195
+ if (elapsed > 0) {
196
+ // Calculate tokens to add based on elapsed time
197
+ const tokensToAdd = elapsed / this.refillInterval;
198
+
199
+ // Add tokens, capped at maxBurst
200
+ this.tokens = Math.min(this.tokens + tokensToAdd, this.maxBurst);
201
+ this.lastRefill = now;
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Sleep helper for async waiting
207
+ */
208
+ private sleep(ms: number): Promise<void> {
209
+ return new Promise((resolve) => setTimeout(resolve, ms));
210
+ }
211
+ }