Files
familienarchiv/frontend/src/lib/hooks/useBlockAutoSave.svelte.ts

128 lines
3.2 KiB
TypeScript

import { SvelteMap } from 'svelte/reactivity';
export type SaveState = 'idle' | 'saving' | 'saved' | 'fading' | 'error';
type Options = {
saveFn: (blockId: string, text: string) => Promise<void>;
documentId: string;
};
export function createBlockAutoSave({ saveFn, documentId }: Options) {
const saveStates = new SvelteMap<string, SaveState>();
const debounceTimers = new SvelteMap<string, ReturnType<typeof setTimeout>>();
const pendingTexts = new SvelteMap<string, string>();
const fadeTimers: ReturnType<typeof setTimeout>[] = [];
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<void> {
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<void> {
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
};
}