feat: add project management features including open/save functionality and recovery support
This commit is contained in:
86
operations/project/lifecycle.ts
Normal file
86
operations/project/lifecycle.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
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();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user