ghp / packages /client /src /lib /socket.ts
QSLY's picture
deploy: build Hugging Face Space from source
00a912e
Raw
History Blame Contribute Delete
4.2 kB
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: <E extends keyof SocketInboundEvents>(event: E, handler: SocketInboundEvents[E]) => void
off: <E extends keyof SocketInboundEvents>(event: E, handler: SocketInboundEvents[E]) => void
emit: <E extends keyof ClientToServerEvents>(event: E, ...args: Parameters<ClientToServerEvents[E]>) => 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<TypedSocket> {
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<string, Set<StoredHandler>>()
let reconnectTimer: ReturnType<typeof setTimeout> | 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 = <E extends keyof SocketInboundEvents>(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 = <E extends keyof SocketInboundEvents>(event: E, handler: SocketInboundEvents[E]) => {
const set = handlers.get(event as string)
if (set) {
set.delete(handler as unknown as StoredHandler)
}
}
const emit = <E extends keyof ClientToServerEvents>(event: E, ...args: Parameters<ClientToServerEvents[E]>) => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ event, data: args[0] }))
}
}
return {
get connected() {
return connected
},
connect,
disconnect,
on,
off,
emit,
}
}