robtaussig / react-use-websocket

React Hook for WebSocket communication
MIT License
1.63k stars 135 forks source link

Feature suggestion/request: Send periodic heartbeat #133

Closed danielcrk closed 1 year ago

danielcrk commented 2 years ago

Hi Robert! First of all, thank you so much for this amazing hook - the ability to share a single connection between multiple instances really sets it apart!

I'm currently using the hook in a project where the backend requires a periodical heartbeat (just an empty message every 10 seconds or so) to keep the connection alive. I think this is a pretty common pattern.

Given the fact that the hook supports connection sharing and invites the usage of multiple instances, writing the heartbeat functionality isn't super trivial. I've written my own wrapper for your hook (mostly to implement message type generics), and that wrapper hook must basically use and share static variables between instances, as to not send multiple heartbeat messages from each of them.

It would be amazing if this was provided out of the box by react-use-websocket.

Thank you again for your amazing work!

robtaussig commented 2 years ago

Hi @danielcrk!

Thank you for the kind words and for your thoughtful consideration and suggestion. That definitely sounds like a useful feature to provide OOTB -- could you please share your current solution/wrapper? I have a few ideas myself of how this might work, but would love to start with an example of how someone using this library had to solve it by hand

danielcrk commented 2 years ago

Hey @robtaussig, sure, here's a stripped-down version of my wrapper.

The solution is far from perfect, but good enough for my usecase. The interval is set to 2 seconds and the ping message needs to be sent every 10 seconds, so the actual ping timing wont be exact, but keeping things simple was worth the trade-off.

So basically, each instance of the hook will have its own timer, but will only actually send the ping message if another instance hasn't already done so within the last 10-ish seconds for that specific url.

import { useEffect, useRef, useState } from 'react';
import useReactWebSocket from 'react-use-websocket';

// Multiple instances of the hook can exist simultaneously.
// This stores the timestamp of the last heartbeat for a given socket url,
// preventing other instances to send unnecessary heartbeats.
const previousHeartbeats: Record<string, number> = {};

const useWebSocket = <T>(
  url?: string,
  options?: IWebSocketOptions
): T | undefined => {
  const [message, setMessage] = useState<T>();

  // Stores the heartbeat interval.
  const heartbeatIntervalRef = useRef<number>();

  // Instantiate useReactWebSocket.
  const { sendMessage, readyState } = useReactWebSocket(
    url ? getAbsoluteWebSocketUrl(url, options?.appendCustomHeaders) : null,
    {
      share: options?.shareConnection,
      reconnectAttempts: 6,
      reconnectInterval: 1000,

      onMessage: handleOnMessage,
      onError: handleOnError,
      shouldReconnect: handleShouldReconnect,
    },
    !!url
  );

  // Sends a periodical heartbeat message through the websocket connection.
  useEffect(() => {
    if (readyState === 1) {
      heartbeatIntervalRef.current = window.setInterval(() => {
        if (url) {
          const lastHeartbeat = previousHeartbeats[url];
          const deltaFromNow = (Date.now() - lastHeartbeat) / 1000;

          // Send a heartbeat message if it hasn't already been sent within the last 10 seconds.
          if (!lastHeartbeat || deltaFromNow > 10) {
            // Send the heartbeat message and update the heartbeat history.
            sendMessage('');
            previousHeartbeats[url] = Date.now();
          }
        }
      }, 2000);
    }

    return () => {
      clearInterval(heartbeatIntervalRef.current);
    };
  }, [url, readyState, sendMessage]);

  return message;
};

export default useWebSocket;

I'm really curious about your idea as well! :)

robtaussig commented 2 years ago

@danielcrk

Interesting! I'll take a closer look over the weekend, but at first glance it looks good to me!

shmkane commented 2 years ago

This is also something I'd like to see added

danielcrk commented 2 years ago

@robtaussig awesome, thanks for considering it!

allenGKC commented 2 years ago

Awesome idea! Looking forward this feature.

pessato commented 1 year ago

This would be a great feature. Could potentially be enhanced further by including options to configure the heartbeat message, interval and an "onFailure" callback.

Thanks for the great work on this hook 🙌

Arbarwings commented 1 year ago

I've made a basic heartbeat implementation, it's released in version 4.5.0. Here is an example:

const { sendMessage, lastMessage, readyState } = useWebSocket(
  'ws://localhost:3000',
  {
    heartbeat: {
      message: 'ping',
      returnMessage: 'pong',
      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
    },
  }
);