diff options
| author | cashmere <cashmere@autistici.org> | 2026-07-27 01:21:20 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-07-27 01:21:20 +0200 |
| commit | d55d2583a71ae35fa8d0d1bfbec975e67e66aafa (patch) | |
| tree | c7a612f24b12c8602d7fbf139ac2709e4f2f4d9c /apps/web/src/versions/store.ts | |
| parent | 2a372fdec256b8be890e181c221c2a012ead9efa (diff) | |
| download | opencut-d55d2583a71ae35fa8d0d1bfbec975e67e66aafa.tar.gz | |
feat: git-like project versioning (v1)
Linear per-project history per notes/project-versioning.md: named
checkpoints (thumbnail via renderer.captureFrame) plus auto-checkpoints
on save (60s throttle, ring of 20, named never rotate). Restore writes a
safety checkpoint first, then swaps the document outside undo/redo.
Media assets referenced by any version are pinned against deletion.
Migration v31-v32 stamps the schema; the version store lives in its own
IDB database with an in-memory fallback for tests. Versions popover in
the editor header for jump/rename/delete. 12 new tests.
Diffstat (limited to 'apps/web/src/versions/store.ts')
| -rw-r--r-- | apps/web/src/versions/store.ts | 86 |
1 files changed, 86 insertions, 0 deletions
diff --git a/apps/web/src/versions/store.ts b/apps/web/src/versions/store.ts new file mode 100644 index 00000000..ae3f5d11 --- /dev/null +++ b/apps/web/src/versions/store.ts @@ -0,0 +1,86 @@ +import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter"; +import type { ProjectVersion } from "./types"; + +/** + * Persistence backend for project versions. The IndexedDB implementation is + * the production store (its own database, created lazily — the v31→v32 + * migration stamps project records); the in-memory implementation backs + * unit tests. + */ +export interface VersionStore { + put({ version }: { version: ProjectVersion }): Promise<void>; + getAllForProject({ + projectId, + }: { + projectId: string; + }): Promise<ProjectVersion[]>; + delete({ id }: { id: string }): Promise<void>; + deleteForProject({ projectId }: { projectId: string }): Promise<void>; +} + +export class IndexedDBVersionStore implements VersionStore { + private adapter = new IndexedDBAdapter<ProjectVersion>({ + dbName: "video-editor-project-versions", + storeName: "project-versions", + version: 1, + }); + + async put({ version }: { version: ProjectVersion }): Promise<void> { + await this.adapter.set({ key: version.id, value: version }); + } + + async getAllForProject({ + projectId, + }: { + projectId: string; + }): Promise<ProjectVersion[]> { + const all = await this.adapter.getAll(); + return all.filter( + (version) => + typeof version === "object" && + version !== null && + version.projectId === projectId, + ); + } + + async delete({ id }: { id: string }): Promise<void> { + await this.adapter.remove(id); + } + + async deleteForProject({ projectId }: { projectId: string }): Promise<void> { + const versions = await this.getAllForProject({ projectId }); + await Promise.all( + versions.map((version) => this.adapter.remove(version.id)), + ); + } +} + +export class InMemoryVersionStore implements VersionStore { + private versions = new Map<string, ProjectVersion>(); + + async put({ version }: { version: ProjectVersion }): Promise<void> { + this.versions.set(version.id, version); + } + + async getAllForProject({ + projectId, + }: { + projectId: string; + }): Promise<ProjectVersion[]> { + return [...this.versions.values()].filter( + (version) => version.projectId === projectId, + ); + } + + async delete({ id }: { id: string }): Promise<void> { + this.versions.delete(id); + } + + async deleteForProject({ projectId }: { projectId: string }): Promise<void> { + for (const version of this.versions.values()) { + if (version.projectId === projectId) { + this.versions.delete(version.id); + } + } + } +} |
