From addb68f5d0238205de88d33b4e25e1dfba70d95f Mon Sep 17 00:00:00 2001 From: Melvin Oostendorp Date: Thu, 21 Sep 2023 01:13:11 +0000 Subject: [PATCH 1/4] feat: added heartbeat support --- README.md | 28 ++++++++++++++ src/lib/attach-listener.ts | 9 +++++ src/lib/attach-shared-listeners.ts | 9 +++++ src/lib/constants.ts | 5 +++ src/lib/heartbeat.test.ts | 61 ++++++++++++++++++++++++++++++ src/lib/heartbeat.ts | 37 ++++++++++++++++++ src/lib/types.ts | 7 ++++ 7 files changed, 156 insertions(+) create mode 100644 src/lib/heartbeat.test.ts create mode 100644 src/lib/heartbeat.ts diff --git a/README.md b/README.md index 7ba68cb..d0da07e 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ A demo of this can be found [here](https://robtaussig.com/socket/). Each compone - Multiple components can (optionally) use a single WebSocket, which is closed and cleaned up when all subscribed components have unsubscribed/unmounted - Written in TypeScript - Socket.io support +- Heartbeat support - No more waiting for the WebSocket to open before messages can be sent. Pre-connection messages are queued up and sent on connection - Provides direct access to unshared WebSockets, while proxying shared WebSockets. Proxied WebSockets provide subscribers controlled access to the underlying (shared) WebSocket, without allowing unsafe behavior - Seamlessly works with server-sent-events and the [EventSource API](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) @@ -150,6 +151,11 @@ type UseWebSocket = ( filter?: (message: WebSocketEventMap['message']) => boolean; retryOnError?: boolean; eventSourceOptions?: EventSourceInit; + heartbeat?: boolean | { + kind?: "ping" | "pong" | string; + timeout?: number; + interval?: number; + }; } = {}, shouldConnect: boolean = true, ): { @@ -348,6 +354,11 @@ interface Options { }; protocols?: string | string[]; eventSourceOptions?: EventSourceInit; + heartbeat?: boolean | { + kind?: "ping" | "pong" | string; + timeout?: number; + interval?: number; + }; } ``` @@ -408,6 +419,23 @@ const { sendMessage, lastMessage, readyState } = useSocketIO( It is important to note that `lastMessage` will not be a `MessageEvent`, but instead an object with two keys: `type` and `payload`. +### heartbeat + +If set to `true` or it has options, the library will send a ping message every `interval` milliseconds. If no pong message is received within `timeout` milliseconds, the library will attempt to reconnect. The `kind` of the ping message can be changed to any string, but it must be the same on the server side. + +```js +const { sendMessage, lastMessage, readyState } = useWebSocket( + 'ws://localhost:3000', + { + heartbeat: { + kind: 'ping', + timeout: 10000, + interval: 25000, + }, + } +); +``` + ### filter: Callback If a function is provided with the key `filter`, incoming messages will be passed through the function, and only if it returns `true` will the hook pass along the `lastMessage` and update your component. diff --git a/src/lib/attach-listener.ts b/src/lib/attach-listener.ts index 964ef88..ae370a5 100644 --- a/src/lib/attach-listener.ts +++ b/src/lib/attach-listener.ts @@ -1,5 +1,6 @@ import { MutableRefObject } from 'react'; import { setUpSocketIOPing } from './socket-io'; +import { heartbeat } from './heartbeat'; import { DEFAULT_RECONNECT_LIMIT, DEFAULT_RECONNECT_INTERVAL_MS, @@ -139,6 +140,14 @@ export const attachListeners = ( interval = setUpSocketIOPing(sendMessage); } + if (optionsRef.current.heartbeat && webSocketInstance instanceof WebSocket) { + const heartbeatOptions = + typeof optionsRef.current.heartbeat === "boolean" + ? undefined + : optionsRef.current.heartbeat; + heartbeat(webSocketInstance, heartbeatOptions); + } + bindMessageHandler( webSocketInstance, optionsRef, diff --git a/src/lib/attach-shared-listeners.ts b/src/lib/attach-shared-listeners.ts index ae0c4df..1c99293 100644 --- a/src/lib/attach-shared-listeners.ts +++ b/src/lib/attach-shared-listeners.ts @@ -4,6 +4,7 @@ import { getSubscribers } from './manage-subscribers'; import { MutableRefObject } from 'react'; import { Options, SendMessage, WebSocketLike } from './types'; import { setUpSocketIOPing } from './socket-io'; +import { heartbeat } from './heartbeat'; const bindMessageHandler = ( webSocketInstance: WebSocketLike, @@ -122,6 +123,14 @@ export const attachSharedListeners = ( interval = setUpSocketIOPing(sendMessage); } + if (optionsRef.current.heartbeat && webSocketInstance instanceof WebSocket) { + const heartbeatOptions = + typeof optionsRef.current.heartbeat === "boolean" + ? undefined + : optionsRef.current.heartbeat; + heartbeat(webSocketInstance, heartbeatOptions); + } + bindMessageHandler(webSocketInstance, url); bindCloseHandler(webSocketInstance, url); bindOpenHandler(webSocketInstance, url); diff --git a/src/lib/constants.ts b/src/lib/constants.ts index b150d83..594963e 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -15,6 +15,11 @@ export const SOCKET_IO_PING_CODE = '2'; export const DEFAULT_RECONNECT_LIMIT = 20; export const DEFAULT_RECONNECT_INTERVAL_MS = 5000; export const UNPARSABLE_JSON_OBJECT = {}; +export const DEFAULT_HEARTBEAT = { + kind: 'ping', + timeout: 10000, + interval: 25000, +}; export enum ReadyState { UNINSTANTIATED = -1, diff --git a/src/lib/heartbeat.test.ts b/src/lib/heartbeat.test.ts new file mode 100644 index 0000000..cf0055a --- /dev/null +++ b/src/lib/heartbeat.test.ts @@ -0,0 +1,61 @@ +import { heartbeat } from "./heartbeat"; + +describe("heartbeat", () => { + let ws: WebSocket; + let sendSpy: jest.Mock; + let closeSpy: jest.Mock; + let addEventListenerSpy: jest.Mock; + jest.useFakeTimers(); + + beforeEach(() => { + sendSpy = jest.fn(); + closeSpy = jest.fn(); + addEventListenerSpy = jest.fn(); + ws = { + send: sendSpy, + close: closeSpy, + addEventListener: addEventListenerSpy, + } as unknown as WebSocket; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test("sends a ping message at the specified interval", () => { + heartbeat(ws, { interval: 100 }); + expect(sendSpy).not.toHaveBeenCalled(); + jest.advanceTimersByTime(99); + expect(sendSpy).not.toHaveBeenCalled(); + jest.advanceTimersByTime(1); + expect(sendSpy).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(100); + expect(sendSpy).toHaveBeenCalledTimes(2); + }); + + test("closes the WebSocket if no message is received within the specified timeout", () => { + heartbeat(ws, { timeout: 100 }); + expect(closeSpy).not.toHaveBeenCalled(); + jest.advanceTimersByTime(99); + expect(closeSpy).not.toHaveBeenCalled(); + jest.advanceTimersByTime(1); + expect(closeSpy).toHaveBeenCalledTimes(1); + }); + + test("does not close the WebSocket if a message is received within the specified timeout", () => { + heartbeat(ws, { timeout: 100 }); + expect(closeSpy).not.toHaveBeenCalled(); + addEventListenerSpy.mock.calls[0][1]({ data: "pong" }); + jest.advanceTimersByTime(99); + expect(closeSpy).not.toHaveBeenCalled(); + jest.advanceTimersByTime(1); + expect(closeSpy).not.toHaveBeenCalled(); + }); + + test("sends the custom ping message", () => { + heartbeat(ws, { kind: "pong" }); + expect(sendSpy).not.toHaveBeenCalled(); + jest.advanceTimersByTime(25000); + expect(sendSpy).toHaveBeenCalledWith("pong"); + }); +}); diff --git a/src/lib/heartbeat.ts b/src/lib/heartbeat.ts new file mode 100644 index 0000000..4d2d0d9 --- /dev/null +++ b/src/lib/heartbeat.ts @@ -0,0 +1,37 @@ +import { DEFAULT_HEARTBEAT } from "./constants"; +import { HeartbeatOptions } from "./types"; + +export function heartbeat(ws: WebSocket, options?: HeartbeatOptions): void { + const { + interval = DEFAULT_HEARTBEAT.interval, + timeout = DEFAULT_HEARTBEAT.timeout, + kind = DEFAULT_HEARTBEAT.kind, + } = options || {}; + + let messageAccepted = false; + + ws.addEventListener("message", () => { + messageAccepted = true; + }); + + const pingTimer = setInterval(() => { + try { + ws.send(kind); + } catch (error) { + // do nothing + } + }, interval); + + const timeoutTimer = setInterval(() => { + if (!messageAccepted) { + ws.close(); + } else { + messageAccepted = false; + } + }, timeout); + + ws.addEventListener("close", () => { + clearInterval(pingTimer); + clearInterval(timeoutTimer); + }); +} diff --git a/src/lib/types.ts b/src/lib/types.ts index 36c6dc3..70a26ee 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -22,10 +22,17 @@ export interface Options { retryOnError?: boolean; eventSourceOptions?: EventSourceOnly; skipAssert?: boolean; + heartbeat?: boolean | HeartbeatOptions; } export type EventSourceOnly = Omit & EventSourceInit; +export type HeartbeatOptions = { + kind?: "ping" | "pong" | string; + timeout?: number; + interval?: number; +}; + export interface EventSourceEventHandlers { [eventName: string]: (message: EventSourceEventMap['message']) => void; } From cf4da2bda87427cc53728742e3962ca9d25a70a4 Mon Sep 17 00:00:00 2001 From: Robert Taussig Date: Fri, 22 Sep 2023 12:12:49 -0400 Subject: [PATCH 2/4] Modify heartbeat binding to work with shared websockets and add integration test with useWebSocket hook --- src/lib/attach-listener.ts | 26 ++++++--- src/lib/attach-shared-listeners.ts | 27 +++++---- src/lib/heartbeat.test.ts | 8 +-- src/lib/heartbeat.ts | 10 ++-- src/lib/use-websocket.test.ts | 89 ++++++++++++++++++++++++++++++ 5 files changed, 133 insertions(+), 27 deletions(-) diff --git a/src/lib/attach-listener.ts b/src/lib/attach-listener.ts index ae370a5..c8bd752 100644 --- a/src/lib/attach-listener.ts +++ b/src/lib/attach-listener.ts @@ -20,11 +20,29 @@ const bindMessageHandler = ( optionsRef: MutableRefObject, setLastMessage: Setters['setLastMessage'], ) => { + let heartbeatCb: () => void; + + if (optionsRef.current.heartbeat && webSocketInstance instanceof WebSocket) { + const heartbeatOptions = + typeof optionsRef.current.heartbeat === "boolean" + ? undefined + : optionsRef.current.heartbeat; + heartbeatCb = heartbeat(webSocketInstance, heartbeatOptions); + } + webSocketInstance.onmessage = (message: WebSocketEventMap['message']) => { + heartbeatCb?.(); optionsRef.current.onMessage && optionsRef.current.onMessage(message); if (typeof optionsRef.current.filter === 'function' && optionsRef.current.filter(message) !== true) { return; } + if ( + optionsRef.current.heartbeat && + typeof optionsRef.current.heartbeat !== "boolean" && + optionsRef.current.heartbeat?.kind === message.data + ) + return; + setLastMessage(message); }; }; @@ -140,14 +158,6 @@ export const attachListeners = ( interval = setUpSocketIOPing(sendMessage); } - if (optionsRef.current.heartbeat && webSocketInstance instanceof WebSocket) { - const heartbeatOptions = - typeof optionsRef.current.heartbeat === "boolean" - ? undefined - : optionsRef.current.heartbeat; - heartbeat(webSocketInstance, heartbeatOptions); - } - bindMessageHandler( webSocketInstance, optionsRef, diff --git a/src/lib/attach-shared-listeners.ts b/src/lib/attach-shared-listeners.ts index 1c99293..4cb2156 100644 --- a/src/lib/attach-shared-listeners.ts +++ b/src/lib/attach-shared-listeners.ts @@ -2,15 +2,23 @@ import { sharedWebSockets } from './globals'; import { DEFAULT_RECONNECT_LIMIT, DEFAULT_RECONNECT_INTERVAL_MS, ReadyState, isEventSourceSupported } from './constants'; import { getSubscribers } from './manage-subscribers'; import { MutableRefObject } from 'react'; -import { Options, SendMessage, WebSocketLike } from './types'; +import { HeartbeatOptions, Options, SendMessage, WebSocketLike } from './types'; import { setUpSocketIOPing } from './socket-io'; import { heartbeat } from './heartbeat'; const bindMessageHandler = ( webSocketInstance: WebSocketLike, url: string, + heartbeatOptions?: boolean | HeartbeatOptions ) => { + let onMessageCb: () => void; + + if (heartbeatOptions && webSocketInstance instanceof WebSocket) { + onMessageCb = heartbeat(webSocketInstance, typeof heartbeatOptions === 'boolean' ? undefined : heartbeatOptions); + } + webSocketInstance.onmessage = (message: WebSocketEventMap['message']) => { + onMessageCb?.(); getSubscribers(url).forEach(subscriber => { if (subscriber.optionsRef.current.onMessage) { subscriber.optionsRef.current.onMessage(message); @@ -23,6 +31,13 @@ const bindMessageHandler = ( return; } + if ( + heartbeatOptions && + typeof heartbeatOptions !== "boolean" && + heartbeatOptions?.kind === message.data + ) + return; + subscriber.setLastMessage(message); }); }; @@ -123,15 +138,7 @@ export const attachSharedListeners = ( interval = setUpSocketIOPing(sendMessage); } - if (optionsRef.current.heartbeat && webSocketInstance instanceof WebSocket) { - const heartbeatOptions = - typeof optionsRef.current.heartbeat === "boolean" - ? undefined - : optionsRef.current.heartbeat; - heartbeat(webSocketInstance, heartbeatOptions); - } - - bindMessageHandler(webSocketInstance, url); + bindMessageHandler(webSocketInstance, url, optionsRef.current.heartbeat); bindCloseHandler(webSocketInstance, url); bindOpenHandler(webSocketInstance, url); bindErrorHandler(webSocketInstance, url); diff --git a/src/lib/heartbeat.test.ts b/src/lib/heartbeat.test.ts index cf0055a..7b98597 100644 --- a/src/lib/heartbeat.test.ts +++ b/src/lib/heartbeat.test.ts @@ -33,7 +33,7 @@ describe("heartbeat", () => { expect(sendSpy).toHaveBeenCalledTimes(2); }); - test("closes the WebSocket if no message is received within the specified timeout", () => { + test("closes the WebSocket if onMessageCb is not invoked within the specified timeout", () => { heartbeat(ws, { timeout: 100 }); expect(closeSpy).not.toHaveBeenCalled(); jest.advanceTimersByTime(99); @@ -42,11 +42,11 @@ describe("heartbeat", () => { expect(closeSpy).toHaveBeenCalledTimes(1); }); - test("does not close the WebSocket if a message is received within the specified timeout", () => { - heartbeat(ws, { timeout: 100 }); + test("does not close the WebSocket if messageCallback is invoked within the specified timeout", () => { + const onMessageCb = heartbeat(ws, { timeout: 100 }); expect(closeSpy).not.toHaveBeenCalled(); - addEventListenerSpy.mock.calls[0][1]({ data: "pong" }); jest.advanceTimersByTime(99); + onMessageCb(); expect(closeSpy).not.toHaveBeenCalled(); jest.advanceTimersByTime(1); expect(closeSpy).not.toHaveBeenCalled(); diff --git a/src/lib/heartbeat.ts b/src/lib/heartbeat.ts index 4d2d0d9..2ba39f1 100644 --- a/src/lib/heartbeat.ts +++ b/src/lib/heartbeat.ts @@ -1,7 +1,7 @@ import { DEFAULT_HEARTBEAT } from "./constants"; import { HeartbeatOptions } from "./types"; -export function heartbeat(ws: WebSocket, options?: HeartbeatOptions): void { +export function heartbeat(ws: WebSocket, options?: HeartbeatOptions): () => void { const { interval = DEFAULT_HEARTBEAT.interval, timeout = DEFAULT_HEARTBEAT.timeout, @@ -10,10 +10,6 @@ export function heartbeat(ws: WebSocket, options?: HeartbeatOptions): void { let messageAccepted = false; - ws.addEventListener("message", () => { - messageAccepted = true; - }); - const pingTimer = setInterval(() => { try { ws.send(kind); @@ -34,4 +30,8 @@ export function heartbeat(ws: WebSocket, options?: HeartbeatOptions): void { clearInterval(pingTimer); clearInterval(timeoutTimer); }); + + return () => { + messageAccepted = true; + }; } diff --git a/src/lib/use-websocket.test.ts b/src/lib/use-websocket.test.ts index 5edb07f..dd3fd12 100644 --- a/src/lib/use-websocket.test.ts +++ b/src/lib/use-websocket.test.ts @@ -438,4 +438,93 @@ test('Options#eventSourceOptions, if provided, instantiates an EventSource inste expect(result.current.getWebSocket() instanceof EventSource).toBe(true); }); +test.each([false, true])('Options#heartbeat, if provided, sends a message to the server at the specified interval and works when share is %s', async (shareOption) => { + options.heartbeat = { + kind: 'ping', + timeout: 10000, + interval: 500, + }; + options.share = shareOption; + + renderHook(() => useWebSocket(URL, options)); + + if (shareOption) { + renderHook(() => useWebSocket(URL, options)); + } + await server.connected; + await sleep(1600); + await expect(server).toHaveReceivedMessages(["ping", "ping", "ping"]); +}); + +test.each([false, true])('Options#heartbeat, if provided, close websocket if no message is received from server within specified timeout and works when share is %s', async (shareOption) => { + options.heartbeat = { + kind: 'ping', + timeout: 1000, + interval: 500, + }; + options.share = shareOption; + + const { + result, + } = renderHook(() => useWebSocket(URL, options)); + + if (shareOption) { + renderHook(() => useWebSocket(URL, options)); + } + await server.connected; + await sleep(1600); + expect(result.current.readyState).toBe(WebSocket.CLOSED); +}); + +test.each([false, true])('Options#heartbeat, if provided, do not close websocket if a message is received from server within specified timeout and works when share is %s', async (shareOption) => { + options.heartbeat = { + kind: 'ping', + timeout: 1000, + interval: 500, + }; + options.share = shareOption; + + const { + result, + } = renderHook(() => useWebSocket(URL, options)); + + if (shareOption) { + renderHook(() => useWebSocket(URL, options)); + } + + await server.connected; + server.send('ping') + await sleep(500); + server.send('ping') + await sleep(500); + server.send('ping') + await sleep(500); + server.send('ping') + await sleep(500); + expect(result.current.readyState).toBe(WebSocket.OPEN); +}); + +test.each([false, true])('Options#heartbeat, if provided, lastMessage is updated if server message matches the kind property of heartbeatOptions and works when share is %s', async (shareOption) => { + options.heartbeat = { + kind: 'ping', + timeout: 1000, + interval: 500, + }; + options.share = shareOption; + + const { + result, + } = renderHook(() => useWebSocket(URL, options)); + + if (shareOption) { + renderHook(() => useWebSocket(URL, options)); + } + + await server.connected; + server.send('ping'); + expect(result.current.lastMessage?.data).toBe(undefined); + server.send('pong'); + expect(result.current.lastMessage?.data).toBe('pong'); +}); + // //TODO: Write companion tests for useSocketIO \ No newline at end of file From 33ac434e226229a5720e6de8114c2cf3ece21114 Mon Sep 17 00:00:00 2001 From: Melvin Oostendorp Date: Wed, 27 Sep 2023 01:54:22 +0000 Subject: [PATCH 3/4] feat: added support for ping pong heartbeat strategy --- README.md | 11 +++++++---- src/lib/attach-listener.ts | 2 +- src/lib/attach-shared-listeners.ts | 2 +- src/lib/constants.ts | 2 +- src/lib/heartbeat.test.ts | 2 +- src/lib/heartbeat.ts | 4 ++-- src/lib/types.ts | 3 ++- src/lib/use-websocket.test.ts | 17 +++++++++-------- 8 files changed, 24 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index d0da07e..c59ae3f 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,8 @@ type UseWebSocket = ( retryOnError?: boolean; eventSourceOptions?: EventSourceInit; heartbeat?: boolean | { - kind?: "ping" | "pong" | string; + message?: "ping" | "pong" | string; + returnMessage?: "ping" | "pong" | string; timeout?: number; interval?: number; }; @@ -355,7 +356,8 @@ interface Options { protocols?: string | string[]; eventSourceOptions?: EventSourceInit; heartbeat?: boolean | { - kind?: "ping" | "pong" | string; + message?: "ping" | "pong" | string; + returnMessage?: "ping" | "pong" | string; timeout?: number; interval?: number; }; @@ -421,14 +423,15 @@ It is important to note that `lastMessage` will not be a `MessageEvent`, but ins ### heartbeat -If set to `true` or it has options, the library will send a ping message every `interval` milliseconds. If no pong message is received within `timeout` milliseconds, the library will attempt to reconnect. The `kind` of the ping message can be changed to any string, but it must be the same on the server side. +If the `heartbeat` option is set to `true` or has additional options, the library will send a 'ping' message to the server every `interval` milliseconds. If no response is received within `timeout` milliseconds, indicating a potential connection issue, the library will close the connection. You can customize the 'ping' message by changing the `message` property in the `heartbeat` object. If a `returnMessage` is defined, it will be ignored so that it won't be set as the `lastMessage`. ```js const { sendMessage, lastMessage, readyState } = useWebSocket( 'ws://localhost:3000', { heartbeat: { - kind: 'ping', + message: 'ping', + returnMessage: 'pong', timeout: 10000, interval: 25000, }, diff --git a/src/lib/attach-listener.ts b/src/lib/attach-listener.ts index c8bd752..2ae1800 100644 --- a/src/lib/attach-listener.ts +++ b/src/lib/attach-listener.ts @@ -39,7 +39,7 @@ const bindMessageHandler = ( if ( optionsRef.current.heartbeat && typeof optionsRef.current.heartbeat !== "boolean" && - optionsRef.current.heartbeat?.kind === message.data + optionsRef.current.heartbeat?.returnMessage === message.data ) return; diff --git a/src/lib/attach-shared-listeners.ts b/src/lib/attach-shared-listeners.ts index 4cb2156..6badfb3 100644 --- a/src/lib/attach-shared-listeners.ts +++ b/src/lib/attach-shared-listeners.ts @@ -34,7 +34,7 @@ const bindMessageHandler = ( if ( heartbeatOptions && typeof heartbeatOptions !== "boolean" && - heartbeatOptions?.kind === message.data + heartbeatOptions?.returnMessage === message.data ) return; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 594963e..68b1c1b 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -16,7 +16,7 @@ export const DEFAULT_RECONNECT_LIMIT = 20; export const DEFAULT_RECONNECT_INTERVAL_MS = 5000; export const UNPARSABLE_JSON_OBJECT = {}; export const DEFAULT_HEARTBEAT = { - kind: 'ping', + message: 'ping', timeout: 10000, interval: 25000, }; diff --git a/src/lib/heartbeat.test.ts b/src/lib/heartbeat.test.ts index 7b98597..95ae419 100644 --- a/src/lib/heartbeat.test.ts +++ b/src/lib/heartbeat.test.ts @@ -53,7 +53,7 @@ describe("heartbeat", () => { }); test("sends the custom ping message", () => { - heartbeat(ws, { kind: "pong" }); + heartbeat(ws, { message: "pong" }); expect(sendSpy).not.toHaveBeenCalled(); jest.advanceTimersByTime(25000); expect(sendSpy).toHaveBeenCalledWith("pong"); diff --git a/src/lib/heartbeat.ts b/src/lib/heartbeat.ts index 2ba39f1..f254e22 100644 --- a/src/lib/heartbeat.ts +++ b/src/lib/heartbeat.ts @@ -5,14 +5,14 @@ export function heartbeat(ws: WebSocket, options?: HeartbeatOptions): () => void const { interval = DEFAULT_HEARTBEAT.interval, timeout = DEFAULT_HEARTBEAT.timeout, - kind = DEFAULT_HEARTBEAT.kind, + message = DEFAULT_HEARTBEAT.message, } = options || {}; let messageAccepted = false; const pingTimer = setInterval(() => { try { - ws.send(kind); + ws.send(message); } catch (error) { // do nothing } diff --git a/src/lib/types.ts b/src/lib/types.ts index 70a26ee..1715984 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -28,7 +28,8 @@ export interface Options { export type EventSourceOnly = Omit & EventSourceInit; export type HeartbeatOptions = { - kind?: "ping" | "pong" | string; + message?: "ping" | "pong" | string; + returnMessage?: "ping" | "pong" | string; timeout?: number; interval?: number; }; diff --git a/src/lib/use-websocket.test.ts b/src/lib/use-websocket.test.ts index dd3fd12..22c4f7f 100644 --- a/src/lib/use-websocket.test.ts +++ b/src/lib/use-websocket.test.ts @@ -440,7 +440,7 @@ test('Options#eventSourceOptions, if provided, instantiates an EventSource inste test.each([false, true])('Options#heartbeat, if provided, sends a message to the server at the specified interval and works when share is %s', async (shareOption) => { options.heartbeat = { - kind: 'ping', + message: 'ping', timeout: 10000, interval: 500, }; @@ -458,7 +458,7 @@ test.each([false, true])('Options#heartbeat, if provided, sends a message to the test.each([false, true])('Options#heartbeat, if provided, close websocket if no message is received from server within specified timeout and works when share is %s', async (shareOption) => { options.heartbeat = { - kind: 'ping', + message: 'ping', timeout: 1000, interval: 500, }; @@ -478,7 +478,7 @@ test.each([false, true])('Options#heartbeat, if provided, close websocket if no test.each([false, true])('Options#heartbeat, if provided, do not close websocket if a message is received from server within specified timeout and works when share is %s', async (shareOption) => { options.heartbeat = { - kind: 'ping', + message: 'ping', timeout: 1000, interval: 500, }; @@ -504,9 +504,10 @@ test.each([false, true])('Options#heartbeat, if provided, do not close websocket expect(result.current.readyState).toBe(WebSocket.OPEN); }); -test.each([false, true])('Options#heartbeat, if provided, lastMessage is updated if server message matches the kind property of heartbeatOptions and works when share is %s', async (shareOption) => { +test.each([false, true])('Options#heartbeat, if provided, lastMessage is updated if server message does not matches the returnMessage property of heartbeatOptions and works when share is %s', async (shareOption) => { options.heartbeat = { - kind: 'ping', + message: 'ping', + returnMessage: 'pong', timeout: 1000, interval: 500, }; @@ -521,10 +522,10 @@ test.each([false, true])('Options#heartbeat, if provided, lastMessage is updated } await server.connected; - server.send('ping'); - expect(result.current.lastMessage?.data).toBe(undefined); server.send('pong'); - expect(result.current.lastMessage?.data).toBe('pong'); + expect(result.current.lastMessage?.data).toBe(undefined); + server.send('ping'); + expect(result.current.lastMessage?.data).toBe('ping'); }); // //TODO: Write companion tests for useSocketIO \ No newline at end of file From 3c646ad7f688a43acd85eb5d25d652231ce099ee Mon Sep 17 00:00:00 2001 From: Melvin Oostendorp Date: Wed, 27 Sep 2023 02:05:52 +0000 Subject: [PATCH 4/4] fix: fixed default timeout value in heartbeat --- README.md | 4 ++-- src/lib/constants.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c59ae3f..93c2a4f 100644 --- a/README.md +++ b/README.md @@ -432,8 +432,8 @@ const { sendMessage, lastMessage, readyState } = useWebSocket( heartbeat: { message: 'ping', returnMessage: 'pong', - timeout: 10000, - interval: 25000, + timeout: 60000, // 1 minute, if no response is received, the connection will be closed + interval: 25000, // every 25 seconds, a ping message will be sent }, } ); diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 68b1c1b..301caba 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -17,7 +17,7 @@ export const DEFAULT_RECONNECT_INTERVAL_MS = 5000; export const UNPARSABLE_JSON_OBJECT = {}; export const DEFAULT_HEARTBEAT = { message: 'ping', - timeout: 10000, + timeout: 60000, interval: 25000, };