@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,558 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for RetryPolicy - Configurable retry strategies
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { RetryPolicy, RetryStrategySchema } from "./retry-policy";
|
|
7
|
+
|
|
8
|
+
describe("RetryPolicy", () => {
|
|
9
|
+
describe("constructor validation", () => {
|
|
10
|
+
it("should create policy with valid strategy", () => {
|
|
11
|
+
const policy = new RetryPolicy({
|
|
12
|
+
type: "exponential",
|
|
13
|
+
baseDelay: 1000,
|
|
14
|
+
maxDelay: 60000,
|
|
15
|
+
maxAttempts: 10,
|
|
16
|
+
jitter: true,
|
|
17
|
+
backoffMultiplier: 2
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(policy).toBeDefined();
|
|
21
|
+
expect(policy.getStrategy()).toEqual({
|
|
22
|
+
type: "exponential",
|
|
23
|
+
baseDelay: 1000,
|
|
24
|
+
maxDelay: 60000,
|
|
25
|
+
maxAttempts: 10,
|
|
26
|
+
jitter: true,
|
|
27
|
+
backoffMultiplier: 2
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should reject invalid strategy type", () => {
|
|
32
|
+
expect(() => {
|
|
33
|
+
new RetryPolicy({
|
|
34
|
+
type: "invalid" as any,
|
|
35
|
+
baseDelay: 1000,
|
|
36
|
+
maxDelay: 60000,
|
|
37
|
+
maxAttempts: 10,
|
|
38
|
+
jitter: false
|
|
39
|
+
});
|
|
40
|
+
}).toThrow();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should reject negative baseDelay", () => {
|
|
44
|
+
expect(() => {
|
|
45
|
+
new RetryPolicy({
|
|
46
|
+
type: "exponential",
|
|
47
|
+
baseDelay: -1000,
|
|
48
|
+
maxDelay: 60000,
|
|
49
|
+
maxAttempts: 10,
|
|
50
|
+
jitter: false
|
|
51
|
+
});
|
|
52
|
+
}).toThrow();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should reject negative maxAttempts", () => {
|
|
56
|
+
expect(() => {
|
|
57
|
+
new RetryPolicy({
|
|
58
|
+
type: "exponential",
|
|
59
|
+
baseDelay: 1000,
|
|
60
|
+
maxDelay: 60000,
|
|
61
|
+
maxAttempts: -5,
|
|
62
|
+
jitter: false
|
|
63
|
+
});
|
|
64
|
+
}).toThrow();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should reject maxDelay < baseDelay", () => {
|
|
68
|
+
expect(() => {
|
|
69
|
+
new RetryPolicy({
|
|
70
|
+
type: "exponential",
|
|
71
|
+
baseDelay: 60000,
|
|
72
|
+
maxDelay: 1000,
|
|
73
|
+
maxAttempts: 10,
|
|
74
|
+
jitter: false
|
|
75
|
+
});
|
|
76
|
+
}).toThrow("maxDelay must be greater than or equal to baseDelay");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should accept maxDelay = baseDelay", () => {
|
|
80
|
+
const policy = new RetryPolicy({
|
|
81
|
+
type: "constant",
|
|
82
|
+
baseDelay: 5000,
|
|
83
|
+
maxDelay: 5000,
|
|
84
|
+
maxAttempts: 3,
|
|
85
|
+
jitter: false
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(policy).toBeDefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should accept zero maxAttempts (no retries)", () => {
|
|
92
|
+
const policy = new RetryPolicy({
|
|
93
|
+
type: "exponential",
|
|
94
|
+
baseDelay: 1000,
|
|
95
|
+
maxDelay: 60000,
|
|
96
|
+
maxAttempts: 0,
|
|
97
|
+
jitter: false
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(policy.shouldRetry(1)).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("exponential backoff strategy", () => {
|
|
105
|
+
it("should calculate exponential delays with multiplier 2", () => {
|
|
106
|
+
const policy = new RetryPolicy({
|
|
107
|
+
type: "exponential",
|
|
108
|
+
baseDelay: 1000,
|
|
109
|
+
maxDelay: 60000,
|
|
110
|
+
maxAttempts: 10,
|
|
111
|
+
jitter: false,
|
|
112
|
+
backoffMultiplier: 2
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(policy.calculateDelay(1)).toBe(1000); // 1000 * 2^0
|
|
116
|
+
expect(policy.calculateDelay(2)).toBe(2000); // 1000 * 2^1
|
|
117
|
+
expect(policy.calculateDelay(3)).toBe(4000); // 1000 * 2^2
|
|
118
|
+
expect(policy.calculateDelay(4)).toBe(8000); // 1000 * 2^3
|
|
119
|
+
expect(policy.calculateDelay(5)).toBe(16000); // 1000 * 2^4
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should calculate exponential delays with multiplier 3", () => {
|
|
123
|
+
const policy = new RetryPolicy({
|
|
124
|
+
type: "exponential",
|
|
125
|
+
baseDelay: 1000,
|
|
126
|
+
maxDelay: 100000,
|
|
127
|
+
maxAttempts: 10,
|
|
128
|
+
jitter: false,
|
|
129
|
+
backoffMultiplier: 3
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(policy.calculateDelay(1)).toBe(1000); // 1000 * 3^0
|
|
133
|
+
expect(policy.calculateDelay(2)).toBe(3000); // 1000 * 3^1
|
|
134
|
+
expect(policy.calculateDelay(3)).toBe(9000); // 1000 * 3^2
|
|
135
|
+
expect(policy.calculateDelay(4)).toBe(27000); // 1000 * 3^3
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should cap delay at maxDelay", () => {
|
|
139
|
+
const policy = new RetryPolicy({
|
|
140
|
+
type: "exponential",
|
|
141
|
+
baseDelay: 1000,
|
|
142
|
+
maxDelay: 10000,
|
|
143
|
+
maxAttempts: 10,
|
|
144
|
+
jitter: false,
|
|
145
|
+
backoffMultiplier: 2
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(policy.calculateDelay(1)).toBe(1000);
|
|
149
|
+
expect(policy.calculateDelay(2)).toBe(2000);
|
|
150
|
+
expect(policy.calculateDelay(3)).toBe(4000);
|
|
151
|
+
expect(policy.calculateDelay(4)).toBe(8000);
|
|
152
|
+
expect(policy.calculateDelay(5)).toBe(10000); // Capped
|
|
153
|
+
expect(policy.calculateDelay(6)).toBe(10000); // Capped
|
|
154
|
+
expect(policy.calculateDelay(10)).toBe(10000); // Capped
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should use default multiplier of 2 when not specified", () => {
|
|
158
|
+
const policy = new RetryPolicy({
|
|
159
|
+
type: "exponential",
|
|
160
|
+
baseDelay: 1000,
|
|
161
|
+
maxDelay: 60000,
|
|
162
|
+
maxAttempts: 5,
|
|
163
|
+
jitter: false
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(policy.calculateDelay(1)).toBe(1000);
|
|
167
|
+
expect(policy.calculateDelay(2)).toBe(2000);
|
|
168
|
+
expect(policy.calculateDelay(3)).toBe(4000);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("linear backoff strategy", () => {
|
|
173
|
+
it("should calculate linear delays", () => {
|
|
174
|
+
const policy = new RetryPolicy({
|
|
175
|
+
type: "linear",
|
|
176
|
+
baseDelay: 2000,
|
|
177
|
+
maxDelay: 30000,
|
|
178
|
+
maxAttempts: 10,
|
|
179
|
+
jitter: false
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(policy.calculateDelay(1)).toBe(2000); // 2000 * 1
|
|
183
|
+
expect(policy.calculateDelay(2)).toBe(4000); // 2000 * 2
|
|
184
|
+
expect(policy.calculateDelay(3)).toBe(6000); // 2000 * 3
|
|
185
|
+
expect(policy.calculateDelay(4)).toBe(8000); // 2000 * 4
|
|
186
|
+
expect(policy.calculateDelay(5)).toBe(10000); // 2000 * 5
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("should cap delay at maxDelay", () => {
|
|
190
|
+
const policy = new RetryPolicy({
|
|
191
|
+
type: "linear",
|
|
192
|
+
baseDelay: 5000,
|
|
193
|
+
maxDelay: 15000,
|
|
194
|
+
maxAttempts: 10,
|
|
195
|
+
jitter: false
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(policy.calculateDelay(1)).toBe(5000);
|
|
199
|
+
expect(policy.calculateDelay(2)).toBe(10000);
|
|
200
|
+
expect(policy.calculateDelay(3)).toBe(15000); // Capped
|
|
201
|
+
expect(policy.calculateDelay(4)).toBe(15000); // Capped
|
|
202
|
+
expect(policy.calculateDelay(10)).toBe(15000); // Capped
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("constant backoff strategy", () => {
|
|
207
|
+
it("should return constant delay", () => {
|
|
208
|
+
const policy = new RetryPolicy({
|
|
209
|
+
type: "constant",
|
|
210
|
+
baseDelay: 5000,
|
|
211
|
+
maxDelay: 5000,
|
|
212
|
+
maxAttempts: 5,
|
|
213
|
+
jitter: false
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(policy.calculateDelay(1)).toBe(5000);
|
|
217
|
+
expect(policy.calculateDelay(2)).toBe(5000);
|
|
218
|
+
expect(policy.calculateDelay(3)).toBe(5000);
|
|
219
|
+
expect(policy.calculateDelay(10)).toBe(5000);
|
|
220
|
+
expect(policy.calculateDelay(100)).toBe(5000);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("jitter", () => {
|
|
225
|
+
it("should add jitter when enabled", () => {
|
|
226
|
+
const policy = new RetryPolicy({
|
|
227
|
+
type: "constant",
|
|
228
|
+
baseDelay: 5000,
|
|
229
|
+
maxDelay: 5000,
|
|
230
|
+
maxAttempts: 5,
|
|
231
|
+
jitter: true
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Jitter adds 0-1000ms, so delay should be between 5000 and 6000
|
|
235
|
+
const delay1 = policy.calculateDelay(1);
|
|
236
|
+
const delay2 = policy.calculateDelay(1);
|
|
237
|
+
|
|
238
|
+
expect(delay1).toBeGreaterThanOrEqual(5000);
|
|
239
|
+
expect(delay1).toBeLessThanOrEqual(6000);
|
|
240
|
+
expect(delay2).toBeGreaterThanOrEqual(5000);
|
|
241
|
+
expect(delay2).toBeLessThanOrEqual(6000);
|
|
242
|
+
|
|
243
|
+
// Should be random (very unlikely to be exactly equal)
|
|
244
|
+
// Run multiple times to verify randomness
|
|
245
|
+
const delays = Array.from({ length: 10 }, () => policy.calculateDelay(1));
|
|
246
|
+
const uniqueDelays = new Set(delays);
|
|
247
|
+
expect(uniqueDelays.size).toBeGreaterThan(1); // Should have variation
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("should not add jitter when disabled", () => {
|
|
251
|
+
const policy = new RetryPolicy({
|
|
252
|
+
type: "constant",
|
|
253
|
+
baseDelay: 5000,
|
|
254
|
+
maxDelay: 5000,
|
|
255
|
+
maxAttempts: 5,
|
|
256
|
+
jitter: false
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const delays = Array.from({ length: 10 }, () => policy.calculateDelay(1));
|
|
260
|
+
|
|
261
|
+
// All delays should be exactly 5000
|
|
262
|
+
delays.forEach((delay) => {
|
|
263
|
+
expect(delay).toBe(5000);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("should add jitter to exponential backoff", () => {
|
|
268
|
+
const policy = new RetryPolicy({
|
|
269
|
+
type: "exponential",
|
|
270
|
+
baseDelay: 1000,
|
|
271
|
+
maxDelay: 60000,
|
|
272
|
+
maxAttempts: 5,
|
|
273
|
+
jitter: true,
|
|
274
|
+
backoffMultiplier: 2
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// First attempt: 1000ms base + 0-1000ms jitter
|
|
278
|
+
const delay1 = policy.calculateDelay(1);
|
|
279
|
+
expect(delay1).toBeGreaterThanOrEqual(1000);
|
|
280
|
+
expect(delay1).toBeLessThanOrEqual(2000);
|
|
281
|
+
|
|
282
|
+
// Second attempt: 2000ms base + 0-1000ms jitter
|
|
283
|
+
const delay2 = policy.calculateDelay(2);
|
|
284
|
+
expect(delay2).toBeGreaterThanOrEqual(2000);
|
|
285
|
+
expect(delay2).toBeLessThanOrEqual(3000);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe("shouldRetry", () => {
|
|
290
|
+
it("should allow retries within maxAttempts", () => {
|
|
291
|
+
const policy = new RetryPolicy({
|
|
292
|
+
type: "exponential",
|
|
293
|
+
baseDelay: 1000,
|
|
294
|
+
maxDelay: 60000,
|
|
295
|
+
maxAttempts: 3,
|
|
296
|
+
jitter: false
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
expect(policy.shouldRetry(1)).toBe(true);
|
|
300
|
+
expect(policy.shouldRetry(2)).toBe(true);
|
|
301
|
+
expect(policy.shouldRetry(3)).toBe(true);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("should reject retries beyond maxAttempts", () => {
|
|
305
|
+
const policy = new RetryPolicy({
|
|
306
|
+
type: "exponential",
|
|
307
|
+
baseDelay: 1000,
|
|
308
|
+
maxDelay: 60000,
|
|
309
|
+
maxAttempts: 3,
|
|
310
|
+
jitter: false
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
expect(policy.shouldRetry(4)).toBe(false);
|
|
314
|
+
expect(policy.shouldRetry(5)).toBe(false);
|
|
315
|
+
expect(policy.shouldRetry(100)).toBe(false);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("should reject all retries when maxAttempts is 0", () => {
|
|
319
|
+
const policy = new RetryPolicy({
|
|
320
|
+
type: "exponential",
|
|
321
|
+
baseDelay: 1000,
|
|
322
|
+
maxDelay: 60000,
|
|
323
|
+
maxAttempts: 0,
|
|
324
|
+
jitter: false
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(policy.shouldRetry(1)).toBe(false);
|
|
328
|
+
expect(policy.shouldRetry(2)).toBe(false);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe("calculateDelay edge cases", () => {
|
|
333
|
+
it("should throw error for attempt < 1", () => {
|
|
334
|
+
const policy = RetryPolicy.exponential(1000, 60000, 10, false);
|
|
335
|
+
|
|
336
|
+
expect(() => policy.calculateDelay(0)).toThrow("Attempt number must be >= 1");
|
|
337
|
+
expect(() => policy.calculateDelay(-1)).toThrow("Attempt number must be >= 1");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("should handle very large attempt numbers", () => {
|
|
341
|
+
const policy = new RetryPolicy({
|
|
342
|
+
type: "exponential",
|
|
343
|
+
baseDelay: 1000,
|
|
344
|
+
maxDelay: 60000,
|
|
345
|
+
maxAttempts: 100,
|
|
346
|
+
jitter: false,
|
|
347
|
+
backoffMultiplier: 2
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Should cap at maxDelay
|
|
351
|
+
expect(policy.calculateDelay(100)).toBe(60000);
|
|
352
|
+
expect(policy.calculateDelay(1000)).toBe(60000);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("should return integer delays", () => {
|
|
356
|
+
const policy = new RetryPolicy({
|
|
357
|
+
type: "exponential",
|
|
358
|
+
baseDelay: 333,
|
|
359
|
+
maxDelay: 10000,
|
|
360
|
+
maxAttempts: 10,
|
|
361
|
+
jitter: false,
|
|
362
|
+
backoffMultiplier: 2
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const delay = policy.calculateDelay(3);
|
|
366
|
+
expect(Number.isInteger(delay)).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe("getStrategy", () => {
|
|
371
|
+
it("should return copy of strategy", () => {
|
|
372
|
+
const originalStrategy = {
|
|
373
|
+
type: "exponential" as const,
|
|
374
|
+
baseDelay: 1000,
|
|
375
|
+
maxDelay: 60000,
|
|
376
|
+
maxAttempts: 10,
|
|
377
|
+
jitter: true,
|
|
378
|
+
backoffMultiplier: 2
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const policy = new RetryPolicy(originalStrategy);
|
|
382
|
+
const returnedStrategy = policy.getStrategy();
|
|
383
|
+
|
|
384
|
+
expect(returnedStrategy).toEqual(originalStrategy);
|
|
385
|
+
expect(returnedStrategy).not.toBe(originalStrategy); // Different reference
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("should return immutable strategy", () => {
|
|
389
|
+
const policy = RetryPolicy.exponential(1000, 60000, 10, true);
|
|
390
|
+
const strategy = policy.getStrategy();
|
|
391
|
+
|
|
392
|
+
// Attempt to mutate
|
|
393
|
+
(strategy as any).maxAttempts = 999;
|
|
394
|
+
|
|
395
|
+
// Original should be unchanged
|
|
396
|
+
expect(policy.getStrategy().maxAttempts).toBe(10);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe("factory methods", () => {
|
|
401
|
+
describe("exponential", () => {
|
|
402
|
+
it("should create exponential policy with default multiplier", () => {
|
|
403
|
+
const policy = RetryPolicy.exponential(1000, 60000, 10, true);
|
|
404
|
+
|
|
405
|
+
expect(policy.getStrategy()).toEqual({
|
|
406
|
+
type: "exponential",
|
|
407
|
+
baseDelay: 1000,
|
|
408
|
+
maxDelay: 60000,
|
|
409
|
+
maxAttempts: 10,
|
|
410
|
+
jitter: true,
|
|
411
|
+
backoffMultiplier: 2
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("should create exponential policy with custom multiplier", () => {
|
|
416
|
+
const policy = RetryPolicy.exponential(1000, 60000, 10, false, 3);
|
|
417
|
+
|
|
418
|
+
expect(policy.getStrategy().backoffMultiplier).toBe(3);
|
|
419
|
+
expect(policy.calculateDelay(2)).toBe(3000); // 1000 * 3^1
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
describe("linear", () => {
|
|
424
|
+
it("should create linear policy", () => {
|
|
425
|
+
const policy = RetryPolicy.linear(2000, 30000, 5, false);
|
|
426
|
+
|
|
427
|
+
expect(policy.getStrategy()).toEqual({
|
|
428
|
+
type: "linear",
|
|
429
|
+
baseDelay: 2000,
|
|
430
|
+
maxDelay: 30000,
|
|
431
|
+
maxAttempts: 5,
|
|
432
|
+
jitter: false
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
expect(policy.calculateDelay(3)).toBe(6000);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
describe("constant", () => {
|
|
440
|
+
it("should create constant policy with default jitter", () => {
|
|
441
|
+
const policy = RetryPolicy.constant(5000, 3);
|
|
442
|
+
|
|
443
|
+
expect(policy.getStrategy()).toEqual({
|
|
444
|
+
type: "constant",
|
|
445
|
+
baseDelay: 5000,
|
|
446
|
+
maxDelay: 5000,
|
|
447
|
+
maxAttempts: 3,
|
|
448
|
+
jitter: false
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
expect(policy.calculateDelay(1)).toBe(5000);
|
|
452
|
+
expect(policy.calculateDelay(2)).toBe(5000);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("should create constant policy with jitter", () => {
|
|
456
|
+
const policy = RetryPolicy.constant(5000, 3, true);
|
|
457
|
+
|
|
458
|
+
expect(policy.getStrategy().jitter).toBe(true);
|
|
459
|
+
|
|
460
|
+
const delay = policy.calculateDelay(1);
|
|
461
|
+
expect(delay).toBeGreaterThanOrEqual(5000);
|
|
462
|
+
expect(delay).toBeLessThanOrEqual(6000);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
describe("RetryStrategySchema validation", () => {
|
|
468
|
+
it("should validate valid strategy", () => {
|
|
469
|
+
const result = RetryStrategySchema.safeParse({
|
|
470
|
+
type: "exponential",
|
|
471
|
+
baseDelay: 1000,
|
|
472
|
+
maxDelay: 60000,
|
|
473
|
+
maxAttempts: 10,
|
|
474
|
+
jitter: true,
|
|
475
|
+
backoffMultiplier: 2
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
expect(result.success).toBe(true);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it("should reject invalid type", () => {
|
|
482
|
+
const result = RetryStrategySchema.safeParse({
|
|
483
|
+
type: "invalid",
|
|
484
|
+
baseDelay: 1000,
|
|
485
|
+
maxDelay: 60000,
|
|
486
|
+
maxAttempts: 10,
|
|
487
|
+
jitter: true
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
expect(result.success).toBe(false);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("should reject baseDelay out of range", () => {
|
|
494
|
+
const result = RetryStrategySchema.safeParse({
|
|
495
|
+
type: "exponential",
|
|
496
|
+
baseDelay: 500000, // > 300000 max
|
|
497
|
+
maxDelay: 600000,
|
|
498
|
+
maxAttempts: 10,
|
|
499
|
+
jitter: true
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
expect(result.success).toBe(false);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("should reject maxAttempts out of range", () => {
|
|
506
|
+
const result = RetryStrategySchema.safeParse({
|
|
507
|
+
type: "exponential",
|
|
508
|
+
baseDelay: 1000,
|
|
509
|
+
maxDelay: 60000,
|
|
510
|
+
maxAttempts: 2000, // > 1000 max
|
|
511
|
+
jitter: true
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
expect(result.success).toBe(false);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
describe("real-world scenarios", () => {
|
|
519
|
+
it("should handle WebSocket reconnection pattern", () => {
|
|
520
|
+
// Typical WebSocket reconnection: exponential with jitter
|
|
521
|
+
const policy = RetryPolicy.exponential(5000, 60000, 10, true, 2);
|
|
522
|
+
|
|
523
|
+
// First retry: 5s base + jitter
|
|
524
|
+
const delay1 = policy.calculateDelay(1);
|
|
525
|
+
expect(delay1).toBeGreaterThanOrEqual(5000);
|
|
526
|
+
expect(delay1).toBeLessThanOrEqual(6000);
|
|
527
|
+
|
|
528
|
+
// Fifth retry: 80s base capped at 60s + jitter
|
|
529
|
+
const delay5 = policy.calculateDelay(5);
|
|
530
|
+
expect(delay5).toBeGreaterThanOrEqual(60000);
|
|
531
|
+
expect(delay5).toBeLessThanOrEqual(61000);
|
|
532
|
+
|
|
533
|
+
expect(policy.shouldRetry(10)).toBe(true);
|
|
534
|
+
expect(policy.shouldRetry(11)).toBe(false);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it("should handle webhook retry pattern", () => {
|
|
538
|
+
// Typical webhook retry: exponential without jitter
|
|
539
|
+
const policy = RetryPolicy.exponential(1000, 30000, 3, false, 2);
|
|
540
|
+
|
|
541
|
+
expect(policy.calculateDelay(1)).toBe(1000); // 1s
|
|
542
|
+
expect(policy.calculateDelay(2)).toBe(2000); // 2s
|
|
543
|
+
expect(policy.calculateDelay(3)).toBe(4000); // 4s
|
|
544
|
+
|
|
545
|
+
expect(policy.shouldRetry(3)).toBe(true);
|
|
546
|
+
expect(policy.shouldRetry(4)).toBe(false);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("should handle test environment constant delays", () => {
|
|
550
|
+
// For tests: predictable constant delays
|
|
551
|
+
const policy = RetryPolicy.constant(100, 5, false);
|
|
552
|
+
|
|
553
|
+
expect(policy.calculateDelay(1)).toBe(100);
|
|
554
|
+
expect(policy.calculateDelay(2)).toBe(100);
|
|
555
|
+
expect(policy.calculateDelay(3)).toBe(100);
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
});
|