@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.
- package/.dockerignore +14 -0
- package/.env.test.example +14 -0
- package/.eslintrc.json +26 -0
- package/.github/workflows/claude-code-review.yml +78 -0
- package/.github/workflows/claude-reviewer.yml +64 -0
- package/.github/workflows/publish-npm.yml +38 -0
- package/.github/workflows/push-to-main.yml +23 -0
- package/.node-version +1 -0
- package/.prettierrc +11 -0
- package/Dockerfile +25 -0
- package/LICENCE +661 -0
- package/README.md +709 -0
- package/dist/constants.d.ts +42 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +45 -0
- package/dist/constants.js.map +1 -0
- package/dist/core/websocket-client.d.ts +261 -0
- package/dist/core/websocket-client.d.ts.map +1 -0
- package/dist/core/websocket-client.js +875 -0
- package/dist/core/websocket-client.js.map +1 -0
- package/dist/formatters/response-formatter.d.ts +354 -0
- package/dist/formatters/response-formatter.d.ts.map +1 -0
- package/dist/formatters/response-formatter.js +575 -0
- package/dist/formatters/response-formatter.js.map +1 -0
- package/dist/handlers/message-handler-registry.d.ts +155 -0
- package/dist/handlers/message-handler-registry.d.ts.map +1 -0
- package/dist/handlers/message-handler-registry.js +216 -0
- package/dist/handlers/message-handler-registry.js.map +1 -0
- package/dist/handlers/message-handlers/agent-selected-handler.d.ts +112 -0
- package/dist/handlers/message-handlers/agent-selected-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/agent-selected-handler.js +40 -0
- package/dist/handlers/message-handlers/agent-selected-handler.js.map +1 -0
- package/dist/handlers/message-handlers/agents-list-handler.d.ts +14 -0
- package/dist/handlers/message-handlers/agents-list-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/agents-list-handler.js +25 -0
- package/dist/handlers/message-handlers/agents-list-handler.js.map +1 -0
- package/dist/handlers/message-handlers/auth-error-handler.d.ts +71 -0
- package/dist/handlers/message-handlers/auth-error-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/auth-error-handler.js +30 -0
- package/dist/handlers/message-handlers/auth-error-handler.js.map +1 -0
- package/dist/handlers/message-handlers/auth-message-handler.d.ts +18 -0
- package/dist/handlers/message-handlers/auth-message-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/auth-message-handler.js +60 -0
- package/dist/handlers/message-handlers/auth-message-handler.js.map +1 -0
- package/dist/handlers/message-handlers/auth-required-handler.d.ts +76 -0
- package/dist/handlers/message-handlers/auth-required-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/auth-required-handler.js +23 -0
- package/dist/handlers/message-handlers/auth-required-handler.js.map +1 -0
- package/dist/handlers/message-handlers/auth-success-handler.d.ts +18 -0
- package/dist/handlers/message-handlers/auth-success-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/auth-success-handler.js +51 -0
- package/dist/handlers/message-handlers/auth-success-handler.js.map +1 -0
- package/dist/handlers/message-handlers/base-handler.d.ts +55 -0
- package/dist/handlers/message-handlers/base-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/base-handler.js +83 -0
- package/dist/handlers/message-handlers/base-handler.js.map +1 -0
- package/dist/handlers/message-handlers/challenge-handler.d.ts +73 -0
- package/dist/handlers/message-handlers/challenge-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/challenge-handler.js +47 -0
- package/dist/handlers/message-handlers/challenge-handler.js.map +1 -0
- package/dist/handlers/message-handlers/error-message-handler.d.ts +76 -0
- package/dist/handlers/message-handlers/error-message-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/error-message-handler.js +29 -0
- package/dist/handlers/message-handlers/error-message-handler.js.map +1 -0
- package/dist/handlers/message-handlers/index.d.ts +28 -0
- package/dist/handlers/message-handlers/index.d.ts.map +1 -0
- package/dist/handlers/message-handlers/index.js +100 -0
- package/dist/handlers/message-handlers/index.js.map +1 -0
- package/dist/handlers/message-handlers/list-rooms-response-handler.d.ts +122 -0
- package/dist/handlers/message-handlers/list-rooms-response-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/list-rooms-response-handler.js +30 -0
- package/dist/handlers/message-handlers/list-rooms-response-handler.js.map +1 -0
- package/dist/handlers/message-handlers/ping-pong-handler.d.ts +104 -0
- package/dist/handlers/message-handlers/ping-pong-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/ping-pong-handler.js +36 -0
- package/dist/handlers/message-handlers/ping-pong-handler.js.map +1 -0
- package/dist/handlers/message-handlers/regular-message-handler.d.ts +56 -0
- package/dist/handlers/message-handlers/regular-message-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/regular-message-handler.js +59 -0
- package/dist/handlers/message-handlers/regular-message-handler.js.map +1 -0
- package/dist/handlers/message-handlers/subscribe-response-handler.d.ts +81 -0
- package/dist/handlers/message-handlers/subscribe-response-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/subscribe-response-handler.js +48 -0
- package/dist/handlers/message-handlers/subscribe-response-handler.js.map +1 -0
- package/dist/handlers/message-handlers/task-response-handler.d.ts +14 -0
- package/dist/handlers/message-handlers/task-response-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/task-response-handler.js +44 -0
- package/dist/handlers/message-handlers/task-response-handler.js.map +1 -0
- package/dist/handlers/message-handlers/types.d.ts +51 -0
- package/dist/handlers/message-handlers/types.d.ts.map +1 -0
- package/dist/handlers/message-handlers/types.js +7 -0
- package/dist/handlers/message-handlers/types.js.map +1 -0
- package/dist/handlers/message-handlers/unsubscribe-response-handler.d.ts +81 -0
- package/dist/handlers/message-handlers/unsubscribe-response-handler.d.ts.map +1 -0
- package/dist/handlers/message-handlers/unsubscribe-response-handler.js +48 -0
- package/dist/handlers/message-handlers/unsubscribe-response-handler.js.map +1 -0
- package/dist/handlers/webhook-handler.d.ts +202 -0
- package/dist/handlers/webhook-handler.d.ts.map +1 -0
- package/dist/handlers/webhook-handler.js +511 -0
- package/dist/handlers/webhook-handler.js.map +1 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +217 -0
- package/dist/index.js.map +1 -0
- package/dist/managers/agent-registry.d.ts +173 -0
- package/dist/managers/agent-registry.d.ts.map +1 -0
- package/dist/managers/agent-registry.js +310 -0
- package/dist/managers/agent-registry.js.map +1 -0
- package/dist/managers/connection-manager.d.ts +134 -0
- package/dist/managers/connection-manager.d.ts.map +1 -0
- package/dist/managers/connection-manager.js +176 -0
- package/dist/managers/connection-manager.js.map +1 -0
- package/dist/managers/index.d.ts +9 -0
- package/dist/managers/index.d.ts.map +1 -0
- package/dist/managers/index.js +16 -0
- package/dist/managers/index.js.map +1 -0
- package/dist/managers/message-router.d.ts +112 -0
- package/dist/managers/message-router.d.ts.map +1 -0
- package/dist/managers/message-router.js +260 -0
- package/dist/managers/message-router.js.map +1 -0
- package/dist/managers/room-manager.d.ts +165 -0
- package/dist/managers/room-manager.d.ts.map +1 -0
- package/dist/managers/room-manager.js +227 -0
- package/dist/managers/room-manager.js.map +1 -0
- package/dist/teneo-sdk.d.ts +703 -0
- package/dist/teneo-sdk.d.ts.map +1 -0
- package/dist/teneo-sdk.js +907 -0
- package/dist/teneo-sdk.js.map +1 -0
- package/dist/types/config.d.ts +1047 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +720 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/error-codes.d.ts +29 -0
- package/dist/types/error-codes.d.ts.map +1 -0
- package/dist/types/error-codes.js +41 -0
- package/dist/types/error-codes.js.map +1 -0
- package/dist/types/events.d.ts +616 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +261 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/health.d.ts +40 -0
- package/dist/types/health.d.ts.map +1 -0
- package/dist/types/health.js +6 -0
- package/dist/types/health.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +123 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/messages.d.ts +3734 -0
- package/dist/types/messages.d.ts.map +1 -0
- package/dist/types/messages.js +482 -0
- package/dist/types/messages.js.map +1 -0
- package/dist/types/validation.d.ts +81 -0
- package/dist/types/validation.d.ts.map +1 -0
- package/dist/types/validation.js +115 -0
- package/dist/types/validation.js.map +1 -0
- package/dist/utils/bounded-queue.d.ts +127 -0
- package/dist/utils/bounded-queue.d.ts.map +1 -0
- package/dist/utils/bounded-queue.js +181 -0
- package/dist/utils/bounded-queue.js.map +1 -0
- package/dist/utils/circuit-breaker.d.ts +141 -0
- package/dist/utils/circuit-breaker.d.ts.map +1 -0
- package/dist/utils/circuit-breaker.js +215 -0
- package/dist/utils/circuit-breaker.js.map +1 -0
- package/dist/utils/deduplication-cache.d.ts +110 -0
- package/dist/utils/deduplication-cache.d.ts.map +1 -0
- package/dist/utils/deduplication-cache.js +177 -0
- package/dist/utils/deduplication-cache.js.map +1 -0
- package/dist/utils/event-waiter.d.ts +101 -0
- package/dist/utils/event-waiter.d.ts.map +1 -0
- package/dist/utils/event-waiter.js +118 -0
- package/dist/utils/event-waiter.js.map +1 -0
- package/dist/utils/index.d.ts +51 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +72 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +22 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +91 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +122 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +190 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/retry-policy.d.ts +191 -0
- package/dist/utils/retry-policy.d.ts.map +1 -0
- package/dist/utils/retry-policy.js +225 -0
- package/dist/utils/retry-policy.js.map +1 -0
- package/dist/utils/secure-private-key.d.ts +113 -0
- package/dist/utils/secure-private-key.d.ts.map +1 -0
- package/dist/utils/secure-private-key.js +188 -0
- package/dist/utils/secure-private-key.js.map +1 -0
- package/dist/utils/signature-verifier.d.ts +143 -0
- package/dist/utils/signature-verifier.d.ts.map +1 -0
- package/dist/utils/signature-verifier.js +238 -0
- package/dist/utils/signature-verifier.js.map +1 -0
- package/dist/utils/ssrf-validator.d.ts +36 -0
- package/dist/utils/ssrf-validator.d.ts.map +1 -0
- package/dist/utils/ssrf-validator.js +195 -0
- package/dist/utils/ssrf-validator.js.map +1 -0
- package/examples/.env.example +17 -0
- package/examples/basic-usage.ts +211 -0
- package/examples/production-dashboard/.env.example +153 -0
- package/examples/production-dashboard/package.json +39 -0
- package/examples/production-dashboard/public/dashboard.html +642 -0
- package/examples/production-dashboard/server.ts +753 -0
- package/examples/webhook-integration.ts +239 -0
- package/examples/x-influencer-battle-redesign.html +1065 -0
- package/examples/x-influencer-battle-server.ts +217 -0
- package/examples/x-influencer-battle.html +787 -0
- package/package.json +65 -0
- package/src/constants.ts +43 -0
- package/src/core/websocket-client.test.ts +512 -0
- package/src/core/websocket-client.ts +1056 -0
- package/src/formatters/response-formatter.test.ts +571 -0
- package/src/formatters/response-formatter.ts +677 -0
- package/src/handlers/message-handler-registry.ts +239 -0
- package/src/handlers/message-handlers/agent-selected-handler.ts +40 -0
- package/src/handlers/message-handlers/agents-list-handler.ts +26 -0
- package/src/handlers/message-handlers/auth-error-handler.ts +31 -0
- package/src/handlers/message-handlers/auth-message-handler.ts +66 -0
- package/src/handlers/message-handlers/auth-required-handler.ts +23 -0
- package/src/handlers/message-handlers/auth-success-handler.ts +57 -0
- package/src/handlers/message-handlers/base-handler.ts +101 -0
- package/src/handlers/message-handlers/challenge-handler.ts +57 -0
- package/src/handlers/message-handlers/error-message-handler.ts +27 -0
- package/src/handlers/message-handlers/index.ts +77 -0
- package/src/handlers/message-handlers/list-rooms-response-handler.ts +28 -0
- package/src/handlers/message-handlers/ping-pong-handler.ts +30 -0
- package/src/handlers/message-handlers/regular-message-handler.ts +65 -0
- package/src/handlers/message-handlers/subscribe-response-handler.ts +47 -0
- package/src/handlers/message-handlers/task-response-handler.ts +45 -0
- package/src/handlers/message-handlers/types.ts +77 -0
- package/src/handlers/message-handlers/unsubscribe-response-handler.ts +47 -0
- package/src/handlers/webhook-handler.test.ts +789 -0
- package/src/handlers/webhook-handler.ts +576 -0
- package/src/index.ts +269 -0
- package/src/managers/agent-registry.test.ts +466 -0
- package/src/managers/agent-registry.ts +347 -0
- package/src/managers/connection-manager.ts +195 -0
- package/src/managers/index.ts +9 -0
- package/src/managers/message-router.ts +349 -0
- package/src/managers/room-manager.ts +248 -0
- package/src/teneo-sdk.ts +1022 -0
- package/src/types/config.test.ts +325 -0
- package/src/types/config.ts +799 -0
- package/src/types/error-codes.ts +44 -0
- package/src/types/events.test.ts +302 -0
- package/src/types/events.ts +382 -0
- package/src/types/health.ts +46 -0
- package/src/types/index.ts +199 -0
- package/src/types/messages.test.ts +660 -0
- package/src/types/messages.ts +570 -0
- package/src/types/validation.ts +123 -0
- package/src/utils/bounded-queue.test.ts +356 -0
- package/src/utils/bounded-queue.ts +205 -0
- package/src/utils/circuit-breaker.test.ts +394 -0
- package/src/utils/circuit-breaker.ts +262 -0
- package/src/utils/deduplication-cache.test.ts +380 -0
- package/src/utils/deduplication-cache.ts +198 -0
- package/src/utils/event-waiter.test.ts +381 -0
- package/src/utils/event-waiter.ts +172 -0
- package/src/utils/index.ts +74 -0
- package/src/utils/logger.ts +87 -0
- package/src/utils/rate-limiter.test.ts +341 -0
- package/src/utils/rate-limiter.ts +211 -0
- package/src/utils/retry-policy.test.ts +558 -0
- package/src/utils/retry-policy.ts +272 -0
- package/src/utils/secure-private-key.test.ts +356 -0
- package/src/utils/secure-private-key.ts +205 -0
- package/src/utils/signature-verifier.test.ts +464 -0
- package/src/utils/signature-verifier.ts +298 -0
- package/src/utils/ssrf-validator.test.ts +372 -0
- package/src/utils/ssrf-validator.ts +224 -0
- package/tests/integration/real-server.test.ts +740 -0
- package/tests/integration/websocket.test.ts +381 -0
- package/tests/integration-setup.ts +16 -0
- package/tests/setup.ts +34 -0
- package/tsconfig.json +32 -0
- package/vitest.config.ts +42 -0
- 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
|
+
}
|