import { SvelteMap } from 'svelte/reactivity'; export type SaveState = 'idle' | 'saving' | 'saved' | 'fading' | 'error'; type Options = { saveFn: (blockId: string, text: string) => Promise; documentId: string; }; export function createBlockAutoSave({ saveFn, documentId }: Options) { const saveStates = new SvelteMap(); const debounceTimers = new SvelteMap>(); const pendingTexts = new SvelteMap(); const fadeTimers: ReturnType[] = []; function getSaveState(blockId: string): SaveState { return saveStates.get(blockId) ?? 'idle'; } function setSaveState(blockId: string, state: SaveState) { saveStates.set(blockId, state); } async function executeSave(blockId: string): Promise { const text = pendingTexts.get(blockId); if (text === undefined) return; pendingTexts.delete(blockId); setSaveState(blockId, 'saving'); try { await saveFn(blockId, text); setSaveState(blockId, 'saved'); scheduleSavedFade(blockId); } catch { setSaveState(blockId, 'error'); } } function scheduleSavedFade(blockId: string): void { const t1 = setTimeout(() => { if (getSaveState(blockId) === 'saved') { setSaveState(blockId, 'fading'); const t2 = setTimeout(() => { if (getSaveState(blockId) === 'fading') { setSaveState(blockId, 'idle'); } }, 300); fadeTimers.push(t2); } }, 2000); fadeTimers.push(t1); } function scheduleDebounce(blockId: string): void { clearDebounce(blockId); const timer = setTimeout(() => { debounceTimers.delete(blockId); executeSave(blockId); }, 1500); debounceTimers.set(blockId, timer); } function clearDebounce(blockId: string): void { const existing = debounceTimers.get(blockId); if (existing !== undefined) { clearTimeout(existing); debounceTimers.delete(blockId); } } function handleTextChange(blockId: string, text: string): void { pendingTexts.set(blockId, text); scheduleDebounce(blockId); } function handleBlur(): void { for (const [blockId] of [...debounceTimers]) { clearDebounce(blockId); executeSave(blockId); } } async function handleRetry(blockId: string, currentText: string): Promise { const pending = pendingTexts.get(blockId); const text = pending ?? currentText; pendingTexts.set(blockId, text); await executeSave(blockId); } function clearBlock(blockId: string): void { clearDebounce(blockId); pendingTexts.delete(blockId); saveStates.delete(blockId); } function flushViaBeacon(): void { for (const [blockId, text] of pendingTexts) { clearDebounce(blockId); const url = `/api/documents/${documentId}/transcription-blocks/${blockId}`; const body = JSON.stringify({ text }); navigator.sendBeacon(url, new Blob([body], { type: 'application/json' })); pendingTexts.delete(blockId); } } function destroy(): void { for (const timer of debounceTimers.values()) { clearTimeout(timer); } debounceTimers.clear(); for (const timer of fadeTimers) { clearTimeout(timer); } fadeTimers.length = 0; } return { getSaveState, handleTextChange, handleBlur, handleRetry, clearBlock, flushViaBeacon, destroy }; }