import { type ProjectStore, StaleRevisionError } from '../core/store/ProjectStore'; import type { EditorState } from './state'; /** * Debounced autosave: every model change schedules a save ~1 s later. Saves never run * during Play (`state.project` is the runtime copy then; a pending save resumes on Stop) * and never overlap; a change that arrives while a save is in flight triggers another save * afterwards. */ export function attachAutosave( state: EditorState, store: ProjectStore, delayMs = 1000, ): { flush: () => Promise } { let timer = 0; let saving: Promise | null = null; let pending = false; const save = async (): Promise => { if (saving) { pending = true; return saving; } if (state.saveStatus !== 'dirty' || !state.project.id || state.mode === 'play') return; state.setSaveStatus('saving'); saving = (async () => { try { state.revision = await store.save(state.project, state.revision); state.setSaveStatus(pending ? 'dirty' : 'saved'); } catch (error) { if (error instanceof StaleRevisionError) { state.setSaveStatus('conflict'); state.setNotice('This project was changed elsewhere. Reload to continue.', 'error'); } else { state.setSaveStatus('error'); state.setNotice( `Saving failed: ${error instanceof Error ? error.message : String(error)}`, 'error', ); } } finally { saving = null; if (pending) { pending = false; if (state.saveStatus === 'dirty') schedule(); } } })(); return saving; }; const schedule = (): void => { window.clearTimeout(timer); timer = window.setTimeout(() => void save(), delayMs); }; state.on('save', () => { if (state.saveStatus === 'dirty') schedule(); }); const flush = async (): Promise => { window.clearTimeout(timer); await save(); }; window.addEventListener('beforeunload', (event) => { if (state.saveStatus === 'dirty' || state.saveStatus === 'saving') { void flush(); event.preventDefault(); } }); return { flush }; }