From d55d2583a71ae35fa8d0d1bfbec975e67e66aafa Mon Sep 17 00:00:00 2001 From: cashmere Date: Mon, 27 Jul 2026 01:21:20 +0200 Subject: 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. --- apps/web/src/versions/media-references.ts | 52 +++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 apps/web/src/versions/media-references.ts (limited to 'apps/web/src/versions/media-references.ts') diff --git a/apps/web/src/versions/media-references.ts b/apps/web/src/versions/media-references.ts new file mode 100644 index 00000000..86264320 --- /dev/null +++ b/apps/web/src/versions/media-references.ts @@ -0,0 +1,52 @@ +import type { SerializedProject } from "@/services/storage/types"; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function collectFromElements({ + elements, + mediaIds, +}: { + elements: unknown; + mediaIds: Set; +}): void { + if (!Array.isArray(elements)) return; + + for (const element of elements) { + if (!isRecord(element)) continue; + if (typeof element.mediaId === "string" && element.mediaId.length > 0) { + mediaIds.add(element.mediaId); + } + } +} + +/** + * Extracts every media asset id referenced by a serialized project's scenes. + * Duck-typed (like migrations) so snapshots of any project version scan safely. + */ +export function getReferencedMediaIds({ + project, +}: { + project: SerializedProject; +}): Set { + const mediaIds = new Set(); + + for (const scene of project.scenes ?? []) { + const tracks: unknown = scene.tracks; + if (!isRecord(tracks)) continue; + + if (isRecord(tracks.main)) { + collectFromElements({ elements: tracks.main.elements, mediaIds }); + } + for (const list of [tracks.overlay, tracks.audio]) { + if (!Array.isArray(list)) continue; + for (const track of list) { + if (!isRecord(track)) continue; + collectFromElements({ elements: track.elements, mediaIds }); + } + } + } + + return mediaIds; +} -- cgit v1.3.1