feat: add project management features including open/save functionality and recovery support

This commit is contained in:
syntaxbullet
2026-07-10 23:46:01 +02:00
parent 6d652c3264
commit 9697075d29
15 changed files with 613 additions and 2 deletions

View 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();
},
};
}