make storage versioning per-project + few other refactors

This commit is contained in:
Maze Winther
2026-02-05 12:53:56 +01:00
parent 490fc170c0
commit 2257ca2e6f
17 changed files with 301 additions and 540 deletions
+6 -3
View File
@@ -691,8 +691,7 @@ parser.ts
audio.ts audio.ts
export type CollectedAudioElement = Omit<␍ export type CollectedAudioElement = Omit<␍
AudioElement,␍ AudioElement,␍
"type" | "mediaId" | "volume" | "id" | "name" | "sourceType" | "sourceUrl" "type" | "mediaId" | "volume" | "id" | "name" | "sourceType" | "sourceUrl...
...
export function createAudioContext(): AudioContext export function createAudioContext(): AudioContext
export interface DecodedAudio { export interface DecodedAudio {
samples: Float32Array samples: Float32Array
@@ -1220,6 +1219,11 @@ indexeddb-adapter.ts
async getAll(): Promise<T[]> async getAll(): Promise<T[]>
async clear(): Promise<void> async clear(): Promise<void>
} }
export function deleteDatabase({␍
dbName,␍
}: {␍
dbName: string;␍
}): Promise<void>
opfs-adapter.ts opfs-adapter.ts
export class OPFSAdapter implements StorageAdapter<File> { export class OPFSAdapter implements StorageAdapter<File> {
@@ -1256,7 +1260,6 @@ types.ts
fps?: number fps?: number
ephemeral?: boolean ephemeral?: boolean
thumbnailUrl?: string thumbnailUrl?: string
sourceStickerIconName?: string
} }
export type SerializedScene = Omit<TScene, "createdAt" | "updatedAt"> & {␍ export type SerializedScene = Omit<TScene, "createdAt" | "updatedAt"> & {␍
createdAt: string;␍ createdAt: string;␍
+3 -32
View File
@@ -21,7 +21,7 @@ import { buildDefaultScene, getProjectDurationFromScenes } from "@/lib/scenes";
import { buildScene } from "@/services/renderer/scene-builder"; import { buildScene } from "@/services/renderer/scene-builder";
import { CanvasRenderer } from "@/services/renderer/canvas-renderer"; import { CanvasRenderer } from "@/services/renderer/canvas-renderer";
import { import {
CURRENT_STORAGE_VERSION, CURRENT_PROJECT_VERSION,
migrations, migrations,
runStorageMigrations, runStorageMigrations,
} from "@/services/storage/migrations"; } from "@/services/storage/migrations";
@@ -58,31 +58,7 @@ export class ProjectManager {
} }
this.storageMigrationPromise = (async () => { this.storageMigrationPromise = (async () => {
let hasShownState = false; await runStorageMigrations({ migrations });
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 this.storageMigrationPromise; await this.storageMigrationPromise;
@@ -109,7 +85,7 @@ export class ProjectManager {
color: DEFAULT_COLOR, color: DEFAULT_COLOR,
}, },
}, },
version: CURRENT_STORAGE_VERSION, version: CURRENT_PROJECT_VERSION,
}; };
this.active = newProject; this.active = newProject;
@@ -572,11 +548,6 @@ export class ProjectManager {
return this.migrationState; return this.migrationState;
} }
private setMigrationState(state: Partial<MigrationState>): void {
this.migrationState = { ...this.migrationState, ...state };
this.notify();
}
setActiveProject({ project }: { project: TProject }): void { setActiveProject({ project }: { project: TProject }): void {
this.active = project; this.active = project;
this.notify(); this.notify();
@@ -1,10 +1,20 @@
import type { CanvasRenderer } from "../canvas-renderer"; import type { CanvasRenderer } from "../canvas-renderer";
import { BaseNode } from "./base-node"; import { BaseNode } from "./base-node";
import type { BaseMediaNodeParams } from "./video-node";
const IMAGE_EPSILON = 1 / 1000; 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> { export class ImageNode extends BaseNode<ImageNodeParams> {
private image?: HTMLImageElement; private image?: HTMLImageElement;
@@ -18,15 +28,12 @@ export class ImageNode extends BaseNode<ImageNodeParams> {
private async load() { private async load() {
const image = new Image(); const image = new Image();
this.image = image; this.image = image;
const url = URL.createObjectURL(this.params.file);
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
image.onload = () => resolve(); image.onload = () => resolve();
image.onerror = () => reject(new Error("Image load failed")); image.onerror = () => reject(new Error("Image load failed"));
image.src = url; image.src = this.params.url;
}); });
URL.revokeObjectURL(url);
} }
private getImageTime(time: number) { private getImageTime(time: number) {
@@ -4,8 +4,10 @@ import { videoCache } from "@/services/video-cache/service";
const VIDEO_EPSILON = 1 / 1000; const VIDEO_EPSILON = 1 / 1000;
export interface BaseMediaNodeParams { export interface VideoNodeParams {
url: string;
file: File; file: File;
mediaId: string;
duration: number; duration: number;
timeOffset: number; timeOffset: number;
trimStart: number; trimStart: number;
@@ -17,10 +19,6 @@ export interface BaseMediaNodeParams {
opacity?: number; opacity?: number;
} }
export interface VideoNodeParams extends BaseMediaNodeParams {
mediaId: string;
}
export class VideoNode extends BaseNode<VideoNodeParams> { export class VideoNode extends BaseNode<VideoNodeParams> {
private getVideoTime(time: number) { private getVideoTime(time: number) {
return time - this.params.timeOffset + this.params.trimStart; return time - this.params.timeOffset + this.params.trimStart;
@@ -51,7 +51,7 @@ export function buildScene(params: BuildSceneParams) {
for (const element of elements) { for (const element of elements) {
if (element.type === "video" || element.type === "image") { if (element.type === "video" || element.type === "image") {
const mediaAsset = mediaMap.get(element.mediaId); const mediaAsset = mediaMap.get(element.mediaId);
if (!mediaAsset?.file) { if (!mediaAsset?.file || !mediaAsset?.url) {
continue; continue;
} }
@@ -59,6 +59,7 @@ export function buildScene(params: BuildSceneParams) {
contentNodes.push( contentNodes.push(
new VideoNode({ new VideoNode({
mediaId: mediaAsset.id, mediaId: mediaAsset.id,
url: mediaAsset.url,
file: mediaAsset.file, file: mediaAsset.file,
duration: element.duration, duration: element.duration,
timeOffset: element.startTime, timeOffset: element.startTime,
@@ -70,7 +71,7 @@ export function buildScene(params: BuildSceneParams) {
if (mediaAsset.type === "image") { if (mediaAsset.type === "image") {
contentNodes.push( contentNodes.push(
new ImageNode({ new ImageNode({
file: mediaAsset.file, url: mediaAsset.url,
duration: element.duration, duration: element.duration,
timeOffset: element.startTime, timeOffset: element.startTime,
trimStart: element.trimStart, trimStart: element.trimStart,
@@ -1,5 +1,9 @@
import type { MigrationResult, ProjectRecord } from "./transformers/types";
export abstract class StorageMigration { export abstract class StorageMigration {
abstract from: number; abstract from: number;
abstract to: number; abstract to: number;
abstract run(): Promise<void>; abstract transform(
project: ProjectRecord,
): Promise<MigrationResult<ProjectRecord>>;
} }
@@ -1,11 +1,10 @@
export { StorageMigration } from "./base"; export { StorageMigration } from "./base";
export { StorageVersionManager } from "./version-manager";
import { V0toV1Migration } from "./v0-to-v1"; import { V0toV1Migration } from "./v0-to-v1";
import { V1toV2Migration } from "./v1-to-v2"; import { V1toV2Migration } from "./v1-to-v2";
import { V2toV3Migration } from "./v2-to-v3"; import { V2toV3Migration } from "./v2-to-v3";
export { runStorageMigrations } from "./runner"; export { runStorageMigrations } from "./runner";
export const CURRENT_STORAGE_VERSION = 3; export const CURRENT_PROJECT_VERSION = 3;
export const migrations = [ export const migrations = [
new V0toV1Migration(), new V0toV1Migration(),
@@ -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 type { StorageMigration } from "./base";
import { StorageVersionManager } from "./version-manager"; import type { ProjectRecord } from "./transformers/types";
import { getProjectId } from "./transformers/utils";
export interface StorageMigrationResult { export interface StorageMigrationResult {
fromVersion: number; migratedCount: number;
toVersion: number;
migrated: boolean;
} }
export type StorageMigrationCallbacks = { let hasCleanedUpMetaDb = false;
onMigrationStart?: ({
fromVersion,
toVersion,
}: {
fromVersion: number;
toVersion: number;
}) => void;
onMigrationComplete?: ({
fromVersion,
toVersion,
}: {
fromVersion: number;
toVersion: number;
}) => void;
};
type ProjectRecord = Record<string, unknown>;
export async function runStorageMigrations({ export async function runStorageMigrations({
migrations, migrations,
versionManager = new StorageVersionManager(),
callbacks,
}: { }: {
migrations: StorageMigration[]; migrations: StorageMigration[];
versionManager?: StorageVersionManager;
callbacks?: StorageMigrationCallbacks;
}): Promise<StorageMigrationResult> { }): Promise<StorageMigrationResult> {
const versionRecord = await versionManager.getVersionRecord(); // One-time cleanup: delete the old global version database
const inferredVersion = versionRecord if (!hasCleanedUpMetaDb) {
? null try {
: await inferStorageVersionFromProjects(); await deleteDatabase({ dbName: "video-editor-meta" });
const fromVersion = } catch {
versionRecord?.inProgress?.from ?? // Ignore errors - DB might not exist
versionRecord?.version ?? }
inferredVersion ?? hasCleanedUpMetaDb = true;
0;
if (!versionRecord) {
await versionManager.setVersion({ version: fromVersion });
} }
const orderedMigrations = [...migrations].sort((a, b) => a.from - b.from); const projectsAdapter = new IndexedDBAdapter<ProjectRecord>(
let currentVersion = fromVersion;
for (const migration of orderedMigrations) {
if (migration.from !== currentVersion) {
continue;
}
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,
});
}
return {
fromVersion,
toVersion: currentVersion,
migrated: currentVersion !== fromVersion,
};
}
async function inferStorageVersionFromProjects(): Promise<number> {
const projectsAdapter = new IndexedDBAdapter<unknown>(
"video-editor-projects", "video-editor-projects",
"projects", "projects",
1, 1,
); );
const projects = await projectsAdapter.getAll(); const projects = await projectsAdapter.getAll();
if (projects.length === 0) { const orderedMigrations = [...migrations].sort((a, b) => a.from - b.from);
return 0; let migratedCount = 0;
}
let lowestVersion = Number.POSITIVE_INFINITY;
for (const project of projects) { for (const project of projects) {
const projectVersion = checkProjectVersion({ project }); if (typeof project !== "object" || project === null) {
if (projectVersion < lowestVersion) { continue;
lowestVersion = projectVersion; }
let projectRecord = project as ProjectRecord;
let currentVersion = getProjectVersion({ project: projectRecord });
// 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
}
await projectsAdapter.set(projectId, result.project);
migratedCount++;
currentVersion = migration.to;
// Use migrated project for next iteration
projectRecord = result.project;
} }
} }
if (lowestVersion === Number.POSITIVE_INFINITY) { return { migratedCount };
return 0;
}
return lowestVersion;
} }
function checkProjectVersion({ project }: { project: unknown }): number { function getProjectVersion({ project }: { project: ProjectRecord }): number {
if (!isRecord(project)) {
return 0;
}
const versionValue = project.version; const versionValue = project.version;
// v2 and up // v2 and up - has explicit version field
if (typeof versionValue === "number") { if (typeof versionValue === "number") {
return versionValue; return versionValue;
} }
// v1 (got scenes) // v1 - has scenes array
const scenesValue = project.scenes; const scenesValue = project.scenes;
if (Array.isArray(scenesValue) && scenesValue.length > 0) { if (Array.isArray(scenesValue) && scenesValue.length > 0) {
return 1; return 1;
} }
// v0 (didn't have scenes) // v0 - no scenes
return 0; return 0;
} }
function isRecord(value: unknown): value is ProjectRecord {
return typeof value === "object" && value !== null;
}
@@ -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;
}
@@ -1,6 +1,7 @@
import { generateUUID } from "@/utils/id"; import { generateUUID } from "@/utils/id";
import type { SerializedScene } from "@/services/storage/types"; import type { SerializedScene } from "@/services/storage/types";
import type { MigrationResult, ProjectRecord } from "./types"; import type { MigrationResult, ProjectRecord } from "./types";
import { getProjectId, isRecord } from "./utils";
export interface TransformV0ToV1Options { export interface TransformV0ToV1Options {
now?: Date; now?: Date;
@@ -54,29 +55,4 @@ export function transformProjectV0ToV1({
return { project: updatedProject, skipped: false }; return { project: updatedProject, skipped: false };
} }
export function getProjectId({ export { getProjectId } from "./utils";
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;
}
@@ -15,6 +15,7 @@ import type {
VideoElement, VideoElement,
} from "@/types/timeline"; } from "@/types/timeline";
import type { MigrationResult, ProjectRecord } from "./types"; import type { MigrationResult, ProjectRecord } from "./types";
import { getProjectId, isRecord } from "./utils";
interface LegacyTimelineData { interface LegacyTimelineData {
tracks: unknown[]; tracks: unknown[];
@@ -51,10 +52,8 @@ interface LegacyMediaTrack {
export interface TransformV1ToV2Options { export interface TransformV1ToV2Options {
loadMediaAsset?: ({ loadMediaAsset?: ({
projectId,
mediaId, mediaId,
}: { }: {
projectId: string;
mediaId: string; mediaId: string;
}) => Promise<MediaAssetData | null>; }) => Promise<MediaAssetData | null>;
} }
@@ -91,10 +90,8 @@ async function migrateProject({
project: ProjectRecord; project: ProjectRecord;
projectId: string; projectId: string;
loadMediaAsset?: ({ loadMediaAsset?: ({
projectId,
mediaId, mediaId,
}: { }: {
projectId: string;
mediaId: string; mediaId: string;
}) => Promise<MediaAssetData | null>; }) => Promise<MediaAssetData | null>;
}): Promise<ProjectRecord> { }): Promise<ProjectRecord> {
@@ -261,10 +258,8 @@ async function transformTracks({
tracks: unknown[]; tracks: unknown[];
projectId: string; projectId: string;
loadMediaAsset?: ({ loadMediaAsset?: ({
projectId,
mediaId, mediaId,
}: { }: {
projectId: string;
mediaId: string; mediaId: string;
}) => Promise<MediaAssetData | null>; }) => Promise<MediaAssetData | null>;
}): Promise<TimelineTrack[]> { }): Promise<TimelineTrack[]> {
@@ -318,10 +313,8 @@ async function transformMediaTrack({
track: LegacyMediaTrack; track: LegacyMediaTrack;
projectId: string; projectId: string;
loadMediaAsset?: ({ loadMediaAsset?: ({
projectId,
mediaId, mediaId,
}: { }: {
projectId: string;
mediaId: string; mediaId: string;
}) => Promise<MediaAssetData | null>; }) => Promise<MediaAssetData | null>;
isMain: boolean; isMain: boolean;
@@ -342,7 +335,7 @@ async function transformMediaTrack({
let mediaType: "video" | "image" = "video"; let mediaType: "video" | "image" = "video";
if (loadMediaAsset) { if (loadMediaAsset) {
const mediaAsset = await loadMediaAsset({ projectId, mediaId }); const mediaAsset = await loadMediaAsset({ mediaId });
if (mediaAsset) { if (mediaAsset) {
mediaType = mediaAsset.type === "image" ? "image" : "video"; mediaType = mediaAsset.type === "image" ? "image" : "video";
} }
@@ -543,28 +536,7 @@ function transformAudioTrack({
}; };
} }
export function getProjectId({ export { getProjectId } from "./utils";
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 getCurrentSceneId({ function getCurrentSceneId({
value, value,
@@ -772,7 +744,3 @@ function isV2Project({ project }: { project: ProjectRecord }): boolean {
return isRecord(project.metadata) && isRecord(project.settings); return isRecord(project.metadata) && isRecord(project.settings);
} }
function isRecord(value: unknown): value is ProjectRecord {
return typeof value === "object" && value !== null;
}
@@ -1,6 +1,7 @@
import { getProjectDurationFromScenes } from "@/lib/scenes"; import { getProjectDurationFromScenes } from "@/lib/scenes";
import type { TScene } from "@/types/timeline"; import type { TScene } from "@/types/timeline";
import type { MigrationResult, ProjectRecord } from "./types"; import type { MigrationResult, ProjectRecord } from "./types";
import { getProjectId, isRecord } from "./utils";
export function transformProjectV2ToV3({ export function transformProjectV2ToV3({
project, project,
@@ -33,28 +34,7 @@ export function transformProjectV2ToV3({
return { project: migratedProject, skipped: false }; return { project: migratedProject, skipped: false };
} }
export function getProjectId({ export { getProjectId } from "./utils";
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 getScenes({ project }: { project: ProjectRecord }): TScene[] { function getScenes({ project }: { project: ProjectRecord }): TScene[] {
const scenesValue = project.scenes; const scenesValue = project.scenes;
@@ -75,7 +55,3 @@ function isV3Project({ project }: { project: ProjectRecord }): boolean {
isRecord(project.metadata) && typeof project.metadata.duration === "number" isRecord(project.metadata) && typeof project.metadata.duration === "number"
); );
} }
function isRecord(value: unknown): value is ProjectRecord {
return typeof value === "object" && value !== null;
}
@@ -4,41 +4,19 @@
* Sets currentSceneId to the new scene's id * Sets currentSceneId to the new scene's id
*/ */
import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
import { StorageMigration } from "./base"; 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 { export class V0toV1Migration extends StorageMigration {
from = 0; from = 0;
to = 1; to = 1;
async run(): Promise<void> { async transform(project: ProjectRecord): Promise<{
const projectsAdapter = new IndexedDBAdapter<unknown>( project: ProjectRecord;
"video-editor-projects", skipped: boolean;
"projects", reason?: string;
1, }> {
); return transformProjectV0ToV1({ project });
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);
}
} }
} }
@@ -4,6 +4,7 @@ import {
} from "@/services/storage/indexeddb-adapter"; } from "@/services/storage/indexeddb-adapter";
import type { MediaAssetData } from "@/services/storage/types"; import type { MediaAssetData } from "@/services/storage/types";
import { StorageMigration } from "./base"; import { StorageMigration } from "./base";
import type { ProjectRecord } from "./transformers/types";
import { import {
getProjectId, getProjectId,
transformProjectV1ToV2, transformProjectV1ToV2,
@@ -14,41 +15,32 @@ export class V1toV2Migration extends StorageMigration {
from = 1; from = 1;
to = 2; to = 2;
async run(): Promise<void> { async transform(project: ProjectRecord): Promise<{
const projectsAdapter = new IndexedDBAdapter<unknown>( project: ProjectRecord;
"video-editor-projects", skipped: boolean;
"projects", reason?: string;
1, }> {
); const projectId = getProjectId({ project });
const projects = await projectsAdapter.getAll();
for (const project of projects) {
if (typeof project !== "object" || project === null) {
continue;
}
const projectId = getProjectId({
project: project as Record<string, unknown>,
});
if (!projectId) { if (!projectId) {
continue; return { project, skipped: true, reason: "no project id" };
} }
const loadMediaAsset = createMediaAssetLoader({ projectId }); const loadMediaAsset = createMediaAssetLoader({ projectId });
const result = await transformProjectV1ToV2({ const result = await transformProjectV1ToV2({
project: project as Record<string, unknown>, project,
options: { loadMediaAsset }, options: { loadMediaAsset },
}); });
if (result.skipped) { if (!result.skipped) {
continue; // cleanup legacy timeline DBs after successful transformation
await cleanupLegacyTimelineDBs({
projectId,
project: result.project,
});
} }
await projectsAdapter.set(projectId, result.project); return result;
await cleanupLegacyTimelineDBs({ projectId, project: result.project });
}
} }
} }
@@ -73,7 +65,7 @@ async function cleanupLegacyTimelineDBs({
project, project,
}: { }: {
projectId: string; projectId: string;
project: Record<string, unknown>; project: ProjectRecord;
}): Promise<void> { }): Promise<void> {
const scenes = project.scenes; const scenes = project.scenes;
if (!Array.isArray(scenes)) { if (!Array.isArray(scenes)) {
@@ -2,41 +2,19 @@
* Adds a "duration" field to each project's metadata * Adds a "duration" field to each project's metadata
*/ */
import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
import { StorageMigration } from "./base"; 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 { export class V2toV3Migration extends StorageMigration {
from = 2; from = 2;
to = 3; to = 3;
async run(): Promise<void> { async transform(project: ProjectRecord): Promise<{
const projectsAdapter = new IndexedDBAdapter<unknown>( project: ProjectRecord;
"video-editor-projects", skipped: boolean;
"projects", reason?: string;
1, }> {
); return transformProjectV2ToV3({ project });
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);
}
} }
} }
@@ -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 });
}
}
-1
View File
@@ -26,7 +26,6 @@ export interface MediaAssetData {
fps?: number; fps?: number;
ephemeral?: boolean; ephemeral?: boolean;
thumbnailUrl?: string; thumbnailUrl?: string;
sourceStickerIconName?: string;
} }
export type SerializedScene = Omit<TScene, "createdAt" | "updatedAt"> & { export type SerializedScene = Omit<TScene, "createdAt" | "updatedAt"> & {