import type {Namespace, Socket} from 'socket.io'; import type {RelaySessionStore} from './session_store.ts'; import {RelayError, error_ack, success} from '../utils/relay_validation.ts'; export function install_relay_routes(namespace: Namespace, store: RelaySessionStore): () => void { const sockets = new Map void>(); const connect = (socket: Socket) => { if (!store.attach(socket)) return; const handlers = new Map void>(); const route = (name: string, operation: (value: unknown) => unknown | Promise, wrapped = true, ack_required = true) => { const handler = (...args: unknown[]) => { const callback = args[args.length - 1]; if (ack_required && typeof callback !== 'function') return; let replied = false; const ack = (value: unknown) => { if (replied || typeof callback !== 'function' || !store.can_reply(socket)) return; replied = true; try {callback(value);} catch { /* A peer callback cannot disrupt route cleanup. */ } }; if (args.length !== (typeof callback === 'function' ? 2 : 1)) { ack(error_ack(new RelayError('INVALID_REQUEST', 'Expected one request payload'))); return; } void Promise.resolve().then(() => operation(args[0])).then( value => ack(wrapped ? success(value) : value), error => ack(error_ack(error))); }; handlers.set(name, handler); socket.on(name, handler); }; route('join_room', value => store.join(socket, value)); route('reauth', value => store.join(socket, value, true)); route('forward_node_ready', value => store.node_ready(socket, value)); route('relay_request', value => store.request(socket, value), false); route('bot_log', value => store.log(socket, value), true, false); route('info_update', value => store.info(socket, value), true, false); const unknown = (name: string, ...args: unknown[]) => { if (handlers.has(name)) return; const callback = args[args.length - 1]; try { store.unknown_event(socket); if (typeof callback === 'function' && store.can_reply(socket)) callback(error_ack(new RelayError('FORBIDDEN', 'Event is not allowed on the relay data channel'))); } catch { /* The store disconnects unknown-event floods. */ } }; const cleanup = () => { socket.offAny(unknown); socket.off('disconnect', cleanup); for (const [name, handler] of handlers) socket.off(name, handler); sockets.delete(socket); }; socket.onAny(unknown); socket.on('disconnect', cleanup); sockets.set(socket, cleanup); }; namespace.on('connection', connect); let disposed = false; return () => { if (disposed) return; disposed = true; namespace.off('connection', connect); for (const [socket, cleanup] of [...sockets]) { cleanup(); store.disconnect(socket); } }; }