import type { AddressInfo } from 'node:net'; import { WebSocket, WebSocketServer } from 'ws'; import type { PacketChannel } from './channels'; import type { Envelope } from './protocol'; import { type RoomClass, UniversalRoomRuntime } from './runtime'; type EnvelopeHandler = (envelope: Envelope) => void; type CloseHandler = (reason?: string) => void; export class NodeWebSocketPacketChannel implements PacketChannel { readonly mode = 'websocket' as const; private readonly envelopeHandlers = new Set(); private readonly closeHandlers = new Set(); constructor( readonly peerId: string, private readonly socket: WebSocket, ) { socket.on('message', (data) => { const raw = typeof data === 'string' ? data : data.toString(); const envelope = JSON.parse(raw) as Envelope; for (const handler of this.envelopeHandlers) handler(envelope); }); socket.on('close', (_code, reason) => { const text = reason.length > 0 ? reason.toString() : undefined; for (const handler of this.closeHandlers) handler(text); }); } send(envelope: Envelope): void { if (this.socket.readyState !== WebSocket.OPEN) { throw new Error(`Node 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); } close(reason?: string): void { this.socket.close(1000, reason); } } export interface UniversalNodeServer { readonly url: string; close(): Promise; } export async function startUniversalNodeServer( roomName: string, roomClass: RoomClass, port = 0, ): Promise { const runtime = new UniversalRoomRuntime(roomClass, roomName); const server = new WebSocketServer({ port }); let nextPeerId = 1; server.on('connection', (socket) => { runtime.attach(new NodeWebSocketPacketChannel(`client-${nextPeerId++}`, socket)); }); await new Promise((resolve) => server.once('listening', resolve)); const address = server.address() as AddressInfo; return { url: `ws://127.0.0.1:${address.port}`, close: () => new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve()))), }; }