import { createNanoEvents } from 'nanoevents'; import { getWebSocketInterceptor } from './websocket-interceptor'; import { WebSocketEvent, WebSocketEventMap, } from '../../shared/websocket-events'; import type { Inspector } from '../inspector'; import type { WebSocketInterceptor } from './websocket-interceptor'; type NanoEventsMap = { [K in keyof WebSocketEventMap]: (data: WebSocketEventMap[K]) => void; }; export type WebSocketInspector = Inspector; export const WEBSOCKET_EVENTS: (keyof WebSocketEventMap)[] = [ 'websocket-connect', 'websocket-open', 'websocket-close', 'websocket-message-sent', 'websocket-message-received', 'websocket-error', 'websocket-connection-status-changed', ]; export const isWebSocketEvent = ( type: string, ): type is keyof WebSocketEventMap => { return (WEBSOCKET_EVENTS as readonly string[]).includes(type); }; const toSocketId = (socketId: number) => String(socketId); export const createWebSocketInspector = ( webSocketInterceptor: WebSocketInterceptor = getWebSocketInterceptor(), ): WebSocketInspector => { const eventEmitter = createNanoEvents(); const socketUrlMap = new Map(); return { enable: () => { webSocketInterceptor.setConnectCallback( ( url: string, protocols: string[] | null, options: string[], socketId: number, ) => { socketUrlMap.set(socketId, url); const event: WebSocketEvent = { type: 'websocket-connect', url, socketId: toSocketId(socketId), timestamp: Date.now(), protocols, options, source: 'builtin', }; eventEmitter.emit('websocket-connect', event); }, ); webSocketInterceptor.setCloseCallback( (code: number | null, reason: string | null, socketId: number) => { const url = socketUrlMap.get(socketId); if (!url) { return; } const event: WebSocketEvent = { type: 'websocket-close', url, socketId: toSocketId(socketId), timestamp: Date.now(), code: code || 0, reason: reason || undefined, source: 'builtin', }; eventEmitter.emit('websocket-close', event); socketUrlMap.delete(socketId); }, ); webSocketInterceptor.setOnMessageCallback( (data: string, socketId: number) => { const url = socketUrlMap.get(socketId); if (!url) { return; } const event: WebSocketEvent = { type: 'websocket-message-received', url, socketId: toSocketId(socketId), timestamp: Date.now(), data, messageType: typeof data === 'string' ? 'text' : 'binary', source: 'builtin', }; eventEmitter.emit('websocket-message-received', event); }, ); webSocketInterceptor.setOnErrorCallback( (error: string, socketId: number) => { const url = socketUrlMap.get(socketId); if (!url) { return; } const event: WebSocketEvent = { type: 'websocket-error', url, socketId: toSocketId(socketId), timestamp: Date.now(), error, source: 'builtin', }; eventEmitter.emit('websocket-error', event); }, ); webSocketInterceptor.setSendCallback((data: string, socketId: number) => { const url = socketUrlMap.get(socketId); if (!url) { return; } const event: WebSocketEvent = { type: 'websocket-message-sent', url, socketId: toSocketId(socketId), timestamp: Date.now(), data, messageType: typeof data === 'string' ? 'text' : 'binary', source: 'builtin', }; eventEmitter.emit('websocket-message-sent', event); }); webSocketInterceptor.setOnOpenCallback((socketId: number) => { const url = socketUrlMap.get(socketId); if (!url) { return; } const event: WebSocketEvent = { type: 'websocket-open', url, socketId: toSocketId(socketId), timestamp: Date.now(), source: 'builtin', }; eventEmitter.emit('websocket-open', event); }); webSocketInterceptor.setOnCloseCallback( (error: { code: number; reason?: string }, socketId: number) => { const url = socketUrlMap.get(socketId); if (!url) { return; } const event: WebSocketEvent = { type: 'websocket-close', url, socketId: toSocketId(socketId), timestamp: Date.now(), code: error.code, reason: error.reason, source: 'builtin', }; eventEmitter.emit('websocket-close', event); socketUrlMap.delete(socketId); }, ); webSocketInterceptor.enableInterception(); }, disable: () => { webSocketInterceptor.disableInterception(); }, isEnabled: () => webSocketInterceptor.isInterceptorEnabled(), dispose: () => { webSocketInterceptor.disableInterception(); socketUrlMap.clear(); }, on: ( event: TEventType, callback: (data: WebSocketEventMap[TEventType]) => void, ) => eventEmitter.on(event, callback as NanoEventsMap[TEventType]), }; }; export const getWebSocketInspector = (): WebSocketInspector => { return createWebSocketInspector(); };