@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,380 @@
1
+ /**
2
+ * Tests for Deduplication Cache
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, vi } from "vitest";
6
+ import { DeduplicationCache } from "./deduplication-cache";
7
+
8
+ describe("DeduplicationCache", () => {
9
+ describe("constructor", () => {
10
+ it("should create cache with default options", () => {
11
+ const cache = new DeduplicationCache();
12
+ const config = cache.getConfig();
13
+
14
+ expect(config.ttl).toBe(300000); // 5 minutes
15
+ expect(config.maxSize).toBe(10000);
16
+ expect(cache.size()).toBe(0);
17
+ });
18
+
19
+ it("should create cache with custom options", () => {
20
+ const cache = new DeduplicationCache(60000, 5000);
21
+ const config = cache.getConfig();
22
+
23
+ expect(config.ttl).toBe(60000);
24
+ expect(config.maxSize).toBe(5000);
25
+ });
26
+
27
+ it("should throw error if TTL is less than 1000ms", () => {
28
+ expect(() => new DeduplicationCache(999)).toThrow(
29
+ "TTL must be at least 1000ms"
30
+ );
31
+ });
32
+
33
+ it("should throw error if maxSize is less than 1", () => {
34
+ expect(() => new DeduplicationCache(5000, 0)).toThrow(
35
+ "maxSize must be at least 1"
36
+ );
37
+ });
38
+ });
39
+
40
+ describe("has", () => {
41
+ let cache: DeduplicationCache;
42
+
43
+ beforeEach(() => {
44
+ cache = new DeduplicationCache(60000);
45
+ });
46
+
47
+ it("should return false for non-existent keys", () => {
48
+ expect(cache.has("non-existent")).toBe(false);
49
+ });
50
+
51
+ it("should return true for existing keys", () => {
52
+ cache.add("key1");
53
+ expect(cache.has("key1")).toBe(true);
54
+ });
55
+
56
+ it("should return false for expired keys", async () => {
57
+ const cache = new DeduplicationCache(1000); // 1 second TTL
58
+
59
+ cache.add("key1");
60
+ expect(cache.has("key1")).toBe(true);
61
+
62
+ // Wait for expiration
63
+ await new Promise((resolve) => setTimeout(resolve, 1100));
64
+
65
+ expect(cache.has("key1")).toBe(false);
66
+ });
67
+
68
+ it("should remove expired keys when checking", async () => {
69
+ const cache = new DeduplicationCache(1000);
70
+
71
+ cache.add("key1");
72
+ expect(cache.size()).toBe(1);
73
+
74
+ // Wait for expiration
75
+ await new Promise((resolve) => setTimeout(resolve, 1100));
76
+
77
+ cache.has("key1");
78
+ expect(cache.size()).toBe(0);
79
+ });
80
+ });
81
+
82
+ describe("add", () => {
83
+ let cache: DeduplicationCache;
84
+
85
+ beforeEach(() => {
86
+ cache = new DeduplicationCache();
87
+ });
88
+
89
+ it("should add new keys successfully", () => {
90
+ expect(cache.add("key1")).toBe(true);
91
+ expect(cache.has("key1")).toBe(true);
92
+ expect(cache.size()).toBe(1);
93
+ });
94
+
95
+ it("should return false when adding duplicate keys", () => {
96
+ cache.add("key1");
97
+ expect(cache.add("key1")).toBe(false);
98
+ expect(cache.size()).toBe(1);
99
+ });
100
+
101
+ it("should allow re-adding expired keys", async () => {
102
+ const cache = new DeduplicationCache(1000);
103
+
104
+ cache.add("key1");
105
+ expect(cache.has("key1")).toBe(true);
106
+
107
+ // Wait for expiration
108
+ await new Promise((resolve) => setTimeout(resolve, 1100));
109
+
110
+ expect(cache.add("key1")).toBe(true);
111
+ expect(cache.has("key1")).toBe(true);
112
+ });
113
+
114
+ it("should add multiple unique keys", () => {
115
+ cache.add("key1");
116
+ cache.add("key2");
117
+ cache.add("key3");
118
+
119
+ expect(cache.size()).toBe(3);
120
+ expect(cache.has("key1")).toBe(true);
121
+ expect(cache.has("key2")).toBe(true);
122
+ expect(cache.has("key3")).toBe(true);
123
+ });
124
+ });
125
+
126
+ describe("delete", () => {
127
+ let cache: DeduplicationCache;
128
+
129
+ beforeEach(() => {
130
+ cache = new DeduplicationCache();
131
+ });
132
+
133
+ it("should delete existing keys", () => {
134
+ cache.add("key1");
135
+ expect(cache.size()).toBe(1);
136
+
137
+ expect(cache.delete("key1")).toBe(true);
138
+ expect(cache.size()).toBe(0);
139
+ expect(cache.has("key1")).toBe(false);
140
+ });
141
+
142
+ it("should return false when deleting non-existent keys", () => {
143
+ expect(cache.delete("non-existent")).toBe(false);
144
+ });
145
+
146
+ it("should only delete specified key", () => {
147
+ cache.add("key1");
148
+ cache.add("key2");
149
+ cache.add("key3");
150
+
151
+ cache.delete("key2");
152
+
153
+ expect(cache.size()).toBe(2);
154
+ expect(cache.has("key1")).toBe(true);
155
+ expect(cache.has("key2")).toBe(false);
156
+ expect(cache.has("key3")).toBe(true);
157
+ });
158
+ });
159
+
160
+ describe("clear", () => {
161
+ it("should remove all entries", () => {
162
+ const cache = new DeduplicationCache();
163
+
164
+ cache.add("key1");
165
+ cache.add("key2");
166
+ cache.add("key3");
167
+ expect(cache.size()).toBe(3);
168
+
169
+ cache.clear();
170
+
171
+ expect(cache.size()).toBe(0);
172
+ expect(cache.has("key1")).toBe(false);
173
+ expect(cache.has("key2")).toBe(false);
174
+ expect(cache.has("key3")).toBe(false);
175
+ });
176
+
177
+ it("should work on empty cache", () => {
178
+ const cache = new DeduplicationCache();
179
+ expect(() => cache.clear()).not.toThrow();
180
+ expect(cache.size()).toBe(0);
181
+ });
182
+ });
183
+
184
+ describe("cleanup", () => {
185
+ it("should remove expired entries", async () => {
186
+ const cache = new DeduplicationCache(1000);
187
+
188
+ cache.add("key1");
189
+ cache.add("key2");
190
+ cache.add("key3");
191
+
192
+ // Wait for expiration
193
+ await new Promise((resolve) => setTimeout(resolve, 1100));
194
+
195
+ const removed = cache.cleanup();
196
+
197
+ expect(removed).toBe(3);
198
+ expect(cache.size()).toBe(0);
199
+ });
200
+
201
+ it("should not remove non-expired entries", async () => {
202
+ const cache = new DeduplicationCache(1000);
203
+
204
+ cache.add("key1");
205
+
206
+ // Wait less than TTL
207
+ await new Promise((resolve) => setTimeout(resolve, 500));
208
+
209
+ const removed = cache.cleanup();
210
+
211
+ expect(removed).toBe(0);
212
+ expect(cache.size()).toBe(1);
213
+ });
214
+
215
+ it("should only remove expired entries", async () => {
216
+ const cache = new DeduplicationCache(1000);
217
+
218
+ cache.add("key1");
219
+
220
+ // Wait for key1 to expire
221
+ await new Promise((resolve) => setTimeout(resolve, 1100));
222
+
223
+ // Add fresh key
224
+ cache.add("key2");
225
+
226
+ const removed = cache.cleanup();
227
+
228
+ expect(removed).toBe(1);
229
+ expect(cache.size()).toBe(1);
230
+ expect(cache.has("key1")).toBe(false);
231
+ expect(cache.has("key2")).toBe(true);
232
+ });
233
+
234
+ it("should return 0 if no entries expired", () => {
235
+ const cache = new DeduplicationCache();
236
+
237
+ cache.add("key1");
238
+ cache.add("key2");
239
+
240
+ const removed = cache.cleanup();
241
+
242
+ expect(removed).toBe(0);
243
+ expect(cache.size()).toBe(2);
244
+ });
245
+ });
246
+
247
+ describe("automatic cleanup", () => {
248
+ it("should trigger cleanup when approaching max size", () => {
249
+ const cache = new DeduplicationCache(1000, 10); // Small size for testing
250
+
251
+ // Add entries up to 90% capacity (9 items)
252
+ for (let i = 0; i < 9; i++) {
253
+ cache.add(`key${i}`);
254
+ }
255
+
256
+ expect(cache.size()).toBe(9);
257
+
258
+ // 10th item should trigger cleanup check
259
+ cache.add("key9");
260
+
261
+ // All items should still be present (not expired yet)
262
+ expect(cache.size()).toBe(10);
263
+ });
264
+
265
+ it("should clean up expired entries when triggered", async () => {
266
+ const cache = new DeduplicationCache(1000, 10);
267
+
268
+ // Add entries
269
+ for (let i = 0; i < 9; i++) {
270
+ cache.add(`key${i}`);
271
+ }
272
+
273
+ // Wait for expiration
274
+ await new Promise((resolve) => setTimeout(resolve, 1100));
275
+
276
+ // Trigger cleanup by adding 10th item
277
+ cache.add("key9");
278
+
279
+ // Old items should be cleaned up, only new item remains
280
+ expect(cache.size()).toBe(1);
281
+ expect(cache.has("key9")).toBe(true);
282
+ });
283
+ });
284
+
285
+ describe("size", () => {
286
+ it("should return 0 for empty cache", () => {
287
+ const cache = new DeduplicationCache();
288
+ expect(cache.size()).toBe(0);
289
+ });
290
+
291
+ it("should return correct size after additions", () => {
292
+ const cache = new DeduplicationCache();
293
+
294
+ cache.add("key1");
295
+ expect(cache.size()).toBe(1);
296
+
297
+ cache.add("key2");
298
+ expect(cache.size()).toBe(2);
299
+
300
+ cache.add("key3");
301
+ expect(cache.size()).toBe(3);
302
+ });
303
+
304
+ it("should return correct size after deletions", () => {
305
+ const cache = new DeduplicationCache();
306
+
307
+ cache.add("key1");
308
+ cache.add("key2");
309
+ cache.add("key3");
310
+ expect(cache.size()).toBe(3);
311
+
312
+ cache.delete("key2");
313
+ expect(cache.size()).toBe(2);
314
+
315
+ cache.clear();
316
+ expect(cache.size()).toBe(0);
317
+ });
318
+ });
319
+
320
+ describe("edge cases", () => {
321
+ it("should handle very short TTL", async () => {
322
+ const cache = new DeduplicationCache(1000); // 1 second
323
+
324
+ cache.add("key1");
325
+ expect(cache.has("key1")).toBe(true);
326
+
327
+ await new Promise((resolve) => setTimeout(resolve, 1100));
328
+
329
+ expect(cache.has("key1")).toBe(false);
330
+ });
331
+
332
+ it("should handle many entries", () => {
333
+ const cache = new DeduplicationCache(60000, 20000);
334
+
335
+ for (let i = 0; i < 1000; i++) {
336
+ cache.add(`key${i}`);
337
+ }
338
+
339
+ expect(cache.size()).toBe(1000);
340
+
341
+ for (let i = 0; i < 1000; i++) {
342
+ expect(cache.has(`key${i}`)).toBe(true);
343
+ }
344
+ });
345
+
346
+ it("should handle rapid add/has operations", () => {
347
+ const cache = new DeduplicationCache();
348
+
349
+ for (let i = 0; i < 100; i++) {
350
+ const key = `key${i}`;
351
+ expect(cache.add(key)).toBe(true);
352
+ expect(cache.has(key)).toBe(true);
353
+ expect(cache.add(key)).toBe(false); // Duplicate
354
+ }
355
+
356
+ expect(cache.size()).toBe(100);
357
+ });
358
+
359
+ it("should handle empty string keys", () => {
360
+ const cache = new DeduplicationCache();
361
+
362
+ expect(cache.add("")).toBe(true);
363
+ expect(cache.has("")).toBe(true);
364
+ expect(cache.delete("")).toBe(true);
365
+ });
366
+
367
+ it("should treat different keys as separate entries", () => {
368
+ const cache = new DeduplicationCache();
369
+
370
+ cache.add("key");
371
+ cache.add("key ");
372
+ cache.add("KEY");
373
+
374
+ expect(cache.size()).toBe(3);
375
+ expect(cache.has("key")).toBe(true);
376
+ expect(cache.has("key ")).toBe(true);
377
+ expect(cache.has("KEY")).toBe(true);
378
+ });
379
+ });
380
+ });
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Deduplication Cache with TTL
3
+ * Prevents duplicate processing of messages/events
4
+ *
5
+ * Uses time-to-live (TTL) for automatic cleanup of old entries
6
+ * without requiring external cleanup timers.
7
+ */
8
+
9
+ interface CacheEntry {
10
+ timestamp: number;
11
+ }
12
+
13
+ /**
14
+ * TTL-based cache for detecting duplicates
15
+ * Automatically expires entries after configured TTL
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const cache = new DeduplicationCache(60000); // 60 second TTL
20
+ *
21
+ * // Check and add in one operation
22
+ * if (!cache.has('message-123')) {
23
+ * cache.add('message-123');
24
+ * // Process message
25
+ * } else {
26
+ * // Duplicate - skip processing
27
+ * }
28
+ * ```
29
+ */
30
+ export class DeduplicationCache {
31
+ private cache = new Map<string, CacheEntry>();
32
+
33
+ /**
34
+ * Creates a new deduplication cache
35
+ *
36
+ * @param ttl - Time-to-live in milliseconds (default: 300000 / 5 minutes)
37
+ * @param maxSize - Maximum cache size before cleanup (default: 10000)
38
+ * @throws {Error} If TTL is less than 1000ms or maxSize is less than 1
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * // 5 minute TTL, max 10k entries
43
+ * const cache = new DeduplicationCache(300000, 10000);
44
+ * ```
45
+ */
46
+ constructor(
47
+ private readonly ttl: number = 300000,
48
+ private readonly maxSize: number = 10000
49
+ ) {
50
+ if (ttl < 1000) {
51
+ throw new Error("DeduplicationCache TTL must be at least 1000ms");
52
+ }
53
+ if (maxSize < 1) {
54
+ throw new Error("DeduplicationCache maxSize must be at least 1");
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Check if a key exists and hasn't expired
60
+ *
61
+ * @param key - Key to check
62
+ * @returns true if key exists and is not expired, false otherwise
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * if (cache.has('message-id')) {
67
+ * console.log('Duplicate message detected');
68
+ * }
69
+ * ```
70
+ */
71
+ public has(key: string): boolean {
72
+ this.cleanupIfNeeded();
73
+
74
+ const entry = this.cache.get(key);
75
+ if (!entry) {
76
+ return false;
77
+ }
78
+
79
+ // Check if expired
80
+ if (Date.now() - entry.timestamp > this.ttl) {
81
+ this.cache.delete(key);
82
+ return false;
83
+ }
84
+
85
+ return true;
86
+ }
87
+
88
+ /**
89
+ * Add a key to the cache with current timestamp
90
+ *
91
+ * @param key - Key to add
92
+ * @returns true if added, false if already exists
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * if (cache.add('message-id')) {
97
+ * // Process message
98
+ * } else {
99
+ * // Already processed
100
+ * }
101
+ * ```
102
+ */
103
+ public add(key: string): boolean {
104
+ if (this.has(key)) {
105
+ return false;
106
+ }
107
+
108
+ this.cache.set(key, {
109
+ timestamp: Date.now()
110
+ });
111
+
112
+ this.cleanupIfNeeded();
113
+ return true;
114
+ }
115
+
116
+ /**
117
+ * Remove a specific key from the cache
118
+ *
119
+ * @param key - Key to remove
120
+ * @returns true if key was removed, false if didn't exist
121
+ */
122
+ public delete(key: string): boolean {
123
+ return this.cache.delete(key);
124
+ }
125
+
126
+ /**
127
+ * Clear all entries from the cache
128
+ */
129
+ public clear(): void {
130
+ this.cache.clear();
131
+ }
132
+
133
+ /**
134
+ * Get current cache size
135
+ *
136
+ * @returns Number of entries in cache
137
+ */
138
+ public size(): number {
139
+ return this.cache.size;
140
+ }
141
+
142
+ /**
143
+ * Get cache configuration
144
+ */
145
+ public getConfig(): { ttl: number; maxSize: number } {
146
+ return {
147
+ ttl: this.ttl,
148
+ maxSize: this.maxSize
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Clean up expired entries if cache is getting large
154
+ * Called automatically by has() and add()
155
+ */
156
+ private cleanupIfNeeded(): void {
157
+ // Only cleanup if approaching max size
158
+ if (this.cache.size < this.maxSize * 0.9) {
159
+ return;
160
+ }
161
+
162
+ const now = Date.now();
163
+ const keysToDelete: string[] = [];
164
+
165
+ for (const [key, entry] of this.cache.entries()) {
166
+ if (now - entry.timestamp > this.ttl) {
167
+ keysToDelete.push(key);
168
+ }
169
+ }
170
+
171
+ for (const key of keysToDelete) {
172
+ this.cache.delete(key);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Force cleanup of all expired entries
178
+ * Usually not needed as cleanup is automatic
179
+ *
180
+ * @returns Number of entries removed
181
+ */
182
+ public cleanup(): number {
183
+ const now = Date.now();
184
+ const keysToDelete: string[] = [];
185
+
186
+ for (const [key, entry] of this.cache.entries()) {
187
+ if (now - entry.timestamp > this.ttl) {
188
+ keysToDelete.push(key);
189
+ }
190
+ }
191
+
192
+ for (const key of keysToDelete) {
193
+ this.cache.delete(key);
194
+ }
195
+
196
+ return keysToDelete.length;
197
+ }
198
+ }