@teneo-protocol/sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/.dockerignore +14 -0
  2. package/.env.test.example +14 -0
  3. package/.eslintrc.json +26 -0
  4. package/.github/workflows/claude-code-review.yml +78 -0
  5. package/.github/workflows/claude-reviewer.yml +64 -0
  6. package/.github/workflows/publish-npm.yml +38 -0
  7. package/.github/workflows/push-to-main.yml +23 -0
  8. package/.node-version +1 -0
  9. package/.prettierrc +11 -0
  10. package/Dockerfile +25 -0
  11. package/LICENCE +661 -0
  12. package/README.md +709 -0
  13. package/dist/constants.d.ts +42 -0
  14. package/dist/constants.d.ts.map +1 -0
  15. package/dist/constants.js +45 -0
  16. package/dist/constants.js.map +1 -0
  17. package/dist/core/websocket-client.d.ts +261 -0
  18. package/dist/core/websocket-client.d.ts.map +1 -0
  19. package/dist/core/websocket-client.js +875 -0
  20. package/dist/core/websocket-client.js.map +1 -0
  21. package/dist/formatters/response-formatter.d.ts +354 -0
  22. package/dist/formatters/response-formatter.d.ts.map +1 -0
  23. package/dist/formatters/response-formatter.js +575 -0
  24. package/dist/formatters/response-formatter.js.map +1 -0
  25. package/dist/handlers/message-handler-registry.d.ts +155 -0
  26. package/dist/handlers/message-handler-registry.d.ts.map +1 -0
  27. package/dist/handlers/message-handler-registry.js +216 -0
  28. package/dist/handlers/message-handler-registry.js.map +1 -0
  29. package/dist/handlers/message-handlers/agent-selected-handler.d.ts +112 -0
  30. package/dist/handlers/message-handlers/agent-selected-handler.d.ts.map +1 -0
  31. package/dist/handlers/message-handlers/agent-selected-handler.js +40 -0
  32. package/dist/handlers/message-handlers/agent-selected-handler.js.map +1 -0
  33. package/dist/handlers/message-handlers/agents-list-handler.d.ts +14 -0
  34. package/dist/handlers/message-handlers/agents-list-handler.d.ts.map +1 -0
  35. package/dist/handlers/message-handlers/agents-list-handler.js +25 -0
  36. package/dist/handlers/message-handlers/agents-list-handler.js.map +1 -0
  37. package/dist/handlers/message-handlers/auth-error-handler.d.ts +71 -0
  38. package/dist/handlers/message-handlers/auth-error-handler.d.ts.map +1 -0
  39. package/dist/handlers/message-handlers/auth-error-handler.js +30 -0
  40. package/dist/handlers/message-handlers/auth-error-handler.js.map +1 -0
  41. package/dist/handlers/message-handlers/auth-message-handler.d.ts +18 -0
  42. package/dist/handlers/message-handlers/auth-message-handler.d.ts.map +1 -0
  43. package/dist/handlers/message-handlers/auth-message-handler.js +60 -0
  44. package/dist/handlers/message-handlers/auth-message-handler.js.map +1 -0
  45. package/dist/handlers/message-handlers/auth-required-handler.d.ts +76 -0
  46. package/dist/handlers/message-handlers/auth-required-handler.d.ts.map +1 -0
  47. package/dist/handlers/message-handlers/auth-required-handler.js +23 -0
  48. package/dist/handlers/message-handlers/auth-required-handler.js.map +1 -0
  49. package/dist/handlers/message-handlers/auth-success-handler.d.ts +18 -0
  50. package/dist/handlers/message-handlers/auth-success-handler.d.ts.map +1 -0
  51. package/dist/handlers/message-handlers/auth-success-handler.js +51 -0
  52. package/dist/handlers/message-handlers/auth-success-handler.js.map +1 -0
  53. package/dist/handlers/message-handlers/base-handler.d.ts +55 -0
  54. package/dist/handlers/message-handlers/base-handler.d.ts.map +1 -0
  55. package/dist/handlers/message-handlers/base-handler.js +83 -0
  56. package/dist/handlers/message-handlers/base-handler.js.map +1 -0
  57. package/dist/handlers/message-handlers/challenge-handler.d.ts +73 -0
  58. package/dist/handlers/message-handlers/challenge-handler.d.ts.map +1 -0
  59. package/dist/handlers/message-handlers/challenge-handler.js +47 -0
  60. package/dist/handlers/message-handlers/challenge-handler.js.map +1 -0
  61. package/dist/handlers/message-handlers/error-message-handler.d.ts +76 -0
  62. package/dist/handlers/message-handlers/error-message-handler.d.ts.map +1 -0
  63. package/dist/handlers/message-handlers/error-message-handler.js +29 -0
  64. package/dist/handlers/message-handlers/error-message-handler.js.map +1 -0
  65. package/dist/handlers/message-handlers/index.d.ts +28 -0
  66. package/dist/handlers/message-handlers/index.d.ts.map +1 -0
  67. package/dist/handlers/message-handlers/index.js +100 -0
  68. package/dist/handlers/message-handlers/index.js.map +1 -0
  69. package/dist/handlers/message-handlers/list-rooms-response-handler.d.ts +122 -0
  70. package/dist/handlers/message-handlers/list-rooms-response-handler.d.ts.map +1 -0
  71. package/dist/handlers/message-handlers/list-rooms-response-handler.js +30 -0
  72. package/dist/handlers/message-handlers/list-rooms-response-handler.js.map +1 -0
  73. package/dist/handlers/message-handlers/ping-pong-handler.d.ts +104 -0
  74. package/dist/handlers/message-handlers/ping-pong-handler.d.ts.map +1 -0
  75. package/dist/handlers/message-handlers/ping-pong-handler.js +36 -0
  76. package/dist/handlers/message-handlers/ping-pong-handler.js.map +1 -0
  77. package/dist/handlers/message-handlers/regular-message-handler.d.ts +56 -0
  78. package/dist/handlers/message-handlers/regular-message-handler.d.ts.map +1 -0
  79. package/dist/handlers/message-handlers/regular-message-handler.js +59 -0
  80. package/dist/handlers/message-handlers/regular-message-handler.js.map +1 -0
  81. package/dist/handlers/message-handlers/subscribe-response-handler.d.ts +81 -0
  82. package/dist/handlers/message-handlers/subscribe-response-handler.d.ts.map +1 -0
  83. package/dist/handlers/message-handlers/subscribe-response-handler.js +48 -0
  84. package/dist/handlers/message-handlers/subscribe-response-handler.js.map +1 -0
  85. package/dist/handlers/message-handlers/task-response-handler.d.ts +14 -0
  86. package/dist/handlers/message-handlers/task-response-handler.d.ts.map +1 -0
  87. package/dist/handlers/message-handlers/task-response-handler.js +44 -0
  88. package/dist/handlers/message-handlers/task-response-handler.js.map +1 -0
  89. package/dist/handlers/message-handlers/types.d.ts +51 -0
  90. package/dist/handlers/message-handlers/types.d.ts.map +1 -0
  91. package/dist/handlers/message-handlers/types.js +7 -0
  92. package/dist/handlers/message-handlers/types.js.map +1 -0
  93. package/dist/handlers/message-handlers/unsubscribe-response-handler.d.ts +81 -0
  94. package/dist/handlers/message-handlers/unsubscribe-response-handler.d.ts.map +1 -0
  95. package/dist/handlers/message-handlers/unsubscribe-response-handler.js +48 -0
  96. package/dist/handlers/message-handlers/unsubscribe-response-handler.js.map +1 -0
  97. package/dist/handlers/webhook-handler.d.ts +202 -0
  98. package/dist/handlers/webhook-handler.d.ts.map +1 -0
  99. package/dist/handlers/webhook-handler.js +511 -0
  100. package/dist/handlers/webhook-handler.js.map +1 -0
  101. package/dist/index.d.ts +71 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +217 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/managers/agent-registry.d.ts +173 -0
  106. package/dist/managers/agent-registry.d.ts.map +1 -0
  107. package/dist/managers/agent-registry.js +310 -0
  108. package/dist/managers/agent-registry.js.map +1 -0
  109. package/dist/managers/connection-manager.d.ts +134 -0
  110. package/dist/managers/connection-manager.d.ts.map +1 -0
  111. package/dist/managers/connection-manager.js +176 -0
  112. package/dist/managers/connection-manager.js.map +1 -0
  113. package/dist/managers/index.d.ts +9 -0
  114. package/dist/managers/index.d.ts.map +1 -0
  115. package/dist/managers/index.js +16 -0
  116. package/dist/managers/index.js.map +1 -0
  117. package/dist/managers/message-router.d.ts +112 -0
  118. package/dist/managers/message-router.d.ts.map +1 -0
  119. package/dist/managers/message-router.js +260 -0
  120. package/dist/managers/message-router.js.map +1 -0
  121. package/dist/managers/room-manager.d.ts +165 -0
  122. package/dist/managers/room-manager.d.ts.map +1 -0
  123. package/dist/managers/room-manager.js +227 -0
  124. package/dist/managers/room-manager.js.map +1 -0
  125. package/dist/teneo-sdk.d.ts +703 -0
  126. package/dist/teneo-sdk.d.ts.map +1 -0
  127. package/dist/teneo-sdk.js +907 -0
  128. package/dist/teneo-sdk.js.map +1 -0
  129. package/dist/types/config.d.ts +1047 -0
  130. package/dist/types/config.d.ts.map +1 -0
  131. package/dist/types/config.js +720 -0
  132. package/dist/types/config.js.map +1 -0
  133. package/dist/types/error-codes.d.ts +29 -0
  134. package/dist/types/error-codes.d.ts.map +1 -0
  135. package/dist/types/error-codes.js +41 -0
  136. package/dist/types/error-codes.js.map +1 -0
  137. package/dist/types/events.d.ts +616 -0
  138. package/dist/types/events.d.ts.map +1 -0
  139. package/dist/types/events.js +261 -0
  140. package/dist/types/events.js.map +1 -0
  141. package/dist/types/health.d.ts +40 -0
  142. package/dist/types/health.d.ts.map +1 -0
  143. package/dist/types/health.js +6 -0
  144. package/dist/types/health.js.map +1 -0
  145. package/dist/types/index.d.ts +10 -0
  146. package/dist/types/index.d.ts.map +1 -0
  147. package/dist/types/index.js +123 -0
  148. package/dist/types/index.js.map +1 -0
  149. package/dist/types/messages.d.ts +3734 -0
  150. package/dist/types/messages.d.ts.map +1 -0
  151. package/dist/types/messages.js +482 -0
  152. package/dist/types/messages.js.map +1 -0
  153. package/dist/types/validation.d.ts +81 -0
  154. package/dist/types/validation.d.ts.map +1 -0
  155. package/dist/types/validation.js +115 -0
  156. package/dist/types/validation.js.map +1 -0
  157. package/dist/utils/bounded-queue.d.ts +127 -0
  158. package/dist/utils/bounded-queue.d.ts.map +1 -0
  159. package/dist/utils/bounded-queue.js +181 -0
  160. package/dist/utils/bounded-queue.js.map +1 -0
  161. package/dist/utils/circuit-breaker.d.ts +141 -0
  162. package/dist/utils/circuit-breaker.d.ts.map +1 -0
  163. package/dist/utils/circuit-breaker.js +215 -0
  164. package/dist/utils/circuit-breaker.js.map +1 -0
  165. package/dist/utils/deduplication-cache.d.ts +110 -0
  166. package/dist/utils/deduplication-cache.d.ts.map +1 -0
  167. package/dist/utils/deduplication-cache.js +177 -0
  168. package/dist/utils/deduplication-cache.js.map +1 -0
  169. package/dist/utils/event-waiter.d.ts +101 -0
  170. package/dist/utils/event-waiter.d.ts.map +1 -0
  171. package/dist/utils/event-waiter.js +118 -0
  172. package/dist/utils/event-waiter.js.map +1 -0
  173. package/dist/utils/index.d.ts +51 -0
  174. package/dist/utils/index.d.ts.map +1 -0
  175. package/dist/utils/index.js +72 -0
  176. package/dist/utils/index.js.map +1 -0
  177. package/dist/utils/logger.d.ts +22 -0
  178. package/dist/utils/logger.d.ts.map +1 -0
  179. package/dist/utils/logger.js +91 -0
  180. package/dist/utils/logger.js.map +1 -0
  181. package/dist/utils/rate-limiter.d.ts +122 -0
  182. package/dist/utils/rate-limiter.d.ts.map +1 -0
  183. package/dist/utils/rate-limiter.js +190 -0
  184. package/dist/utils/rate-limiter.js.map +1 -0
  185. package/dist/utils/retry-policy.d.ts +191 -0
  186. package/dist/utils/retry-policy.d.ts.map +1 -0
  187. package/dist/utils/retry-policy.js +225 -0
  188. package/dist/utils/retry-policy.js.map +1 -0
  189. package/dist/utils/secure-private-key.d.ts +113 -0
  190. package/dist/utils/secure-private-key.d.ts.map +1 -0
  191. package/dist/utils/secure-private-key.js +188 -0
  192. package/dist/utils/secure-private-key.js.map +1 -0
  193. package/dist/utils/signature-verifier.d.ts +143 -0
  194. package/dist/utils/signature-verifier.d.ts.map +1 -0
  195. package/dist/utils/signature-verifier.js +238 -0
  196. package/dist/utils/signature-verifier.js.map +1 -0
  197. package/dist/utils/ssrf-validator.d.ts +36 -0
  198. package/dist/utils/ssrf-validator.d.ts.map +1 -0
  199. package/dist/utils/ssrf-validator.js +195 -0
  200. package/dist/utils/ssrf-validator.js.map +1 -0
  201. package/examples/.env.example +17 -0
  202. package/examples/basic-usage.ts +211 -0
  203. package/examples/production-dashboard/.env.example +153 -0
  204. package/examples/production-dashboard/package.json +39 -0
  205. package/examples/production-dashboard/public/dashboard.html +642 -0
  206. package/examples/production-dashboard/server.ts +753 -0
  207. package/examples/webhook-integration.ts +239 -0
  208. package/examples/x-influencer-battle-redesign.html +1065 -0
  209. package/examples/x-influencer-battle-server.ts +217 -0
  210. package/examples/x-influencer-battle.html +787 -0
  211. package/package.json +65 -0
  212. package/src/constants.ts +43 -0
  213. package/src/core/websocket-client.test.ts +512 -0
  214. package/src/core/websocket-client.ts +1056 -0
  215. package/src/formatters/response-formatter.test.ts +571 -0
  216. package/src/formatters/response-formatter.ts +677 -0
  217. package/src/handlers/message-handler-registry.ts +239 -0
  218. package/src/handlers/message-handlers/agent-selected-handler.ts +40 -0
  219. package/src/handlers/message-handlers/agents-list-handler.ts +26 -0
  220. package/src/handlers/message-handlers/auth-error-handler.ts +31 -0
  221. package/src/handlers/message-handlers/auth-message-handler.ts +66 -0
  222. package/src/handlers/message-handlers/auth-required-handler.ts +23 -0
  223. package/src/handlers/message-handlers/auth-success-handler.ts +57 -0
  224. package/src/handlers/message-handlers/base-handler.ts +101 -0
  225. package/src/handlers/message-handlers/challenge-handler.ts +57 -0
  226. package/src/handlers/message-handlers/error-message-handler.ts +27 -0
  227. package/src/handlers/message-handlers/index.ts +77 -0
  228. package/src/handlers/message-handlers/list-rooms-response-handler.ts +28 -0
  229. package/src/handlers/message-handlers/ping-pong-handler.ts +30 -0
  230. package/src/handlers/message-handlers/regular-message-handler.ts +65 -0
  231. package/src/handlers/message-handlers/subscribe-response-handler.ts +47 -0
  232. package/src/handlers/message-handlers/task-response-handler.ts +45 -0
  233. package/src/handlers/message-handlers/types.ts +77 -0
  234. package/src/handlers/message-handlers/unsubscribe-response-handler.ts +47 -0
  235. package/src/handlers/webhook-handler.test.ts +789 -0
  236. package/src/handlers/webhook-handler.ts +576 -0
  237. package/src/index.ts +269 -0
  238. package/src/managers/agent-registry.test.ts +466 -0
  239. package/src/managers/agent-registry.ts +347 -0
  240. package/src/managers/connection-manager.ts +195 -0
  241. package/src/managers/index.ts +9 -0
  242. package/src/managers/message-router.ts +349 -0
  243. package/src/managers/room-manager.ts +248 -0
  244. package/src/teneo-sdk.ts +1022 -0
  245. package/src/types/config.test.ts +325 -0
  246. package/src/types/config.ts +799 -0
  247. package/src/types/error-codes.ts +44 -0
  248. package/src/types/events.test.ts +302 -0
  249. package/src/types/events.ts +382 -0
  250. package/src/types/health.ts +46 -0
  251. package/src/types/index.ts +199 -0
  252. package/src/types/messages.test.ts +660 -0
  253. package/src/types/messages.ts +570 -0
  254. package/src/types/validation.ts +123 -0
  255. package/src/utils/bounded-queue.test.ts +356 -0
  256. package/src/utils/bounded-queue.ts +205 -0
  257. package/src/utils/circuit-breaker.test.ts +394 -0
  258. package/src/utils/circuit-breaker.ts +262 -0
  259. package/src/utils/deduplication-cache.test.ts +380 -0
  260. package/src/utils/deduplication-cache.ts +198 -0
  261. package/src/utils/event-waiter.test.ts +381 -0
  262. package/src/utils/event-waiter.ts +172 -0
  263. package/src/utils/index.ts +74 -0
  264. package/src/utils/logger.ts +87 -0
  265. package/src/utils/rate-limiter.test.ts +341 -0
  266. package/src/utils/rate-limiter.ts +211 -0
  267. package/src/utils/retry-policy.test.ts +558 -0
  268. package/src/utils/retry-policy.ts +272 -0
  269. package/src/utils/secure-private-key.test.ts +356 -0
  270. package/src/utils/secure-private-key.ts +205 -0
  271. package/src/utils/signature-verifier.test.ts +464 -0
  272. package/src/utils/signature-verifier.ts +298 -0
  273. package/src/utils/ssrf-validator.test.ts +372 -0
  274. package/src/utils/ssrf-validator.ts +224 -0
  275. package/tests/integration/real-server.test.ts +740 -0
  276. package/tests/integration/websocket.test.ts +381 -0
  277. package/tests/integration-setup.ts +16 -0
  278. package/tests/setup.ts +34 -0
  279. package/tsconfig.json +32 -0
  280. package/vitest.config.ts +42 -0
  281. package/vitest.integration.config.ts +23 -0
@@ -0,0 +1,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
+ });