Skip to content

Commit 4c3d0ee

Browse files
authored
fix: close WebSocket on pagehide to support bfcache (#162)
* fix: close WebSocket on pagehide to support bfcache * fix: lint * fix: remove log * chore: add socket test * chore: update version * fix: test update for better clarity
1 parent 2d3c81e commit 4c3d0ee

File tree

3 files changed

+113
-2
lines changed

3 files changed

+113
-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.17",
3+
"version": "3.2.18",
44
"description": "JavaScript errors tracking for Hawk.so",
55
"files": [
66
"dist"

packages/javascript/src/modules/socket.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ export default class Socket implements Transport {
5454
*/
5555
private reconnectionAttempts: number;
5656

57+
/**
58+
* Page hide event handler reference (for removal)
59+
*/
60+
private pageHideHandler: () => void;
61+
5762
/**
5863
* Creates new Socket instance. Setup initial socket params.
5964
*
@@ -77,6 +82,10 @@ export default class Socket implements Transport {
7782
this.reconnectionTimeout = reconnectionTimeout;
7883
this.reconnectionAttempts = reconnectionAttempts;
7984

85+
this.pageHideHandler = () => {
86+
this.close();
87+
};
88+
8089
this.eventsQueue = [];
8190
this.ws = null;
8291

@@ -120,7 +129,21 @@ export default class Socket implements Transport {
120129
}
121130

122131
/**
123-
* Create new WebSocket connection and setup event listeners
132+
* Setup window event listeners
133+
*/
134+
private setupListeners(): void {
135+
window.addEventListener('pagehide', this.pageHideHandler, { capture: true });
136+
}
137+
138+
/**
139+
* Remove window event listeners
140+
*/
141+
private destroyListeners(): void {
142+
window.removeEventListener('pagehide', this.pageHideHandler, { capture: true });
143+
}
144+
145+
/**
146+
* Create new WebSocket connection and setup socket event listeners
124147
*/
125148
private init(): Promise<void> {
126149
return new Promise((resolve, reject) => {
@@ -139,6 +162,8 @@ export default class Socket implements Transport {
139162
* @param event - websocket event on closing
140163
*/
141164
this.ws.onclose = (event: CloseEvent): void => {
165+
this.destroyListeners();
166+
142167
if (typeof this.onClose === 'function') {
143168
this.onClose(event);
144169
}
@@ -154,6 +179,8 @@ export default class Socket implements Transport {
154179
};
155180

156181
this.ws.onopen = (event: Event): void => {
182+
this.setupListeners();
183+
157184
if (typeof this.onOpen === 'function') {
158185
this.onOpen(event);
159186
}
@@ -163,6 +190,16 @@ export default class Socket implements Transport {
163190
});
164191
}
165192

193+
/**
194+
* Closes socket connection
195+
*/
196+
private close(): void {
197+
if (this.ws) {
198+
this.ws.close();
199+
this.ws = null;
200+
}
201+
}
202+
166203
/**
167204
* Tries to reconnect to the server for specified number of times with the interval
168205
*
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { describe, it, expect, afterEach, vi } from 'vitest';
2+
import Socket from '../src/modules/socket';
3+
import type { CatcherMessage } from '@hawk.so/types';
4+
5+
const MOCK_WEBSOCKET_URL = 'ws://localhost:1234';
6+
7+
type MockWebSocket = {
8+
url: string;
9+
readyState: number;
10+
send: ReturnType<typeof vi.fn>;
11+
close: ReturnType<typeof vi.fn>;
12+
onopen?: (event: Event) => void;
13+
onclose?: (event: CloseEvent) => void;
14+
onerror?: (event: Event) => void;
15+
onmessage?: (event: MessageEvent) => void;
16+
};
17+
18+
describe('Socket', () => {
19+
afterEach(() => {
20+
vi.restoreAllMocks();
21+
});
22+
23+
it('should close websocket on pagehide and recreate connection on next send()', async () => {
24+
const closeSpy = vi.fn(function (this: MockWebSocket) {
25+
this.readyState = WebSocket.CLOSED;
26+
this.onclose?.({ code: 1000 } as CloseEvent);
27+
});
28+
29+
let webSocket!: MockWebSocket;
30+
const WebSocketConstructor = vi.fn<(url: string) => void>().mockImplementation(function (
31+
this: MockWebSocket,
32+
url: string
33+
) {
34+
this.url = url;
35+
this.readyState = WebSocket.CONNECTING;
36+
this.send = vi.fn();
37+
this.close = closeSpy;
38+
this.onopen = undefined;
39+
this.onclose = undefined;
40+
this.onerror = undefined;
41+
this.onmessage = undefined;
42+
webSocket = this;
43+
});
44+
globalThis.WebSocket = WebSocketConstructor;
45+
46+
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
47+
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
48+
49+
// initialize socket and open fake websocket connection
50+
const socket = new Socket({ collectorEndpoint: MOCK_WEBSOCKET_URL });
51+
webSocket.readyState = WebSocket.OPEN;
52+
webSocket.onopen?.(new Event('open'));
53+
54+
// capture pagehide handler to verify it's properly removed
55+
const pagehideCall = addEventListenerSpy.mock.calls.find(([event]) => event === 'pagehide');
56+
expect(pagehideCall).toBeDefined();
57+
const pagehideHandler = pagehideCall![1] as EventListener;
58+
59+
// trigger pagehide event
60+
window.dispatchEvent(new Event('pagehide'));
61+
62+
// websocket connection should be closed
63+
expect(closeSpy).toHaveBeenCalledOnce();
64+
expect(removeEventListenerSpy).toHaveBeenCalledWith('pagehide', pagehideHandler, { capture: true });
65+
66+
// send socket method should make websocket reconnect
67+
const sendPromise = socket.send({ foo: 'bar' } as CatcherMessage);
68+
webSocket.readyState = WebSocket.OPEN;
69+
webSocket.onopen?.(new Event('open'));
70+
await sendPromise;
71+
72+
expect(WebSocketConstructor).toHaveBeenCalledTimes(2);
73+
});
74+
});

0 commit comments

Comments
 (0)