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