UNPKG

use-stomp

Version:

react provider, class decorator, and a hook for websockets using the stomp protocol

458 lines (387 loc) 13 kB
import SockJS from 'sockjs-client'; import type {Client, Subscription} from 'stompjs'; import {v4} from 'uuid'; import Stomp from './stomp'; import {Events, States} from './webSocketConfigs'; type WEBSOCKET_CONNECTION = { WEBSOCKET: Client; WEBSOCKET_AUTH_HEADER: string; WEBSOCKET_CONNECTION: string; WEBSOCKET_EXPLICIT_DISCONNECT: boolean; WEBSOCKET_HEADER: string; WEBSOCKET_URL: string; WEBSOCKET_STATE: { [channel: string]: {id: string; message: any}[]; }; WEBSOCKET_PORTS: Map<string, MessagePort>; WEBSOCKET_CHANNELS: Map<string, Subscription>; WEBSOCKET_RECONNECT_COUNT: number; WEBSOCKET_RECONNECT_INTERVAL: NodeJS.Timer; WEBSOCKET_RECONNECT_INTERVAL_MS: number; WEBSOCKET_RECONNECT_MAX_ATTEMPTS: number; WEBSOCKET_SUBSCRIPTIONS: Map<string, string[]>; WEBSOCKET_VISIBLE_TABS: Set<string>; }; interface SharedWorkerGlobalScope { addEventListener( type: string, listener: (event: MessageEvent) => void, options?: boolean | AddEventListenerOptions ): void; onconnect: (event: MessageEvent) => void; ws: Client; } const _self: SharedWorkerGlobalScope = self as any; const ctx: WEBSOCKET_CONNECTION = { WEBSOCKET: null, WEBSOCKET_AUTH_HEADER: '', WEBSOCKET_CONNECTION: States.DISCONNECTED, WEBSOCKET_HEADER: '', WEBSOCKET_EXPLICIT_DISCONNECT: false, WEBSOCKET_URL: '', WEBSOCKET_STATE: {}, WEBSOCKET_PORTS: new Map(), WEBSOCKET_CHANNELS: new Map(), WEBSOCKET_RECONNECT_COUNT: 0, WEBSOCKET_RECONNECT_INTERVAL: null, WEBSOCKET_RECONNECT_INTERVAL_MS: 10000, WEBSOCKET_RECONNECT_MAX_ATTEMPTS: 10, WEBSOCKET_SUBSCRIPTIONS: new Map(), WEBSOCKET_VISIBLE_TABS: new Set() }; function packageMessage(message) { try { return typeof message === 'object' && message !== null ? JSON.stringify(message) : message; } catch (e) { return message; } } function parseMessage(message) { try { const parsed = JSON.parse(message); return typeof parsed === 'object' && parsed !== null && parsed.content ? parsed.content : parsed; } catch (e) { return message; } } const broadcast = (type, payload) => { ctx.WEBSOCKET_PORTS.forEach((port, id) => { emit(id, type, payload); }); }; const emit = (id, type, payload, onlyVisible = false) => { if (ctx.WEBSOCKET_PORTS.has(id)) { // if (!onlyVisible || ctx.WEBSOCKET_VISIBLE_TABS.has(id)) { ctx.WEBSOCKET_PORTS.get(id).postMessage({ type, payload }); } }; const onConnected = () => { console.log('[use-stomp::ws::connected]'); resubscribeAll(); setState(States.CONNECTED); }; const onChannelMessage = (channel) => (msg) => { const message = parseMessage(msg.body); if (message?.status === 'END') { disconnect(); } else { // if (isAnyTabVisible()) { ctx.WEBSOCKET_SUBSCRIPTIONS.get(channel).forEach((id) => { emit(id, Events.MESSAGE, { channel, message }); }); // } else { // } } }; const onChannelMessageSync = (channel) => (msg) => { const message = parseMessage(msg.body); if (message?.status === 'END') { disconnect(); } else { ctx.WEBSOCKET_STATE[channel] = [ ...(ctx.WEBSOCKET_STATE[channel] || []), { id: v4(), message } ]; (ctx.WEBSOCKET_SUBSCRIPTIONS.get(channel) || []).forEach((id) => { emit(id, Events.MESSAGE, { channel, message: ctx.WEBSOCKET_STATE[channel] }); }); } }; const onDismissSync = (e) => { ctx.WEBSOCKET_STATE[e.data.payload.channel] = ctx.WEBSOCKET_STATE[ e.data.payload.channel ].filter((message) => message.id !== e.data.payload.id); (ctx.WEBSOCKET_SUBSCRIPTIONS.get(e.data.payload.channel) || []).forEach( (id) => { emit(id, Events.MESSAGE, { channel: e.data.payload.channel, message: ctx.WEBSOCKET_STATE[e.data.payload.channel] }); } ); }; const onDisconnected = () => { console.log('[use-stomp::ws::disconnected]'); setState(States.DISCONNECTED); if ( !ctx.WEBSOCKET_EXPLICIT_DISCONNECT && !ctx.WEBSOCKET_RECONNECT_INTERVAL && !ctx.WEBSOCKET_RECONNECT_COUNT ) { ctx.WEBSOCKET_RECONNECT_COUNT = 1; const maxAttempts = ctx.WEBSOCKET_RECONNECT_MAX_ATTEMPTS; const reconnect = () => { console.log( '[use-stomp::ws::reconnecting]', `${ctx.WEBSOCKET_RECONNECT_COUNT} / ${maxAttempts}` ); const hasMaxAttempts = ctx.WEBSOCKET_RECONNECT_COUNT > maxAttempts; const canTerminate = isConnected() || hasMaxAttempts; if (canTerminate) { console.log('[use-stomp::ws::reconnected]'); clearInterval(ctx.WEBSOCKET_RECONNECT_INTERVAL); ctx.WEBSOCKET_RECONNECT_COUNT = 0; ctx.WEBSOCKET_RECONNECT_INTERVAL = null; } else if (!isConnecting() && !isConnected() && !hasMaxAttempts) { ctx.WEBSOCKET_RECONNECT_COUNT = ctx.WEBSOCKET_RECONNECT_COUNT + 1; connect(); } }; ctx.WEBSOCKET_RECONNECT_INTERVAL = setInterval( reconnect, ctx.WEBSOCKET_RECONNECT_INTERVAL_MS ); reconnect(); } }; const onError = (e) => { broadcast(Events.ERROR, e); }; const isConnected = () => ctx.WEBSOCKET_CONNECTION === States.CONNECTED; const isConnecting = () => ctx.WEBSOCKET_CONNECTION === States.CONNECTING; const isDisconnected = () => ctx.WEBSOCKET_CONNECTION === States.DISCONNECTED; const isDisconnecting = () => ctx.WEBSOCKET_CONNECTION === States.DISCONNECTING; const isAnyTabVisible = () => ctx.WEBSOCKET_VISIBLE_TABS.size > 0; const connect = () => { if (isConnecting() || isConnected() || isDisconnecting()) { return; } disconnect(); setState(States.CONNECTING); ctx.WEBSOCKET = Stomp.over(new SockJS(ctx.WEBSOCKET_URL, null, {})); (ctx.WEBSOCKET.connect as any)( ctx.WEBSOCKET_AUTH_HEADER ? {Authorization: ctx.WEBSOCKET_AUTH_HEADER} : ctx.WEBSOCKET_HEADER, onConnected, onDisconnected, onError ); }; const disconnect = () => { if (isDisconnecting() || isDisconnected()) { return; } ctx.WEBSOCKET_EXPLICIT_DISCONNECT = true; setState(States.DISCONNECTING); unsubscribeAll(); if (ctx.WEBSOCKET) { (ctx.WEBSOCKET.disconnect as any)(() => { ctx.WEBSOCKET_EXPLICIT_DISCONNECT = false; }); } setState(States.DISCONNECTED); }; const testDisconnect = () => { if (isConnected() && !(isDisconnected() || isDisconnecting())) { setState(States.DISCONNECTING); unsubscribeAll(); if (ctx.WEBSOCKET) { (ctx.WEBSOCKET.disconnect as any)(); } setState(States.DISCONNECTED); } }; const resubscribeAll = () => { if (ctx.WEBSOCKET?.subscribe && ctx.WEBSOCKET?.connected && isConnected()) { ctx.WEBSOCKET_CHANNELS.forEach((subscription, channel) => { ctx.WEBSOCKET_CHANNELS.set( channel, ctx.WEBSOCKET.subscribe(channel, onChannelMessage(channel)) ); }); } }; const subscribe = (e) => { if (isConnected() && !ctx.WEBSOCKET_CHANNELS.has(e.data.payload.channel)) { ctx.WEBSOCKET_CHANNELS.set( e.data.payload.channel, ctx.WEBSOCKET.subscribe( e.data.payload.channel, onChannelMessage(e.data.payload.channel) ) ); } // if channel has no subscriptions, set it as an array and add id if (!ctx.WEBSOCKET_SUBSCRIPTIONS.has(e.data.payload.channel)) { ctx.WEBSOCKET_SUBSCRIPTIONS.set(e.data.payload.channel, [ e.data.payload.id ]); } else { // if channel does exist, update the ids to include id ctx.WEBSOCKET_SUBSCRIPTIONS.set(e.data.payload.channel, [ ...ctx.WEBSOCKET_SUBSCRIPTIONS.get(e.data.payload.channel), e.data.payload.id ]); } }; const subscribeSync = (e) => { if (isConnected() && !ctx.WEBSOCKET_CHANNELS.has(e.data.payload.channel)) { ctx.WEBSOCKET_CHANNELS.set( e.data.payload.channel, ctx.WEBSOCKET.subscribe( e.data.payload.channel, onChannelMessageSync(e.data.payload.channel) ) ); } // if channel has no subscriptions, set it as an array and add id if (!ctx.WEBSOCKET_SUBSCRIPTIONS.has(e.data.payload.channel)) { ctx.WEBSOCKET_SUBSCRIPTIONS.set(e.data.payload.channel, [ e.data.payload.id ]); } else { // if channel does exist, update the ids to include id ctx.WEBSOCKET_SUBSCRIPTIONS.set(e.data.payload.channel, [ ...ctx.WEBSOCKET_SUBSCRIPTIONS.get(e.data.payload.channel), e.data.payload.id ]); } }; const sendMessage = (e) => { if (ctx.WEBSOCKET && isConnected()) { ctx.WEBSOCKET.send( e.data.payload.channel, null, packageMessage(e.data.payload.message) ); } }; const setAuthHeader = (e) => { ctx.WEBSOCKET_AUTH_HEADER = e.data.payload; }; const setHeader = (e) => { ctx.WEBSOCKET_HEADER = e.data.payload; }; const setUrl = (e) => { ctx.WEBSOCKET_URL = e.data.payload; }; const setVisibility = (e) => { const visible = e.data.payload.visibility; const current = ctx.WEBSOCKET_VISIBLE_TABS.has(e.data.payload.id); if (visible && !current) { ctx.WEBSOCKET_VISIBLE_TABS.add(e.data.payload.id); } if (!visible && current) { ctx.WEBSOCKET_VISIBLE_TABS.delete(e.data.payload.id); } }; const setState = (state) => { ctx.WEBSOCKET_CONNECTION = state; broadcast(Events.CONNECTION, ctx.WEBSOCKET_CONNECTION); }; const unregister = (e) => { ctx.WEBSOCKET_PORTS.delete(e.data.payload.id); ctx.WEBSOCKET_VISIBLE_TABS.delete(e.data.payload.id); ctx.WEBSOCKET_SUBSCRIPTIONS.forEach((ids, channel) => { unsubscribe({ ...e, data: { payload: { ...e.data.payload, channel } } }); }); }; const unsubscribe = (e) => { const uuid = e.data.payload.id; const channel = e.data.payload.channel; if (ctx.WEBSOCKET_SUBSCRIPTIONS.has(channel)) { const ids = ctx.WEBSOCKET_SUBSCRIPTIONS.get(channel).filter( (id) => id !== uuid ); if (!ids.length) { ctx.WEBSOCKET_SUBSCRIPTIONS.delete(channel); ctx.WEBSOCKET_CHANNELS.get(channel).unsubscribe(); ctx.WEBSOCKET_CHANNELS.delete(channel); } else { ctx.WEBSOCKET_SUBSCRIPTIONS.set(channel, ids); } } }; const unsubscribeAll = () => { ctx.WEBSOCKET_CHANNELS.forEach((subscription, channel) => { subscription.unsubscribe(); ctx.WEBSOCKET_CHANNELS.delete(channel); ctx.WEBSOCKET_SUBSCRIPTIONS.delete(channel); }); }; _self.addEventListener('connect', (e) => { const port = e.ports[0]; const register = (e) => { ctx.WEBSOCKET_PORTS.set(e.data.payload.id, port); ctx.WEBSOCKET_RECONNECT_INTERVAL_MS = e.data.payload.reconnectInterval; ctx.WEBSOCKET_RECONNECT_MAX_ATTEMPTS = e.data.payload.reconnectMaxAttempts; if (e.data.payload.visible) { ctx.WEBSOCKET_VISIBLE_TABS.add(e.data.payload.id); } }; port.addEventListener('message', (e) => { const messageBroker = { [Events.CONNECT]: connect, [Events.DISMISS_SYNC]: onDismissSync, [Events.DISCONNECT]: disconnect, [Events.REGISTER]: register, [Events.SEND_MESSAGE]: sendMessage, [Events.SET_AUTH_HEADER]: setAuthHeader, [Events.SET_HEADER]: setHeader, [Events.SET_URL]: setUrl, [Events.SET_VISIBILITY]: setVisibility, [Events.SUBSCRIBE]: subscribe, [Events.SUBSCRIBE_SYNC]: subscribeSync, [Events.TEST_DISCONNECT]: testDisconnect, [Events.UNREGISTER]: unregister, [Events.UNSUBSCRIBE]: unsubscribe, [Events.UNSUBSCRIBE_SYNC]: unsubscribe }; if (messageBroker[e.data.type]) { messageBroker[e.data.type](e); } }); port.start(); console.log('[use-stomp::port::connected]'); port.postMessage({ type: Events.CONNECTION, payload: ctx.WEBSOCKET_CONNECTION }); });