// The entire contract the server side needs from a socket. Four methods. // // This is what replaces the old `ws` dependency. Before, `SessionServer` (then // `WebSocketServer`) built its own `ws` server from an `http.Server`, which made // `ws` a hard dependency of the package and pinned it to Node. Now the caller // brings the socket and this package never touches a WebSocket implementation on // the server side — so it runs on Node (`ws`), Bun, Deno and Cloudflare Workers // without knowing which. // // Subscriptions return their own unsubscribe function rather than taking an // `off(type, listener)`: it is the one shape every runtime can implement without // keeping a listener registry, and it is what makes the adapters below ~10 lines. export interface SocketTransport { send(data: string): void; close(): void; /** @returns unsubscribe */ onMessage(listener: (data: string) => void): () => void; /** @returns unsubscribe */ onClose(listener: () => void): () => void; } /** The `addEventListener` shape: browsers, Deno, Cloudflare Workers. */ interface EventTargetLike { send(data: string): void; close(): void; addEventListener(type: string, listener: (event: any) => void): void; removeEventListener(type: string, listener: (event: any) => void): void; } /** The `.on()` shape: `ws` on Node. */ interface EventEmitterLike { send(data: string): void; close(): void; on(type: string, listener: (...args: any[]) => void): void; off(type: string, listener: (...args: any[]) => void): void; } /** * Adapts an `addEventListener`-style socket — a browser `WebSocket`, * `Deno.upgradeWebSocket`'s socket, or a Cloudflare Workers `WebSocketPair` end. */ export function fromEventTarget(socket: EventTargetLike): SocketTransport { return { send: data => socket.send(data), close: () => socket.close(), onMessage(listener) { const handler = (event: MessageEvent) => listener(String(event.data)); socket.addEventListener('message', handler); return () => socket.removeEventListener('message', handler); }, onClose(listener) { const handler = () => listener(); socket.addEventListener('close', handler); return () => socket.removeEventListener('close', handler); }, }; } /** * Adapts a Node `EventEmitter`-style socket — a `ws` connection. * * `ws` v8 hands the payload as a `Buffer` even for text frames, so it is * stringified here. Binary frames therefore arrive as their UTF-8 * interpretation; this package only ever speaks JSON. */ export function fromEventEmitter(socket: EventEmitterLike): SocketTransport { return { send: data => socket.send(data), close: () => socket.close(), onMessage(listener) { const handler = (data: unknown) => listener(String(data)); socket.on('message', handler); return () => socket.off('message', handler); }, onClose(listener) { const handler = () => listener(); socket.on('close', handler); return () => socket.off('close', handler); }, }; }