import { useCallback, useEffect, useRef, useState } from 'react' import { api, UnauthorizedError, type DocUpdate } from '../api/client' import { clearDraft, stashDraft } from '../lib/drafts' export type SaveStatus = 'idle' | 'pending' | 'saving' | 'saved' | 'error' | 'signed-out' const DEBOUNCE_MS = 1500 const SAVED_FADE_MS = 3000 // useAutoSave debounces document saves. Call schedule() on every edit; it fires // PUT /api/docs/:id 1.5s after the last change. status drives the StatusBar: // pending → saving → saved (fades to idle after 3s). export function useAutoSave(docId: string | null) { const [status, setStatus] = useState('idle') const debounceRef = useRef>(undefined) const fadeRef = useRef>(undefined) const pendingRef = useRef(null) // Latest doc id, read inside the timer so a doc switch doesn't save to the old one. const docIdRef = useRef(docId) docIdRef.current = docId // Set once the server stops recognising the session. Further saves are // pointless (every one would 401) and would keep overwriting the stash, so // the loop stops here and the writing waits in localStorage instead. const signedOutRef = useRef(false) const flush = useCallback(async () => { const id = docIdRef.current const body = pendingRef.current pendingRef.current = null if (!id || !body) return if (signedOutRef.current) { stashDraft(id, body) setStatus('signed-out') return } setStatus('saving') try { await api.updateDoc(id, body) clearDraft(id) // it's on the server now; the rescue copy is redundant setStatus('saved') clearTimeout(fadeRef.current) fadeRef.current = setTimeout(() => setStatus('idle'), SAVED_FADE_MS) } catch (err) { if (err instanceof UnauthorizedError) { // The session went away mid-draft. Keep the body — on disk, where a // full-page trip through the identity provider can't take it with it — // and stop trying until there's a session again. signedOutRef.current = true stashDraft(id, body) setStatus('signed-out') return } console.error('auto-save failed', err) // Put the body back so the next edit retries it rather than dropping it. pendingRef.current = { ...body, ...(pendingRef.current ?? {}) } setStatus('error') } }, []) const schedule = useCallback( (update: DocUpdate) => { pendingRef.current = { ...pendingRef.current, ...update } setStatus('pending') clearTimeout(fadeRef.current) clearTimeout(debounceRef.current) debounceRef.current = setTimeout(flush, DEBOUNCE_MS) }, [flush], ) // Save any pending edits immediately (e.g. before switching documents). const saveNow = useCallback(() => { clearTimeout(debounceRef.current) return flush() }, [flush]) useEffect( () => () => { clearTimeout(debounceRef.current) clearTimeout(fadeRef.current) }, [], ) return { status, schedule, saveNow } }