87 lines
2.7 KiB
TypeScript
87 lines
2.7 KiB
TypeScript
import { commandIds } from "@commands/ids";
|
|
import type { AppStore } from "@editor/store";
|
|
import type { ProjectFile } from "./format";
|
|
import { parseProject, serializeProject } from "./format";
|
|
import type { RecoveryStorage } from "@platform/projectStorage";
|
|
|
|
export const RECOVERY_STORAGE_KEY = "image-studio.recovery.v1";
|
|
|
|
type TimerHandle = number | ReturnType<typeof setTimeout>;
|
|
|
|
export type ProjectLifecycle = {
|
|
recover(): Promise<ProjectFile | undefined>;
|
|
open(source: string): Promise<ProjectFile>;
|
|
snapshot(): string;
|
|
markSaved(): Promise<void>;
|
|
dispose(): void;
|
|
};
|
|
|
|
export function createProjectLifecycle(options: {
|
|
store: AppStore;
|
|
storage: RecoveryStorage;
|
|
debounceMs?: number;
|
|
schedule?: (callback: () => void, delay: number) => TimerHandle;
|
|
cancel?: (handle: TimerHandle) => void;
|
|
}): ProjectLifecycle {
|
|
const debounceMs = options.debounceMs ?? 750;
|
|
const schedule = options.schedule ?? setTimeout;
|
|
const cancel = options.cancel ?? ((handle: TimerHandle) => clearTimeout(handle));
|
|
let observedDocument = options.store.getState().document;
|
|
let pending: TimerHandle | undefined;
|
|
let disposed = false;
|
|
|
|
const cancelPending = () => {
|
|
if (pending === undefined) return;
|
|
cancel(pending);
|
|
pending = undefined;
|
|
};
|
|
|
|
const unsubscribe = options.store.subscribe((state) => {
|
|
if (state.document === observedDocument) return;
|
|
observedDocument = state.document;
|
|
cancelPending();
|
|
pending = schedule(() => {
|
|
pending = undefined;
|
|
void options.storage.write(RECOVERY_STORAGE_KEY, serializeProject(options.store.getState().document)).catch(() => undefined);
|
|
}, debounceMs);
|
|
});
|
|
|
|
const replace = (project: ProjectFile) => {
|
|
options.store.dispatch(commandIds.projectOpen, { document: project.document });
|
|
cancelPending();
|
|
observedDocument = options.store.getState().document;
|
|
return project;
|
|
};
|
|
|
|
return {
|
|
async recover() {
|
|
const source = await options.storage.read(RECOVERY_STORAGE_KEY);
|
|
if (!source || disposed) return undefined;
|
|
try {
|
|
return replace(parseProject(source));
|
|
} catch {
|
|
await options.storage.remove(RECOVERY_STORAGE_KEY);
|
|
return undefined;
|
|
}
|
|
},
|
|
async open(source) {
|
|
const project = replace(parseProject(source));
|
|
await options.storage.remove(RECOVERY_STORAGE_KEY);
|
|
return project;
|
|
},
|
|
snapshot() {
|
|
return serializeProject(options.store.getState().document);
|
|
},
|
|
async markSaved() {
|
|
cancelPending();
|
|
observedDocument = options.store.getState().document;
|
|
await options.storage.remove(RECOVERY_STORAGE_KEY);
|
|
},
|
|
dispose() {
|
|
disposed = true;
|
|
unsubscribe();
|
|
cancelPending();
|
|
},
|
|
};
|
|
}
|