import { useEffect, useState } from "react"; import { getAuthToken, refreshAuthToken } from "../lib/auth-client"; import type { RelayEvent } from "./useSessionRelay"; interface UsePersistedSessionEventsOptions { taskId: string; sessionId: string; enabled?: boolean; } type Phase = "loading" | "ready" | "error" | "empty"; /** * A malformed row should cost one event, not the whole replay — a throw here * would reject the map() and drop the user to the error state with nothing * rendered. */ function parseEventData(raw: unknown): RelayEvent["event"] { if (typeof raw !== "string") return raw as RelayEvent["event"]; try { return JSON.parse(raw) as RelayEvent["event"]; } catch { return {} as RelayEvent["event"]; } } /** * Load persisted session events from the database. * Used when the relay WebSocket is unavailable (task done/in_review). */ export function usePersistedSessionEvents({ taskId, sessionId, enabled = true }: UsePersistedSessionEventsOptions) { const [events, setEvents] = useState([]); const [phase, setPhase] = useState("loading"); useEffect(() => { if (!enabled || !taskId || !sessionId) { setPhase("empty"); return; } let cancelled = false; async function load() { try { setPhase("loading"); const token = (await refreshAuthToken()) ?? getAuthToken(); if (!token) { setPhase("error"); return; } const res = await fetch(`/api/tasks/${taskId}/session-events?session_id=${encodeURIComponent(sessionId)}`, { headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) { setPhase("error"); return; } const data = (await res.json()) as Array<{ id: string; session_id: string; task_id: string; event_type: string; // event_data is a JSONB column, so the API returns it already // decoded. The string branch below only covers rows written before // the column was migrated from TEXT. event_data: unknown; created_at: string; }>; if (cancelled) return; if (data.length === 0) { setPhase("empty"); return; } // Convert DB rows to RelayEvent format const relayEvents: RelayEvent[] = data.map((row) => ({ id: row.id, event: parseEventData(row.event_data), timestamp: row.created_at, })); setEvents(relayEvents); setPhase("ready"); } catch { if (!cancelled) setPhase("error"); } } load(); return () => { cancelled = true; }; }, [taskId, sessionId, enabled]); return { events, phase }; }