import type { ServerWebSocket, Socket } from 'bun'; import { VNC_HOST, VNC_PORT } from './session'; /** * A byte relay between the browser and x11vnc, in the same spirit as the terminal's relay onto the pty * sidecar — this file authenticates the upgrade (upgradeWs, in server.tsx) and moves bytes without * reading them. * * One difference decides the shape: x11vnc serves raw TCP, not a WebSocket. So this is the job * websockify would otherwise do, done in-process with `Bun.connect` rather than by adding a second * daemon and a second port to protect. RFB is a stream protocol, so frame boundaries carry no meaning * and chunks are forwarded exactly as they arrive. */ export type MinesweeperWSData = { provider: 'minesweeper'; search: string }; type Relay = { socket: Socket | null; queue: Uint8Array[]; done: boolean }; const relays = new Map, Relay>(); const closeClient = (ws: ServerWebSocket, code: number, reason: string) => { const relay = relays.get(ws); relays.delete(ws); if (relay) relay.done = true; try { relay?.socket?.end(); } catch { /* already gone */ } try { ws.close(code, reason); } catch { /* already closed */ } }; export const minesweeperWebsocket = { open(ws: ServerWebSocket) { // Registered synchronously, so bytes arriving before the TCP connection is up are queued rather // than dropped. The RFB handshake starts with the SERVER talking, but a client that pipelines its // version string would otherwise lose it. const relay: Relay = { socket: null, queue: [], done: false }; relays.set(ws, relay); void Bun.connect({ hostname: VNC_HOST, port: VNC_PORT, socket: { open(socket) { if (relay.done) { socket.end(); return; } relay.socket = socket; for (const chunk of relay.queue) socket.write(chunk); relay.queue.length = 0; }, data(_socket, chunk) { try { ws.send(chunk); } catch { /* client gone; the close handler tears down the socket */ } }, close: () => closeClient(ws, 1000, 'vnc closed'), error: () => closeClient(ws, 1011, 'vnc error'), }, }).catch(() => { // No x11vnc listening — the session is not running. 1011 rather than a silent close, so the // panel can say so instead of showing an empty canvas. closeClient(ws, 1011, 'minesweeper session is not running'); }); }, message(ws: ServerWebSocket, raw: string | Buffer) { const relay = relays.get(ws); if (!relay) return; // RFB is binary in both directions; a text frame is a client that is not speaking it. const bytes = typeof raw === 'string' ? new TextEncoder().encode(raw) : new Uint8Array(raw); if (relay.socket) relay.socket.write(bytes); else relay.queue.push(bytes); }, close(ws: ServerWebSocket) { const relay = relays.get(ws); relays.delete(ws); if (relay) relay.done = true; try { relay?.socket?.end(); } catch { /* ignore */ } }, };