import type { ClientToServerEvents, ServerToClientEvents } from '@music-together/shared' import { SERVER_URL } from './config' interface ConnectionEvents { connect: () => void disconnect: () => void connect_error: (error: Error) => void } type SocketInboundEvents = ServerToClientEvents & ConnectionEvents type StoredHandler = (...args: unknown[]) => void export type TypedSocket = { connected: boolean connect: () => void disconnect: () => void on: (event: E, handler: SocketInboundEvents[E]) => void off: (event: E, handler: SocketInboundEvents[E]) => void emit: (event: E, ...args: Parameters) => void } let socket: TypedSocket | null = null export function getSocket(): TypedSocket { if (!socket) { socket = createWebSocket() } return socket } export function connectSocket(): TypedSocket { const s = getSocket() s.connect() return s } export function disconnectSocket(): void { if (socket) { socket.disconnect() socket = null } } export function waitForConnect(): Promise { const s = getSocket() if (s.connected) return Promise.resolve(s) return new Promise((resolve) => { const handler = () => { s.off('connect', handler) resolve(s) } s.on('connect', handler) s.connect() }) } function createWebSocket(): TypedSocket { let ws: WebSocket | null = null let connected = false const handlers = new Map>() let reconnectTimer: ReturnType | null = null let shouldReconnect = true const connect = () => { shouldReconnect = true if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) { return } const wsUrl = SERVER_URL.replace(/^http/, 'ws') + '/ws' ws = new WebSocket(wsUrl) ws.onopen = () => { connected = true const connectHandlers = handlers.get('connect') if (connectHandlers) { for (const h of connectHandlers) h() } } ws.onmessage = (event) => { try { const msg = JSON.parse(event.data) if (msg && typeof msg.event === 'string') { const eventHandlers = handlers.get(msg.event) if (eventHandlers) { for (const h of eventHandlers) h(msg.data) } } } catch (err) { console.error('Failed to parse WebSocket message', err) } } ws.onclose = () => { connected = false const disconnectHandlers = handlers.get('disconnect') if (disconnectHandlers) { for (const h of disconnectHandlers) h() } if (shouldReconnect) { reconnectTimer = setTimeout(() => { connect() }, 2000) } } ws.onerror = (err) => { console.error('WebSocket error', err) const errorHandlers = handlers.get('connect_error') if (errorHandlers) { for (const h of errorHandlers) h(new Error('WebSocket error')) } } } const disconnect = () => { shouldReconnect = false if (reconnectTimer) { clearTimeout(reconnectTimer) reconnectTimer = null } if (ws) { ws.close() ws = null } connected = false } const on = (event: E, handler: SocketInboundEvents[E]) => { let set = handlers.get(event as string) if (!set) { set = new Set() handlers.set(event as string, set) } set.add(handler as unknown as StoredHandler) } const off = (event: E, handler: SocketInboundEvents[E]) => { const set = handlers.get(event as string) if (set) { set.delete(handler as unknown as StoredHandler) } } const emit = (event: E, ...args: Parameters) => { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ event, data: args[0] })) } } return { get connected() { return connected }, connect, disconnect, on, off, emit, } }