Files
gerbeur/api/services/ws-service.ts
2026-03-16 07:34:49 +00:00

88 lines
2.2 KiB
TypeScript

import type { Dump, OnlineUser } from "../model/interfaces.ts";
export interface WsClient {
socket: WebSocket;
userId?: string;
username?: string;
avatarMime?: string;
}
const clients = new Set<WsClient>();
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<string, OnlineUser>();
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): void {
for (const client of clients) {
send(client.socket, { type: "votes_update", dumpId, voteCount });
}
}
// 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);