diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-02-05 12:53:56 +0100 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-02-05 12:53:56 +0100 |
| commit | 2257ca2e6f9f8834880ea9bab4355c168ee6c33b (patch) | |
| tree | f2ead774beda94d95fb771bc3eb16972fb08468a | |
| parent | 490fc170c0705703728a889cbc35a04ca41ae7eb (diff) | |
| download | opencut-2257ca2e6f9f8834880ea9bab4355c168ee6c33b.tar.gz | |
make storage versioning per-project + few other refactors
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"> & { |
