"use client"; import { useEffect, useState } from "react"; export interface PresenceParticipant { id: number; name: string; lastSeen: string; } interface PresenceEvent { channel?: string; action?: "snapshot" | "join" | "update" | "leave"; participants?: PresenceParticipant[]; participant?: PresenceParticipant; participantId?: number; } const HEARTBEAT_MS = 15_000; export function useRealtimePresence(channel: string, active: boolean, observe = true): PresenceParticipant[] { const [participants, setParticipants] = useState([]); useEffect(() => { if (!observe) { setParticipants([]); return; } let cancelled = false; const endpoint = `/api/realtime/presence?channel=${encodeURIComponent(channel)}`; const send = (method: "POST" | "DELETE") => { void fetch(endpoint, { method, keepalive: method === "DELETE" }).catch(() => undefined); }; const source = new EventSource(`/api/realtime?channel=${encodeURIComponent(channel)}`); const onPresence = (event: MessageEvent) => { if (cancelled) return; let detail: PresenceEvent; try { detail = JSON.parse(event.data) as PresenceEvent; } catch { return; } if (detail.channel !== channel) return; if (detail.action === "snapshot") setParticipants(detail.participants ?? []); if ((detail.action === "join" || detail.action === "update") && detail.participant) { setParticipants((current) => [ ...current.filter((item) => item.id !== detail.participant!.id), detail.participant!, ]); } if (detail.action === "leave" && detail.participantId !== undefined) { setParticipants((current) => current.filter((item) => item.id !== detail.participantId)); } }; source.addEventListener("presence", onPresence); if (active) send("POST"); const heartbeat = active ? window.setInterval(() => send("POST"), HEARTBEAT_MS) : undefined; return () => { cancelled = true; if (heartbeat) window.clearInterval(heartbeat); source.removeEventListener("presence", onPresence); source.close(); if (active) send("DELETE"); }; }, [channel, active, observe]); return participants; }