import type { PacketChannel } from './channels'; import type { Envelope } from './protocol'; interface WebSocketLike { readonly readyState: number; send(data: string): void; close(code?: number, reason?: string): void; addEventListener(type: 'message', handler: (event: { data: unknown }) => void): void; addEventListener(type: 'close', handler: (event?: { reason?: string }) => void): void; } type EnvelopeHandler = (envelope: Envelope) => void; type CloseHandler = (reason?: string) => void; type ProtocolErrorHandler = (reason: string) => void; const OPEN = 1; export class WebSocketPacketChannel implements PacketChannel { readonly mode = 'websocket' as const; private readonly envelopeHandlers = new Set(); private readonly closeHandlers = new Set(); private readonly protocolErrorHandlers = new Set(); constructor( readonly peerId: string, private readonly socket: WebSocketLike, ) { socket.addEventListener('message', (event) => { if (typeof event.data !== 'string') { // A real Colyseus server speaks a binary schema-diff protocol, not // this package's JSON Envelope format — a binary frame here is a // live signal the peer is wire-incompatible, not something to // silently discard. for (const handler of this.protocolErrorHandlers) { handler('received a non-text (binary) WebSocket frame'); } return; } const envelope = JSON.parse(event.data) as Envelope; for (const handler of this.envelopeHandlers) handler(envelope); }); socket.addEventListener('close', (event) => { for (const handler of this.closeHandlers) handler(event?.reason); }); } send(envelope: Envelope): void { if (this.socket.readyState !== OPEN) throw new Error(`WebSocket for ${this.peerId} is not open`); this.socket.send(JSON.stringify(envelope)); } onEnvelope(handler: EnvelopeHandler): () => void { this.envelopeHandlers.add(handler); return () => this.envelopeHandlers.delete(handler); } onClose(handler: CloseHandler): () => void { this.closeHandlers.add(handler); return () => this.closeHandlers.delete(handler); } onProtocolError(handler: ProtocolErrorHandler): () => void { this.protocolErrorHandlers.add(handler); return () => this.protocolErrorHandlers.delete(handler); } close(reason?: string): void { this.socket.close(1000, reason); } } export function createWebSocketPacketChannel(peerId: string, socket: WebSocket): PacketChannel { return new WebSocketPacketChannel(peerId, socket); }