89 lines
2.6 KiB
TypeScript
89 lines
2.6 KiB
TypeScript
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<void>; stop: () => void } {
|
||
let timer = 0;
|
||
let saving: Promise<void> | null = null;
|
||
let pending = false;
|
||
let stopped = false;
|
||
|
||
const save = async (): Promise<void> => {
|
||
if (stopped) return;
|
||
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 => {
|
||
if (stopped) return;
|
||
window.clearTimeout(timer);
|
||
timer = window.setTimeout(() => void save(), delayMs);
|
||
};
|
||
|
||
state.on('save', () => {
|
||
if (state.saveStatus === 'dirty') schedule();
|
||
});
|
||
|
||
const flush = async (): Promise<void> => {
|
||
window.clearTimeout(timer);
|
||
await save();
|
||
};
|
||
|
||
const onBeforeUnload = (event: BeforeUnloadEvent) => {
|
||
if (state.saveStatus === 'dirty' || state.saveStatus === 'saving') {
|
||
void flush();
|
||
event.preventDefault();
|
||
}
|
||
};
|
||
window.addEventListener('beforeunload', onBeforeUnload);
|
||
|
||
/**
|
||
* Ends the autosave: nothing pending fires afterwards, and the closed-over state is let go of.
|
||
* Signing out does this while the page stays up (#177) – it flushes first, then stops.
|
||
*/
|
||
const stop = (): void => {
|
||
stopped = true;
|
||
window.clearTimeout(timer);
|
||
window.removeEventListener('beforeunload', onBeforeUnload);
|
||
};
|
||
|
||
return { flush, stop };
|
||
}
|