UNPKG

@firesystem/indexeddb

Version:

IndexedDB implementation of Virtual File System

530 lines (395 loc) 14.1 kB
# @firesystem/indexeddb IndexedDB implementation of the Virtual File System (VFS) interface. Provides persistent storage in web browsers with an intuitive file system API. ## Features - 🌐 **Browser Native** - Uses IndexedDB for persistent storage - 📦 **Large Storage** - Store gigabytes of data (browser limits apply) - 🔄 **Full VFS API** - Implements complete @firesystem/core interface - 🚀 **Async Performance** - Non-blocking operations - 🔍 **Indexing Support** - Fast file lookups and queries - 💾 **Persistent** - Data survives browser restarts - ⚡ **Native Events** - Built-in reactive event system for all operations - 📊 **Progress Tracking** - Monitor initialization and file loading - 🔔 **Real-time Updates** - Get notified of all file system changes - 🔌 **Workspace Ready** - Optional integration with @workspace-fs/core - 🔄 **Multi-tab Sync** - Changes sync across browser tabs ## Installation ```bash npm install @firesystem/indexeddb # or yarn add @firesystem/indexeddb # or pnpm add @firesystem/indexeddb ``` ## Usage ```typescript import { IndexedDBFileSystem } from "@firesystem/indexeddb"; // Create a new file system instance const fs = new IndexedDBFileSystem({ dbName: "my-app-storage", // Optional, defaults to "vfs" version: 1, // Optional, defaults to 1 }); // Use it like any other VFS implementation await fs.writeFile("/data.json", { message: "Hello, IndexedDB!" }); const file = await fs.readFile("/data.json"); console.log(file.content); // { message: "Hello, IndexedDB!" } // NEW: Native event support fs.events.on("file:written", ({ path, size }) => { console.log(`File ${path} saved to IndexedDB (${size} bytes)`); }); ``` ## Configuration Options ```typescript interface IndexedDBConfig { dbName?: string; // Database name (default: "vfs") version?: number; // Database version (default: 1) storeName?: string; // Object store name (default: "files") enableExternalSync?: boolean; // Enable cross-tab sync and external change detection (default: true) deepChangeDetection?: boolean; // Check content even if date hasn't changed (default: false) } const fs = new IndexedDBFileSystem({ dbName: "my-app", version: 1, storeName: "documents", enableExternalSync: true, // Enable external change detection }); ``` ## Event System IndexedDBFileSystem includes a native event system that emits events for all operations: ### Basic Events ```typescript import { IndexedDBFileSystem, FileSystemEvents } from "@firesystem/indexeddb"; const fs = new IndexedDBFileSystem({ dbName: "my-app" }); // Listen to file operations fs.events.on(FileSystemEvents.FILE_WRITTEN, ({ path, size }) => { console.log(`File written: ${path} (${size} bytes)`); }); fs.events.on(FileSystemEvents.FILE_DELETED, ({ path }) => { console.log(`File deleted: ${path}`); }); // Listen to directory operations fs.events.on(FileSystemEvents.DIR_CREATED, ({ path }) => { console.log(`Directory created: ${path}`); }); // Operation tracking with timing fs.events.on(FileSystemEvents.OPERATION_END, ({ operation, duration }) => { console.log(`${operation} completed in ${duration}ms`); }); ``` ### Initialization with Progress Load all existing files from IndexedDB with progress tracking: ```typescript const fs = new IndexedDBFileSystem({ dbName: "my-app" }); // Track initialization progress fs.events.on(FileSystemEvents.INIT_PROGRESS, ({ loaded, total, phase }) => { console.log(`${phase}: ${loaded}/${total} files loaded`); }); // Get notified of each file during initialization fs.events.on(FileSystemEvents.FILE_READ, ({ path, size }) => { console.log(`Loaded: ${path} (${size} bytes)`); }); fs.events.on(FileSystemEvents.DIR_READ, ({ path }) => { console.log(`Found directory: ${path}`); }); // Initialize and load all existing files await fs.initialize(); ``` ### Integration with State Management ```typescript // Example with React and Zustand import { create } from 'zustand'; import { IndexedDBFileSystem, FileSystemEvents } from '@firesystem/indexeddb'; const useFileStore = create((set) => ({ files: [], isLoading: true, progress: { loaded: 0, total: 0 }, initializeFS: async () => { const fs = new IndexedDBFileSystem({ dbName: "my-app" }); // Update progress fs.events.on(FileSystemEvents.INIT_PROGRESS, ({ loaded, total }) => { set({ progress: { loaded, total } }); }); // Populate files as they load fs.events.on(FileSystemEvents.FILE_READ, ({ path, size }) => { set(state => ({ files: [...state.files, { path, size, type: 'file' }] })); }); fs.events.on(FileSystemEvents.DIR_READ, ({ path }) => { set(state => ({ files: [...state.files, { path, type: 'directory' }] })); }); // Mark as loaded when done fs.events.on(FileSystemEvents.INITIALIZED, () => { set({ isLoading: false }); }); // Initialize and load all files from IndexedDB await fs.initialize(); } })); // In your component function FileExplorer() { const { files, isLoading, progress, initializeFS } = useFileStore(); useEffect(() => { initializeFS(); // Load all files from IndexedDB }, []); if (isLoading) { return <div>Loading files: {progress.loaded}/{progress.total}</div>; } return <FileTree files={files} />; } ``` ### Storage Events ```typescript // Monitor storage operations fs.events.on(FileSystemEvents.STORAGE_CLEARING, () => { console.log("Clearing all files..."); }); fs.events.on(FileSystemEvents.STORAGE_CLEARED, ({ duration }) => { console.log(`Storage cleared in ${duration}ms`); }); fs.events.on(FileSystemEvents.STORAGE_SIZE_CALCULATED, ({ size }) => { console.log(`Total storage used: ${size} bytes`); }); ``` ## Workspace Integration IndexedDBFileSystem can be integrated with `@workspace-fs/core` through the `IndexedDBWorkspaceProvider`: ### Standalone Usage (Default) ```typescript import { IndexedDBFileSystem } from "@firesystem/indexeddb"; // Use directly without workspace const fs = new IndexedDBFileSystem({ dbName: "my-app" }); await fs.initialize(); await fs.writeFile("/data.json", { message: "Hello!" }); ``` ### With Workspace ```typescript import { WorkspaceFileSystem } from "@workspace-fs/core"; import { indexedDBProvider } from "@firesystem/indexeddb/provider"; // Register the provider const workspace = new WorkspaceFileSystem(); workspace.registerProvider(indexedDBProvider); // Create persistent projects const project = await workspace.loadProject({ id: "my-project", name: "My Project", source: { type: "indexeddb", config: { dbName: "project-storage", version: 1, enableExternalSync: true, }, }, }); // Work with the project - data persists across sessions await workspace.writeFile("/src/app.js", "// Application code"); await workspace.writeFile("/data/users.json", JSON.stringify([])); // Switch between multiple persistent projects const devProject = await workspace.loadProject({ id: "dev", name: "Development", source: { type: "indexeddb", config: { dbName: "dev-db" } }, }); const prodProject = await workspace.loadProject({ id: "prod", name: "Production", source: { type: "indexeddb", config: { dbName: "prod-db" } }, }); await workspace.setActiveProject("dev"); ``` ### Provider Configuration The IndexedDB provider accepts all standard IndexedDBFileSystem configuration options: ```typescript interface IndexedDBConfig { // Database name (required for persistence) dbName?: string; // Database version (default: 1) version?: number; // Object store name (default: "files") storeName?: string; // Enable cross-tab sync (default: true) enableExternalSync?: boolean; // Deep change detection (default: false) deepChangeDetection?: boolean; } ``` ## External Change Detection IndexedDBFileSystem can detect changes made to the database from external sources (other tabs, windows, or direct IndexedDB access): ### Cross-Tab Synchronization Changes made in one tab are automatically synchronized to other tabs using BroadcastChannel: ```typescript // Tab 1 const fs1 = new IndexedDBFileSystem({ dbName: "my-app" }); fs1.watch("**/*", (event) => { console.log(`Change detected: ${event.type} ${event.path}`); }); // Tab 2 const fs2 = new IndexedDBFileSystem({ dbName: "my-app" }); await fs2.writeFile("/data.json", { updated: true }); // Tab 1 will receive the change notification automatically ``` ### Polling for External Changes To detect changes made directly to IndexedDB (outside of FileSystem API): ```typescript const fs = new IndexedDBFileSystem({ dbName: "my-app" }); // Start watching for external changes (default: 250ms polling) await fs.startWatching(); // Poll every 250ms // or with custom interval await fs.startWatching(1000); // Poll every 1 second // Your watch handlers will now receive notifications for external changes fs.watch("**/*", (event) => { console.log(`External change: ${event.type} ${event.path}`); }); // Stop watching when done await fs.stopWatching(); ``` ### Deep Change Detection By default, changes are detected by file modification date and size. To detect content changes even when date/size remain the same: ```typescript const fs = new IndexedDBFileSystem({ dbName: "my-app", deepChangeDetection: true, // Enable content hash comparison }); // Now changes will be detected even if: // - File content changes but date doesn't update // - Metadata changes but file size remains same await fs.startWatching(); ``` **Note:** Deep change detection has performance implications as it requires calculating hashes for all file contents. Use it only when necessary. ### Disable External Sync If you don't need external change detection: ```typescript const fs = new IndexedDBFileSystem({ dbName: "my-app", enableExternalSync: false, // Disable all external sync features }); ``` ## Browser Storage Limits IndexedDB storage limits vary by browser: - **Chrome/Edge**: Up to 60% of total disk space - **Firefox**: Up to 50% of free disk space - **Safari**: Up to 1GB initially, can request more Check available storage: ```typescript if ("storage" in navigator && "estimate" in navigator.storage) { const estimate = await navigator.storage.estimate(); console.log(`Using ${estimate.usage} of ${estimate.quota} bytes`); } ``` ## Use Cases ### 1. Offline Web Apps ```typescript const fs = new IndexedDBFileSystem({ dbName: "offline-app" }); // Save user documents locally await fs.writeFile("/documents/report.pdf", pdfBlob); await fs.writeFile("/documents/data.xlsx", excelBlob); // Access offline const files = await fs.readDir("/documents"); ``` ### 2. Browser-Based IDEs ```typescript const fs = new IndexedDBFileSystem({ dbName: "web-ide" }); // Save project files await fs.mkdir("/src"); await fs.writeFile("/src/index.js", sourceCode); await fs.writeFile("/package.json", packageJson); // Watch for changes fs.watch("**/*.js", (event) => { console.log(`File ${event.type}: ${event.path}`); }); ``` ### 3. Client-Side Caching ```typescript const fs = new IndexedDBFileSystem({ dbName: "app-cache" }); // Cache API responses await fs.writeFile("/cache/users.json", await fetchUsers()); // Read from cache try { const cached = await fs.readFile("/cache/users.json"); return cached.content; } catch { // Cache miss, fetch fresh data const fresh = await fetchUsers(); await fs.writeFile("/cache/users.json", fresh); return fresh; } ``` ## Error Handling ```typescript try { await fs.writeFile("/data.json", data); } catch (error) { if (error.name === "QuotaExceededError") { console.error("Storage quota exceeded"); // Handle cleanup or request more storage } } ``` ## Performance Tips 1. **Batch Operations**: Group multiple operations when possible 2. **Large Files**: Consider chunking very large files 3. **Indexing**: Use meaningful paths for better organization 4. **Event Listeners**: Remove unused listeners to prevent memory leaks 5. **Initialization**: Call `initialize()` once to load all files efficiently ### Event Performance ```typescript // Dispose of event listeners when done const disposable = fs.events.on(FileSystemEvents.FILE_WRITTEN, handler); // Later... disposable.dispose(); // Remove the listener // Or remove all listeners for an event fs.events.removeAllListeners(FileSystemEvents.FILE_WRITTEN); ``` ### Cleanup Always dispose of the file system instance when done to clean up resources: ```typescript const fs = new IndexedDBFileSystem({ dbName: "my-app" }); // Use the file system... // Clean up when done fs.dispose(); // Closes DB connection, stops watching, cleans up listeners ``` ## Testing When testing code that uses IndexedDBFileSystem in Node.js environments, you have two options: ### Option 1: Using the testing module (Recommended) ```typescript // Import from the testing module which handles the polyfill automatically import { IndexedDBFileSystem } from "@firesystem/indexeddb/testing"; describe("My app", () => { it("should work with IndexedDB", async () => { const fs = new IndexedDBFileSystem({ dbName: "test" }); await fs.writeFile("/test.txt", "Hello"); // ... }); }); ``` Note: You still need to install `fake-indexeddb` as a dev dependency: ```bash npm install --save-dev fake-indexeddb ``` ### Option 2: Manual setup ```bash npm install --save-dev fake-indexeddb # or yarn add -D fake-indexeddb # or pnpm add -D fake-indexeddb ``` Then import it before your tests: ```typescript // In your test setup file or at the top of test files import "fake-indexeddb/auto"; // Now you can test IndexedDBFileSystem import { IndexedDBFileSystem } from "@firesystem/indexeddb"; describe("My app", () => { it("should work with IndexedDB", async () => { const fs = new IndexedDBFileSystem({ dbName: "test" }); await fs.writeFile("/test.txt", "Hello"); // ... }); }); ``` ## API Reference See [@firesystem/core](../core) for the complete VFS API documentation. ## License MIT © Anderson D. Rosa