1
0
Fork 0
polaris-task-force/src/hooks/useRealtimePresence.ts

70 lines
2.2 KiB
TypeScript

"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<PresenceParticipant[]>([]);
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<string>) => {
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;
}