UNPKG

@firesystem/indexeddb

Version:

IndexedDB implementation of Virtual File System

1 lines 67.3 kB
{"version":3,"sources":["../src/index.ts","../src/IndexedDBFileSystem.ts"],"sourcesContent":["export { IndexedDBFileSystem } from \"./IndexedDBFileSystem\";\nexport type { IndexedDBConfig } from \"./types\";\n","import type {\n FileEntry,\n FileStat,\n FileMetadata,\n FSEvent,\n Disposable,\n FileSystemEventPayloads,\n IFileSystemCapabilities,\n} from \"@firesystem/core\";\nimport {\n normalizePath,\n dirname,\n basename,\n join,\n TypedEventEmitter,\n FileSystemEvents,\n BaseFileSystem,\n} from \"@firesystem/core\";\nimport type { IndexedDBConfig } from \"./types\";\n\ninterface StoredFile {\n path: string;\n type: \"file\" | \"directory\";\n content?: any;\n size: number;\n created: Date;\n modified: Date;\n metadata?: FileMetadata;\n parent: string;\n}\n\ninterface WatchListener {\n id: string;\n pattern: string;\n callback: (event: FSEvent) => void;\n}\n\ninterface FileSnapshot {\n path: string;\n type: \"file\" | \"directory\";\n modified: Date;\n size: number;\n contentHash?: string;\n metadataHash?: string;\n}\n\nexport class IndexedDBFileSystem extends BaseFileSystem {\n private db: IDBDatabase | null = null;\n private config: Required<Omit<IndexedDBConfig, \"enableExternalSync\" | \"deepChangeDetection\">> & { \n enableExternalSync: boolean;\n deepChangeDetection: boolean;\n };\n private watchers: WatchListener[] = [];\n private isInitialized = false;\n public readonly events = new TypedEventEmitter<FileSystemEventPayloads>();\n private broadcastChannel: BroadcastChannel | null = null;\n private fileSnapshots: Map<string, FileSnapshot> = new Map();\n private pollingInterval: NodeJS.Timeout | null = null;\n private enableExternalSync = true;\n private deepChangeDetection = false;\n\n // Capabilities do IndexedDB\n readonly capabilities: IFileSystemCapabilities = {\n readonly: false,\n caseSensitive: true,\n atomicRename: false, // IndexedDB não tem rename atômico verdadeiro\n supportsWatch: true,\n supportsMetadata: true,\n maxFileSize: undefined, // Depende do browser e quota\n maxPathLength: 1024\n };\n\n constructor(config: IndexedDBConfig) {\n super();\n this.config = {\n dbName: config.dbName,\n version: config.version || 1,\n storeName: config.storeName || \"files\",\n enableExternalSync: config.enableExternalSync ?? true,\n deepChangeDetection: config.deepChangeDetection ?? false,\n };\n this.enableExternalSync = config.enableExternalSync ?? true;\n this.deepChangeDetection = config.deepChangeDetection ?? false;\n \n if (this.enableExternalSync && typeof BroadcastChannel !== \"undefined\") {\n this.setupBroadcastChannel();\n }\n }\n\n private async ensureDB(): Promise<IDBDatabase> {\n if (this.db && this.db.name === this.config.dbName) {\n return this.db;\n }\n\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(this.config.dbName, this.config.version);\n\n request.onerror = () => {\n reject(new Error(`Failed to open IndexedDB: ${request.error}`));\n };\n\n request.onsuccess = async () => {\n this.db = request.result;\n this.isInitialized = true;\n\n // Handle connection close\n this.db.onclose = () => {\n this.db = null;\n this.isInitialized = false;\n };\n\n // Ensure root exists\n await this.ensureRootExists();\n\n resolve(this.db);\n };\n\n request.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result;\n\n // Create object store if it doesn't exist\n if (!db.objectStoreNames.contains(this.config.storeName)) {\n const store = db.createObjectStore(this.config.storeName, {\n keyPath: \"path\",\n });\n\n // Create indexes for efficient queries\n store.createIndex(\"type\", \"type\", { unique: false });\n store.createIndex(\"parent\", \"parent\", { unique: false });\n store.createIndex(\"modified\", \"modified\", { unique: false });\n\n // Add root directory\n const now = new Date();\n store.add({\n path: \"/\",\n type: \"directory\",\n size: 0,\n created: now,\n modified: now,\n parent: \"\",\n });\n }\n };\n });\n }\n\n async initialize(): Promise<void> {\n const startTime = Date.now();\n this.events.emit(FileSystemEvents.INITIALIZING, undefined as any);\n\n try {\n // Ensure database connection\n const db = await this.ensureDB();\n\n // Load ALL files from database\n const allFiles = await this.loadAllFiles();\n const fileCount = allFiles.length;\n\n // Emit progress for each file loaded\n for (let i = 0; i < allFiles.length; i++) {\n const file = allFiles[i];\n\n // Emit individual file events\n if (file.type === \"file\") {\n this.events.emit(FileSystemEvents.FILE_READ, {\n path: file.path,\n size: file.size,\n });\n } else {\n this.events.emit(FileSystemEvents.DIR_READ, {\n path: file.path,\n count: 0, // We'll count children later if needed\n });\n }\n\n // Emit progress\n this.events.emit(FileSystemEvents.INIT_PROGRESS, {\n loaded: i + 1,\n total: fileCount,\n phase: \"loading-files\",\n });\n }\n\n this.events.emit(FileSystemEvents.INITIALIZED, {\n duration: Date.now() - startTime,\n });\n } catch (error) {\n this.events.emit(FileSystemEvents.INIT_ERROR, {\n error: error as Error,\n });\n throw error;\n }\n }\n\n private async loadAllFiles(): Promise<StoredFile[]> {\n const db = await this.ensureDB();\n return new Promise((resolve, reject) => {\n const transaction = db.transaction([this.config.storeName], \"readonly\");\n const store = transaction.objectStore(this.config.storeName);\n const request = store.getAll();\n\n request.onsuccess = () => {\n const files = request.result as StoredFile[];\n // Exclude root directory from the list\n const userFiles = files.filter((f) => f.path !== \"/\");\n resolve(userFiles);\n };\n\n request.onerror = () => {\n reject(new Error(`Failed to load files: ${request.error}`));\n };\n });\n }\n\n async readFile(path: string): Promise<FileEntry> {\n const normalized = normalizePath(path);\n const db = await this.ensureDB();\n\n return new Promise((resolve, reject) => {\n const transaction = db.transaction([this.config.storeName], \"readonly\");\n const store = transaction.objectStore(this.config.storeName);\n const request = store.get(normalized);\n\n request.onsuccess = () => {\n const file = request.result as StoredFile;\n if (!file) {\n reject(\n new Error(`ENOENT: no such file or directory, open '${path}'`),\n );\n return;\n }\n if (file.type === \"directory\") {\n reject(\n new Error(\n `EISDIR: illegal operation on a directory, read '${path}'`,\n ),\n );\n return;\n }\n resolve({\n path: file.path,\n name: basename(file.path),\n type: file.type,\n size: file.size,\n created: file.created,\n modified: file.modified,\n metadata: file.metadata,\n content: file.content,\n });\n };\n\n request.onerror = () => {\n reject(new Error(`Failed to read file: ${request.error}`));\n };\n });\n }\n\n async writeFile(\n path: string,\n content: any,\n metadata?: FileMetadata,\n ): Promise<FileEntry> {\n const normalized = normalizePath(path);\n const db = await this.ensureDB();\n\n // Ensure parent directory exists\n await this.ensureParentExists(normalized);\n\n const now = new Date();\n const size = this.calculateSize(content);\n\n return new Promise((resolve, reject) => {\n const transaction = db.transaction([this.config.storeName], \"readwrite\");\n const store = transaction.objectStore(this.config.storeName);\n\n // Check if file exists to preserve created date\n const getRequest = store.get(normalized);\n\n getRequest.onsuccess = () => {\n const existing = getRequest.result as StoredFile | undefined;\n const isUpdate = !!existing;\n\n const storedFile: StoredFile = {\n path: normalized,\n type: \"file\",\n content,\n size,\n created: existing?.created || now,\n modified: now,\n metadata,\n parent: dirname(normalized),\n } as StoredFile;\n\n const putRequest = store.put(storedFile);\n\n putRequest.onsuccess = () => {\n const entry: FileEntry = {\n path: normalized,\n name: basename(normalized),\n type: \"file\",\n size,\n created: storedFile.created,\n modified: storedFile.modified,\n metadata,\n content,\n };\n\n // Notify watchers\n const event = {\n type: isUpdate ? \"updated\" : \"created\" as \"updated\" | \"created\",\n path: normalized,\n timestamp: now,\n };\n this.notifyWatchers(event);\n \n // Broadcast to other tabs/windows\n this.broadcastChange({\n ...event,\n size,\n isDirectory: false,\n fileType: \"file\" as const,\n });\n\n resolve(entry);\n };\n\n putRequest.onerror = () => {\n reject(new Error(`Failed to write file: ${putRequest.error}`));\n };\n };\n\n getRequest.onerror = () => {\n reject(new Error(`Failed to check existing file: ${getRequest.error}`));\n };\n });\n }\n\n async deleteFile(path: string): Promise<void> {\n const normalized = normalizePath(path);\n const db = await this.ensureDB();\n\n // First check if it exists and get type\n const file = await this.stat(normalized).catch(() => null);\n if (!file) {\n throw new Error(`ENOENT: no such file or directory, unlink '${path}'`);\n }\n\n return new Promise((resolve, reject) => {\n const transaction = db.transaction([this.config.storeName], \"readwrite\");\n const store = transaction.objectStore(this.config.storeName);\n\n if (file.type === \"directory\") {\n // Check if directory is empty\n const index = store.index(\"parent\");\n const request = index.count(normalized);\n\n request.onsuccess = () => {\n if (request.result > 0) {\n reject(\n new Error(`ENOTEMPTY: directory not empty, rmdir '${path}'`),\n );\n return;\n }\n\n // Directory is empty, delete it\n const deleteRequest = store.delete(normalized);\n\n deleteRequest.onsuccess = () => {\n const event = {\n type: \"deleted\" as const,\n path: normalized,\n timestamp: new Date(),\n };\n this.notifyWatchers(event);\n this.broadcastChange(event);\n resolve();\n };\n\n deleteRequest.onerror = () => {\n reject(\n new Error(`Failed to delete directory: ${deleteRequest.error}`),\n );\n };\n };\n\n request.onerror = () => {\n reject(\n new Error(`Failed to check directory contents: ${request.error}`),\n );\n };\n } else {\n // It's a file, delete directly\n const request = store.delete(normalized);\n\n request.onsuccess = () => {\n const event = {\n type: \"deleted\" as const,\n path: normalized,\n timestamp: new Date(),\n };\n this.notifyWatchers(event);\n this.broadcastChange(event);\n resolve();\n };\n\n request.onerror = () => {\n reject(new Error(`Failed to delete file: ${request.error}`));\n };\n }\n });\n }\n\n async exists(path: string): Promise<boolean> {\n const normalized = normalizePath(path);\n const db = await this.ensureDB();\n\n return new Promise((resolve, reject) => {\n const transaction = db.transaction([this.config.storeName], \"readonly\");\n const store = transaction.objectStore(this.config.storeName);\n const request = store.count(normalized);\n\n request.onsuccess = () => {\n resolve(request.result > 0);\n };\n\n request.onerror = () => {\n reject(new Error(`Failed to check existence: ${request.error}`));\n };\n });\n }\n\n async readDir(path: string): Promise<FileEntry[]> {\n const normalized = normalizePath(path);\n const db = await this.ensureDB();\n\n // First check if directory exists\n if (normalized !== \"/\" && !(await this.exists(normalized))) {\n throw new Error(`ENOENT: no such file or directory, scandir '${path}'`);\n }\n\n return new Promise((resolve, reject) => {\n const transaction = db.transaction([this.config.storeName], \"readonly\");\n const store = transaction.objectStore(this.config.storeName);\n const index = store.index(\"parent\");\n const request = index.getAll(normalized);\n\n request.onsuccess = () => {\n const files = request.result as StoredFile[];\n const entries = files.map((file) => ({\n path: file.path,\n name: basename(file.path),\n type: file.type,\n size: file.size,\n created: file.created,\n modified: file.modified,\n metadata: file.metadata,\n }));\n\n resolve(entries);\n };\n\n request.onerror = () => {\n reject(new Error(`Failed to read directory: ${request.error}`));\n };\n });\n }\n\n async mkdir(path: string, recursive: boolean = false): Promise<FileEntry> {\n const normalized = normalizePath(path);\n\n if (normalized === \"/\") {\n throw new Error(`EEXIST: file already exists, mkdir '${path}'`);\n }\n\n const db = await this.ensureDB();\n\n // Check if already exists\n if (await this.exists(normalized)) {\n throw new Error(`EEXIST: file already exists, mkdir '${path}'`);\n }\n\n // Ensure parent exists if not recursive\n const parent = dirname(normalized);\n if (!recursive && parent !== \"/\" && !(await this.exists(parent))) {\n throw new Error(`ENOENT: no such file or directory, mkdir '${path}'`);\n }\n\n // Create parent directories if recursive\n if (recursive && parent !== \"/\" && !(await this.exists(parent))) {\n await this.mkdir(parent, true);\n }\n\n const now = new Date();\n\n return new Promise((resolve, reject) => {\n const transaction = db.transaction([this.config.storeName], \"readwrite\");\n const store = transaction.objectStore(this.config.storeName);\n\n const storedFile: StoredFile = {\n path: normalized,\n type: \"directory\",\n size: 0,\n created: now,\n modified: now,\n parent: dirname(normalized),\n } as StoredFile;\n\n const request = store.add(storedFile);\n\n request.onsuccess = () => {\n const entry: FileEntry = {\n path: normalized,\n name: basename(normalized),\n type: \"directory\",\n size: 0,\n created: now,\n modified: now,\n };\n\n const event = {\n type: \"created\" as const,\n path: normalized,\n timestamp: now,\n };\n this.notifyWatchers(event);\n this.broadcastChange({\n ...event,\n isDirectory: true,\n fileType: \"directory\" as const,\n });\n\n resolve(entry);\n };\n\n request.onerror = () => {\n reject(new Error(`Failed to create directory: ${request.error}`));\n };\n });\n }\n\n async rmdir(path: string, recursive: boolean = false): Promise<void> {\n const normalized = normalizePath(path);\n\n if (normalized === \"/\") {\n throw new Error(`EBUSY: resource busy or locked, rmdir '${path}'`);\n }\n\n const db = await this.ensureDB();\n\n // Check if directory exists\n const stat = await this.stat(normalized).catch(() => null);\n if (!stat) {\n throw new Error(`ENOENT: no such file or directory, rmdir '${path}'`);\n }\n if (stat.type !== \"directory\") {\n throw new Error(`ENOTDIR: not a directory, rmdir '${path}'`);\n }\n\n if (recursive) {\n // Delete all contents recursively\n await this.deleteRecursive(normalized);\n } else {\n // Check if empty\n const contents = await this.readDir(normalized);\n if (contents.length > 0) {\n throw new Error(`ENOTEMPTY: directory not empty, rmdir '${path}'`);\n }\n }\n\n // Delete the directory itself\n await this.deleteFile(normalized);\n }\n\n async rename(oldPath: string, newPath: string): Promise<FileEntry> {\n const oldNormalized = normalizePath(oldPath);\n const newNormalized = normalizePath(newPath);\n const db = await this.ensureDB();\n\n // Get the file/directory\n const oldStat = await this.stat(oldNormalized);\n\n // Ensure new parent exists\n await this.ensureParentExists(newNormalized);\n\n // Check if new path already exists\n if (await this.exists(newNormalized)) {\n throw new Error(\n `EEXIST: file already exists, rename '${oldPath}' -> '${newPath}'`,\n );\n }\n\n const now = new Date();\n\n return new Promise((resolve, reject) => {\n const transaction = db.transaction([this.config.storeName], \"readwrite\");\n const store = transaction.objectStore(this.config.storeName);\n\n // If it's a directory, we need to update all children\n if (oldStat.type === \"directory\") {\n // Get all descendants\n const request = store.openCursor();\n const updates: StoredFile[] = [];\n\n request.onsuccess = (event) => {\n const cursor = (event.target as IDBRequest).result;\n\n if (cursor) {\n const file = cursor.value as StoredFile;\n\n // Check if this file is under the old path\n if (\n file.path.startsWith(oldNormalized + \"/\") ||\n file.path === oldNormalized\n ) {\n const newFilePath = file.path.replace(\n oldNormalized,\n newNormalized,\n );\n updates.push({\n ...file,\n path: newFilePath,\n parent: dirname(newFilePath),\n modified: file.path === oldNormalized ? now : file.modified,\n });\n }\n\n cursor.continue();\n } else {\n // All files collected, now update them\n let completed = 0;\n const total = updates.length;\n\n if (total === 0) {\n reject(new Error(`Directory not found: ${oldPath}`));\n return;\n }\n\n updates.forEach((file) => {\n // Delete old entry\n store.delete(file.path.replace(newNormalized, oldNormalized));\n\n // Add new entry\n const addRequest = store.add(file);\n\n addRequest.onsuccess = () => {\n completed++;\n if (completed === total) {\n const entry = updates.find((f) => f.path === newNormalized)!;\n\n const event = {\n type: \"renamed\" as const,\n path: newNormalized,\n oldPath: oldNormalized,\n timestamp: now,\n };\n this.notifyWatchers(event);\n this.broadcastChange(event);\n\n resolve({\n path: entry.path,\n name: basename(entry.path),\n type: entry.type,\n size: entry.size,\n created: entry.created,\n modified: entry.modified,\n metadata: entry.metadata,\n });\n }\n };\n\n addRequest.onerror = () => {\n reject(\n new Error(`Failed to rename directory: ${addRequest.error}`),\n );\n };\n });\n }\n };\n\n request.onerror = () => {\n reject(new Error(`Failed to read files: ${request.error}`));\n };\n } else {\n // It's a file, simpler case\n const getRequest = store.get(oldNormalized);\n\n getRequest.onsuccess = () => {\n const file = getRequest.result as StoredFile;\n if (!file) {\n reject(new Error(`File not found: ${oldPath}`));\n return;\n }\n\n // Delete old entry\n store.delete(oldNormalized);\n\n // Create new entry\n const newFile: StoredFile = {\n ...file,\n path: newNormalized,\n parent: dirname(newNormalized),\n modified: now,\n };\n\n const addRequest = store.add(newFile);\n\n addRequest.onsuccess = () => {\n const entry: FileEntry = {\n path: newNormalized,\n name: basename(newNormalized),\n type: file.type,\n size: file.size,\n created: file.created,\n modified: now,\n metadata: file.metadata,\n content: file.content,\n };\n\n const event = {\n type: \"renamed\" as const,\n path: newNormalized,\n oldPath: oldNormalized,\n timestamp: now,\n };\n this.notifyWatchers(event);\n this.broadcastChange(event);\n\n resolve(entry);\n };\n\n addRequest.onerror = () => {\n reject(new Error(`Failed to rename file: ${addRequest.error}`));\n };\n };\n\n getRequest.onerror = () => {\n reject(new Error(`Failed to read file: ${getRequest.error}`));\n };\n }\n });\n }\n\n async move(sourcePaths: string[], targetPath: string): Promise<void> {\n const targetNormalized = normalizePath(targetPath);\n\n // Ensure target is a directory\n const targetStat = await this.stat(targetNormalized).catch(() => null);\n if (!targetStat || targetStat.type !== \"directory\") {\n throw new Error(`ENOTDIR: not a directory, move to '${targetPath}'`);\n }\n\n for (const sourcePath of sourcePaths) {\n const name = basename(sourcePath);\n const newPath = join(targetNormalized, name);\n await this.rename(sourcePath, newPath);\n }\n }\n\n async copy(sourcePath: string, targetPath: string): Promise<FileEntry> {\n const sourceNormalized = normalizePath(sourcePath);\n const targetNormalized = normalizePath(targetPath);\n\n // Read source file\n const source = await this.readFile(sourceNormalized);\n\n // Write to target\n return this.writeFile(targetNormalized, source.content, source.metadata);\n }\n\n watch(pattern: string, callback: (event: FSEvent) => void): Disposable {\n const id = this.generateWatcherId();\n const listener: WatchListener = { id, pattern, callback };\n\n this.watchers.push(listener);\n\n return {\n dispose: () => {\n const index = this.watchers.findIndex((w) => w.id === id);\n if (index !== -1) {\n this.watchers.splice(index, 1);\n }\n },\n };\n }\n\n async stat(path: string): Promise<FileStat> {\n const normalized = normalizePath(path);\n const db = await this.ensureDB();\n\n return new Promise((resolve, reject) => {\n const transaction = db.transaction([this.config.storeName], \"readonly\");\n const store = transaction.objectStore(this.config.storeName);\n const request = store.get(normalized);\n\n request.onsuccess = () => {\n const file = request.result as StoredFile;\n if (!file) {\n reject(\n new Error(`ENOENT: no such file or directory, stat '${path}'`),\n );\n return;\n }\n\n resolve({\n path: file.path,\n size: file.size,\n type: file.type,\n created: file.created,\n modified: file.modified,\n readonly: false, // Sempre false em IndexedDB\n });\n };\n\n request.onerror = () => {\n reject(new Error(`Failed to stat file: ${request.error}`));\n };\n });\n }\n\n async glob(pattern: string): Promise<string[]> {\n const db = await this.ensureDB();\n\n return new Promise((resolve, reject) => {\n const transaction = db.transaction([this.config.storeName], \"readonly\");\n const store = transaction.objectStore(this.config.storeName);\n const request = store.getAll();\n\n request.onsuccess = () => {\n const files = request.result as StoredFile[];\n const paths = files\n .map((file) => file.path)\n .filter((path) => this.matchesPattern(path, pattern))\n .sort();\n\n resolve(paths);\n };\n\n request.onerror = () => {\n reject(new Error(`Failed to glob: ${request.error}`));\n };\n });\n }\n\n async clear(): Promise<void> {\n const startTime = Date.now();\n const operationId = `clear-${startTime}`;\n\n this.events.emit(FileSystemEvents.OPERATION_START, {\n operation: \"clear\",\n id: operationId,\n });\n this.events.emit(FileSystemEvents.STORAGE_CLEARING, undefined as any);\n\n try {\n const db = await this.ensureDB();\n\n await new Promise<void>((resolve, reject) => {\n const transaction = db.transaction(\n [this.config.storeName],\n \"readwrite\",\n );\n const store = transaction.objectStore(this.config.storeName);\n const request = store.clear();\n\n request.onsuccess = () => {\n // Always ensure root directory exists after clear\n const rootTransaction = db.transaction(\n [this.config.storeName],\n \"readwrite\",\n );\n const rootStore = rootTransaction.objectStore(this.config.storeName);\n const now = new Date();\n\n const rootDir: StoredFile = {\n path: \"/\",\n type: \"directory\",\n size: 0,\n created: now,\n modified: now,\n parent: \"\",\n };\n\n const rootRequest = rootStore.add(rootDir);\n rootRequest.onsuccess = () => resolve();\n rootRequest.onerror = () =>\n reject(new Error(`Failed to recreate root: ${rootRequest.error}`));\n };\n\n request.onerror = () => {\n reject(new Error(`Failed to clear store: ${request.error}`));\n };\n });\n\n const duration = Date.now() - startTime;\n this.events.emit(FileSystemEvents.STORAGE_CLEARED, { duration });\n this.events.emit(FileSystemEvents.OPERATION_END, {\n operation: \"clear\",\n id: operationId,\n duration,\n });\n } catch (error) {\n this.events.emit(FileSystemEvents.OPERATION_ERROR, {\n operation: \"clear\",\n error: error as Error,\n });\n throw error;\n }\n }\n\n async size(): Promise<number> {\n const startTime = Date.now();\n const operationId = `size-${startTime}`;\n\n this.events.emit(FileSystemEvents.OPERATION_START, {\n operation: \"size\",\n id: operationId,\n });\n\n try {\n const db = await this.ensureDB();\n\n const totalSize = await new Promise<number>((resolve, reject) => {\n const transaction = db.transaction([this.config.storeName], \"readonly\");\n const store = transaction.objectStore(this.config.storeName);\n const request = store.getAll();\n\n request.onsuccess = () => {\n const files = request.result as StoredFile[];\n const size = files.reduce((sum, file) => sum + file.size, 0);\n resolve(size);\n };\n\n request.onerror = () => {\n reject(new Error(`Failed to calculate size: ${request.error}`));\n };\n });\n\n this.events.emit(FileSystemEvents.STORAGE_SIZE_CALCULATED, {\n size: totalSize,\n });\n this.events.emit(FileSystemEvents.OPERATION_END, {\n operation: \"size\",\n id: operationId,\n duration: Date.now() - startTime,\n });\n\n return totalSize;\n } catch (error) {\n this.events.emit(FileSystemEvents.OPERATION_ERROR, {\n operation: \"size\",\n error: error as Error,\n });\n throw error;\n }\n }\n\n // Helper methods\n\n private async calculateHash(data: any): Promise<string> {\n // Simple hash for content comparison\n const str = typeof data === 'string' ? data : JSON.stringify(data);\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash; // Convert to 32-bit integer\n }\n return hash.toString(36);\n }\n\n private calculateSize(content: any): number {\n if (content === null || content === undefined) {\n return 0;\n }\n if (typeof content === \"string\") {\n return new Blob([content]).size;\n }\n if (content instanceof ArrayBuffer) {\n return content.byteLength;\n }\n if (content instanceof Blob) {\n return content.size;\n }\n if (typeof content === \"object\") {\n return new Blob([JSON.stringify(content)]).size;\n }\n return 0;\n }\n\n private async ensureParentExists(path: string): Promise<void> {\n const parent = dirname(path);\n if (parent === \"/\" || parent === path) return;\n\n if (!(await this.exists(parent))) {\n throw new Error(`ENOENT: no such file or directory, open '${path}'`);\n }\n }\n\n private async deleteRecursive(path: string): Promise<void> {\n const entries = await this.readDir(path);\n\n for (const entry of entries) {\n if (entry.type === \"directory\") {\n await this.deleteRecursive(entry.path);\n }\n await this.deleteFile(entry.path);\n }\n }\n\n private matchesPattern(path: string, pattern: string): boolean {\n // Handle simple cases\n if (pattern === \"**\" || pattern === \"**/*\") {\n return true;\n }\n\n if (pattern === \"*\") {\n // * should only match files in root directory\n return path.split(\"/\").length === 2 && path !== \"/\";\n }\n\n // Convert glob pattern to regex\n let regex = pattern\n // Escape special regex characters except glob ones\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n // Handle ** (matches any number of directories)\n .replace(/\\*\\*/g, \"___GLOBSTAR___\")\n // Handle * (matches any characters except /)\n .replace(/\\*/g, \"[^/]*\")\n // Handle ? (matches single character except /)\n .replace(/\\?/g, \"[^/]\")\n // Restore ** handling\n .replace(/___GLOBSTAR___\\//g, \"(.*/)?\")\n .replace(/\\/___GLOBSTAR___/g, \"(/.*)?\")\n .replace(/___GLOBSTAR___/g, \".*\");\n\n // Anchor the pattern\n regex = \"^\" + regex + \"$\";\n\n return new RegExp(regex).test(path);\n }\n\n private notifyWatchers(event: FSEvent): void {\n for (const watcher of this.watchers) {\n if (this.matchesPattern(event.path, watcher.pattern)) {\n try {\n watcher.callback(event);\n } catch (error) {\n console.error(\n `Error in watch callback for pattern \"${watcher.pattern}\":`,\n error,\n );\n }\n }\n }\n }\n\n private generateWatcherId(): string {\n return `watch_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;\n }\n\n private async ensureRootExists(): Promise<void> {\n if (!this.db) return;\n\n const transaction = this.db.transaction(\n [this.config.storeName],\n \"readonly\",\n );\n const store = transaction.objectStore(this.config.storeName);\n const request = store.get(\"/\");\n\n return new Promise((resolve) => {\n request.onsuccess = async () => {\n if (!request.result) {\n // Root doesn't exist, create it\n const writeTransaction = this.db!.transaction(\n [this.config.storeName],\n \"readwrite\",\n );\n const writeStore = writeTransaction.objectStore(\n this.config.storeName,\n );\n const now = new Date();\n\n await new Promise<void>((res, rej) => {\n const addRequest = writeStore.add({\n path: \"/\",\n type: \"directory\",\n size: 0,\n created: now,\n modified: now,\n parent: \"\",\n });\n\n addRequest.onsuccess = () => res();\n addRequest.onerror = () =>\n rej(new Error(\"Failed to create root directory\"));\n });\n }\n resolve();\n };\n\n request.onerror = () => resolve(); // Ignore errors, we'll try next time\n });\n }\n\n private setupBroadcastChannel(): void {\n this.broadcastChannel = new BroadcastChannel(`vfs-${this.config.dbName}`);\n \n this.broadcastChannel.onmessage = (event) => {\n const { type, path, oldPath, timestamp } = event.data;\n \n // Notify local watchers about external changes\n this.notifyWatchers({\n type,\n path,\n oldPath,\n timestamp: new Date(timestamp),\n });\n \n // Emit events for external changes\n switch (type) {\n case \"created\":\n if (event.data.isDirectory) {\n this.events.emit(FileSystemEvents.DIR_CREATED, { path });\n } else {\n this.events.emit(FileSystemEvents.FILE_WRITTEN, { \n path, \n size: event.data.size || 0 \n });\n }\n break;\n case \"updated\":\n this.events.emit(FileSystemEvents.FILE_WRITTEN, { \n path, \n size: event.data.size || 0 \n });\n break;\n case \"deleted\":\n this.events.emit(FileSystemEvents.FILE_DELETED, { path });\n break;\n case \"renamed\":\n // Emit as both deleted and created since there's no FILE_RENAMED event\n if (oldPath) {\n this.events.emit(FileSystemEvents.FILE_DELETED, { path: oldPath });\n }\n this.events.emit(FileSystemEvents.FILE_WRITTEN, { \n path, \n size: event.data.size || 0 \n });\n break;\n }\n };\n }\n\n private broadcastChange(event: FSEvent & { size?: number; isDirectory?: boolean; fileType?: \"file\" | \"directory\" }): void {\n if (this.broadcastChannel) {\n this.broadcastChannel.postMessage({\n ...event,\n timestamp: event.timestamp.toISOString(),\n });\n }\n }\n\n async startWatching(pollingIntervalMs: number = 250): Promise<void> {\n if (!this.enableExternalSync) return;\n \n // Take initial snapshot\n await this.updateFileSnapshots();\n \n // Start polling for changes\n this.pollingInterval = setInterval(async () => {\n await this.checkForExternalChanges();\n }, pollingIntervalMs);\n }\n\n async stopWatching(): Promise<void> {\n if (this.pollingInterval) {\n clearInterval(this.pollingInterval);\n this.pollingInterval = null;\n }\n }\n\n private async updateFileSnapshots(): Promise<void> {\n const db = await this.ensureDB();\n \n return new Promise((resolve, reject) => {\n const transaction = db.transaction([this.config.storeName], \"readonly\");\n const store = transaction.objectStore(this.config.storeName);\n const request = store.getAll();\n \n request.onsuccess = async () => {\n const files = request.result as StoredFile[];\n this.fileSnapshots.clear();\n \n for (const file of files) {\n // Include both files and directories in snapshots\n if (file.path !== \"/\") { // Skip root directory\n const snapshot: FileSnapshot = {\n path: file.path,\n type: file.type,\n modified: file.modified,\n size: file.size,\n };\n \n // Calculate content hash if deep detection is enabled\n if (this.deepChangeDetection && file.type === \"file\") {\n snapshot.contentHash = await this.calculateHash(file.content);\n if (file.metadata) {\n snapshot.metadataHash = await this.calculateHash(file.metadata);\n }\n }\n \n this.fileSnapshots.set(file.path, snapshot);\n }\n }\n \n resolve();\n };\n \n request.onerror = () => {\n reject(new Error(`Failed to update snapshots: ${request.error}`));\n };\n });\n }\n\n private async checkForExternalChanges(): Promise<void> {\n const db = await this.ensureDB();\n const currentFiles = new Map<string, FileSnapshot>();\n const filesToDeepCheck: string[] = [];\n \n return new Promise((resolve) => {\n const transaction = db.transaction([this.config.storeName], \"readonly\");\n const store = transaction.objectStore(this.config.storeName);\n const request = store.getAll();\n \n request.onsuccess = async () => {\n const files = request.result as StoredFile[];\n \n // Build current state map - include both files and directories\n files.forEach(file => {\n if (file.path !== \"/\") { // Skip root directory\n currentFiles.set(file.path, {\n path: file.path,\n type: file.type,\n modified: file.modified,\n size: file.size,\n });\n }\n });\n \n // Phase 1: Quick check (date/size)\n currentFiles.forEach((current, path) => {\n const snapshot = this.fileSnapshots.get(path);\n \n if (!snapshot) {\n // New file or directory\n this.notifyWatchers({\n type: \"created\",\n path,\n timestamp: new Date(),\n });\n \n // Also emit proper events\n if (current.type === \"directory\") {\n this.events.emit(FileSystemEvents.DIR_CREATED, { path });\n } else {\n this.events.emit(FileSystemEvents.FILE_WRITTEN, { path, size: current.size });\n }\n } else if (\n current.modified.getTime() !== snapshot.modified.getTime() ||\n current.size !== snapshot.size\n ) {\n // Modified file - detected by date/size\n this.notifyWatchers({\n type: \"updated\",\n path,\n timestamp: new Date(),\n });\n \n if (current.type === \"file\") {\n this.events.emit(FileSystemEvents.FILE_WRITTEN, { path, size: current.size });\n }\n } else if (this.deepChangeDetection && current.type === \"file\") {\n // Mark for deep check\n filesToDeepCheck.push(path);\n }\n });\n \n // Check for deleted files/directories\n this.fileSnapshots.forEach((snapshot, path) => {\n if (!currentFiles.has(path)) {\n this.notifyWatchers({\n type: \"deleted\",\n path,\n timestamp: new Date(),\n });\n \n // Also emit proper events\n if (snapshot.type === \"directory\") {\n this.events.emit(FileSystemEvents.DIR_DELETED, { path });\n } else {\n this.events.emit(FileSystemEvents.FILE_DELETED, { path });\n }\n }\n });\n \n // Phase 2: Deep check (content) - only if enabled and needed\n if (filesToDeepCheck.length > 0) {\n await this.performDeepContentCheck(filesToDeepCheck, files);\n }\n \n // Update snapshots with content hashes if deep check is enabled\n if (this.deepChangeDetection) {\n for (const file of files) {\n if (file.type === \"file\" && file.path !== \"/\") {\n const snapshot = currentFiles.get(file.path);\n if (snapshot) {\n snapshot.contentHash = await this.calculateHash(file.content);\n if (file.metadata) {\n snapshot.metadataHash = await this.calculateHash(file.metadata);\n }\n }\n }\n }\n }\n \n // Update snapshots\n this.fileSnapshots = currentFiles;\n resolve();\n };\n \n request.onerror = () => resolve();\n });\n }\n \n private async performDeepContentCheck(paths: string[], allFiles: StoredFile[]): Promise<void> {\n for (const path of paths) {\n const file = allFiles.find(f => f.path === path);\n const snapshot = this.fileSnapshots.get(path);\n \n if (file && snapshot) {\n const currentContentHash = await this.calculateHash(file.content);\n const currentMetadataHash = file.metadata ? await this.calculateHash(file.metadata) : undefined;\n \n const contentChanged = snapshot.contentHash && snapshot.contentHash !== currentContentHash;\n const metadataChanged = snapshot.metadataHash && currentMetadataHash && snapshot.metadataHash !== currentMetadataHash;\n \n if (contentChanged || metadataChanged) {\n // Content or metadata changed even though date/size didn't\n this.notifyWatchers({\n type: \"updated\",\n path,\n timestamp: new Date(),\n });\n \n this.events.emit(FileSystemEvents.FILE_WRITTEN, { \n path, \n size: file.size \n });\n }\n }\n }\n }\n\n dispose(): void {\n this.stopWatching();\n \n if (this.broadcastChannel) {\n this.broadcastChannel.close();\n this.broadcastChannel = null;\n }\n \n if (this.db) {\n this.db.close();\n this.db = null;\n }\n \n this.watchers = [];\n this.fileSnapshots.clear();\n }\n\n // Sobrescrever métodos de permissão (sempre permitido em IndexedDB)\n async canModify(path: string): Promise<boolean> {\n return true; // Sempre permitido em IndexedDB\n }\n\n async canCreateIn(parentPath: string): Promise<boolean> {\n try {\n const stat = await this.stat(parentPath);\n return stat.type === \"directory\";\n } catch {\n // Se não existe, verifica se é o root\n return normalizePath(parentPath) === \"/\";\n }\n }\n\n // Não sobrescrever writeFileAtomic - usar implementação base\n // IndexedDB não tem rename verdadeiramente atômico\n}\n"],"mappings":"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