// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import { NativeEventEmitter, NativeModules } from "react-native"; import isURL from "validator/es/lib/isURL"; import type { WebSocketClientConfiguration, WebSocketClientErrorEvent, WebSocketClientErrorEventHandler, WebSocketClientInterface, WebSocketEvent, WebSocketEventHandler, WebSocketReadyState, } from "@philippeweidmann/react-native-network-client"; const { WebSocketClient: NativeWebSocketClient } = NativeModules; const Emitter = new NativeEventEmitter(NativeWebSocketClient); const { EVENTS, READY_STATE } = NativeWebSocketClient.getConstants(); const CLIENTS: { [key: string]: WebSocketClient } = {}; /** * Configurable WebSocket client */ class WebSocketClient implements WebSocketClientInterface { url: string; readyState: WebSocketReadyState; onReadyStateSubscription: EmitterSubscription; onWebSocketOpenSubscription?: EmitterSubscription; onWebSocketCloseSubscription?: EmitterSubscription; onWebSocketErrorSubscription?: EmitterSubscription; onWebSocketMessageSubscription?: EmitterSubscription; onWebSocketClientErrorSubscription?: EmitterSubscription; constructor(url: string) { this.url = url; this.readyState = READY_STATE.CLOSED; this.onReadyStateSubscription = Emitter.addListener( EVENTS.READY_STATE_EVENT, (event: WebSocketEvent) => { if (event.url === this.url) { this.readyState = event.message as WebSocketReadyState; } } ); } open = () => NativeWebSocketClient.connectFor(this.url); close = () => { this.readyState = READY_STATE.CLOSED; return NativeWebSocketClient.disconnectFor(this.url); }; send = (data: string) => NativeWebSocketClient.sendDataFor(this.url, data); onOpen = (callback: WebSocketEventHandler) => { if (this.onWebSocketOpenSubscription) { this.onWebSocketOpenSubscription.remove(); } this.onWebSocketOpenSubscription = Emitter.addListener( EVENTS.OPEN_EVENT, (event: WebSocketEvent) => { if (event.url === this.url && callback) { callback(event); } } ); }; onClose = (callback: WebSocketEventHandler) => { if (this.onWebSocketCloseSubscription) { this.onWebSocketCloseSubscription.remove(); } this.onWebSocketCloseSubscription = Emitter.addListener( EVENTS.CLOSE_EVENT, (event: WebSocketEvent) => { if (event.url === this.url && callback) { callback(event); } } ); }; onError = (callback: WebSocketEventHandler) => { if (this.onWebSocketErrorSubscription) { this.onWebSocketErrorSubscription.remove(); } this.onWebSocketErrorSubscription = Emitter.addListener( EVENTS.ERROR_EVENT, (event: WebSocketEvent) => { if (event.url === this.url && callback) { callback(event); } } ); }; onMessage = (callback: WebSocketEventHandler) => { if (this.onWebSocketMessageSubscription) { this.onWebSocketMessageSubscription.remove(); } this.onWebSocketMessageSubscription = Emitter.addListener( EVENTS.MESSAGE_EVENT, (event: WebSocketEvent) => { if (event.url === this.url && callback) { callback(event); } } ); }; onClientError = (callback: WebSocketClientErrorEventHandler) => { if (this.onWebSocketClientErrorSubscription) { this.onWebSocketClientErrorSubscription.remove(); } this.onWebSocketClientErrorSubscription = Emitter.addListener( EVENTS.ERROR_EVENT, (event: WebSocketClientErrorEvent) => { if (event.url === this.url) { callback(event); } } ); }; invalidate = (): Promise => { this.onReadyStateSubscription.remove(); this.onWebSocketOpenSubscription?.remove(); this.onWebSocketCloseSubscription?.remove(); this.onWebSocketErrorSubscription?.remove(); this.onWebSocketMessageSubscription?.remove(); this.onWebSocketClientErrorSubscription?.remove(); delete CLIENTS[this.url]; return NativeWebSocketClient.invalidateClientFor(this.url); }; } async function getOrCreateWebSocketClient( url: string, config: WebSocketClientConfiguration = {}, clientErrorEventHandler?: WebSocketClientErrorEventHandler ): Promise<{ client: WebSocketClient; created: boolean }> { if (!isValidWebSocketURL(url)) { throw new Error(`"${url}" is not a valid WebSocket URL`); } let created = false; let client = CLIENTS[url]; if (!client) { created = true; client = new WebSocketClient(url); if (clientErrorEventHandler) { client.onClientError(clientErrorEventHandler); } await NativeWebSocketClient.createClientFor(url, config); CLIENTS[url] = client; } return { client, created }; } const isValidWebSocketURL = (url: string) => { return isURL(url, { protocols: ["ws", "wss"], require_protocol: true, require_valid_protocol: true, require_host: true, }); }; export { getOrCreateWebSocketClient };