Files
feuerwerk/src/editor/autosave.ts
T

89 lines
2.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };
}