Download lib/useBackendSync.ts from Etiya/d2l-ui: direct link, hf CLI and curl.
- Browser
- Download file 3.34 kB
-
https://huggingface.co/spaces/Etiya/d2l-ui/resolve/main/lib/useBackendSync.ts
- Command line
-
hf download hf://spaces/Etiya/d2l-ui/lib/useBackendSync.ts
-
curl -L -o useBackendSync.ts https://huggingface.co/spaces/Etiya/d2l-ui/resolve/main/lib/useBackendSync.ts
3.34 kB
| "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<ReturnType<typeof setTimeout> | null>(null); | |
| const inFlight = useRef<AbortController | null>(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<string, typeof conversations[string]> = {}; | |
| 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]); | |
| } | |