99 lines
2.3 KiB
TypeScript
99 lines
2.3 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,
|
|
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);
|