2026-10-16 07:40:29 -06:00
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
|
import type { PresenceUser } from "./types";
|
|
|
|
|
|
|
|
|
|
interface EventHandlers {
|
|
|
|
|
/** Called when the server revokes this browser's own session. */
|
|
|
|
|
onRevoked: () => void;
|
|
|
|
|
/** Called for every `changed` event. Later phases refresh their data from it. */
|
|
|
|
|
onChanged?: (payload: unknown) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parse(data: unknown): unknown {
|
|
|
|
|
if (typeof data !== "string") return null;
|
|
|
|
|
try {
|
|
|
|
|
return JSON.parse(data) as unknown;
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Opens /api/events with EventSource while `sessionId` is set.
|
|
|
|
|
* A `session` event with this session id and action "revoked" calls onRevoked.
|
|
|
|
|
* `presence` is kept in state for later phases.
|
|
|
|
|
*/
|
|
|
|
|
export function useEvents(sessionId: string | null, handlers: EventHandlers): PresenceUser[] {
|
|
|
|
|
const [presence, setPresence] = useState<PresenceUser[]>([]);
|
|
|
|
|
const ref = useRef(handlers);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
ref.current = handlers;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!sessionId || typeof EventSource === "undefined") return;
|
|
|
|
|
const es = new EventSource("/api/events", { withCredentials: true });
|
|
|
|
|
|
|
|
|
|
es.addEventListener("session", (ev) => {
|
|
|
|
|
const p = parse((ev as MessageEvent).data);
|
|
|
|
|
if (p && typeof p === "object") {
|
|
|
|
|
const o = p as { session_id?: unknown; action?: unknown };
|
|
|
|
|
if (o.session_id === sessionId && o.action === "revoked") ref.current.onRevoked();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
es.addEventListener("presence", (ev) => {
|
|
|
|
|
const p = parse((ev as MessageEvent).data);
|
|
|
|
|
if (p && typeof p === "object" && Array.isArray((p as { users?: unknown }).users)) {
|
|
|
|
|
setPresence((p as { users: PresenceUser[] }).users);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
es.addEventListener("changed", (ev) => {
|
|
|
|
|
ref.current.onChanged?.(parse((ev as MessageEvent).data));
|
|
|
|
|
});
|
2026-10-16 14:45:38 -06:00
|
|
|
// Phase 3: the worker finished a feed run. Fanned out with the changed events, marked event "feed".
|
|
|
|
|
es.addEventListener("feed", (ev) => {
|
|
|
|
|
const p = parse((ev as MessageEvent).data);
|
|
|
|
|
ref.current.onChanged?.({ ...(p && typeof p === "object" ? p : {}), event: "feed" });
|
|
|
|
|
});
|
2026-10-16 07:40:29 -06:00
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
es.close();
|
|
|
|
|
setPresence([]);
|
|
|
|
|
};
|
|
|
|
|
}, [sessionId]);
|
|
|
|
|
|
|
|
|
|
return presence;
|
|
|
|
|
}
|