WIP! A BB-style forum, on the ATmosphere!
We're still working... we'll be back soon when we have something to show off!
node
typescript
hono
htmx
atproto
1import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
2import { ReconnectionManager } from "../reconnection-manager.js";
3import { createMockLogger } from "./mock-logger.js";
4
5describe("ReconnectionManager", () => {
6 let reconnectionManager: ReconnectionManager;
7 let reconnectFn: ReturnType<typeof vi.fn>;
8 let mockLogger: ReturnType<typeof createMockLogger>;
9
10 beforeEach(() => {
11 vi.useFakeTimers();
12 reconnectFn = vi.fn().mockResolvedValue(undefined);
13 mockLogger = createMockLogger();
14 });
15
16 afterEach(() => {
17 vi.clearAllMocks();
18 vi.useRealTimers();
19 });
20
21 describe("Construction", () => {
22 it("should initialize with maxAttempts and baseDelayMs", () => {
23 expect(() => {
24 reconnectionManager = new ReconnectionManager(5, 1000, mockLogger);
25 }).not.toThrow();
26
27 expect(reconnectionManager.getAttemptCount()).toBe(0);
28 });
29 });
30
31 describe("attemptReconnect", () => {
32 beforeEach(() => {
33 reconnectionManager = new ReconnectionManager(3, 1000, mockLogger);
34 });
35
36 it("should attempt reconnection with exponential backoff", async () => {
37 // First attempt: 1000ms delay (1000 * 2^0)
38 const promise1 = reconnectionManager.attemptReconnect(reconnectFn);
39 expect(mockLogger.info).toHaveBeenCalledWith(
40 "Attempting to reconnect",
41 expect.objectContaining({ attempt: 1, maxAttempts: 3, delayMs: 1000 })
42 );
43 await vi.advanceTimersByTimeAsync(1000);
44 await promise1;
45 expect(reconnectFn).toHaveBeenCalledTimes(1);
46
47 // Second attempt: 2000ms delay (1000 * 2^1)
48 const promise2 = reconnectionManager.attemptReconnect(reconnectFn);
49 expect(mockLogger.info).toHaveBeenCalledWith(
50 "Attempting to reconnect",
51 expect.objectContaining({ attempt: 2, maxAttempts: 3, delayMs: 2000 })
52 );
53 await vi.advanceTimersByTimeAsync(2000);
54 await promise2;
55 expect(reconnectFn).toHaveBeenCalledTimes(2);
56
57 // Third attempt: 4000ms delay (1000 * 2^2)
58 const promise3 = reconnectionManager.attemptReconnect(reconnectFn);
59 expect(mockLogger.info).toHaveBeenCalledWith(
60 "Attempting to reconnect",
61 expect.objectContaining({ attempt: 3, maxAttempts: 3, delayMs: 4000 })
62 );
63 await vi.advanceTimersByTimeAsync(4000);
64 await promise3;
65 expect(reconnectFn).toHaveBeenCalledTimes(3);
66 });
67
68 it("should throw error when max attempts exceeded", async () => {
69 // Exhaust all attempts
70 const promise1 = reconnectionManager.attemptReconnect(reconnectFn);
71 await vi.runAllTimersAsync();
72 await promise1;
73
74 const promise2 = reconnectionManager.attemptReconnect(reconnectFn);
75 await vi.runAllTimersAsync();
76 await promise2;
77
78 const promise3 = reconnectionManager.attemptReconnect(reconnectFn);
79 await vi.runAllTimersAsync();
80 await promise3;
81
82 // Fourth attempt should throw
83 await expect(reconnectionManager.attemptReconnect(reconnectFn)).rejects.toThrow(
84 "Max reconnection attempts exceeded"
85 );
86
87 expect(mockLogger.fatal).toHaveBeenCalledWith(
88 expect.stringContaining("Max reconnect attempts reached"),
89 expect.objectContaining({ maxAttempts: 3 })
90 );
91 });
92
93 it("should log reconnection attempts", async () => {
94 const promise = reconnectionManager.attemptReconnect(reconnectFn);
95 await vi.runAllTimersAsync();
96 await promise;
97
98 expect(mockLogger.info).toHaveBeenCalledWith(
99 "Attempting to reconnect",
100 expect.any(Object)
101 );
102 });
103
104 it("should propagate errors from reconnectFn", async () => {
105 const error = new Error("Reconnection failed");
106 reconnectFn.mockRejectedValueOnce(error);
107
108 // Attach rejection handler BEFORE advancing timers
109 const promise = reconnectionManager.attemptReconnect(reconnectFn);
110 const assertion = expect(promise).rejects.toThrow("Reconnection failed");
111 await vi.runAllTimersAsync();
112 await assertion;
113 });
114 });
115
116 describe("reset", () => {
117 beforeEach(() => {
118 reconnectionManager = new ReconnectionManager(3, 1000, mockLogger);
119 });
120
121 it("should reset attempt counter", async () => {
122 // Make one attempt
123 const promise = reconnectionManager.attemptReconnect(reconnectFn);
124 await vi.runAllTimersAsync();
125 await promise;
126
127 expect(reconnectionManager.getAttemptCount()).toBe(1);
128
129 // Reset
130 reconnectionManager.reset();
131 expect(reconnectionManager.getAttemptCount()).toBe(0);
132 });
133
134 it("should allow reconnection after reset", async () => {
135 // Exhaust all attempts
136 const promise1 = reconnectionManager.attemptReconnect(reconnectFn);
137 await vi.runAllTimersAsync();
138 await promise1;
139
140 const promise2 = reconnectionManager.attemptReconnect(reconnectFn);
141 await vi.runAllTimersAsync();
142 await promise2;
143
144 const promise3 = reconnectionManager.attemptReconnect(reconnectFn);
145 await vi.runAllTimersAsync();
146 await promise3;
147
148 expect(reconnectionManager.getAttemptCount()).toBe(3);
149
150 // Reset should allow new attempts
151 reconnectionManager.reset();
152
153 const promise4 = reconnectionManager.attemptReconnect(reconnectFn);
154 expect(mockLogger.info).toHaveBeenCalledWith(
155 "Attempting to reconnect",
156 expect.objectContaining({ attempt: 1, maxAttempts: 3, delayMs: 1000 })
157 );
158 await vi.runAllTimersAsync();
159 await promise4;
160 });
161 });
162
163 describe("getAttemptCount", () => {
164 beforeEach(() => {
165 reconnectionManager = new ReconnectionManager(3, 1000, mockLogger);
166 });
167
168 it("should return current attempt count", async () => {
169 expect(reconnectionManager.getAttemptCount()).toBe(0);
170
171 const promise1 = reconnectionManager.attemptReconnect(reconnectFn);
172 await vi.runAllTimersAsync();
173 await promise1;
174 expect(reconnectionManager.getAttemptCount()).toBe(1);
175
176 const promise2 = reconnectionManager.attemptReconnect(reconnectFn);
177 await vi.runAllTimersAsync();
178 await promise2;
179 expect(reconnectionManager.getAttemptCount()).toBe(2);
180 });
181 });
182
183 describe("Edge Cases", () => {
184 it("should handle maxAttempts of 1", async () => {
185 reconnectionManager = new ReconnectionManager(1, 1000, mockLogger);
186
187 const promise = reconnectionManager.attemptReconnect(reconnectFn);
188 await vi.runAllTimersAsync();
189 await promise;
190
191 expect(reconnectionManager.getAttemptCount()).toBe(1);
192
193 // Second attempt should fail
194 await expect(reconnectionManager.attemptReconnect(reconnectFn)).rejects.toThrow(
195 "Max reconnection attempts exceeded"
196 );
197 });
198
199 it("should handle very small base delays", async () => {
200 reconnectionManager = new ReconnectionManager(2, 10, mockLogger);
201
202 const promise = reconnectionManager.attemptReconnect(reconnectFn);
203 await vi.advanceTimersByTimeAsync(10);
204 await promise;
205
206 expect(reconnectFn).toHaveBeenCalled();
207 });
208
209 it("should calculate exponential backoff correctly for many attempts", async () => {
210 reconnectionManager = new ReconnectionManager(5, 100, mockLogger);
211
212 // Attempt 1: 100ms (100 * 2^0)
213 let promise = reconnectionManager.attemptReconnect(reconnectFn);
214 expect(mockLogger.info).toHaveBeenCalledWith(
215 "Attempting to reconnect",
216 expect.objectContaining({ delayMs: 100 })
217 );
218 await vi.runAllTimersAsync();
219 await promise;
220
221 // Attempt 2: 200ms (100 * 2^1)
222 promise = reconnectionManager.attemptReconnect(reconnectFn);
223 expect(mockLogger.info).toHaveBeenCalledWith(
224 "Attempting to reconnect",
225 expect.objectContaining({ delayMs: 200 })
226 );
227 await vi.runAllTimersAsync();
228 await promise;
229
230 // Attempt 3: 400ms (100 * 2^2)
231 promise = reconnectionManager.attemptReconnect(reconnectFn);
232 expect(mockLogger.info).toHaveBeenCalledWith(
233 "Attempting to reconnect",
234 expect.objectContaining({ delayMs: 400 })
235 );
236 await vi.runAllTimersAsync();
237 await promise;
238
239 // Attempt 4: 800ms (100 * 2^3)
240 promise = reconnectionManager.attemptReconnect(reconnectFn);
241 expect(mockLogger.info).toHaveBeenCalledWith(
242 "Attempting to reconnect",
243 expect.objectContaining({ delayMs: 800 })
244 );
245 await vi.runAllTimersAsync();
246 await promise;
247
248 // Attempt 5: 1600ms (100 * 2^4)
249 promise = reconnectionManager.attemptReconnect(reconnectFn);
250 expect(mockLogger.info).toHaveBeenCalledWith(
251 "Attempting to reconnect",
252 expect.objectContaining({ delayMs: 1600 })
253 );
254 await vi.runAllTimersAsync();
255 await promise;
256 });
257 });
258});