2026-05-14 18:40:34 +07:00
|
|
|
import type { WebSocket } from "ws";
|
2026-05-14 21:16:03 +07:00
|
|
|
import { createChildLogger } from "../logger";
|
2026-05-14 18:40:34 +07:00
|
|
|
import type {
|
|
|
|
|
AnalysisQueueStatus,
|
|
|
|
|
AttachmentRecord,
|
|
|
|
|
MessageRecord,
|
|
|
|
|
ModerationWsEvent,
|
|
|
|
|
} from "./types";
|
|
|
|
|
|
|
|
|
|
type ClientLike = Pick<WebSocket, "readyState" | "send">;
|
|
|
|
|
|
2026-05-14 18:44:47 +07:00
|
|
|
const log = createChildLogger("broadcaster");
|
|
|
|
|
|
2026-05-14 18:40:34 +07:00
|
|
|
function sendJson(clients: Set<ClientLike>, event: ModerationWsEvent): void {
|
|
|
|
|
const payload = JSON.stringify({ ...event, timestamp: Date.now() });
|
|
|
|
|
for (const client of clients) {
|
2026-05-14 18:44:47 +07:00
|
|
|
if (client.readyState === 1) {
|
|
|
|
|
try {
|
|
|
|
|
client.send(payload);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
log.warn(
|
|
|
|
|
{ error, eventType: event.type },
|
2026-05-14 19:46:47 +07:00
|
|
|
"Failed to send event to client",
|
2026-05-14 18:44:47 +07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-14 18:40:34 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createBroadcaster() {
|
|
|
|
|
const clients = new Set<ClientLike>();
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
addClient(client: ClientLike) {
|
|
|
|
|
clients.add(client);
|
2026-05-14 18:44:47 +07:00
|
|
|
log.debug({ clientCount: clients.size }, "Client added");
|
2026-05-14 18:40:34 +07:00
|
|
|
},
|
|
|
|
|
removeClient(client: ClientLike) {
|
|
|
|
|
clients.delete(client);
|
2026-05-14 18:44:47 +07:00
|
|
|
log.debug({ clientCount: clients.size }, "Client removed");
|
2026-05-14 18:40:34 +07:00
|
|
|
},
|
|
|
|
|
clientCount() {
|
|
|
|
|
return clients.size;
|
|
|
|
|
},
|
2026-05-14 19:46:47 +07:00
|
|
|
getClients() {
|
|
|
|
|
return Array.from(clients);
|
|
|
|
|
},
|
2026-05-14 18:40:34 +07:00
|
|
|
uiState(state: unknown) {
|
|
|
|
|
sendJson(clients, { type: "ui_state", state });
|
|
|
|
|
},
|
|
|
|
|
userState(users: unknown[]) {
|
|
|
|
|
sendJson(clients, { type: "user_state", users });
|
|
|
|
|
},
|
|
|
|
|
messageCreated(data: MessageRecord) {
|
|
|
|
|
sendJson(clients, { type: "message_created", data });
|
|
|
|
|
},
|
|
|
|
|
messageUpdated(data: Partial<MessageRecord> & { id: string }) {
|
|
|
|
|
sendJson(clients, { type: "message_updated", data });
|
|
|
|
|
},
|
|
|
|
|
messageDeleted(data: { id: string; deleted_at: number }) {
|
|
|
|
|
sendJson(clients, { type: "message_deleted", data });
|
|
|
|
|
},
|
|
|
|
|
messageAnalyzed(data: MessageRecord) {
|
|
|
|
|
sendJson(clients, { type: "message_analyzed", data });
|
|
|
|
|
},
|
|
|
|
|
attachmentCreated(data: AttachmentRecord) {
|
|
|
|
|
sendJson(clients, { type: "attachment_created", data });
|
|
|
|
|
},
|
|
|
|
|
analysisQueueStatus(data: AnalysisQueueStatus) {
|
|
|
|
|
sendJson(clients, { type: "analysis_queue_status", data });
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ModerationBroadcaster = ReturnType<typeof createBroadcaster>;
|