Skip to content

Commit 295d07f

Browse files
authored
Merge pull request #178 from codex-team/fix/javascript-ws-reconnect-flush-queue
fix(javascript): flush queued events after WebSocket reconnect
2 parents ad0fd34 + d86d780 commit 295d07f

File tree

3 files changed

+108
-2
lines changed

3 files changed

+108
-2
lines changed

packages/javascript/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hawk.so/javascript",
3-
"version": "3.2.18",
3+
"version": "3.2.19",
44
"description": "JavaScript errors tracking for Hawk.so",
55
"files": [
66
"dist"

packages/javascript/src/modules/socket.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,10 @@ export default class Socket<T extends CatcherMessageType = 'errors/javascript'>
111111
if (this.ws === null) {
112112
this.eventsQueue.push(message);
113113

114-
return this.init();
114+
await this.init();
115+
this.sendQueue();
116+
117+
return;
115118
}
116119

117120
switch (this.ws.readyState) {
@@ -218,6 +221,7 @@ export default class Socket<T extends CatcherMessageType = 'errors/javascript'>
218221
await this.init();
219222

220223
log('Successfully reconnected.', 'info');
224+
this.sendQueue();
221225
} catch (error) {
222226
this.reconnectionAttempts--;
223227

packages/javascript/tests/socket.test.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ import type { CatcherMessage } from '@hawk.so/types';
44

55
const MOCK_WEBSOCKET_URL = 'ws://localhost:1234';
66

7+
/**
8+
* vi.fn() replacement has no WebSocket.OPEN/CLOSED; Socket uses them in switch — without this,
9+
* `undefined === undefined` always hits the first `case WebSocket.OPEN` and reconnect never runs.
10+
*/
11+
function patchWebSocketMockConstructor(ctor: { CONNECTING?: number; OPEN?: number; CLOSING?: number; CLOSED?: number }): void {
12+
ctor.CONNECTING = 0;
13+
ctor.OPEN = 1;
14+
ctor.CLOSING = 2;
15+
ctor.CLOSED = 3;
16+
}
17+
718
type MockWebSocket = {
819
url: string;
920
readyState: number;
@@ -72,3 +83,94 @@ describe('Socket', () => {
7283
expect(WebSocketConstructor).toHaveBeenCalledTimes(2);
7384
});
7485
});
86+
87+
/**
88+
* Regression: queued events must be flushed after reconnect / init, not only on first constructor connect.
89+
*/
90+
describe('Socket — events queue after connection loss', () => {
91+
afterEach(() => {
92+
vi.restoreAllMocks();
93+
});
94+
95+
function mockWebSocketFactory(sockets: MockWebSocket[], closeSpy: ReturnType<typeof vi.fn>) {
96+
const ctor = vi.fn<(url: string) => void>().mockImplementation(function (
97+
this: MockWebSocket,
98+
url: string
99+
) {
100+
this.url = url;
101+
this.readyState = WebSocket.CONNECTING;
102+
this.send = vi.fn();
103+
this.close = closeSpy;
104+
this.onopen = undefined;
105+
this.onclose = undefined;
106+
this.onerror = undefined;
107+
this.onmessage = undefined;
108+
sockets.push(this);
109+
});
110+
patchWebSocketMockConstructor(ctor);
111+
112+
return ctor;
113+
}
114+
115+
it('should flush queued event after reconnect when socket is CLOSED', async () => {
116+
const sockets: MockWebSocket[] = [];
117+
const closeSpy = vi.fn(function (this: MockWebSocket) {
118+
this.readyState = WebSocket.CLOSED;
119+
this.onclose?.({ code: 1001 } as CloseEvent);
120+
});
121+
122+
const WebSocketConstructor = mockWebSocketFactory(sockets, closeSpy);
123+
globalThis.WebSocket = WebSocketConstructor as unknown as typeof WebSocket;
124+
125+
const socket = new Socket({
126+
collectorEndpoint: MOCK_WEBSOCKET_URL,
127+
reconnectionTimeout: 10,
128+
});
129+
130+
const ws1 = sockets[0];
131+
ws1.readyState = WebSocket.OPEN;
132+
ws1.onopen?.(new Event('open'));
133+
await Promise.resolve();
134+
135+
ws1.readyState = WebSocket.CLOSED;
136+
137+
const payload = { type: 'errors/javascript', title: 'queued-after-drop' } as unknown as CatcherMessage<'errors/javascript'>;
138+
const sendPromise = socket.send(payload);
139+
140+
const ws2 = sockets[1];
141+
expect(ws2).toBeDefined();
142+
ws2.readyState = WebSocket.OPEN;
143+
ws2.onopen?.(new Event('open'));
144+
await sendPromise;
145+
146+
expect(ws2.send).toHaveBeenCalledTimes(1);
147+
expect(ws2.send).toHaveBeenCalledWith(JSON.stringify(payload));
148+
});
149+
150+
it('should flush queued event when ws is null after pagehide and send()', async () => {
151+
const closeSpy = vi.fn(function (this: MockWebSocket) {
152+
this.readyState = WebSocket.CLOSED;
153+
this.onclose?.({ code: 1000 } as CloseEvent);
154+
});
155+
156+
const sockets: MockWebSocket[] = [];
157+
const WebSocketConstructor = mockWebSocketFactory(sockets, closeSpy);
158+
globalThis.WebSocket = WebSocketConstructor as unknown as typeof WebSocket;
159+
160+
const socket = new Socket({ collectorEndpoint: MOCK_WEBSOCKET_URL });
161+
sockets[0].readyState = WebSocket.OPEN;
162+
sockets[0].onopen?.(new Event('open'));
163+
await Promise.resolve();
164+
165+
window.dispatchEvent(new Event('pagehide'));
166+
167+
const queued = { foo: 'bar' } as unknown as CatcherMessage<'errors/javascript'>;
168+
const sendPromise = socket.send(queued);
169+
sockets[1].readyState = WebSocket.OPEN;
170+
sockets[1].onopen?.(new Event('open'));
171+
await sendPromise;
172+
173+
expect(sockets[1].send).toHaveBeenCalledTimes(1);
174+
expect(sockets[1].send).toHaveBeenCalledWith(JSON.stringify(queued));
175+
});
176+
});

0 commit comments

Comments
 (0)