refactor: migrate project storage from IndexedDB to OPFS with folder-based structure

- Replace monolithic KGStorage with KGProjectStorage (OPFS) and KGConfigStorage (IndexedDB)
- Each project stored as folder: project.json + meta.json + media/
- Add KGConfigUpgrader with V1 upgrader to auto-migrate existing IndexedDB projects to OPFS
- Request persistent storage via navigator.storage.persist() to prevent browser eviction
- Show loading spinner during one-time migration
- Enforce safe project name characters (letters, numbers, space, hyphen, underscore, period, parens)
- Export projects as .kgstudio zip bundles instead of raw JSON
- Import supports .kgstudio bundles (with meta.json validation) and legacy JSON files
- Auto-deduplicate project names on import with (1), (2), etc. suffix
- Add OPFS shell debugger (pwd, ls, cd, cat, dl) accessible via KGDebugger.opfs()
- Add ESLint semi rule for consistent semicolons
- Delete KGStorage.ts — all logic absorbed by new storage classes
- Add jszip dependency for zip export/import
This commit is contained in:
Xiaohan-Tian
2026-04-09 19:25:08 -07:00
parent 17610f12f3
commit 2d3ea33ce6
21 changed files with 1781 additions and 235 deletions
+35 -3
View File
@@ -2,6 +2,8 @@ import type { Selectable } from '../components/interfaces';
import { KGProject } from './KGProject';
import { KGAudioInterface } from './audio-interface/KGAudioInterface';
import { ConfigManager } from './config/ConfigManager';
import { KGProjectStorage } from './io/KGProjectStorage';
import { KGConfigUpgrader } from './config-upgrader/KGConfigUpgrader';
import { KGMidiRegion } from './region/KGMidiRegion';
import { KGMidiNote } from './midi/KGMidiNote';
import { KGRegion } from './region/KGRegion';
@@ -30,6 +32,8 @@ export class KGCore {
private copiedItems: Selectable[] = [];
private isPlaying: boolean = false;
private isMigrating: boolean = false;
private migrationStateChangeCallback: ((isMigrating: boolean) => void) | null = null;
// Timer management for playback
private playbackIntervalId: number | null = null;
@@ -71,11 +75,23 @@ export class KGCore {
// Initialize configuration manager
const configManager = ConfigManager.instance();
await configManager.initialize();
// Initialize OPFS project storage
const projectStorage = KGProjectStorage.getInstance();
await projectStorage.initialize();
// Run app-level migrations (e.g., IndexedDB -> OPFS project migration)
this.setMigrating(true);
try {
await KGConfigUpgrader.upgradeToLatest();
} finally {
this.setMigrating(false);
}
// Initialize audio interface
const audioInterface = KGAudioInterface.instance();
await audioInterface.initialize();
console.log("KGCore components initialized successfully");
} catch (error) {
console.error("Failed to initialize KGCore:", error);
@@ -194,7 +210,23 @@ export class KGCore {
this.selectionChangeCallbacks.forEach(callback => callback());
}
// play
// Migration state
public getIsMigrating(): boolean {
return this.isMigrating;
}
private setMigrating(value: boolean): void {
this.isMigrating = value;
if (this.migrationStateChangeCallback) {
this.migrationStateChangeCallback(value);
}
}
public setMigrationStateChangeCallback(callback: (isMigrating: boolean) => void): void {
this.migrationStateChangeCallback = callback;
}
// play
public getIsPlaying(): boolean {
return this.isPlaying;
}