summaryrefslogtreecommitdiff
path: root/apps/web/src/versions/store.ts
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-07-27 01:21:20 +0200
committercashmere <cashmere@autistici.org>2026-07-27 01:21:20 +0200
commitd55d2583a71ae35fa8d0d1bfbec975e67e66aafa (patch)
treec7a612f24b12c8602d7fbf139ac2709e4f2f4d9c /apps/web/src/versions/store.ts
parent2a372fdec256b8be890e181c221c2a012ead9efa (diff)
downloadopencut-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.ts86
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);
+ }
+ }
+ }
+}