"use client"; /** * Drives hydration of the chat store from the backend on mount and * pushes changes back up via debounced PUT. * * Behavior: * - On first mount: GET /conversations → merge into store → mark hydrated. * - On any change after hydration: schedule a PUT (debounced 1500ms). * - In-flight saves are aborted by the next save to avoid stale writes. * - Pending turns are excluded from the saved snapshot — the assistant * response lands as a normal change after pending=false flips. */ import { useEffect, useRef } from "react"; import { useChatStore } from "./chatStore"; import { loadConversations, saveConversations } from "./sync"; const SAVE_DEBOUNCE_MS = 1500; export function useBackendSync() { const hydrated = useChatStore((s) => s.hydrated); const conversations = useChatStore((s) => s.conversations); const activeId = useChatStore((s) => s.activeId); const initialFetchStarted = useRef(false); const saveTimer = useRef | null>(null); const inFlight = useRef(null); // ── 1) Hydrate on mount ─────────────────────────────────────────── useEffect(() => { if (initialFetchStarted.current) return; initialFetchStarted.current = true; let cancelled = false; (async () => { const remote = await loadConversations(); if (cancelled) return; const store = useChatStore.getState(); if (remote && remote.conversations) { store._hydrateFromRemote({ conversations: remote.conversations, activeId: remote.activeId ?? null, }); } else { // No remote / fetch failed — proceed with whatever localStorage had. store._setHydrated(true); } })(); return () => { cancelled = true; }; }, []); // ── 2) Debounced save on change (only after hydrated) ───────────── useEffect(() => { if (!hydrated) return; if (saveTimer.current) clearTimeout(saveTimer.current); saveTimer.current = setTimeout(() => { // Abort any in-flight save before starting a new one if (inFlight.current) inFlight.current.abort(); const ctrl = new AbortController(); inFlight.current = ctrl; // Strip in-flight pending turns from the saved snapshot — those // resolve when the request returns and trigger their own save. const sanitized: Record = {}; for (const [id, conv] of Object.entries(conversations)) { sanitized[id] = { ...conv, turns: conv.turns.filter((t) => !t.pending), }; } const setStatus = useChatStore.getState()._setSyncStatus; setStatus("saving"); saveConversations( { conversations: sanitized, activeId, }, ctrl.signal ).then((ok) => { if (ctrl.signal.aborted) return; setStatus(ok ? "idle" : "error"); if (ok) { useChatStore.setState({ lastSavedAt: Date.now() }); } }); }, SAVE_DEBOUNCE_MS); return () => { if (saveTimer.current) clearTimeout(saveTimer.current); }; }, [hydrated, conversations, activeId]); }