70 lines
2.2 KiB
TypeScript
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;
|
|
}
|