import type { Dump, OnlineUser } from "../model/interfaces.ts"; export interface WsClient { socket: WebSocket; userId?: string; username?: string; avatarMime?: string; } const clients = new Set(); export function register(client: WsClient): void { clients.add(client); } export function unregister(client: WsClient): void { clients.delete(client); } export function updateClientAvatar(userId: string, avatarMime: string): void { for (const client of clients) { if (client.userId === userId) { client.avatarMime = avatarMime; } } broadcastPresence(); } export function getOnlineUsers(): OnlineUser[] { const seen = new Map(); for (const client of clients) { if (client.userId && !seen.has(client.userId)) { seen.set(client.userId, { userId: client.userId, username: client.username!, hasAvatar: !!client.avatarMime, }); } } return Array.from(seen.values()); } function send(socket: WebSocket, data: unknown): void { if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify(data)); } } export function broadcastPresence(): void { const users = getOnlineUsers(); for (const client of clients) { send(client.socket, { type: "presence_update", users }); } } export function broadcastNewDump(dump: Dump): void { for (const client of clients) { send(client.socket, { type: "dump_created", dump }); } } export function broadcastDumpDeleted(dumpId: string): void { for (const client of clients) { send(client.socket, { type: "dump_deleted", dumpId }); } } export function broadcastVoteUpdate( dumpId: string, voteCount: number, voterId: string, action: "cast" | "remove", ): void { for (const client of clients) { send(client.socket, { type: "votes_update", dumpId, voteCount, voterId, action, }); } } // Keepalive: ping all clients every 30s, remove non-responsive ones const PING_INTERVAL = 30_000; const _PONG_TIMEOUT = 5_000; setInterval(() => { for (const client of clients) { if (client.socket.readyState !== WebSocket.OPEN) { clients.delete(client); continue; } send(client.socket, { type: "ping" }); // Schedule removal if no pong (tracked via heartbeat flag) } }, PING_INTERVAL);