import { MutableRefObject } from 'react'; import { setUpSocketIOPing } from './socket-io'; import { DEFAULT_RECONNECT_LIMIT, DEFAULT_RECONNECT_INTERVAL_MS, ReadyState, isEventSourceSupported, } from './constants'; import { Options, SendMessage, WebSocketLike } from './types'; import { assertIsWebSocket } from './util'; export interface Setters { setLastMessage: (message: WebSocketEventMap['message']) => void; setReadyState: (readyState: ReadyState) => void; } const bindMessageHandler = ( webSocketInstance: WebSocketLike, optionsRef: MutableRefObject, setLastMessage: Setters['setLastMessage'], ) => { webSocketInstance.onmessage = (message: WebSocketEventMap['message']) => { optionsRef.current.onMessage && optionsRef.current.onMessage(message); if (typeof optionsRef.current.filter === 'function' && optionsRef.current.filter(message) !== true) { return; } setLastMessage(message); }; }; const bindOpenHandler = ( webSocketInstance: WebSocketLike, optionsRef: MutableRefObject, setReadyState: Setters['setReadyState'], reconnectCount: MutableRefObject, ) => { webSocketInstance.onopen = (event: WebSocketEventMap['open']) => { optionsRef.current.onOpen && optionsRef.current.onOpen(event); reconnectCount.current = 0; setReadyState(ReadyState.OPEN); }; }; const bindCloseHandler = ( webSocketInstance: WebSocketLike, optionsRef: MutableRefObject, setReadyState: Setters['setReadyState'], reconnect: () => void, reconnectCount: MutableRefObject, ) => { if (isEventSourceSupported && webSocketInstance instanceof EventSource) { return () => {}; } assertIsWebSocket(webSocketInstance, optionsRef.current.skipAssert); let reconnectTimeout: number; webSocketInstance.onclose = (event: WebSocketEventMap['close']) => { optionsRef.current.onClose && optionsRef.current.onClose(event); setReadyState(ReadyState.CLOSED); if (optionsRef.current.shouldReconnect && optionsRef.current.shouldReconnect(event)) { const reconnectAttempts = optionsRef.current.reconnectAttempts ?? DEFAULT_RECONNECT_LIMIT; if (reconnectCount.current < reconnectAttempts) { const nextReconnectInterval = typeof optionsRef.current.reconnectInterval === 'function' ? optionsRef.current.reconnectInterval(reconnectCount.current) : optionsRef.current.reconnectInterval; reconnectTimeout = window.setTimeout(() => { reconnectCount.current++; reconnect(); }, nextReconnectInterval ?? DEFAULT_RECONNECT_INTERVAL_MS); } else { optionsRef.current.onReconnectStop && optionsRef.current.onReconnectStop(reconnectAttempts); console.warn(`Max reconnect attempts of ${reconnectAttempts} exceeded`); } } }; return () => reconnectTimeout && window.clearTimeout(reconnectTimeout); }; const bindErrorHandler = ( webSocketInstance: WebSocketLike, optionsRef: MutableRefObject, setReadyState: Setters['setReadyState'], reconnect: () => void, reconnectCount: MutableRefObject, ) => { let reconnectTimeout: number; webSocketInstance.onerror = (error: WebSocketEventMap['error']) => { optionsRef.current.onError && optionsRef.current.onError(error); if (isEventSourceSupported && webSocketInstance instanceof EventSource) { optionsRef.current.onClose && optionsRef.current.onClose({ ...error, code: 1006, reason: `An error occurred with the EventSource: ${error}`, wasClean: false, }); setReadyState(ReadyState.CLOSED); webSocketInstance.close(); } if (optionsRef.current.retryOnError) { if (reconnectCount.current < (optionsRef.current.reconnectAttempts ?? DEFAULT_RECONNECT_LIMIT)) { const nextReconnectInterval = typeof optionsRef.current.reconnectInterval === 'function' ? optionsRef.current.reconnectInterval(reconnectCount.current) : optionsRef.current.reconnectInterval; reconnectTimeout = window.setTimeout(() => { reconnectCount.current++; reconnect(); }, nextReconnectInterval ?? DEFAULT_RECONNECT_INTERVAL_MS); } else { optionsRef.current.onReconnectStop && optionsRef.current.onReconnectStop(optionsRef.current.reconnectAttempts as number); console.warn(`Max reconnect attempts of ${optionsRef.current.reconnectAttempts} exceeded`); } } }; return () => reconnectTimeout && window.clearTimeout(reconnectTimeout); }; export const attachListeners = ( webSocketInstance: WebSocketLike, setters: Setters, optionsRef: MutableRefObject, reconnect: () => void, reconnectCount: MutableRefObject, sendMessage: SendMessage, ): (() => void) => { const { setLastMessage, setReadyState } = setters; let interval: number; let cancelReconnectOnClose: () => void; let cancelReconnectOnError: () => void; if (optionsRef.current.fromSocketIO) { interval = setUpSocketIOPing(sendMessage); } bindMessageHandler( webSocketInstance, optionsRef, setLastMessage, ); bindOpenHandler( webSocketInstance, optionsRef, setReadyState, reconnectCount, ); cancelReconnectOnClose = bindCloseHandler( webSocketInstance, optionsRef, setReadyState, reconnect, reconnectCount, ); cancelReconnectOnError = bindErrorHandler( webSocketInstance, optionsRef, setReadyState, reconnect, reconnectCount, ); return () => { setReadyState(ReadyState.CLOSING); cancelReconnectOnClose(); cancelReconnectOnError(); webSocketInstance.close(); if (interval) clearInterval(interval); }; };