summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-02-05 12:53:56 +0100
committerMaze Winther <mazewinther@gmail.com>2026-02-05 12:53:56 +0100
commit2257ca2e6f9f8834880ea9bab4355c168ee6c33b (patch)
treef2ead774beda94d95fb771bc3eb16972fb08468a
parent490fc170c0705703728a889cbc35a04ca41ae7eb (diff)
downloadopencut-2257ca2e6f9f8834880ea9bab4355c168ee6c33b.tar.gz
make storage versioning per-project + few other refactors
-rw-r--r--.cursor/rules/codebase-index.mdc279
-rw-r--r--apps/web/src/core/managers/project-manager.ts35
-rw-r--r--apps/web/src/services/renderer/nodes/image-node.ts19
-rw-r--r--apps/web/src/services/renderer/nodes/video-node.ts8
-rw-r--r--apps/web/src/services/renderer/scene-builder.ts5
-rw-r--r--apps/web/src/services/storage/migrations/base.ts6
-rw-r--r--apps/web/src/services/storage/migrations/index.ts3
-rw-r--r--apps/web/src/services/storage/migrations/runner.ts150
-rw-r--r--apps/web/src/services/storage/migrations/transformers/utils.ts28
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v0-to-v1.ts28
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts38
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v2-to-v3.ts28
-rw-r--r--apps/web/src/services/storage/migrations/v0-to-v1.ts38
-rw-r--r--apps/web/src/services/storage/migrations/v1-to-v2.ts54
-rw-r--r--apps/web/src/services/storage/migrations/v2-to-v3.ts38
-rw-r--r--apps/web/src/services/storage/migrations/version-manager.ts73
-rw-r--r--apps/web/src/services/storage/types.ts1
17 files changed, 296 insertions, 535 deletions
diff --git a/.cursor/rules/codebase-index.mdc b/.cursor/rules/codebase-index.mdc
index c4b8de48..c63ad1e7 100644
--- a/.cursor/rules/codebase-index.mdc
+++ b/.cursor/rules/codebase-index.mdc
@@ -454,115 +454,115 @@ rate-limit.ts
scenes.ts
export function getMainScene({ scenes }: { scenes: TScene[] }): TScene | null
export function ensureMainScene({ scenes }: { scenes: TScene[] }): TScene[]
- export function buildDefaultScene({
- name,
- isMain,
- }: {
- name: string;
- isMain: boolean;
+ export function buildDefaultScene({
+ name,
+ isMain,
+ }: {
+ name: string;
+ isMain: boolean;
}): TScene
- export function canDeleteScene({ scene }: { scene: TScene }): {
- canDelete: boolean;
- reason?: string;
+ export function canDeleteScene({ scene }: { scene: TScene }): {
+ canDelete: boolean;
+ reason?: string;
}
- export function getFallbackSceneAfterDelete({
- scenes,
- deletedSceneId,
- currentSceneId,
- }: {
- scenes: TScene[];
- deletedSceneId: string;
- currentSceneId: string | null;
+ export function getFallbackSceneAfterDelete({
+ scenes,
+ deletedSceneId,
+ currentSceneId,
+ }: {
+ scenes: TScene[];
+ deletedSceneId: string;
+ currentSceneId: string | null;
}): TScene | null
- export function findCurrentScene({
- scenes,
- currentSceneId,
- }: {
- scenes: TScene[];
- currentSceneId: string;
+ export function findCurrentScene({
+ scenes,
+ currentSceneId,
+ }: {
+ scenes: TScene[];
+ currentSceneId: string;
}): TScene | null
- export function getProjectDurationFromScenes({
- scenes,
- }: {
- scenes: TScene[];
+ export function getProjectDurationFromScenes({
+ scenes,
+ }: {
+ scenes: TScene[];
}): number
- export function updateSceneInArray({
- scenes,
- sceneId,
- updates,
- }: {
- scenes: TScene[];
- sceneId: string;
- updates: Partial<TScene>;
+ export function updateSceneInArray({
+ scenes,
+ sceneId,
+ updates,
+ }: {
+ scenes: TScene[];
+ sceneId: string;
+ updates: Partial<TScene>;
}): TScene[]
time.ts
- export function roundToFrame({
- time,
- fps,
- }: {
- time: number;
- fps: number;
+ export function roundToFrame({
+ time,
+ fps,
+ }: {
+ time: number;
+ fps: number;
}): number
- export function formatTimeCode({
- timeInSeconds,
- format = "HH:MM:SS:CS",
- fps,
- }: {
- timeInSeconds: number;
- format?: TTimeCode;
- fps?: number;
+ export function formatTimeCode({
+ timeInSeconds,
+ format = "HH:MM:SS:CS",
+ fps,
+ }: {
+ timeInSeconds: number;
+ format?: TTimeCode;
+ fps?: number;
}): string
- export function parseTimeCode({
- timeCode,
- format = "HH:MM:SS:CS",
- fps,
- }: {
- timeCode: string;
- format?: TTimeCode;
- fps: number;
+ export function parseTimeCode({
+ timeCode,
+ format = "HH:MM:SS:CS",
+ fps,
+ }: {
+ timeCode: string;
+ format?: TTimeCode;
+ fps: number;
}): number | null
- export function guessTimeCodeFormat({
- timeCode,
- }: {
- timeCode: string;
+ export function guessTimeCodeFormat({
+ timeCode,
+ }: {
+ timeCode: string;
}): TTimeCode | null
- export function timeToFrame({
- time,
- fps,
- }: {
- time: number;
- fps: number;
+ export function timeToFrame({
+ time,
+ fps,
+ }: {
+ time: number;
+ fps: number;
}): number
- export function frameToTime({
- frame,
- fps,
- }: {
- frame: number;
- fps: number;
+ export function frameToTime({
+ frame,
+ fps,
+ }: {
+ frame: number;
+ fps: number;
}): number
- export function snapTimeToFrame({
- time,
- fps,
- }: {
- time: number;
- fps: number;
+ export function snapTimeToFrame({
+ time,
+ fps,
+ }: {
+ time: number;
+ fps: number;
}): number
- export function getSnappedSeekTime({
- rawTime,
- duration,
- fps,
- }: {
- rawTime: number;
- duration: number;
- fps: number;
+ export function getSnappedSeekTime({
+ rawTime,
+ duration,
+ fps,
+ }: {
+ rawTime: number;
+ duration: number;
+ fps: number;
}): number
- export function getLastFrameTime({
- duration,
- fps,
- }: {
- duration: number;
- fps: number;
+ export function getLastFrameTime({
+ duration,
+ fps,
+ }: {
+ duration: number;
+ fps: number;
}): number
## apps/web/src/lib/actions
@@ -689,28 +689,27 @@ parser.ts
## apps/web/src/lib/media
audio.ts
- export type CollectedAudioElement = Omit<
- AudioElement,
- "type" | "mediaId" | "volume" | "id" | "name" | "sourceType" | "sourceUrl"
- ...
+ export type CollectedAudioElement = Omit<
+ AudioElement,
+ "type" | "mediaId" | "volume" | "id" | "name" | "sourceType" | "sourceUrl...
export function createAudioContext(): AudioContext
export interface DecodedAudio {
samples: Float32Array
sampleRate: number
}
- export function decodeAudioToFloat32({
- audioBlob,
- }: {
- audioBlob: Blob;
+ export function decodeAudioToFloat32({
+ audioBlob,
+ }: {
+ audioBlob: Blob;
}): Promise<DecodedAudio>
- export function collectAudioElements({
- tracks,
- mediaAssets,
- audioContext,
- }: {
- tracks: TimelineTrack[];
- mediaAssets: MediaAsset[];
- audioContext: AudioContext;
+ export function collectAudioElements({
+ tracks,
+ mediaAssets,
+ audioContext,
+ }: {
+ tracks: TimelineTrack[];
+ mediaAssets: MediaAsset[];
+ audioContext: AudioContext;
}): Promise<CollectedAudioElement[]>
export interface AudioClipSource {
id: string
@@ -722,32 +721,32 @@ audio.ts
trimEnd: number
muted: boolean
}
- export function collectAudioMixSources({
- tracks,
- mediaAssets,
- }: {
- tracks: TimelineTrack[];
- mediaAssets: MediaAsset[];
+ export function collectAudioMixSources({
+ tracks,
+ mediaAssets,
+ }: {
+ tracks: TimelineTrack[];
+ mediaAssets: MediaAsset[];
}): Promise<AudioMixSource[]>
- export function collectAudioClips({
- tracks,
- mediaAssets,
- }: {
- tracks: TimelineTrack[];
- mediaAssets: MediaAsset[];
+ export function collectAudioClips({
+ tracks,
+ mediaAssets,
+ }: {
+ tracks: TimelineTrack[];
+ mediaAssets: MediaAsset[];
}): Promise<AudioClipSource[]>
- export function createTimelineAudioBuffer({
- tracks,
- mediaAssets,
- duration,
- sampleRate = 44100,
- audioContext,
- }: {
- tracks: TimelineTrack[];
- mediaAssets: MediaAsset[];
- duration: number;
- sampleRate?: number;
- audioContext?: AudioContext;
+ export function createTimelineAudioBuffer({
+ tracks,
+ mediaAssets,
+ duration,
+ sampleRate = 44100,
+ audioContext,
+ }: {
+ tracks: TimelineTrack[];
+ mediaAssets: MediaAsset[];
+ duration: number;
+ sampleRate?: number;
+ audioContext?: AudioContext;
}): Promise<AudioBuffer | null>
media-utils.ts
@@ -977,10 +976,10 @@ element-utils.ts
}): { trackId: string; elementId: string }[]
index.ts
- export function calculateTotalDuration({
- tracks,
- }: {
- tracks: TimelineTrack[];
+ export function calculateTotalDuration({
+ tracks,
+ }: {
+ tracks: TimelineTrack[];
}): number
ruler-utils.ts
@@ -1220,6 +1219,11 @@ indexeddb-adapter.ts
async getAll(): Promise<T[]>
async clear(): Promise<void>
}
+ export function deleteDatabase({
+ dbName,
+ }: {
+ dbName: string;
+ }): Promise<void>
opfs-adapter.ts
export class OPFSAdapter implements StorageAdapter<File> {
@@ -1256,7 +1260,6 @@ types.ts
fps?: number
ephemeral?: boolean
thumbnailUrl?: string
- sourceStickerIconName?: string
}
export type SerializedScene = Omit<TScene, "createdAt" | "updatedAt"> & {
createdAt: string;
diff --git a/apps/web/src/core/managers/project-manager.ts b/apps/web/src/core/managers/project-manager.ts
index fc0b2a36..94576879 100644
--- a/apps/web/src/core/managers/project-manager.ts
+++ b/apps/web/src/core/managers/project-manager.ts
@@ -21,7 +21,7 @@ import { buildDefaultScene, getProjectDurationFromScenes } from "@/lib/scenes";
import { buildScene } from "@/services/renderer/scene-builder";
import { CanvasRenderer } from "@/services/renderer/canvas-renderer";
import {
- CURRENT_STORAGE_VERSION,
+ CURRENT_PROJECT_VERSION,
migrations,
runStorageMigrations,
} from "@/services/storage/migrations";
@@ -58,31 +58,7 @@ export class ProjectManager {
}
this.storageMigrationPromise = (async () => {
- let hasShownState = false;
-
- await runStorageMigrations({
- migrations,
- callbacks: {
- onMigrationStart: ({ fromVersion, toVersion }) => {
- hasShownState = true;
- this.setMigrationState({
- isMigrating: true,
- fromVersion,
- toVersion,
- projectName: null,
- });
- },
- },
- });
-
- if (hasShownState) {
- this.setMigrationState({
- isMigrating: false,
- fromVersion: null,
- toVersion: null,
- projectName: null,
- });
- }
+ await runStorageMigrations({ migrations });
})();
await this.storageMigrationPromise;
@@ -109,7 +85,7 @@ export class ProjectManager {
color: DEFAULT_COLOR,
},
},
- version: CURRENT_STORAGE_VERSION,
+ version: CURRENT_PROJECT_VERSION,
};
this.active = newProject;
@@ -572,11 +548,6 @@ export class ProjectManager {
return this.migrationState;
}
- private setMigrationState(state: Partial<MigrationState>): void {
- this.migrationState = { ...this.migrationState, ...state };
- this.notify();
- }
-
setActiveProject({ project }: { project: TProject }): void {
this.active = project;
this.notify();
diff --git a/apps/web/src/services/renderer/nodes/image-node.ts b/apps/web/src/services/renderer/nodes/image-node.ts
index 2a684d5f..5405b60b 100644
--- a/apps/web/src/services/renderer/nodes/image-node.ts
+++ b/apps/web/src/services/renderer/nodes/image-node.ts
@@ -1,10 +1,20 @@
import type { CanvasRenderer } from "../canvas-renderer";
import { BaseNode } from "./base-node";
-import type { BaseMediaNodeParams } from "./video-node";
const IMAGE_EPSILON = 1 / 1000;
-export type ImageNodeParams = BaseMediaNodeParams;
+export interface ImageNodeParams {
+ url: string;
+ duration: number;
+ timeOffset: number;
+ trimStart: number;
+ trimEnd: number;
+ x?: number;
+ y?: number;
+ width?: number;
+ height?: number;
+ opacity?: number;
+}
export class ImageNode extends BaseNode<ImageNodeParams> {
private image?: HTMLImageElement;
@@ -18,15 +28,12 @@ export class ImageNode extends BaseNode<ImageNodeParams> {
private async load() {
const image = new Image();
this.image = image;
- const url = URL.createObjectURL(this.params.file);
await new Promise<void>((resolve, reject) => {
image.onload = () => resolve();
image.onerror = () => reject(new Error("Image load failed"));
- image.src = url;
+ image.src = this.params.url;
});
-
- URL.revokeObjectURL(url);
}
private getImageTime(time: number) {
diff --git a/apps/web/src/services/renderer/nodes/video-node.ts b/apps/web/src/services/renderer/nodes/video-node.ts
index b6911df7..841eb459 100644
--- a/apps/web/src/services/renderer/nodes/video-node.ts
+++ b/apps/web/src/services/renderer/nodes/video-node.ts
@@ -4,8 +4,10 @@ import { videoCache } from "@/services/video-cache/service";
const VIDEO_EPSILON = 1 / 1000;
-export interface BaseMediaNodeParams {
+export interface VideoNodeParams {
+ url: string;
file: File;
+ mediaId: string;
duration: number;
timeOffset: number;
trimStart: number;
@@ -17,10 +19,6 @@ export interface BaseMediaNodeParams {
opacity?: number;
}
-export interface VideoNodeParams extends BaseMediaNodeParams {
- mediaId: string;
-}
-
export class VideoNode extends BaseNode<VideoNodeParams> {
private getVideoTime(time: number) {
return time - this.params.timeOffset + this.params.trimStart;
diff --git a/apps/web/src/services/renderer/scene-builder.ts b/apps/web/src/services/renderer/scene-builder.ts
index b36edda1..598854ea 100644
--- a/apps/web/src/services/renderer/scene-builder.ts
+++ b/apps/web/src/services/renderer/scene-builder.ts
@@ -51,7 +51,7 @@ export function buildScene(params: BuildSceneParams) {
for (const element of elements) {
if (element.type === "video" || element.type === "image") {
const mediaAsset = mediaMap.get(element.mediaId);
- if (!mediaAsset?.file) {
+ if (!mediaAsset?.file || !mediaAsset?.url) {
continue;
}
@@ -59,6 +59,7 @@ export function buildScene(params: BuildSceneParams) {
contentNodes.push(
new VideoNode({
mediaId: mediaAsset.id,
+ url: mediaAsset.url,
file: mediaAsset.file,
duration: element.duration,
timeOffset: element.startTime,
@@ -70,7 +71,7 @@ export function buildScene(params: BuildSceneParams) {
if (mediaAsset.type === "image") {
contentNodes.push(
new ImageNode({
- file: mediaAsset.file,
+ url: mediaAsset.url,
duration: element.duration,
timeOffset: element.startTime,
trimStart: element.trimStart,
diff --git a/apps/web/src/services/storage/migrations/base.ts b/apps/web/src/services/storage/migrations/base.ts
index c7558264..5b7e9db0 100644
--- a/apps/web/src/services/storage/migrations/base.ts
+++ b/apps/web/src/services/storage/migrations/base.ts
@@ -1,5 +1,9 @@
+import type { MigrationResult, ProjectRecord } from "./transformers/types";
+
export abstract class StorageMigration {
abstract from: number;
abstract to: number;
- abstract run(): Promise<void>;
+ abstract transform(
+ project: ProjectRecord,
+ ): Promise<MigrationResult<ProjectRecord>>;
}
diff --git a/apps/web/src/services/storage/migrations/index.ts b/apps/web/src/services/storage/migrations/index.ts
index 158db957..285dda0c 100644
--- a/apps/web/src/services/storage/migrations/index.ts
+++ b/apps/web/src/services/storage/migrations/index.ts
@@ -1,11 +1,10 @@
export { StorageMigration } from "./base";
-export { StorageVersionManager } from "./version-manager";
import { V0toV1Migration } from "./v0-to-v1";
import { V1toV2Migration } from "./v1-to-v2";
import { V2toV3Migration } from "./v2-to-v3";
export { runStorageMigrations } from "./runner";
-export const CURRENT_STORAGE_VERSION = 3;
+export const CURRENT_PROJECT_VERSION = 3;
export const migrations = [
new V0toV1Migration(),
diff --git a/apps/web/src/services/storage/migrations/runner.ts b/apps/web/src/services/storage/migrations/runner.ts
index cbc3739f..83e7edae 100644
--- a/apps/web/src/services/storage/migrations/runner.ts
+++ b/apps/web/src/services/storage/migrations/runner.ts
@@ -1,138 +1,94 @@
-import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
+import {
+ IndexedDBAdapter,
+ deleteDatabase,
+} from "@/services/storage/indexeddb-adapter";
import type { StorageMigration } from "./base";
-import { StorageVersionManager } from "./version-manager";
+import type { ProjectRecord } from "./transformers/types";
+import { getProjectId } from "./transformers/utils";
export interface StorageMigrationResult {
- fromVersion: number;
- toVersion: number;
- migrated: boolean;
+ migratedCount: number;
}
-export type StorageMigrationCallbacks = {
- onMigrationStart?: ({
- fromVersion,
- toVersion,
- }: {
- fromVersion: number;
- toVersion: number;
- }) => void;
- onMigrationComplete?: ({
- fromVersion,
- toVersion,
- }: {
- fromVersion: number;
- toVersion: number;
- }) => void;
-};
-
-type ProjectRecord = Record<string, unknown>;
+let hasCleanedUpMetaDb = false;
export async function runStorageMigrations({
migrations,
- versionManager = new StorageVersionManager(),
- callbacks,
}: {
migrations: StorageMigration[];
- versionManager?: StorageVersionManager;
- callbacks?: StorageMigrationCallbacks;
}): Promise<StorageMigrationResult> {
- const versionRecord = await versionManager.getVersionRecord();
- const inferredVersion = versionRecord
- ? null
- : await inferStorageVersionFromProjects();
- const fromVersion =
- versionRecord?.inProgress?.from ??
- versionRecord?.version ??
- inferredVersion ??
- 0;
-
- if (!versionRecord) {
- await versionManager.setVersion({ version: fromVersion });
- }
-
- const orderedMigrations = [...migrations].sort((a, b) => a.from - b.from);
- let currentVersion = fromVersion;
-
- for (const migration of orderedMigrations) {
- if (migration.from !== currentVersion) {
- continue;
+ // One-time cleanup: delete the old global version database
+ if (!hasCleanedUpMetaDb) {
+ try {
+ await deleteDatabase({ dbName: "video-editor-meta" });
+ } catch {
+ // Ignore errors - DB might not exist
}
-
- await versionManager.setInProgress({
- from: migration.from,
- to: migration.to,
- });
- callbacks?.onMigrationStart?.({
- fromVersion: migration.from,
- toVersion: migration.to,
- });
- await migration.run();
- currentVersion = migration.to;
- await versionManager.setVersion({ version: currentVersion });
- await versionManager.clearInProgress();
- callbacks?.onMigrationComplete?.({
- fromVersion: migration.from,
- toVersion: migration.to,
- });
+ hasCleanedUpMetaDb = true;
}
- return {
- fromVersion,
- toVersion: currentVersion,
- migrated: currentVersion !== fromVersion,
- };
-}
-
-async function inferStorageVersionFromProjects(): Promise<number> {
- const projectsAdapter = new IndexedDBAdapter<unknown>(
+ const projectsAdapter = new IndexedDBAdapter<ProjectRecord>(
"video-editor-projects",
"projects",
1,
);
const projects = await projectsAdapter.getAll();
- if (projects.length === 0) {
- return 0;
- }
-
- let lowestVersion = Number.POSITIVE_INFINITY;
+ const orderedMigrations = [...migrations].sort((a, b) => a.from - b.from);
+ let migratedCount = 0;
for (const project of projects) {
- const projectVersion = checkProjectVersion({ project });
- if (projectVersion < lowestVersion) {
- lowestVersion = projectVersion;
+ if (typeof project !== "object" || project === null) {
+ continue;
}
- }
- if (lowestVersion === Number.POSITIVE_INFINITY) {
- return 0;
- }
+ let projectRecord = project as ProjectRecord;
+ let currentVersion = getProjectVersion({ project: projectRecord });
- return lowestVersion;
-}
+ // Apply migrations sequentially until project is up to date
+ for (const migration of orderedMigrations) {
+ if (migration.from !== currentVersion) {
+ continue;
+ }
+
+ const result = await migration.transform(projectRecord);
+
+ if (result.skipped) {
+ break; // Project is already at this version or higher
+ }
+
+ // Update project with migrated version
+ const projectId = getProjectId({ project: result.project });
+ if (!projectId) {
+ break; // Can't save without ID
+ }
-function checkProjectVersion({ project }: { project: unknown }): number {
- if (!isRecord(project)) {
- return 0;
+ await projectsAdapter.set(projectId, result.project);
+ migratedCount++;
+ currentVersion = migration.to;
+
+ // Use migrated project for next iteration
+ projectRecord = result.project;
+ }
}
+ return { migratedCount };
+}
+
+function getProjectVersion({ project }: { project: ProjectRecord }): number {
const versionValue = project.version;
- // v2 and up
+ // v2 and up - has explicit version field
if (typeof versionValue === "number") {
return versionValue;
}
- // v1 (got scenes)
+ // v1 - has scenes array
const scenesValue = project.scenes;
if (Array.isArray(scenesValue) && scenesValue.length > 0) {
return 1;
}
- // v0 (didn't have scenes)
+ // v0 - no scenes
return 0;
}
-
-function isRecord(value: unknown): value is ProjectRecord {
- return typeof value === "object" && value !== null;
-}
diff --git a/apps/web/src/services/storage/migrations/transformers/utils.ts b/apps/web/src/services/storage/migrations/transformers/utils.ts
new file mode 100644
index 00000000..108489bf
--- /dev/null
+++ b/apps/web/src/services/storage/migrations/transformers/utils.ts
@@ -0,0 +1,28 @@
+import type { ProjectRecord } from "./types";
+
+export function isRecord(value: unknown): value is ProjectRecord {
+ return typeof value === "object" && value !== null;
+}
+
+export function getProjectId({
+ project,
+}: {
+ project: ProjectRecord;
+}): string | null {
+ const idValue = project.id;
+ if (typeof idValue === "string" && idValue.length > 0) {
+ return idValue;
+ }
+
+ const metadataValue = project.metadata;
+ if (!isRecord(metadataValue)) {
+ return null;
+ }
+
+ const metadataId = metadataValue.id;
+ if (typeof metadataId === "string" && metadataId.length > 0) {
+ return metadataId;
+ }
+
+ return null;
+}
diff --git a/apps/web/src/services/storage/migrations/transformers/v0-to-v1.ts b/apps/web/src/services/storage/migrations/transformers/v0-to-v1.ts
index a2350acb..e8183297 100644
--- a/apps/web/src/services/storage/migrations/transformers/v0-to-v1.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v0-to-v1.ts
@@ -1,6 +1,7 @@
import { generateUUID } from "@/utils/id";
import type { SerializedScene } from "@/services/storage/types";
import type { MigrationResult, ProjectRecord } from "./types";
+import { getProjectId, isRecord } from "./utils";
export interface TransformV0ToV1Options {
now?: Date;
@@ -54,29 +55,4 @@ export function transformProjectV0ToV1({
return { project: updatedProject, skipped: false };
}
-export function getProjectId({
- project,
-}: {
- project: ProjectRecord;
-}): string | null {
- const idValue = project.id;
- if (typeof idValue === "string" && idValue.length > 0) {
- return idValue;
- }
-
- const metadataValue = project.metadata;
- if (!isRecord(metadataValue)) {
- return null;
- }
-
- const metadataId = metadataValue.id;
- if (typeof metadataId === "string" && metadataId.length > 0) {
- return metadataId;
- }
-
- return null;
-}
-
-function isRecord(value: unknown): value is ProjectRecord {
- return typeof value === "object" && value !== null;
-}
+export { getProjectId } from "./utils";
diff --git a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts
index b4ed5a9f..0e5ffbf7 100644
--- a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts
@@ -15,6 +15,7 @@ import type {
VideoElement,
} from "@/types/timeline";
import type { MigrationResult, ProjectRecord } from "./types";
+import { getProjectId, isRecord } from "./utils";
interface LegacyTimelineData {
tracks: unknown[];
@@ -51,10 +52,8 @@ interface LegacyMediaTrack {
export interface TransformV1ToV2Options {
loadMediaAsset?: ({
- projectId,
mediaId,
}: {
- projectId: string;
mediaId: string;
}) => Promise<MediaAssetData | null>;
}
@@ -91,10 +90,8 @@ async function migrateProject({
project: ProjectRecord;
projectId: string;
loadMediaAsset?: ({
- projectId,
mediaId,
}: {
- projectId: string;
mediaId: string;
}) => Promise<MediaAssetData | null>;
}): Promise<ProjectRecord> {
@@ -261,10 +258,8 @@ async function transformTracks({
tracks: unknown[];
projectId: string;
loadMediaAsset?: ({
- projectId,
mediaId,
}: {
- projectId: string;
mediaId: string;
}) => Promise<MediaAssetData | null>;
}): Promise<TimelineTrack[]> {
@@ -318,10 +313,8 @@ async function transformMediaTrack({
track: LegacyMediaTrack;
projectId: string;
loadMediaAsset?: ({
- projectId,
mediaId,
}: {
- projectId: string;
mediaId: string;
}) => Promise<MediaAssetData | null>;
isMain: boolean;
@@ -342,7 +335,7 @@ async function transformMediaTrack({
let mediaType: "video" | "image" = "video";
if (loadMediaAsset) {
- const mediaAsset = await loadMediaAsset({ projectId, mediaId });
+ const mediaAsset = await loadMediaAsset({ mediaId });
if (mediaAsset) {
mediaType = mediaAsset.type === "image" ? "image" : "video";
}
@@ -543,28 +536,7 @@ function transformAudioTrack({
};
}
-export function getProjectId({
- project,
-}: {
- project: ProjectRecord;
-}): string | null {
- const idValue = project.id;
- if (typeof idValue === "string" && idValue.length > 0) {
- return idValue;
- }
-
- const metadataValue = project.metadata;
- if (!isRecord(metadataValue)) {
- return null;
- }
-
- const metadataId = metadataValue.id;
- if (typeof metadataId === "string" && metadataId.length > 0) {
- return metadataId;
- }
-
- return null;
-}
+export { getProjectId } from "./utils";
function getCurrentSceneId({
value,
@@ -772,7 +744,3 @@ function isV2Project({ project }: { project: ProjectRecord }): boolean {
return isRecord(project.metadata) && isRecord(project.settings);
}
-
-function isRecord(value: unknown): value is ProjectRecord {
- return typeof value === "object" && value !== null;
-}
diff --git a/apps/web/src/services/storage/migrations/transformers/v2-to-v3.ts b/apps/web/src/services/storage/migrations/transformers/v2-to-v3.ts
index 33656109..094b8bd8 100644
--- a/apps/web/src/services/storage/migrations/transformers/v2-to-v3.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v2-to-v3.ts
@@ -1,6 +1,7 @@
import { getProjectDurationFromScenes } from "@/lib/scenes";
import type { TScene } from "@/types/timeline";
import type { MigrationResult, ProjectRecord } from "./types";
+import { getProjectId, isRecord } from "./utils";
export function transformProjectV2ToV3({
project,
@@ -33,28 +34,7 @@ export function transformProjectV2ToV3({
return { project: migratedProject, skipped: false };
}
-export function getProjectId({
- project,
-}: {
- project: ProjectRecord;
-}): string | null {
- const idValue = project.id;
- if (typeof idValue === "string" && idValue.length > 0) {
- return idValue;
- }
-
- const metadataValue = project.metadata;
- if (!isRecord(metadataValue)) {
- return null;
- }
-
- const metadataId = metadataValue.id;
- if (typeof metadataId === "string" && metadataId.length > 0) {
- return metadataId;
- }
-
- return null;
-}
+export { getProjectId } from "./utils";
function getScenes({ project }: { project: ProjectRecord }): TScene[] {
const scenesValue = project.scenes;
@@ -75,7 +55,3 @@ function isV3Project({ project }: { project: ProjectRecord }): boolean {
isRecord(project.metadata) && typeof project.metadata.duration === "number"
);
}
-
-function isRecord(value: unknown): value is ProjectRecord {
- return typeof value === "object" && value !== null;
-}
diff --git a/apps/web/src/services/storage/migrations/v0-to-v1.ts b/apps/web/src/services/storage/migrations/v0-to-v1.ts
index e183ddf2..a2225c76 100644
--- a/apps/web/src/services/storage/migrations/v0-to-v1.ts
+++ b/apps/web/src/services/storage/migrations/v0-to-v1.ts
@@ -4,41 +4,19 @@
* Sets currentSceneId to the new scene's id
*/
-import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
import { StorageMigration } from "./base";
-import { getProjectId, transformProjectV0ToV1 } from "./transformers/v0-to-v1";
+import type { ProjectRecord } from "./transformers/types";
+import { transformProjectV0ToV1 } from "./transformers/v0-to-v1";
export class V0toV1Migration extends StorageMigration {
from = 0;
to = 1;
- async run(): Promise<void> {
- const projectsAdapter = new IndexedDBAdapter<unknown>(
- "video-editor-projects",
- "projects",
- 1,
- );
- const projects = await projectsAdapter.getAll();
-
- for (const project of projects) {
- if (typeof project !== "object" || project === null) {
- continue;
- }
-
- const result = transformProjectV0ToV1({
- project: project as Record<string, unknown>,
- });
-
- if (result.skipped) {
- continue;
- }
-
- const projectId = getProjectId({ project: result.project });
- if (!projectId) {
- continue;
- }
-
- await projectsAdapter.set(projectId, result.project);
- }
+ async transform(project: ProjectRecord): Promise<{
+ project: ProjectRecord;
+ skipped: boolean;
+ reason?: string;
+ }> {
+ return transformProjectV0ToV1({ project });
}
}
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 d593d8a1..eff4397e 100644
--- a/apps/web/src/services/storage/migrations/v1-to-v2.ts
+++ b/apps/web/src/services/storage/migrations/v1-to-v2.ts
@@ -4,6 +4,7 @@ import {
} from "@/services/storage/indexeddb-adapter";
import type { MediaAssetData } from "@/services/storage/types";
import { StorageMigration } from "./base";
+import type { ProjectRecord } from "./transformers/types";
import {
getProjectId,
transformProjectV1ToV2,
@@ -14,41 +15,32 @@ export class V1toV2Migration extends StorageMigration {
from = 1;
to = 2;
- async run(): Promise<void> {
- const projectsAdapter = new IndexedDBAdapter<unknown>(
- "video-editor-projects",
- "projects",
- 1,
- );
- const projects = await projectsAdapter.getAll();
-
- for (const project of projects) {
- if (typeof project !== "object" || project === null) {
- continue;
- }
+ 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 projectId = getProjectId({
- project: project as Record<string, unknown>,
- });
- if (!projectId) {
- continue;
- }
+ const loadMediaAsset = createMediaAssetLoader({ projectId });
- const loadMediaAsset = createMediaAssetLoader({ projectId });
+ const result = await transformProjectV1ToV2({
+ project,
+ options: { loadMediaAsset },
+ });
- const result = await transformProjectV1ToV2({
- project: project as Record<string, unknown>,
- options: { loadMediaAsset },
+ if (!result.skipped) {
+ // cleanup legacy timeline DBs after successful transformation
+ await cleanupLegacyTimelineDBs({
+ projectId,
+ project: result.project,
});
-
- if (result.skipped) {
- continue;
- }
-
- await projectsAdapter.set(projectId, result.project);
-
- await cleanupLegacyTimelineDBs({ projectId, project: result.project });
}
+
+ return result;
}
}
@@ -73,7 +65,7 @@ async function cleanupLegacyTimelineDBs({
project,
}: {
projectId: string;
- project: Record<string, unknown>;
+ project: ProjectRecord;
}): Promise<void> {
const scenes = project.scenes;
if (!Array.isArray(scenes)) {
diff --git a/apps/web/src/services/storage/migrations/v2-to-v3.ts b/apps/web/src/services/storage/migrations/v2-to-v3.ts
index 7c52f79d..7b9c7fda 100644
--- a/apps/web/src/services/storage/migrations/v2-to-v3.ts
+++ b/apps/web/src/services/storage/migrations/v2-to-v3.ts
@@ -2,41 +2,19 @@
* Adds a "duration" field to each project's metadata
*/
-import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
import { StorageMigration } from "./base";
-import { getProjectId, transformProjectV2ToV3 } from "./transformers/v2-to-v3";
+import type { ProjectRecord } from "./transformers/types";
+import { transformProjectV2ToV3 } from "./transformers/v2-to-v3";
export class V2toV3Migration extends StorageMigration {
from = 2;
to = 3;
- async run(): Promise<void> {
- const projectsAdapter = new IndexedDBAdapter<unknown>(
- "video-editor-projects",
- "projects",
- 1,
- );
- const projects = await projectsAdapter.getAll();
-
- for (const project of projects) {
- if (typeof project !== "object" || project === null) {
- continue;
- }
-
- const result = transformProjectV2ToV3({
- project: project as Record<string, unknown>,
- });
-
- if (result.skipped) {
- continue;
- }
-
- const projectId = getProjectId({ project: result.project });
- if (!projectId) {
- continue;
- }
-
- await projectsAdapter.set(projectId, result.project);
- }
+ async transform(project: ProjectRecord): Promise<{
+ project: ProjectRecord;
+ skipped: boolean;
+ reason?: string;
+ }> {
+ return transformProjectV2ToV3({ project });
}
}
diff --git a/apps/web/src/services/storage/migrations/version-manager.ts b/apps/web/src/services/storage/migrations/version-manager.ts
deleted file mode 100644
index 879498ce..00000000
--- a/apps/web/src/services/storage/migrations/version-manager.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
-
-type StorageVersionRecord = {
- version: number;
- inProgress?: {
- from: number;
- to: number;
- };
-};
-
-const DEFAULT_DB_NAME = "video-editor-meta";
-const DEFAULT_STORE_NAME = "storage-version";
-const DEFAULT_DB_VERSION = 1;
-const STORAGE_VERSION_KEY = "storage-version";
-
-export class StorageVersionManager {
- private adapter: IndexedDBAdapter<StorageVersionRecord>;
-
- constructor({
- dbName = DEFAULT_DB_NAME,
- storeName = DEFAULT_STORE_NAME,
- version = DEFAULT_DB_VERSION,
- }: {
- dbName?: string;
- storeName?: string;
- version?: number;
- } = {}) {
- this.adapter = new IndexedDBAdapter<StorageVersionRecord>(
- dbName,
- storeName,
- version,
- );
- }
-
- async getVersion(): Promise<number> {
- const record = await this.adapter.get(STORAGE_VERSION_KEY);
- return record?.version ?? 0;
- }
-
- async getVersionRecord(): Promise<StorageVersionRecord | null> {
- return this.adapter.get(STORAGE_VERSION_KEY);
- }
-
- async setVersion({ version }: { version: number }): Promise<void> {
- const record = await this.getVersionRecord();
- const inProgress = record?.inProgress;
- await this.adapter.set(STORAGE_VERSION_KEY, {
- version,
- ...(inProgress ? { inProgress } : {}),
- });
- }
-
- async setInProgress({
- from,
- to,
- }: {
- from: number;
- to: number;
- }): Promise<void> {
- const record = await this.getVersionRecord();
- const version = record?.version ?? 0;
- await this.adapter.set(STORAGE_VERSION_KEY, {
- version,
- inProgress: { from, to },
- });
- }
-
- async clearInProgress(): Promise<void> {
- const record = await this.getVersionRecord();
- const version = record?.version ?? 0;
- await this.adapter.set(STORAGE_VERSION_KEY, { version });
- }
-}
diff --git a/apps/web/src/services/storage/types.ts b/apps/web/src/services/storage/types.ts
index e7e045f5..7481c73c 100644
--- a/apps/web/src/services/storage/types.ts
+++ b/apps/web/src/services/storage/types.ts
@@ -26,7 +26,6 @@ export interface MediaAssetData {
fps?: number;
ephemeral?: boolean;
thumbnailUrl?: string;
- sourceStickerIconName?: string;
}
export type SerializedScene = Omit<TScene, "createdAt" | "updatedAt"> & {