128 lines
3.2 KiB
TypeScript
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
|
|
};
|
|
}
|