Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/javascript/src/modules/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@ export default class Socket<T extends CatcherMessageType = 'errors/javascript'>
if (this.ws === null) {
this.eventsQueue.push(message);

return this.init();
await this.init();
this.sendQueue();

return;
}

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

log('Successfully reconnected.', 'info');
this.sendQueue();
} catch (error) {
this.reconnectionAttempts--;

Expand Down
102 changes: 102 additions & 0 deletions packages/javascript/tests/socket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ import type { CatcherMessage } from '@hawk.so/types';

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

/**
* vi.fn() replacement has no WebSocket.OPEN/CLOSED; Socket uses them in switch — without this,
* `undefined === undefined` always hits the first `case WebSocket.OPEN` and reconnect never runs.
*/
function patchWebSocketMockConstructor(ctor: { CONNECTING?: number; OPEN?: number; CLOSING?: number; CLOSED?: number }): void {
ctor.CONNECTING = 0;
Comment thread
Dobrunia marked this conversation as resolved.
ctor.OPEN = 1;
ctor.CLOSING = 2;
ctor.CLOSED = 3;
}

type MockWebSocket = {
url: string;
readyState: number;
Expand Down Expand Up @@ -72,3 +83,94 @@ describe('Socket', () => {
expect(WebSocketConstructor).toHaveBeenCalledTimes(2);
});
});

/**
* Regression: queued events must be flushed after reconnect / init, not only on first constructor connect.
*/
describe('Socket — events queue after connection loss', () => {
afterEach(() => {
vi.restoreAllMocks();
});

function mockWebSocketFactory(sockets: MockWebSocket[], closeSpy: ReturnType<typeof vi.fn>) {
const ctor = vi.fn<(url: string) => void>().mockImplementation(function (
this: MockWebSocket,
url: string
) {
this.url = url;
this.readyState = WebSocket.CONNECTING;
this.send = vi.fn();
this.close = closeSpy;
this.onopen = undefined;
this.onclose = undefined;
this.onerror = undefined;
this.onmessage = undefined;
sockets.push(this);
});
patchWebSocketMockConstructor(ctor);

return ctor;
}

it('should flush queued event after reconnect when socket is CLOSED', async () => {
const sockets: MockWebSocket[] = [];
const closeSpy = vi.fn(function (this: MockWebSocket) {
this.readyState = WebSocket.CLOSED;
this.onclose?.({ code: 1001 } as CloseEvent);
});

const WebSocketConstructor = mockWebSocketFactory(sockets, closeSpy);
globalThis.WebSocket = WebSocketConstructor as unknown as typeof WebSocket;

Comment thread
Dobrunia marked this conversation as resolved.
const socket = new Socket({
collectorEndpoint: MOCK_WEBSOCKET_URL,
reconnectionTimeout: 10,
});

const ws1 = sockets[0];
ws1.readyState = WebSocket.OPEN;
ws1.onopen?.(new Event('open'));
await Promise.resolve();

ws1.readyState = WebSocket.CLOSED;

const payload = { type: 'errors/javascript', title: 'queued-after-drop' } as unknown as CatcherMessage<'errors/javascript'>;
const sendPromise = socket.send(payload);

const ws2 = sockets[1];
expect(ws2).toBeDefined();
ws2.readyState = WebSocket.OPEN;
ws2.onopen?.(new Event('open'));
await sendPromise;

expect(ws2.send).toHaveBeenCalledTimes(1);
expect(ws2.send).toHaveBeenCalledWith(JSON.stringify(payload));
});

it('should flush queued event when ws is null after pagehide and send()', async () => {
const closeSpy = vi.fn(function (this: MockWebSocket) {
this.readyState = WebSocket.CLOSED;
this.onclose?.({ code: 1000 } as CloseEvent);
});

const sockets: MockWebSocket[] = [];
const WebSocketConstructor = mockWebSocketFactory(sockets, closeSpy);
globalThis.WebSocket = WebSocketConstructor as unknown as typeof WebSocket;

const socket = new Socket({ collectorEndpoint: MOCK_WEBSOCKET_URL });
sockets[0].readyState = WebSocket.OPEN;
sockets[0].onopen?.(new Event('open'));
await Promise.resolve();

window.dispatchEvent(new Event('pagehide'));

const queued = { foo: 'bar' } as unknown as CatcherMessage<'errors/javascript'>;
const sendPromise = socket.send(queued);
sockets[1].readyState = WebSocket.OPEN;
sockets[1].onopen?.(new Event('open'));
await sendPromise;

expect(sockets[1].send).toHaveBeenCalledTimes(1);
expect(sockets[1].send).toHaveBeenCalledWith(JSON.stringify(queued));
});
});
Loading