summaryrefslogtreecommitdiff
path: root/apps/web/src/services/storage/migrations/v1-to-v2.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-22 02:41:18 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-22 02:41:18 +0200
commitf5f31b43df9ef46f1afdd63ba759b2f9bd80e7a3 (patch)
tree779132620064669a21c5fd9a787ba28187c0384e /apps/web/src/services/storage/migrations/v1-to-v2.ts
parenta709af0059dae4702b52197f34af85a817c59a6a (diff)
downloadopencut-f5f31b43df9ef46f1afdd63ba759b2f9bd80e7a3.tar.gz
refactor: migration run() and pure v1-to-v2 transform
Diffstat (limited to 'apps/web/src/services/storage/migrations/v1-to-v2.ts')
-rw-r--r--apps/web/src/services/storage/migrations/v1-to-v2.ts269
1 files changed, 216 insertions, 53 deletions
diff --git a/apps/web/src/services/storage/migrations/v1-to-v2.ts b/apps/web/src/services/storage/migrations/v1-to-v2.ts
index ef381541..a87b4a86 100644
--- a/apps/web/src/services/storage/migrations/v1-to-v2.ts
+++ b/apps/web/src/services/storage/migrations/v1-to-v2.ts
@@ -3,97 +3,260 @@ import {
deleteDatabase,
} from "@/services/storage/indexeddb-adapter";
import type { MediaAssetData } from "@/services/storage/types";
-import { StorageMigration } from "./base";
-import type { ProjectRecord } from "./transformers/types";
+import { StorageMigration, type StorageMigrationRunArgs } from "./base";
+import type { MigrationResult, ProjectRecord } from "./transformers/types";
import {
- getProjectId,
transformProjectV1ToV2,
- type TransformV1ToV2Options,
+ type V1ToV2Context,
} from "./transformers/v1-to-v2";
+import { isRecord } from "./transformers/utils";
+
+interface LegacyTimelineData {
+ tracks: unknown[];
+ lastModified: string;
+}
export class V1toV2Migration extends StorageMigration {
from = 1;
to = 2;
- async transform(project: ProjectRecord): Promise<{
- project: ProjectRecord;
- skipped: boolean;
- reason?: string;
- }> {
- const projectId = getProjectId({ project });
- if (!projectId) {
- return { project, skipped: true, reason: "no project id" };
- }
-
- const loadMediaAsset = createMediaAssetLoader({ projectId });
-
- const result = await transformProjectV1ToV2({
- project,
- options: { loadMediaAsset },
- });
+ async run({
+ projectId,
+ project,
+ }: StorageMigrationRunArgs): Promise<MigrationResult<ProjectRecord>> {
+ const context = await loadV1ToV2Context({ projectId, project });
+ const result = transformProjectV1ToV2({ project, context });
if (!result.skipped) {
- void cleanupLegacyTimelineDBs({
- projectId,
- project: result.project,
- });
+ try {
+ await deleteLegacyTimelineDbs({ projectId, project: result.project });
+ } catch (error) {
+ console.error(
+ `V1→V2 migration cleanup failed for project ${projectId}:`,
+ error,
+ );
+ }
}
return result;
}
}
-function createMediaAssetLoader({
+async function loadV1ToV2Context({
projectId,
+ project,
}: {
projectId: string;
-}): TransformV1ToV2Options["loadMediaAsset"] {
- return async ({ mediaId }: { mediaId: string }) => {
- const mediaMetadataAdapter = new IndexedDBAdapter<MediaAssetData>(
- `video-editor-media-${projectId}`,
- "media-metadata",
- 1,
- );
+ project: ProjectRecord;
+}): Promise<V1ToV2Context> {
+ const legacyTracksBySceneId = await loadLegacyTracksBySceneId({
+ projectId,
+ project,
+ });
+ const mediaTypesById = await loadMediaTypesById({
+ projectId,
+ legacyTracksBySceneId,
+ });
- return mediaMetadataAdapter.get(mediaId);
+ return {
+ legacyTracksBySceneId,
+ mediaTypesById,
};
}
-function cleanupLegacyTimelineDBs({
+async function loadLegacyTracksBySceneId({
projectId,
project,
}: {
projectId: string;
project: ProjectRecord;
-}): void {
+}): Promise<V1ToV2Context["legacyTracksBySceneId"]> {
const scenes = project.scenes;
if (!Array.isArray(scenes)) {
- return;
+ return {};
+ }
+
+ const sceneEntries = await Promise.all(
+ scenes.map(async (scene) => {
+ if (!isRecord(scene)) {
+ return null;
+ }
+
+ const sceneId = scene.id;
+ if (typeof sceneId !== "string") {
+ return null;
+ }
+
+ const tracks = await loadLegacyTracksForScene({
+ projectId,
+ sceneId,
+ isMain: scene.isMain === true,
+ });
+
+ return [sceneId, tracks] as const;
+ }),
+ );
+
+ return Object.fromEntries(
+ sceneEntries.filter(
+ (
+ sceneEntry,
+ ): sceneEntry is readonly [
+ string,
+ V1ToV2Context["legacyTracksBySceneId"][string],
+ ] => sceneEntry !== null,
+ ),
+ );
+}
+
+async function loadLegacyTracksForScene({
+ projectId,
+ sceneId,
+ isMain,
+}: {
+ projectId: string;
+ sceneId: string;
+ isMain: boolean;
+}): Promise<unknown[]> {
+ const sceneDbName = `video-editor-timelines-${projectId}-${sceneId}`;
+ const projectDbName = `video-editor-timelines-${projectId}`;
+
+ const adapter = new IndexedDBAdapter<LegacyTimelineData>(
+ sceneDbName,
+ "timeline",
+ 1,
+ );
+
+ let data = await adapter.get("timeline");
+
+ if (!data && isMain) {
+ const projectAdapter = new IndexedDBAdapter<LegacyTimelineData>(
+ projectDbName,
+ "timeline",
+ 1,
+ );
+ data = await projectAdapter.get("timeline");
+ }
+
+ if (!data || !Array.isArray(data.tracks)) {
+ return [];
+ }
+
+ return data.tracks;
+}
+
+async function loadMediaTypesById({
+ projectId,
+ legacyTracksBySceneId,
+}: {
+ projectId: string;
+ legacyTracksBySceneId: V1ToV2Context["legacyTracksBySceneId"];
+}): Promise<V1ToV2Context["mediaTypesById"]> {
+ const mediaIds = collectLegacyMediaIds({ legacyTracksBySceneId });
+ if (mediaIds.length === 0) {
+ return {};
}
- const dbNamesToDelete: string[] = [];
+ const mediaMetadataAdapter = new IndexedDBAdapter<MediaAssetData>(
+ `video-editor-media-${projectId}`,
+ "media-metadata",
+ 1,
+ );
+
+ const mediaEntries = await Promise.all(
+ mediaIds.map(async (mediaId) => {
+ const mediaMetadata = await mediaMetadataAdapter.get(mediaId);
+ if (!mediaMetadata) {
+ return null;
+ }
+
+ return [mediaId, mediaMetadata.type] as const;
+ }),
+ );
+
+ return Object.fromEntries(
+ mediaEntries.filter(
+ (
+ mediaEntry,
+ ): mediaEntry is readonly [
+ string,
+ V1ToV2Context["mediaTypesById"][string],
+ ] => mediaEntry !== null,
+ ),
+ );
+}
+
+function collectLegacyMediaIds({
+ legacyTracksBySceneId,
+}: {
+ legacyTracksBySceneId: V1ToV2Context["legacyTracksBySceneId"];
+}): string[] {
+ const mediaIds = new Set<string>();
- for (const scene of scenes) {
- if (typeof scene !== "object" || scene === null) {
+ for (const tracks of Object.values(legacyTracksBySceneId)) {
+ if (!Array.isArray(tracks)) {
continue;
}
- const sceneId = scene.id;
- if (typeof sceneId === "string") {
- const sceneDbName = `video-editor-timelines-${projectId}-${sceneId}`;
- dbNamesToDelete.push(sceneDbName);
+ for (const track of tracks) {
+ if (!isRecord(track) || track.type !== "media") {
+ continue;
+ }
+
+ const elements = track.elements;
+ if (!Array.isArray(elements)) {
+ continue;
+ }
+
+ for (const element of elements) {
+ if (!isRecord(element) || element.type !== "media") {
+ continue;
+ }
+
+ if (typeof element.mediaId !== "string") {
+ continue;
+ }
+
+ mediaIds.add(element.mediaId);
+ }
}
}
- const projectDbName = `video-editor-timelines-${projectId}`;
- dbNamesToDelete.push(projectDbName);
+ return Array.from(mediaIds);
+}
- // Fire-and-forget: delete in parallel, don't block migration
- void Promise.all(
- dbNamesToDelete.map((dbName) =>
- deleteDatabase({ dbName }).catch(() => {
- // ignore errors, DB might not exist or already deleted
- }),
- ),
- );
+async function deleteLegacyTimelineDbs({
+ projectId,
+ project,
+}: {
+ projectId: string;
+ project: ProjectRecord;
+}): Promise<void> {
+ const dbNames = getLegacyTimelineDbNames({ projectId, project });
+ await Promise.all(dbNames.map((dbName) => deleteDatabase({ dbName })));
+}
+
+function getLegacyTimelineDbNames({
+ projectId,
+ project,
+}: {
+ projectId: string;
+ project: ProjectRecord;
+}): string[] {
+ const scenes = project.scenes;
+ if (!Array.isArray(scenes)) {
+ return [`video-editor-timelines-${projectId}`];
+ }
+
+ const sceneDbNames = scenes.flatMap((scene) => {
+ if (!isRecord(scene)) {
+ return [];
+ }
+
+ return typeof scene.id === "string"
+ ? [`video-editor-timelines-${projectId}-${scene.id}`]
+ : [];
+ });
+
+ return [...sceneDbNames, `video-editor-timelines-${projectId}`];
}