UNPKG

@workspace-fs/core

Version:

Multi-project workspace manager for Firesystem with support for multiple sources

330 lines (280 loc) 8.55 kB
import type { ProjectConfig, WorkspaceSettings, ProjectMetadata, } from "./types"; const DB_NAME = "@firesystem/workspace"; const DB_VERSION = 1; export interface StoredProject { id: string; name: string; source: any; // ProjectSource metadata: ProjectMetadata; lastAccessed: Date; enabled?: boolean; disabledAt?: Date; enabledAt?: Date; } export interface WorkspaceState { id: "current"; // Single record key activeProjectId: string | null; recentProjectIds: string[]; settings: WorkspaceSettings; } /** * Manages workspace persistence in IndexedDB */ export class WorkspaceDatabase { private db: IDBDatabase | null = null; private dbName: string; private dbVersion: number; constructor(dbName: string = DB_NAME, dbVersion: number = DB_VERSION) { this.dbName = dbName; this.dbVersion = dbVersion; } /** * Open or create the workspace database */ async open(): Promise<void> { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, this.dbVersion); request.onerror = () => { reject(new Error(`Failed to open database: ${request.error}`)); }; request.onsuccess = () => { this.db = request.result; resolve(); }; request.onupgradeneeded = (event) => { const db = request.result; const oldVersion = event.oldVersion; // Create object stores for v1 if (oldVersion < 1) { // Projects store if (!db.objectStoreNames.contains("projects")) { const projectStore = db.createObjectStore("projects", { keyPath: "id", }); projectStore.createIndex("name", "name", { unique: false }); projectStore.createIndex("lastAccessed", "lastAccessed", { unique: false, }); } // Workspace state store if (!db.objectStoreNames.contains("workspaceState")) { db.createObjectStore("workspaceState", { keyPath: "id" }); } } }; }); } /** * Ensure database is open */ private ensureOpen(): void { if (!this.db) { throw new Error("Database not opened. Call open() first."); } } /** * Close the database */ close(): void { if (this.db) { this.db.close(); this.db = null; } } // Project CRUD operations /** * Save or update a project configuration */ async saveProject(project: StoredProject): Promise<void> { this.ensureOpen(); const tx = this.db!.transaction(["projects"], "readwrite"); const store = tx.objectStore("projects"); await new Promise<void>((resolve, reject) => { const request = store.put(project); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } /** * Get a project by ID */ async getProject(id: string): Promise<StoredProject | null> { this.ensureOpen(); const tx = this.db!.transaction(["projects"], "readonly"); const store = tx.objectStore("projects"); return new Promise((resolve, reject) => { const request = store.get(id); request.onsuccess = () => resolve(request.result || null); request.onerror = () => reject(request.error); }); } /** * List all projects */ async listProjects(): Promise<StoredProject[]> { this.ensureOpen(); const tx = this.db!.transaction(["projects"], "readonly"); const store = tx.objectStore("projects"); return new Promise((resolve, reject) => { const request = store.getAll(); request.onsuccess = () => resolve(request.result || []); request.onerror = () => reject(request.error); }); } /** * List recent projects */ async listRecentProjects(limit = 10): Promise<StoredProject[]> { this.ensureOpen(); const tx = this.db!.transaction(["projects"], "readonly"); const index = tx.objectStore("projects").index("lastAccessed"); const projects: StoredProject[] = []; return new Promise((resolve, reject) => { // Open cursor in reverse order (most recent first) const request = index.openCursor(null, "prev"); request.onsuccess = () => { const cursor = request.result; if (cursor && projects.length < limit) { projects.push(cursor.value); cursor.continue(); } else { resolve(projects); } }; request.onerror = () => reject(request.error); }); } /** * Delete a project */ async deleteProject(id: string): Promise<void> { this.ensureOpen(); const tx = this.db!.transaction(["projects"], "readwrite"); const store = tx.objectStore("projects"); await new Promise<void>((resolve, reject) => { const request = store.delete(id); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } /** * Update project last accessed time */ async touchProject(id: string): Promise<void> { const project = await this.getProject(id); if (project) { project.lastAccessed = new Date(); await this.saveProject(project); } } /** * Update project state */ async updateProjectState( id: string, updates: Partial< Pick<StoredProject, "enabled" | "disabledAt" | "enabledAt"> >, ): Promise<void> { const project = await this.getProject(id); if (project) { // Apply updates Object.assign(project, updates); await this.saveProject(project); } } // Workspace state operations /** * Save workspace state */ async saveWorkspaceState(state: WorkspaceState): Promise<void> { this.ensureOpen(); const tx = this.db!.transaction(["workspaceState"], "readwrite"); const store = tx.objectStore("workspaceState"); await new Promise<void>((resolve, reject) => { const request = store.put(state); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } /** * Get workspace state */ async getWorkspaceState(): Promise<WorkspaceState | null> { this.ensureOpen(); const tx = this.db!.transaction(["workspaceState"], "readonly"); const store = tx.objectStore("workspaceState"); return new Promise((resolve, reject) => { const request = store.get("current"); request.onsuccess = () => resolve(request.result || null); request.onerror = () => reject(request.error); }); } /** * Clear all data */ async clear(): Promise<void> { this.ensureOpen(); const tx = this.db!.transaction( ["projects", "workspaceState"], "readwrite", ); await Promise.all([ new Promise<void>((resolve, reject) => { const request = tx.objectStore("projects").clear(); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }), new Promise<void>((resolve, reject) => { const request = tx.objectStore("workspaceState").clear(); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }), ]); } /** * Check if a project exists by checking IndexedDB databases */ static async projectDatabaseExists(dbName: string): Promise<boolean> { if (!("databases" in indexedDB)) { // Fallback for browsers that don't support databases() return new Promise((resolve) => { const testOpen = indexedDB.open(dbName); testOpen.onsuccess = () => { const db = testOpen.result; const exists = db.version > 0; db.close(); resolve(exists); }; testOpen.onerror = () => resolve(false); }); } const databases = await indexedDB.databases(); return databases.some((db) => db.name === dbName); } /** * Discover existing Firesystem IndexedDB databases */ async discoverIndexedDBProjects(): Promise<string[]> { if (!("databases" in indexedDB)) { console.warn("IndexedDB.databases() not supported in this browser"); return []; } const databases = await indexedDB.databases(); // Filter databases that look like Firesystem projects return databases .filter((db) => { const name = db.name || ""; return ( name.startsWith("firesystem-") || name.startsWith("@firesystem/") || name.includes("-filesystem") ); }) .map((db) => db.name!); } }