From 68247c9a6ca4cdf08d22492add6b644162689fbf Mon Sep 17 00:00:00 2001 From: dpsifr Date: Thu, 24 Jul 2025 11:36:17 -0700 Subject: [PATCH] **feat(storage): remove FileSystemStorage and OPFSStorage implementations** - **Removal**: Deleted `FileSystemStorage` and `OPFSStorage` adapters from the `/storage` directory. - **Refactor**: Simplifies the codebase by eliminating unused, redundant, or outdated storage implementations. - **Impact**: Ensures maintainability by focusing on actively supported storage solutions. **Purpose**: Streamline the project by removing deprecated or unused storage adapters, reducing complexity and maintenance overhead. --- src/storage/fileSystemStorage.ts | 451 -------- src/storage/opfsStorage.ts | 1550 ---------------------------- src/storage/s3CompatibleStorage.ts | 952 ----------------- 3 files changed, 2953 deletions(-) delete mode 100644 src/storage/fileSystemStorage.ts delete mode 100644 src/storage/opfsStorage.ts delete mode 100644 src/storage/s3CompatibleStorage.ts diff --git a/src/storage/fileSystemStorage.ts b/src/storage/fileSystemStorage.ts deleted file mode 100644 index 07ea78ce..00000000 --- a/src/storage/fileSystemStorage.ts +++ /dev/null @@ -1,451 +0,0 @@ -import { GraphVerb, HNSWNoun, StorageAdapter } from '../coreTypes.js' - -// Dynamically and asynchronously load Node.js modules at the top level. -// This ensures they are available as soon as this module is imported, -// preventing race conditions with dependencies like TensorFlow.js. -let fs: any -let path: any - -const nodeModulesPromise = (async () => { - // A reliable check for a Node.js environment. - const isNode = - typeof process !== 'undefined' && - process.versions != null && - process.versions.node != null - - if (!isNode) { - return { fs: null, path: null } - } - - try { - // Use the 'node:' prefix for unambiguous importing of built-in modules. - const fsModule = await import('node:fs') - const pathModule = await import('node:path') - // Return the modules, preferring the default export if it exists. - return { - fs: fsModule.default || fsModule, - path: pathModule.default || pathModule - } - } catch (error) { - console.error( - 'FileSystemStorage: Failed to load Node.js modules. This adapter is not supported in this environment.', - error - ) - return { fs: null, path: null } - } -})() - -// Immediately assign the modules once the promise resolves. -nodeModulesPromise.then((modules) => { - fs = modules.fs - path = modules.path -}) - -// --- End of Refactored Code --- - -// Constants for directory and file names -const ROOT_DIR = 'brainy-data' -const NOUNS_DIR = 'nouns' -const VERBS_DIR = 'verbs' -const METADATA_DIR = 'metadata' - -// All nouns now use the same directory - no separate directories per noun type - -/** - * File system storage adapter for Node.js environments - */ -export class FileSystemStorage implements StorageAdapter { - private rootDir: string - private nounsDir: string - private verbsDir: string - private metadataDir: string - private isInitialized = false - - constructor(rootDirectory?: string) { - // We'll set the paths in the init method after dynamically importing the modules - this.rootDir = rootDirectory || '' - this.nounsDir = '' - this.verbsDir = '' - this.metadataDir = '' - } - - /** - * Initialize the storage adapter - */ - public async init(): Promise { - if (this.isInitialized) { - return - } - - // Wait for the top-level module loading to complete. - await nodeModulesPromise - - // Check if the modules were loaded successfully. - if (!fs || !path) { - throw new Error( - 'FileSystemStorage requires a Node.js environment, but `fs` and `path` modules could not be loaded.' - ) - } - - try { - // Now set up the directory paths - const rootDir = this.rootDir || process.cwd() - - // Check if rootDir already ends with ROOT_DIR to prevent duplication - if (rootDir.endsWith(ROOT_DIR)) { - this.rootDir = rootDir - } else { - this.rootDir = path.resolve(rootDir, ROOT_DIR) - } - - this.nounsDir = path.join(this.rootDir, NOUNS_DIR) - this.verbsDir = path.join(this.rootDir, VERBS_DIR) - this.metadataDir = path.join(this.rootDir, METADATA_DIR) - - // Create directories if they don't exist - await this.ensureDirectoryExists(this.rootDir) - await this.ensureDirectoryExists(this.nounsDir) - await this.ensureDirectoryExists(this.verbsDir) - await this.ensureDirectoryExists(this.metadataDir) - - this.isInitialized = true - } catch (error: any) { - console.error('Error initializing FileSystemStorage:', error) - throw error - } - } - - private async ensureDirectoryExists(dirPath: string): Promise { - try { - await fs.promises.mkdir(dirPath, { recursive: true }) - } catch (error: any) { - // Ignore EEXIST error, which means the directory already exists - if (error.code !== 'EEXIST') { - throw error - } - } - } - - private getNounPath(id: string, nounType?: string): string { - // All nouns now use the same directory regardless of type - return path.join(this.nounsDir, `${id}.json`) - } - - public async saveNoun( - noun: HNSWNoun & { metadata?: { noun?: string } } - ): Promise { - if (!this.isInitialized) await this.init() - const nounType = (noun as any).metadata?.noun - const filePath = this.getNounPath(noun.id, nounType) - await this.ensureDirectoryExists(path.dirname(filePath)) - await fs.promises.writeFile(filePath, JSON.stringify(noun, null, 2)) - } - - public async getNoun(id: string): Promise { - if (!this.isInitialized) await this.init() - const filePath = path.join(this.nounsDir, `${id}.json`) - try { - const data = await fs.promises.readFile(filePath, 'utf-8') - return JSON.parse(data) - } catch (error: any) { - if (error.code !== 'ENOENT') { - console.error(`Error reading noun ${id}:`, error) - } - return null - } - } - - public async deleteNoun(id: string): Promise { - if (!this.isInitialized) await this.init() - const noun = await this.getNoun(id) - if (noun) { - const nounType = (noun as any).metadata?.noun - const filePath = this.getNounPath(id, nounType) - try { - await fs.promises.unlink(filePath) - } catch (error: any) { - if (error.code !== 'ENOENT') { - console.error(`Error deleting noun file ${filePath}:`, error) - throw error - } - } - } - } - - public async getAllNouns(): Promise { - if (!this.isInitialized) await this.init() - const allNouns: HNSWNoun[] = [] - try { - const files = await fs.promises.readdir(this.nounsDir) - for (const file of files) { - if (file.endsWith('.json')) { - const filePath = path.join(this.nounsDir, file) - const data = await fs.promises.readFile(filePath, 'utf-8') - allNouns.push(JSON.parse(data)) - } - } - } catch (error: any) { - if (error.code !== 'ENOENT') { - console.error(`Error reading directory ${this.nounsDir}:`, error) - } - } - return allNouns - } - - /** - * Get nouns by noun type - * @param nounType The noun type to filter by - * @returns Promise that resolves to an array of nouns of the specified noun type - */ - public async getNounsByNounType(nounType: string): Promise { - if (!this.isInitialized) await this.init() - - const nouns: HNSWNoun[] = [] - try { - const files = await fs.promises.readdir(this.nounsDir) - for (const file of files) { - if (file.endsWith('.json')) { - const filePath = path.join(this.nounsDir, file) - const data = await fs.promises.readFile(filePath, 'utf-8') - const noun = JSON.parse(data) - - // Filter by noun type using metadata - const metadata = await this.getMetadata(noun.id) - if (metadata && metadata.noun === nounType) { - nouns.push(noun) - } - } - } - } catch (error: any) { - if (error.code !== 'ENOENT') { - console.error(`Error reading directory ${this.nounsDir}:`, error) - } - } - - return nouns - } - - public async saveVerb(verb: GraphVerb): Promise { - if (!this.isInitialized) await this.init() - const filePath = path.join(this.verbsDir, `${verb.id}.json`) - await fs.promises.writeFile(filePath, JSON.stringify(verb, null, 2)) - } - - /** - * Get a verb by its ID - * @param id The ID of the verb to retrieve - * @returns Promise that resolves to the verb or null if not found - */ - public async getVerb(id: string): Promise { - if (!this.isInitialized) await this.init() - const filePath = path.join(this.verbsDir, `${id}.json`) - try { - const data = await fs.promises.readFile(filePath, 'utf-8') - return JSON.parse(data) - } catch (error: any) { - if (error.code !== 'ENOENT') { - console.error(`Error reading verb ${id}:`, error) - } - return null - } - } - - public async getVerbsBySource(sourceId: string): Promise { - if (!this.isInitialized) await this.init() - const allVerbs = await this.getAllVerbs() - return allVerbs.filter((verb) => verb.sourceId === sourceId) - } - - /** - * Get verbs by target ID - * @param targetId The target ID to filter by - * @returns Promise that resolves to an array of verbs with the specified target ID - */ - public async getVerbsByTarget(targetId: string): Promise { - if (!this.isInitialized) await this.init() - const allVerbs = await this.getAllVerbs() - return allVerbs.filter((verb) => verb.targetId === targetId) - } - - /** - * Get verbs by type - * @param type The verb type to filter by - * @returns Promise that resolves to an array of verbs of the specified type - */ - public async getVerbsByType(type: string): Promise { - if (!this.isInitialized) await this.init() - const allVerbs = await this.getAllVerbs() - return allVerbs.filter((verb) => verb.type === type) - } - - public async getAllVerbs(): Promise { - if (!this.isInitialized) await this.init() - const allVerbs: GraphVerb[] = [] - try { - const files = await fs.promises.readdir(this.verbsDir) - for (const file of files) { - if (file.endsWith('.json')) { - const filePath = path.join(this.verbsDir, file) - const data = await fs.promises.readFile(filePath, 'utf-8') - allVerbs.push(JSON.parse(data)) - } - } - } catch (error: any) { - if (error.code !== 'ENOENT') { - console.error(`Error reading verbs directory ${this.verbsDir}:`, error) - } - } - return allVerbs - } - - public async deleteVerb(id: string): Promise { - if (!this.isInitialized) await this.init() - const filePath = path.join(this.verbsDir, `${id}.json`) - try { - await fs.promises.unlink(filePath) - } catch (error: any) { - if (error.code !== 'ENOENT') { - console.error(`Error deleting verb file ${filePath}:`, error) - throw error - } - } - } - - /** - * Save metadata for an entity - * @param id The ID of the entity - * @param metadata The metadata to save - */ - public async saveMetadata(id: string, metadata: any): Promise { - if (!this.isInitialized) await this.init() - const filePath = path.join(this.metadataDir, `${id}.json`) - await this.ensureDirectoryExists(path.dirname(filePath)) - await fs.promises.writeFile(filePath, JSON.stringify(metadata, null, 2)) - } - - /** - * Get metadata for an entity - * @param id The ID of the entity - * @returns Promise that resolves to the metadata or null if not found - */ - public async getMetadata(id: string): Promise { - if (!this.isInitialized) await this.init() - const filePath = path.join(this.metadataDir, `${id}.json`) - try { - const data = await fs.promises.readFile(filePath, 'utf-8') - return JSON.parse(data) - } catch (error: any) { - if (error.code !== 'ENOENT') { - console.error(`Error reading metadata for ${id}:`, error) - } - return null - } - } - - public async clear(): Promise { - if (!this.isInitialized) await this.init() - - // Helper function to recursively remove directory contents - const removeDirectoryContents = async (dirPath: string): Promise => { - try { - const files = await fs.promises.readdir(dirPath, { withFileTypes: true }) - - for (const file of files) { - const fullPath = path.join(dirPath, file.name) - - if (file.isDirectory()) { - await removeDirectoryContents(fullPath) - // Use fs.promises.rm with recursive option instead of rmdir - try { - await fs.promises.rm(fullPath, { recursive: true, force: true }) - } catch (rmError: any) { - // Fallback to rmdir if rm fails - await fs.promises.rmdir(fullPath) - } - } else { - await fs.promises.unlink(fullPath) - } - } - } catch (error: any) { - if (error.code !== 'ENOENT') { - console.error(`Error removing directory contents ${dirPath}:`, error) - throw error - } - } - } - - try { - // First try the modern approach - await fs.promises.rm(this.rootDir, { recursive: true, force: true }) - } catch (error: any) { - console.warn('Modern rm failed, falling back to manual cleanup:', error) - - // Fallback: manually remove contents then directory - try { - await removeDirectoryContents(this.rootDir) - // Use fs.promises.rm with recursive option instead of rmdir - try { - await fs.promises.rm(this.rootDir, { recursive: true, force: true }) - } catch (rmError: any) { - // Final fallback to rmdir if rm fails - await fs.promises.rmdir(this.rootDir) - } - } catch (fallbackError: any) { - if (fallbackError.code !== 'ENOENT') { - console.error('Manual cleanup also failed:', fallbackError) - throw fallbackError - } - } - } - - this.isInitialized = false // Reset state - await this.init() // Re-create directories - } - - public async getStorageStatus(): Promise<{ - type: string - used: number - quota: number | null - details?: Record - }> { - if (!this.isInitialized) await this.init() - - const calculateSize = async (dirPath: string): Promise => { - let size = 0 - try { - const files = await fs.promises.readdir(dirPath, { - withFileTypes: true - }) - for (const file of files) { - const fullPath = path.join(dirPath, file.name) - if (file.isDirectory()) { - size += await calculateSize(fullPath) - } else { - const stats = await fs.promises.stat(fullPath) - size += stats.size - } - } - } catch (error: any) { - if (error.code !== 'ENOENT') { - console.error(`Could not calculate size for ${dirPath}:`, error) - } - } - return size - } - - const totalSize = await calculateSize(this.rootDir) - const nouns = await this.getAllNouns() - const verbs = await this.getAllVerbs() - - return { - type: 'FileSystem', - used: totalSize, - quota: null, // File system quota is not easily available from Node.js - details: { - rootDir: this.rootDir, - nounCount: nouns.length, - verbCount: verbs.length - } - } - } -} diff --git a/src/storage/opfsStorage.ts b/src/storage/opfsStorage.ts deleted file mode 100644 index 137a9a12..00000000 --- a/src/storage/opfsStorage.ts +++ /dev/null @@ -1,1550 +0,0 @@ -/** - * OPFS (Origin Private File System) Storage Adapter - * Provides persistent storage for the vector database using the Origin Private File System API - */ - -import {GraphVerb, HNSWNoun, StorageAdapter} from '../coreTypes.js' -import '../types/fileSystemTypes.js' -// Make sure TypeScript recognizes the FileSystemDirectoryHandle interface extension -declare global { - interface FileSystemDirectoryHandle { - entries(): AsyncIterableIterator<[string, FileSystemHandle]> - } -} - -// Type aliases for compatibility -type HNSWNode = HNSWNoun -type Edge = GraphVerb - -// Directory and file names -const ROOT_DIR = 'opfs-vector-db' - -const NOUNS_DIR = 'nouns' -const VERBS_DIR = 'verbs' -const METADATA_DIR = 'metadata' -const DB_INFO_FILE = 'db-info.json' - -// All nouns now use the same directory - no separate directories per noun type - -export class OPFSStorage implements StorageAdapter { - private rootDir: FileSystemDirectoryHandle | null = null - private nounsDir: FileSystemDirectoryHandle | null = null - private verbsDir: FileSystemDirectoryHandle | null = null - private metadataDir: FileSystemDirectoryHandle | null = null - private isInitialized = false - private isAvailable = false - private isPersistentRequested = false - private isPersistentGranted = false - - constructor() { - // Check if OPFS is available - this.isAvailable = - typeof navigator !== 'undefined' && - 'storage' in navigator && - 'getDirectory' in navigator.storage - } - - /** - * Initialize the storage adapter - */ - public async init(): Promise { - if (this.isInitialized) { - return - } - - if (!this.isAvailable) { - throw new Error( - 'Origin Private File System is not available in this environment' - ) - } - - try { - // Get the root directory - const root = await navigator.storage.getDirectory() - - // Create or get our app's root directory - this.rootDir = await root.getDirectoryHandle(ROOT_DIR, {create: true}) - - // Create or get nouns directory - this.nounsDir = await this.rootDir.getDirectoryHandle(NOUNS_DIR, { - create: true - }) - - // Create or get verbs directory - this.verbsDir = await this.rootDir.getDirectoryHandle(VERBS_DIR, { - create: true - }) - - // Create or get metadata directory - this.metadataDir = await this.rootDir.getDirectoryHandle(METADATA_DIR, { - create: true - }) - - this.isInitialized = true - } catch (error) { - console.error('Failed to initialize OPFS storage:', error) - throw new Error(`Failed to initialize OPFS storage: ${error}`) - } - } - - /** - * Check if OPFS is available in the current environment - */ - public isOPFSAvailable(): boolean { - return this.isAvailable - } - - /** - * Request persistent storage permission from the user - * @returns Promise that resolves to true if permission was granted, false otherwise - */ - public async requestPersistentStorage(): Promise { - if (!this.isAvailable) { - console.warn('Cannot request persistent storage: OPFS is not available') - return false - } - - try { - // Check if persistence is already granted - this.isPersistentGranted = await navigator.storage.persisted() - - if (!this.isPersistentGranted) { - // Request permission for persistent storage - this.isPersistentGranted = await navigator.storage.persist() - } - - this.isPersistentRequested = true - return this.isPersistentGranted - } catch (error) { - console.warn('Failed to request persistent storage:', error) - return false - } - } - - /** - * Check if persistent storage is granted - * @returns Promise that resolves to true if persistent storage is granted, false otherwise - */ - public async isPersistent(): Promise { - if (!this.isAvailable) { - return false - } - - try { - this.isPersistentGranted = await navigator.storage.persisted() - return this.isPersistentGranted - } catch (error) { - console.warn('Failed to check persistent storage status:', error) - return false - } - } - - /** - * Save a noun to storage - */ - public async saveNoun(noun: HNSWNoun): Promise { - await this.ensureInitialized() - - try { - // Convert connections Map to a serializable format - const serializableNoun = { - ...noun, - connections: this.mapToObject(noun.connections, (set) => - Array.from(set as Set) - ) - } - - // Get the appropriate directory based on the noun's metadata - const nounDir = await this.getNodeDirectory(noun.id) - - // Create or get the file for this noun - const fileHandle = await nounDir.getFileHandle(noun.id, { - create: true - }) - - // Write the noun data to the file - const writable = await fileHandle.createWritable() - await writable.write(JSON.stringify(serializableNoun)) - await writable.close() - } catch (error) { - console.error(`Failed to save noun ${noun.id}:`, error) - throw new Error(`Failed to save noun ${noun.id}: ${error}`) - } - } - - /** - * Get a noun from storage - */ - public async getNoun(id: string): Promise { - await this.ensureInitialized() - - try { - // Get the nouns directory - all nouns are now in the same directory - const nounsDir = this.nounsDir! - - try { - // Get the file handle for this node - const fileHandle = await nounsDir.getFileHandle(id) - - // Read the node data from the file - const file = await fileHandle.getFile() - const text = await file.text() - const data = JSON.parse(text) - - // Convert serialized connections back to Map> - const connections = new Map>() - for (const [level, nodeIds] of Object.entries(data.connections)) { - connections.set(Number(level), new Set(nodeIds as string[])) - } - - return { - id: data.id, - vector: data.vector, - connections - } - } catch (error) { - // File doesn't exist - return null - } - } catch (error) { - console.error(`Failed to get node ${id}:`, error) - throw new Error(`Failed to get node ${id}: ${error}`) - } - } - - /** - * Get nouns by noun type - * @param nounType The noun type to filter by - * @returns Promise that resolves to an array of nouns of the specified noun type - */ - public async getNounsByNounType(nounType: string): Promise { - await this.ensureInitialized() - - try { - const nouns: HNSWNoun[] = [] - - // Use the consolidated nouns directory - const dir = this.nounsDir! - - try { - // Get all entries (filename and handle pairs) in this directory - const entries = dir.entries() - - // Iterate through all entries and get the corresponding nodes - for await (const [name, handle] of entries) { - try { - // The handle is already a FileSystemHandle, but we need to ensure it's a file - if (handle.kind !== 'file') continue - - // Cast to FileSystemFileHandle - const fileHandle = handle as FileSystemFileHandle - - // Read the node data from the file - const file = await fileHandle.getFile() - const text = await file.text() - const data = JSON.parse(text) - - // Get the metadata to check the noun type - const metadata = await this.getMetadata(data.id) - - // Skip if metadata doesn't exist or noun type doesn't match - if (!metadata || metadata.noun !== nounType) { - continue - } - - // Convert serialized connections back to Map> - const connections = new Map>() - for (const [level, nodeIds] of Object.entries(data.connections)) { - connections.set(Number(level), new Set(nodeIds as string[])) - } - - nouns.push({ - id: data.id, - vector: data.vector, - connections - }) - } catch (nodeError) { - console.warn( - `Failed to read node ${name} from directory:`, - nodeError - ) - // Continue to the next node - } - } - } catch (dirError) { - console.warn( - `Failed to read directory for noun type ${nounType}:`, - dirError - ) - } - - return nouns - } catch (error) { - console.error(`Failed to get nouns for noun type ${nounType}:`, error) - throw new Error(`Failed to get nouns for noun type ${nounType}: ${error}`) - } - } - - /** - * Get all nouns from storage - */ - public async getAllNouns(): Promise { - await this.ensureInitialized() - - try { - const nouns: HNSWNoun[] = [] - - // Use the consolidated nouns directory - const dir = this.nounsDir! - - try { - // Get all entries (filename and handle pairs) in this directory - const entries = dir.entries() - - // Iterate through all entries and get the corresponding nodes - for await (const [name, handle] of entries) { - try { - // The handle is already a FileSystemHandle, but we need to ensure it's a file - if (handle.kind !== 'file') continue - - // Cast to FileSystemFileHandle - const fileHandle = handle as FileSystemFileHandle - - // Read the node data from the file - const file = await fileHandle.getFile() - const text = await file.text() - const data = JSON.parse(text) - - // Convert serialized connections back to Map> - const connections = new Map>() - for (const [level, nodeIds] of Object.entries(data.connections)) { - connections.set(Number(level), new Set(nodeIds as string[])) - } - - nouns.push({ - id: data.id, - vector: data.vector, - connections - }) - } catch (nodeError) { - console.warn( - `Failed to read node ${name} from directory:`, - nodeError - ) - // Continue to the next node - } - } - } catch (dirError) { - console.warn( - `Failed to read nouns directory:`, - dirError - ) - } - - return nouns - } catch (error) { - console.error('Failed to get all nouns:', error) - throw new Error(`Failed to get all nouns: ${error}`) - } - } - - /** - * Delete a noun from storage - */ - public async deleteNoun(id: string): Promise { - await this.ensureInitialized() - - try { - // Get the nouns directory - all nouns are now in the same directory - const nounsDir = this.nounsDir! - - try { - // Try to delete the noun from the nouns directory - await nounsDir.removeEntry(id) - return // Noun deleted successfully - } catch (error) { - // If the file doesn't exist, that's fine - return - } - } catch (error) { - console.error(`Failed to delete noun ${id}:`, error) - throw new Error(`Failed to delete noun ${id}: ${error}`) - } - } - - /** - * Save a verb to storage - */ - public async saveVerb(verb: GraphVerb): Promise { - await this.ensureInitialized() - - try { - // Convert connections Map to a serializable format - const serializableVerb = { - ...verb, - connections: this.mapToObject(verb.connections, (set) => - Array.from(set as Set) - ) - } - - // Create or get the file for this verb - const fileHandle = await this.verbsDir!.getFileHandle(verb.id, { - create: true - }) - - // Write the verb data to the file - const writable = await fileHandle.createWritable() - await writable.write(JSON.stringify(serializableVerb)) - await writable.close() - } catch (error) { - console.error(`Failed to save verb ${verb.id}:`, error) - throw new Error(`Failed to save verb ${verb.id}: ${error}`) - } - } - - /** - * Get a verb from storage - */ - public async getVerb(id: string): Promise { - await this.ensureInitialized() - - try { - // Get the file handle for this verb - const fileHandle = await this.verbsDir!.getFileHandle(id) - - // Read the verb data from the file - const file = await fileHandle.getFile() - const text = await file.text() - const data = JSON.parse(text) - - // Convert serialized connections back to Map> - const connections = new Map>() - for (const [level, nodeIds] of Object.entries(data.connections)) { - connections.set(Number(level), new Set(nodeIds as string[])) - } - - return { - id: data.id, - vector: data.vector, - connections, - sourceId: data.sourceId, - targetId: data.targetId, - type: data.type, - weight: data.weight, - metadata: data.metadata - } - } catch (error) { - // If the file doesn't exist, return null - if ((error as any).name === 'NotFoundError') { - return null - } - - console.error(`Failed to get verb ${id}:`, error) - throw new Error(`Failed to get verb ${id}: ${error}`) - } - } - - /** - * Get all edges from storage - */ - public async getAllEdges(): Promise { - await this.ensureInitialized() - - try { - const edges: Edge[] = [] - - // Get all entries (filename and handle pairs) in the verbs directory - const entries = this.verbsDir!.entries() - - // Iterate through all entries and get the corresponding edges - for await (const [name, handle] of entries) { - // Skip if not a file - if (handle.kind !== 'file') continue - - const edge = await this.getVerb(name) - if (edge) { - edges.push(edge) - } - } - - return edges - } catch (error) { - console.error('Failed to get all edges:', error) - throw new Error(`Failed to get all edges: ${error}`) - } - } - - /** - * Get all verbs from storage (alias for getAllEdges) - */ - public async getAllVerbs(): Promise { - return this.getAllEdges() - } - - /** - * Delete an edge from storage - */ - public async deleteEdge(id: string): Promise { - await this.ensureInitialized() - - try { - await this.verbsDir!.removeEntry(id) - } catch (error) { - // Ignore if the file doesn't exist - if ((error as any).name !== 'NotFoundError') { - console.error(`Failed to delete edge ${id}:`, error) - throw new Error(`Failed to delete edge ${id}: ${error}`) - } - } - } - - /** - * Delete a verb from storage (alias for deleteEdge) - */ - public async deleteVerb(id: string): Promise { - return this.deleteEdge(id) - } - - /** - * Get edges by source node ID - */ - public async getEdgesBySource(sourceId: string): Promise { - await this.ensureInitialized() - - try { - const allEdges = await this.getAllEdges() - return allEdges.filter((edge) => edge.sourceId === sourceId) - } catch (error) { - console.error(`Failed to get edges by source ${sourceId}:`, error) - throw new Error(`Failed to get edges by source ${sourceId}: ${error}`) - } - } - - /** - * Get verbs by source node ID (alias for getEdgesBySource) - */ - public async getVerbsBySource(sourceId: string): Promise { - return this.getEdgesBySource(sourceId) - } - - /** - * Get edges by target node ID - */ - public async getEdgesByTarget(targetId: string): Promise { - await this.ensureInitialized() - - try { - const allEdges = await this.getAllEdges() - return allEdges.filter((edge) => edge.targetId === targetId) - } catch (error) { - console.error(`Failed to get edges by target ${targetId}:`, error) - throw new Error(`Failed to get edges by target ${targetId}: ${error}`) - } - } - - /** - * Get verbs by target node ID (alias for getEdgesByTarget) - */ - public async getVerbsByTarget(targetId: string): Promise { - return this.getEdgesByTarget(targetId) - } - - /** - * Get edges by type - */ - public async getEdgesByType(type: string): Promise { - await this.ensureInitialized() - - try { - const allEdges = await this.getAllEdges() - return allEdges.filter((edge) => edge.type === type) - } catch (error) { - console.error(`Failed to get edges by type ${type}:`, error) - throw new Error(`Failed to get edges by type ${type}: ${error}`) - } - } - - /** - * Get verbs by type (alias for getEdgesByType) - */ - public async getVerbsByType(type: string): Promise { - return this.getEdgesByType(type) - } - - /** - * Save metadata for a node - */ - public async saveMetadata(id: string, metadata: any): Promise { - await this.ensureInitialized() - - try { - // Create or get the file for this metadata - const fileHandle = await this.metadataDir!.getFileHandle(id, { - create: true - }) - - // Write the metadata to the file - const writable = await fileHandle.createWritable() - await writable.write(JSON.stringify(metadata)) - await writable.close() - } catch (error) { - console.error(`Failed to save metadata for ${id}:`, error) - throw new Error(`Failed to save metadata for ${id}: ${error}`) - } - } - - /** - * Get metadata for a node - */ - public async getMetadata(id: string): Promise { - await this.ensureInitialized() - - try { - // Get the file handle for this metadata - const fileHandle = await this.metadataDir!.getFileHandle(id) - - // Read the metadata from the file - const file = await fileHandle.getFile() - const text = await file.text() - return JSON.parse(text) - } catch (error) { - // If the file doesn't exist, return null - if ((error as any).name === 'NotFoundError') { - return null - } - - console.error(`Failed to get metadata for ${id}:`, error) - throw new Error(`Failed to get metadata for ${id}: ${error}`) - } - } - - /** - * Clear all data from storage - */ - public async clear(): Promise { - await this.ensureInitialized() - - try { - // Delete and recreate the nouns directory - await this.rootDir!.removeEntry(NOUNS_DIR, {recursive: true}) - this.nounsDir = await this.rootDir!.getDirectoryHandle(NOUNS_DIR, { - create: true - }) - - // Delete and recreate the verbs directory - await this.rootDir!.removeEntry(VERBS_DIR, {recursive: true}) - this.verbsDir = await this.rootDir!.getDirectoryHandle(VERBS_DIR, { - create: true - }) - - // Delete and recreate the metadata directory - await this.rootDir!.removeEntry(METADATA_DIR, {recursive: true}) - this.metadataDir = await this.rootDir!.getDirectoryHandle(METADATA_DIR, { - create: true - }) - } catch (error) { - console.error('Failed to clear storage:', error) - throw new Error(`Failed to clear storage: ${error}`) - } - } - - /** - * Ensure the storage adapter is initialized - */ - private async ensureInitialized(): Promise { - if (!this.isInitialized) { - await this.init() - } - } - - /** - * Convert a Map to a plain object for serialization - */ - private mapToObject( - map: Map, - valueTransformer: (value: V) => any = (v) => v - ): Record { - const obj: Record = {} - for (const [key, value] of map.entries()) { - obj[key.toString()] = valueTransformer(value) - } - return obj - } - - /** - * Get the directory for a node - now all nouns use the same directory - */ - private async getNodeDirectory( - id: string - ): Promise { - // All nouns now use the same directory regardless of type - return this.nounsDir! - } - - /** - * Get information about storage usage and capacity - */ - public async getStorageStatus(): Promise<{ - type: string - used: number - quota: number | null - details?: Record - }> { - await this.ensureInitialized() - - try { - // Calculate the total size of all files in the storage directories - let totalSize = 0 - - // Helper function to calculate directory size - const calculateDirSize = async ( - dirHandle: FileSystemDirectoryHandle - ): Promise => { - let size = 0 - try { - for await (const [name, handle] of dirHandle.entries()) { - if (handle.kind === 'file') { - const file = await (handle as FileSystemFileHandle).getFile() - size += file.size - } else if (handle.kind === 'directory') { - size += await calculateDirSize( - handle as FileSystemDirectoryHandle - ) - } - } - } catch (error) { - console.warn(`Error calculating size for directory:`, error) - } - return size - } - - // Helper function to count files in a directory - const countFilesInDirectory = async ( - dirHandle: FileSystemDirectoryHandle - ): Promise => { - let count = 0 - try { - for await (const [name, handle] of dirHandle.entries()) { - if (handle.kind === 'file') { - count++ - } - } - } catch (error) { - console.warn(`Error counting files in directory:`, error) - } - return count - } - - // Calculate size for each directory - if (this.nounsDir) { - totalSize += await calculateDirSize(this.nounsDir) - } - if (this.verbsDir) { - totalSize += await calculateDirSize(this.verbsDir) - } - if (this.metadataDir) { - totalSize += await calculateDirSize(this.metadataDir) - } - - // Get storage quota information using the Storage API - let quota = null - let details: Record = { - isPersistent: await this.isPersistent(), - nounTypes: { - // Count nouns by type using metadata - // This will be populated later if needed - } - } - - try { - if (navigator.storage && navigator.storage.estimate) { - const estimate = await navigator.storage.estimate() - quota = estimate.quota || null - details = { - ...details, - usage: estimate.usage, - quota: estimate.quota, - freePercentage: estimate.quota - ? ((estimate.quota - (estimate.usage || 0)) / estimate.quota) * - 100 - : null - } - } - } catch (error) { - console.warn('Unable to get storage estimate:', error) - } - - return { - type: 'opfs', - used: totalSize, - quota, - details - } - } catch (error) { - console.error('Failed to get storage status:', error) - return { - type: 'opfs', - used: 0, - quota: null, - details: {error: String(error)} - } - } - } -} - -/** - * In-memory storage adapter for environments where OPFS is not available - */ -export class MemoryStorage implements StorageAdapter { - // Single map of noun ID to noun - private nouns: Map = new Map() - private verbs: Map = new Map() - private metadata: Map = new Map() - - constructor() { - // No need to initialize separate maps for each noun type - } - - // Alias methods to match StorageAdapter interface - public async saveNoun(noun: HNSWNoun): Promise { - return this.saveNode(noun) - } - - public async getNoun(id: string): Promise { - return this.getNode(id) - } - - public async getAllNouns(): Promise { - return this.getAllNodes() - } - - public async getNounsByNounType(nounType: string): Promise { - return this.getNodesByNounType(nounType) - } - - public async deleteNoun(id: string): Promise { - return this.deleteNode(id) - } - - public async saveVerb(verb: GraphVerb): Promise { - return this.saveEdge(verb) - } - - public async getVerb(id: string): Promise { - return this.getEdge(id) - } - - public async getAllVerbs(): Promise { - return this.getAllEdges() - } - - public async getVerbsBySource(sourceId: string): Promise { - return this.getEdgesBySource(sourceId) - } - - public async getVerbsByTarget(targetId: string): Promise { - return this.getEdgesByTarget(targetId) - } - - public async getVerbsByType(type: string): Promise { - return this.getEdgesByType(type) - } - - public async deleteVerb(id: string): Promise { - return this.deleteEdge(id) - } - - public async init(): Promise { - // Nothing to initialize for in-memory storage - } - - /** - * Get the noun type for a node from its metadata - */ - private async getNounType(id: string): Promise { - try { - // Try to get the metadata for the node - const metadata = await this.getMetadata(id) - - // If metadata exists and has a noun field, return it - if (metadata && metadata.noun) { - return metadata.noun - } - - // If no metadata or no noun field, return null - return null - } catch (error) { - // If there's an error getting the metadata, return null - return null - } - } - - public async saveNode(node: HNSWNode): Promise { - // Create a deep copy to avoid reference issues - const nodeCopy: HNSWNode = { - id: node.id, - vector: [...node.vector], - connections: new Map() - } - - // Copy connections - for (const [level, connections] of node.connections.entries()) { - nodeCopy.connections.set(level, new Set(connections)) - } - - // Save the node directly in the nouns map - this.nouns.set(node.id, nodeCopy) - } - - public async getNode(id: string): Promise { - // Get the node directly from the nouns map - const node = this.nouns.get(id) - - // If not found, return null - if (!node) { - return null - } - - // Return a deep copy to avoid reference issues - const nodeCopy: HNSWNode = { - id: node.id, - vector: [...node.vector], - connections: new Map() - } - - // Copy connections - for (const [level, connections] of node.connections.entries()) { - nodeCopy.connections.set(level, new Set(connections)) - } - - return nodeCopy - } - - /** - * Get nodes by noun type - * @param nounType The noun type to filter by - * @returns Promise that resolves to an array of nodes of the specified noun type - */ - public async getNodesByNounType(nounType: string): Promise { - const nodes: HNSWNode[] = [] - - // Iterate through all nodes and filter by noun type using metadata - for (const [nodeId, node] of this.nouns.entries()) { - // Get the metadata to check the noun type - const metadata = await this.getMetadata(nodeId) - - // Include the node if its noun type matches the requested type - if (metadata && metadata.noun === nounType) { - // Return a deep copy to avoid reference issues - const nodeCopy: HNSWNode = { - id: node.id, - vector: [...node.vector], - connections: new Map() - } - - // Copy connections - for (const [level, connections] of node.connections.entries()) { - nodeCopy.connections.set(level, new Set(connections)) - } - - nodes.push(nodeCopy) - } - } - - return nodes - } - - public async getAllNodes(): Promise { - const allNodes: HNSWNode[] = [] - - // Iterate through all nodes in the nouns map - for (const [nodeId, node] of this.nouns.entries()) { - // Return a deep copy to avoid reference issues - const nodeCopy: HNSWNode = { - id: node.id, - vector: [...node.vector], - connections: new Map() - } - - // Copy connections - for (const [level, connections] of node.connections.entries()) { - nodeCopy.connections.set(level, new Set(connections)) - } - - allNodes.push(nodeCopy) - } - - return allNodes - } - - public async deleteNode(id: string): Promise { - // Delete the node directly from the nouns map - this.nouns.delete(id) - } - - public async saveMetadata(id: string, metadata: any): Promise { - this.metadata.set(id, JSON.parse(JSON.stringify(metadata))) - } - - public async getMetadata(id: string): Promise { - const metadata = this.metadata.get(id) - if (!metadata) { - return null - } - - return JSON.parse(JSON.stringify(metadata)) - } - - public async saveEdge(edge: Edge): Promise { - // Create a deep copy to avoid reference issues - const edgeCopy: Edge = { - id: edge.id, - vector: [...edge.vector], - connections: new Map(), - sourceId: edge.sourceId, - targetId: edge.targetId, - type: edge.type, - weight: edge.weight, - metadata: edge.metadata - ? JSON.parse(JSON.stringify(edge.metadata)) - : undefined - } - - // Copy connections - for (const [level, connections] of edge.connections.entries()) { - edgeCopy.connections.set(level, new Set(connections)) - } - - this.verbs.set(edge.id, edgeCopy) - } - - public async getEdge(id: string): Promise { - const edge = this.verbs.get(id) - if (!edge) { - return null - } - - // Return a deep copy to avoid reference issues - const edgeCopy: Edge = { - id: edge.id, - vector: [...edge.vector], - connections: new Map(), - sourceId: edge.sourceId, - targetId: edge.targetId, - type: edge.type, - weight: edge.weight, - metadata: edge.metadata - ? JSON.parse(JSON.stringify(edge.metadata)) - : undefined - } - - // Copy connections - for (const [level, connections] of edge.connections.entries()) { - edgeCopy.connections.set(level, new Set(connections)) - } - - return edgeCopy - } - - public async getAllEdges(): Promise { - const edges: Edge[] = [] - - for (const edgeId of this.verbs.keys()) { - const edge = await this.getEdge(edgeId) - if (edge) { - edges.push(edge) - } - } - - return edges - } - - public async getEdgesBySource(sourceId: string): Promise { - const edges: Edge[] = [] - - for (const edge of this.verbs.values()) { - if (edge.sourceId === sourceId) { - const edgeCopy = await this.getEdge(edge.id) - if (edgeCopy) { - edges.push(edgeCopy) - } - } - } - - return edges - } - - public async getEdgesByTarget(targetId: string): Promise { - const edges: Edge[] = [] - - for (const edge of this.verbs.values()) { - if (edge.targetId === targetId) { - const edgeCopy = await this.getEdge(edge.id) - if (edgeCopy) { - edges.push(edgeCopy) - } - } - } - - return edges - } - - public async getEdgesByType(type: string): Promise { - const edges: Edge[] = [] - - for (const edge of this.verbs.values()) { - if (edge.type === type) { - const edgeCopy = await this.getEdge(edge.id) - if (edgeCopy) { - edges.push(edgeCopy) - } - } - } - - return edges - } - - public async deleteEdge(id: string): Promise { - this.verbs.delete(id) - } - - public async clear(): Promise { - // Clear the single nouns map - this.nouns.clear() - this.verbs.clear() - this.metadata.clear() - } - - /** - * Get information about storage usage and capacity - */ - public async getStorageStatus(): Promise<{ - type: string - used: number - quota: number | null - details?: Record - }> { - try { - // Estimate the size of data in memory - let totalSize = 0 - - // Helper function to estimate object size in bytes - const estimateSize = (obj: any): number => { - if (obj === null || obj === undefined) return 0 - - const type = typeof obj - - // Handle primitive types - if (type === 'number') return 8 - if (type === 'string') return obj.length * 2 - if (type === 'boolean') return 4 - - // Handle arrays and objects - if (Array.isArray(obj)) { - return obj.reduce((size, item) => size + estimateSize(item), 0) - } - - if (type === 'object') { - if (obj instanceof Map) { - let mapSize = 0 - for (const [key, value] of obj.entries()) { - mapSize += estimateSize(key) + estimateSize(value) - } - return mapSize - } - - if (obj instanceof Set) { - let setSize = 0 - for (const item of obj) { - setSize += estimateSize(item) - } - return setSize - } - - // Regular object - return Object.entries(obj).reduce( - (size, [key, value]) => size + key.length * 2 + estimateSize(value), - 0 - ) - } - - return 0 - } - - // Calculate sizes and counts for each noun type - const nounTypeDetails: Record = {} - - // Initialize counts for all noun types - const nodeTypes = [ - 'person', - 'place', - 'thing', - 'event', - 'concept', - 'content', - 'group', - 'list', - 'category', - 'default' - ] - - for (const type of nodeTypes) { - nounTypeDetails[type] = { - size: 0, - count: 0 - } - } - - let totalNodeCount = 0 - let nodesSize = 0 - - // Process all nouns and categorize them by type using metadata - for (const [nodeId, node] of this.nouns.entries()) { - // Get the metadata to determine the noun type - const metadata = this.metadata.get(nodeId) - const nounType = metadata?.noun || 'default' - - // Calculate size of this node - const nodeSize = estimateSize(node) - - // Update counts for this noun type - if (nounTypeDetails[nounType]) { - nounTypeDetails[nounType].size += nodeSize - nounTypeDetails[nounType].count += 1 - } else { - nounTypeDetails.default.size += nodeSize - nounTypeDetails.default.count += 1 - } - - // Update totals - nodesSize += nodeSize - totalNodeCount += 1 - } - - totalSize += nodesSize - - // Estimate size of edges - let edgesSize = 0 - for (const edge of this.verbs.values()) { - edgesSize += estimateSize(edge) - } - totalSize += edgesSize - - // Estimate size of metadata - let metadataSize = 0 - for (const meta of this.metadata.values()) { - metadataSize += estimateSize(meta) - } - totalSize += metadataSize - - // Get memory information if available - let quota = null - let details: Record = { - nodeCount: totalNodeCount, - edgeCount: this.verbs.size, - metadataCount: this.metadata.size, - nounTypes: nounTypeDetails, - nodesSize, - edgesSize, - metadataSize - } - - // Try to get memory information if in a browser environment - if ( - typeof window !== 'undefined' && - (window as any).performance && - (window as any).performance.memory - ) { - const memory = (window as any).performance.memory - quota = memory.jsHeapSizeLimit - details = { - ...details, - totalJSHeapSize: memory.totalJSHeapSize, - usedJSHeapSize: memory.usedJSHeapSize, - jsHeapSizeLimit: memory.jsHeapSizeLimit - } - } - - return { - type: 'memory', - used: totalSize, - quota, - details - } - } catch (error) { - console.error('Failed to get memory storage status:', error) - return { - type: 'memory', - used: 0, - quota: null, - details: {error: String(error)} - } - } - } -} - -/** - * Factory function to create the appropriate storage adapter based on the environment - * @param options Configuration options for storage - * @returns Promise that resolves to a StorageAdapter instance - */ -export async function createStorage( - options: { - requestPersistentStorage?: boolean - r2Storage?: { - bucketName?: string - accountId?: string - accessKeyId?: string - secretAccessKey?: string - } - s3Storage?: { - bucketName?: string - accessKeyId?: string - secretAccessKey?: string - region?: string - } - gcsStorage?: { - bucketName?: string - accessKeyId?: string - secretAccessKey?: string - endpoint?: string - } - customS3Storage?: { - bucketName?: string - accessKeyId?: string - secretAccessKey?: string - endpoint?: string - region?: string - } - forceFileSystemStorage?: boolean - forceMemoryStorage?: boolean - } = {} -): Promise { - // Detect environment - const environment = { - isBrowser: typeof window !== 'undefined', - isNode: - typeof process !== 'undefined' && - process.versions != null && - process.versions.node != null, - isServerless: - typeof window === 'undefined' && - (typeof process === 'undefined' || - !process.versions || - !process.versions.node) - } - - // If force memory storage is specified, use MemoryStorage regardless of environment - if (options.forceMemoryStorage) { - console.log('Using in-memory storage (forced by configuration)') - return new MemoryStorage() - } - - // Default empty values for environment variables - const defaultEnvStorage = { - bucketName: undefined, - accountId: undefined, - accessKeyId: undefined, - secretAccessKey: undefined, - region: undefined, - endpoint: undefined - } - - // Try to use cloud storage if configured - if ( - options.r2Storage || - options.s3Storage || - options.gcsStorage || - options.customS3Storage || - (environment.isNode && - (process.env.R2_BUCKET_NAME || - process.env.S3_BUCKET_NAME || - process.env.GCS_BUCKET_NAME)) - ) { - try { - // Only try to access process.env in Node.js environment - const envR2Storage = environment.isNode - ? { - bucketName: process.env.R2_BUCKET_NAME, - accountId: process.env.R2_ACCOUNT_ID, - accessKeyId: process.env.R2_ACCESS_KEY_ID, - secretAccessKey: process.env.R2_SECRET_ACCESS_KEY - } - : defaultEnvStorage - - const envS3Storage = environment.isNode - ? { - bucketName: process.env.S3_BUCKET_NAME, - accessKeyId: - process.env.S3_ACCESS_KEY_ID || process.env.AWS_ACCESS_KEY_ID, - secretAccessKey: - process.env.S3_SECRET_ACCESS_KEY || - process.env.AWS_SECRET_ACCESS_KEY, - region: process.env.S3_REGION || process.env.AWS_REGION - } - : defaultEnvStorage - - const envGCSStorage = environment.isNode - ? { - bucketName: process.env.GCS_BUCKET_NAME, - accessKeyId: process.env.GCS_ACCESS_KEY_ID, - secretAccessKey: process.env.GCS_SECRET_ACCESS_KEY, - endpoint: process.env.GCS_ENDPOINT - } - : defaultEnvStorage - - // Merge environment variables with provided options - const mergedOptions = { - ...options, - r2Storage: options.r2Storage - ? { - ...envR2Storage, - ...options.r2Storage - } - : // Only use environment variables if they have the required fields - envR2Storage.bucketName && - envR2Storage.accessKeyId && - envR2Storage.secretAccessKey - ? envR2Storage - : undefined, - s3Storage: options.s3Storage - ? { - ...envS3Storage, - ...options.s3Storage - } - : // Only use environment variables if they have the required fields - envS3Storage.bucketName && - envS3Storage.accessKeyId && - envS3Storage.secretAccessKey - ? envS3Storage - : undefined, - gcsStorage: options.gcsStorage - ? { - ...envGCSStorage, - ...options.gcsStorage - } - : // Only use environment variables if they have the required fields - envGCSStorage.bucketName && - envGCSStorage.accessKeyId && - envGCSStorage.secretAccessKey - ? envGCSStorage - : undefined - } - - const s3Module = await import('./s3CompatibleStorage.js') - - if ( - mergedOptions.r2Storage && - mergedOptions.r2Storage.bucketName && - mergedOptions.r2Storage.accountId && - mergedOptions.r2Storage.accessKeyId && - mergedOptions.r2Storage.secretAccessKey - ) { - console.log('Using Cloudflare R2 storage') - return new s3Module.R2Storage({ - bucketName: mergedOptions.r2Storage.bucketName, - accountId: mergedOptions.r2Storage.accountId, - accessKeyId: mergedOptions.r2Storage.accessKeyId, - secretAccessKey: mergedOptions.r2Storage.secretAccessKey, - serviceType: 'r2' - }) - } else if ( - mergedOptions.s3Storage && - mergedOptions.s3Storage.bucketName && - mergedOptions.s3Storage.accessKeyId && - mergedOptions.s3Storage.secretAccessKey - ) { - console.log('Using Amazon S3 storage') - return new s3Module.S3CompatibleStorage({ - bucketName: mergedOptions.s3Storage.bucketName, - accessKeyId: mergedOptions.s3Storage.accessKeyId, - secretAccessKey: mergedOptions.s3Storage.secretAccessKey, - region: mergedOptions.s3Storage.region, - serviceType: 's3' - }) - } else if ( - mergedOptions.gcsStorage && - mergedOptions.gcsStorage.bucketName && - mergedOptions.gcsStorage.accessKeyId && - mergedOptions.gcsStorage.secretAccessKey - ) { - console.log('Using Google Cloud Storage') - return new s3Module.S3CompatibleStorage({ - bucketName: mergedOptions.gcsStorage.bucketName, - accessKeyId: mergedOptions.gcsStorage.accessKeyId, - secretAccessKey: mergedOptions.gcsStorage.secretAccessKey, - endpoint: mergedOptions.gcsStorage.endpoint, - serviceType: 'gcs' - }) - } else if ( - mergedOptions.customS3Storage && - mergedOptions.customS3Storage.bucketName && - mergedOptions.customS3Storage.accessKeyId && - mergedOptions.customS3Storage.secretAccessKey - ) { - console.log('Using custom S3-compatible storage') - return new s3Module.S3CompatibleStorage({ - bucketName: mergedOptions.customS3Storage.bucketName, - accessKeyId: mergedOptions.customS3Storage.accessKeyId, - secretAccessKey: mergedOptions.customS3Storage.secretAccessKey, - endpoint: mergedOptions.customS3Storage.endpoint, - region: mergedOptions.customS3Storage.region, - serviceType: 'custom' - }) - } - } catch (error) { - console.warn( - 'Failed to load S3CompatibleStorage, falling back to environment-specific storage:', - error - ) - // Continue to environment-specific storage selection - } - } - - // Environment-specific storage selection - if (environment.isBrowser) { - // In browser environments - if (!options.forceFileSystemStorage) { - try { - // Try OPFS first (Origin Private File System - browser-specific) - const opfsStorage = new OPFSStorage() - - if (opfsStorage.isOPFSAvailable()) { - // Request persistent storage if specified - if (options.requestPersistentStorage) { - const isPersistentGranted = - await opfsStorage.requestPersistentStorage() - if (isPersistentGranted) { - console.log('Persistent storage permission granted') - } else { - console.warn('Persistent storage permission denied') - } - } - console.log('Using Origin Private File System (OPFS) storage') - return opfsStorage - } - } catch (error) { - console.warn('OPFS storage initialization failed:', error) - } - } - - // Fall back to memory storage for browser environments - console.log('Using in-memory storage for browser environment') - return new MemoryStorage() - } else if (environment.isNode) { - // In Node.js environments - try { - const fileSystemModule = await import('./fileSystemStorage.js') - console.log('Using file system storage for Node.js environment') - return new fileSystemModule.FileSystemStorage() - } catch (error) { - console.warn('Failed to load FileSystemStorage:', error) - console.log('Using in-memory storage for Node.js environment') - return new MemoryStorage() - } - } else { - // In serverless or other environments - console.log('Using in-memory storage for serverless/unknown environment') - return new MemoryStorage() - } -} diff --git a/src/storage/s3CompatibleStorage.ts b/src/storage/s3CompatibleStorage.ts deleted file mode 100644 index 291e177d..00000000 --- a/src/storage/s3CompatibleStorage.ts +++ /dev/null @@ -1,952 +0,0 @@ -import { GraphVerb, HNSWNoun, StorageAdapter } from '../coreTypes.js' - -// Type aliases for compatibility -type HNSWNode = HNSWNoun -type Edge = GraphVerb - -// Export R2Storage as an alias for S3CompatibleStorage -export { S3CompatibleStorage as R2Storage } - -// Constants for S3 bucket prefixes -const NOUNS_PREFIX = 'nouns/' -const VERBS_PREFIX = 'verbs/' -const EDGES_PREFIX = 'verbs/' // Alias for VERBS_PREFIX for edge operations -const METADATA_PREFIX = 'metadata/' - -// All nouns now use the same prefix - no separate directories per noun type -const NOUN_PREFIX = 'nouns/' // Single directory for all noun types - -/** - * S3-compatible storage adapter for server environments - * Uses the AWS S3 client to interact with S3-compatible storage services - * including Amazon S3, Cloudflare R2, and Google Cloud Storage - * - * To use this adapter with Cloudflare R2, you need to provide: - * - bucketName: Your bucket name - * - accountId: Your Cloudflare account ID - * - accessKeyId: R2 access key ID - * - secretAccessKey: R2 secret access key - * - serviceType: 'r2' - * - * To use this adapter with Amazon S3, you need to provide: - * - bucketName: Your S3 bucket name - * - accessKeyId: AWS access key ID - * - secretAccessKey: AWS secret access key - * - region: AWS region (e.g., 'us-east-1') - * - serviceType: 's3' - * - * To use this adapter with Google Cloud Storage, you need to provide: - * - bucketName: Your GCS bucket name - * - accessKeyId: HMAC access key - * - secretAccessKey: HMAC secret - * - endpoint: GCS endpoint (e.g., 'https://storage.googleapis.com') - * - serviceType: 'gcs' - * - * For other S3-compatible services, provide: - * - bucketName: Your bucket name - * - accessKeyId: Access key ID - * - secretAccessKey: Secret access key - * - endpoint: Service endpoint URL - * - region: Region (if required) - * - serviceType: 'custom' - */ -export class S3CompatibleStorage implements StorageAdapter { - private bucketName: string - private accessKeyId: string - private secretAccessKey: string - private endpoint?: string - private region?: string - private accountId?: string - private serviceType: 'r2' | 's3' | 'gcs' | 'custom' - private s3Client: any // Will be initialized in init() - private isInitialized = false - - // Alias methods to match StorageAdapter interface - public async saveNoun(noun: HNSWNoun): Promise { - return this.saveNode(noun) - } - - public async getNoun(id: string): Promise { - return this.getNode(id) - } - - public async getAllNouns(): Promise { - return this.getAllNodes() - } - - public async getNounsByNounType(nounType: string): Promise { - return this.getNodesByNounType(nounType) - } - - public async deleteNoun(id: string): Promise { - return this.deleteNode(id) - } - - public async saveVerb(verb: GraphVerb): Promise { - return this.saveEdge(verb) - } - - public async getVerb(id: string): Promise { - return this.getEdge(id) - } - - public async getAllVerbs(): Promise { - return this.getAllEdges() - } - - public async getVerbsBySource(sourceId: string): Promise { - return this.getEdgesBySource(sourceId) - } - - public async getVerbsByTarget(targetId: string): Promise { - return this.getEdgesByTarget(targetId) - } - - public async getVerbsByType(type: string): Promise { - return this.getEdgesByType(type) - } - - public async deleteVerb(id: string): Promise { - return this.deleteEdge(id) - } - - constructor(options: { - bucketName: string - accessKeyId: string - secretAccessKey: string - serviceType: 'r2' | 's3' | 'gcs' | 'custom' - accountId?: string - region?: string - endpoint?: string - }) { - this.bucketName = options.bucketName - this.accessKeyId = options.accessKeyId - this.secretAccessKey = options.secretAccessKey - this.serviceType = options.serviceType - this.accountId = options.accountId - this.region = options.region - this.endpoint = options.endpoint - } - - /** - * Initialize the storage adapter - */ - public async init(): Promise { - if (this.isInitialized) { - return - } - - try { - // Dynamically import the AWS SDK to avoid bundling it in browser environments - try { - const { S3Client } = await import('@aws-sdk/client-s3') - - // Configure the S3 client based on the service type - const clientConfig: any = { - credentials: { - accessKeyId: this.accessKeyId, - secretAccessKey: this.secretAccessKey - } - } - - switch (this.serviceType) { - case 'r2': - if (!this.accountId) { - throw new Error('accountId is required for Cloudflare R2') - } - clientConfig.region = 'auto' // R2 uses 'auto' as the region - clientConfig.endpoint = `https://${this.accountId}.r2.cloudflarestorage.com` - break - - case 's3': - if (!this.region) { - throw new Error('region is required for Amazon S3') - } - clientConfig.region = this.region - // No endpoint needed for standard S3 - break - - case 'gcs': - if (!this.endpoint) { - // Default GCS endpoint if not provided - this.endpoint = 'https://storage.googleapis.com' - } - clientConfig.endpoint = this.endpoint - clientConfig.region = this.region || 'auto' - break - - case 'custom': - if (!this.endpoint) { - throw new Error( - 'endpoint is required for custom S3-compatible services' - ) - } - clientConfig.endpoint = this.endpoint - if (this.region) { - clientConfig.region = this.region - } - break - - default: - throw new Error(`Unsupported service type: ${this.serviceType}`) - } - - // Initialize the S3 client with the configured options - this.s3Client = new S3Client(clientConfig) - - this.isInitialized = true - } catch (importError) { - throw new Error( - `Failed to import AWS SDK: ${importError}. Make sure @aws-sdk/client-s3 is installed.` - ) - } - } catch (error) { - console.error(`Failed to initialize ${this.serviceType} storage:`, error) - throw new Error( - `Failed to initialize ${this.serviceType} storage: ${error}` - ) - } - } - - /** - * Save a node to storage - */ - public async saveNode(node: HNSWNode): Promise { - await this.ensureInitialized() - - try { - // Convert connections Map to a serializable format - const serializableNode = { - ...node, - connections: this.mapToObject(node.connections, (set) => - Array.from(set as Set) - ) - } - - // Get the appropriate prefix based on the node's metadata - const nodePrefix = await this.getNodePrefix(node.id) - - // Import the PutObjectCommand only when needed - const { PutObjectCommand } = await import('@aws-sdk/client-s3') - - // Save the node to S3-compatible storage - await this.s3Client.send( - new PutObjectCommand({ - Bucket: this.bucketName, - Key: `${nodePrefix}${node.id}.json`, - Body: JSON.stringify(serializableNode, null, 2), - ContentType: 'application/json' - }) - ) - } catch (error) { - console.error(`Failed to save node ${node.id}:`, error) - throw new Error(`Failed to save node ${node.id}: ${error}`) - } - } - - /** - * Get a node from storage - */ - public async getNode(id: string): Promise { - await this.ensureInitialized() - - try { - // Import the GetObjectCommand only when needed - const { GetObjectCommand } = await import('@aws-sdk/client-s3') - - // Try to get the node from the consolidated nouns directory - const response = await this.s3Client.send( - new GetObjectCommand({ - Bucket: this.bucketName, - Key: `${NOUN_PREFIX}${id}.json` - }) - ) - - // Convert the response body to a string - const bodyContents = await response.Body.transformToString() - const parsedNode = JSON.parse(bodyContents) - - // Convert serialized connections back to Map> - const connections = new Map>() - for (const [level, nodeIds] of Object.entries(parsedNode.connections)) { - connections.set(Number(level), new Set(nodeIds as string[])) - } - - return { - id: parsedNode.id, - vector: parsedNode.vector, - connections - } - } catch (error) { - // Node not found or other error - return null - } - } - - /** - * Get nodes by noun type - * @param nounType The noun type to filter by - * @returns Promise that resolves to an array of nodes of the specified noun type - */ - public async getNodesByNounType(nounType: string): Promise { - await this.ensureInitialized() - - try { - // Import the ListObjectsV2Command and GetObjectCommand only when needed - const { ListObjectsV2Command, GetObjectCommand } = await import( - '@aws-sdk/client-s3' - ) - - // List all objects in the consolidated nouns directory - const listResponse = await this.s3Client.send( - new ListObjectsV2Command({ - Bucket: this.bucketName, - Prefix: NOUN_PREFIX - }) - ) - - const nodes: HNSWNode[] = [] - - // If there are no objects, return an empty array - if (!listResponse.Contents || listResponse.Contents.length === 0) { - return nodes - } - - // Get each node and filter by noun type - const nodePromises = listResponse.Contents.map( - async (object: { Key: string }) => { - try { - // Extract node ID from the key (remove prefix and .json extension) - const nodeId = object.Key.replace(NOUN_PREFIX, '').replace('.json', '') - - // Get the metadata to check the noun type - const metadata = await this.getMetadata(nodeId) - - // Skip if metadata doesn't exist or noun type doesn't match - if (!metadata || metadata.noun !== nounType) { - return null - } - - const response = await this.s3Client.send( - new GetObjectCommand({ - Bucket: this.bucketName, - Key: object.Key - }) - ) - - const bodyContents = await response.Body.transformToString() - const parsedNode = JSON.parse(bodyContents) - - // Convert serialized connections back to Map> - const connections = new Map>() - for (const [level, nodeIds] of Object.entries( - parsedNode.connections - )) { - connections.set(Number(level), new Set(nodeIds as string[])) - } - - return { - id: parsedNode.id, - vector: parsedNode.vector, - connections - } - } catch (error) { - console.error(`Failed to get node from ${object.Key}:`, error) - return null - } - } - ) - - const nodeResults = await Promise.all(nodePromises) - return nodeResults.filter((node): node is HNSWNode => node !== null) - } catch (error) { - console.error(`Failed to get nodes for noun type ${nounType}:`, error) - throw new Error(`Failed to get nodes for noun type ${nounType}: ${error}`) - } - } - - /** - * Get all nodes from storage - */ - public async getAllNodes(): Promise { - await this.ensureInitialized() - - try { - // Import the ListObjectsV2Command and GetObjectCommand only when needed - const { ListObjectsV2Command, GetObjectCommand } = await import( - '@aws-sdk/client-s3' - ) - - // List all objects in the consolidated nouns directory - const listResponse = await this.s3Client.send( - new ListObjectsV2Command({ - Bucket: this.bucketName, - Prefix: NOUN_PREFIX - }) - ) - - const nodes: HNSWNode[] = [] - - // If there are no objects, return an empty array - if (!listResponse.Contents || listResponse.Contents.length === 0) { - return nodes - } - - // Get each node - const nodePromises = listResponse.Contents.map( - async (object: { Key: string }) => { - try { - const response = await this.s3Client.send( - new GetObjectCommand({ - Bucket: this.bucketName, - Key: object.Key - }) - ) - - const bodyContents = await response.Body.transformToString() - const parsedNode = JSON.parse(bodyContents) - - // Convert serialized connections back to Map> - const connections = new Map>() - for (const [level, nodeIds] of Object.entries( - parsedNode.connections - )) { - connections.set(Number(level), new Set(nodeIds as string[])) - } - - return { - id: parsedNode.id, - vector: parsedNode.vector, - connections - } - } catch (error) { - console.error(`Failed to get node from ${object.Key}:`, error) - return null - } - } - ) - - const nodeResults = await Promise.all(nodePromises) - return nodeResults.filter((node): node is HNSWNode => node !== null) - } catch (error) { - console.error('Failed to get all nodes:', error) - throw new Error(`Failed to get all nodes: ${error}`) - } - } - - /** - * Delete a node from storage - */ - public async deleteNode(id: string): Promise { - await this.ensureInitialized() - - try { - // Import the DeleteObjectCommand only when needed - const { DeleteObjectCommand } = await import('@aws-sdk/client-s3') - - // Delete the node from the consolidated nouns directory - await this.s3Client.send( - new DeleteObjectCommand({ - Bucket: this.bucketName, - Key: `${NOUN_PREFIX}${id}.json` - }) - ) - } catch (error) { - console.error(`Failed to delete node ${id}:`, error) - throw new Error(`Failed to delete node ${id}: ${error}`) - } - } - - /** - * Save an edge to storage - */ - public async saveEdge(edge: Edge): Promise { - await this.ensureInitialized() - - try { - // Convert connections Map to a serializable format - const serializableEdge = { - ...edge, - connections: this.mapToObject(edge.connections, (set) => - Array.from(set as Set) - ) - } - - // Import the PutObjectCommand only when needed - const { PutObjectCommand } = await import('@aws-sdk/client-s3') - - // Save the edge to S3-compatible storage - await this.s3Client.send( - new PutObjectCommand({ - Bucket: this.bucketName, - Key: `${VERBS_PREFIX}${edge.id}.json`, - Body: JSON.stringify(serializableEdge, null, 2), - ContentType: 'application/json' - }) - ) - } catch (error) { - console.error(`Failed to save edge ${edge.id}:`, error) - throw new Error(`Failed to save edge ${edge.id}: ${error}`) - } - } - - /** - * Get an edge from storage - */ - public async getEdge(id: string): Promise { - await this.ensureInitialized() - - try { - // Import the GetObjectCommand only when needed - const { GetObjectCommand } = await import('@aws-sdk/client-s3') - - try { - // Try to get the edge from S3-compatible storage - const response = await this.s3Client.send( - new GetObjectCommand({ - Bucket: this.bucketName, - Key: `${VERBS_PREFIX}${id}.json` - }) - ) - - // Convert the response body to a string - const bodyContents = await response.Body.transformToString() - const parsedEdge = JSON.parse(bodyContents) - - // Convert serialized connections back to Map> - const connections = new Map>() - for (const [level, nodeIds] of Object.entries(parsedEdge.connections)) { - connections.set(Number(level), new Set(nodeIds as string[])) - } - - return { - id: parsedEdge.id, - vector: parsedEdge.vector, - connections, - sourceId: parsedEdge.sourceId, - targetId: parsedEdge.targetId, - type: parsedEdge.type, - weight: parsedEdge.weight, - metadata: parsedEdge.metadata - } - } catch { - return null // Edge not found - } - } catch (error) { - console.error(`Failed to get edge ${id}:`, error) - return null - } - } - - /** - * Get all edges from storage - */ - public async getAllEdges(): Promise { - await this.ensureInitialized() - - try { - // Import the ListObjectsV2Command and GetObjectCommand only when needed - const { ListObjectsV2Command, GetObjectCommand } = await import( - '@aws-sdk/client-s3' - ) - - // List all objects with the edges prefix - const listResponse = await this.s3Client.send( - new ListObjectsV2Command({ - Bucket: this.bucketName, - Prefix: VERBS_PREFIX - }) - ) - - const edges: Edge[] = [] - - // If there are no objects, return an empty array - if (!listResponse.Contents || listResponse.Contents.length === 0) { - return edges - } - - // Get each edge - const edgePromises = listResponse.Contents.map( - async (object: { Key: string }) => { - try { - const response = await this.s3Client.send( - new GetObjectCommand({ - Bucket: this.bucketName, - Key: object.Key - }) - ) - - const bodyContents = await response.Body.transformToString() - const parsedEdge = JSON.parse(bodyContents) - - // Convert serialized connections back to Map> - const connections = new Map>() - for (const [level, nodeIds] of Object.entries( - parsedEdge.connections - )) { - connections.set(Number(level), new Set(nodeIds as string[])) - } - - return { - id: parsedEdge.id, - vector: parsedEdge.vector, - connections, - sourceId: parsedEdge.sourceId, - targetId: parsedEdge.targetId, - type: parsedEdge.type, - weight: parsedEdge.weight, - metadata: parsedEdge.metadata - } - } catch (error) { - console.error(`Failed to get edge from ${object.Key}:`, error) - return null - } - } - ) - - const edgeResults = await Promise.all(edgePromises) - return edgeResults.filter((edge): edge is Edge => edge !== null) - } catch (error) { - console.error('Failed to get all edges:', error) - throw new Error(`Failed to get all edges: ${error}`) - } - } - - /** - * Get edges by source node ID - */ - public async getEdgesBySource(sourceId: string): Promise { - await this.ensureInitialized() - - try { - const allEdges = await this.getAllEdges() - return allEdges.filter((edge) => edge.sourceId === sourceId) - } catch (error) { - console.error(`Failed to get edges by source ${sourceId}:`, error) - throw new Error(`Failed to get edges by source ${sourceId}: ${error}`) - } - } - - /** - * Get edges by target node ID - */ - public async getEdgesByTarget(targetId: string): Promise { - await this.ensureInitialized() - - try { - const allEdges = await this.getAllEdges() - return allEdges.filter((edge) => edge.targetId === targetId) - } catch (error) { - console.error(`Failed to get edges by target ${targetId}:`, error) - throw new Error(`Failed to get edges by target ${targetId}: ${error}`) - } - } - - /** - * Get edges by type - */ - public async getEdgesByType(type: string): Promise { - await this.ensureInitialized() - - try { - const allEdges = await this.getAllEdges() - return allEdges.filter((edge) => edge.type === type) - } catch (error) { - console.error(`Failed to get edges by type ${type}:`, error) - throw new Error(`Failed to get edges by type ${type}: ${error}`) - } - } - - /** - * Delete an edge from storage - */ - public async deleteEdge(id: string): Promise { - await this.ensureInitialized() - - try { - // Import the DeleteObjectCommand and GetObjectCommand only when needed - const { DeleteObjectCommand, GetObjectCommand } = await import( - '@aws-sdk/client-s3' - ) - - try { - // Check if the edge exists before deleting - await this.s3Client.send( - new GetObjectCommand({ - Bucket: this.bucketName, - Key: `${EDGES_PREFIX}${id}.json` - }) - ) - - // Delete the edge - await this.s3Client.send( - new DeleteObjectCommand({ - Bucket: this.bucketName, - Key: `${EDGES_PREFIX}${id}.json` - }) - ) - } catch { - // Edge not found, nothing to delete - return - } - } catch (error) { - console.error(`Failed to delete edge ${id}:`, error) - throw new Error(`Failed to delete edge ${id}: ${error}`) - } - } - - /** - * Save metadata to storage - */ - public async saveMetadata(id: string, metadata: any): Promise { - await this.ensureInitialized() - - try { - // Import the PutObjectCommand only when needed - const { PutObjectCommand } = await import('@aws-sdk/client-s3') - - // Save the metadata to S3-compatible storage - await this.s3Client.send( - new PutObjectCommand({ - Bucket: this.bucketName, - Key: `${METADATA_PREFIX}${id}.json`, - Body: JSON.stringify(metadata, null, 2), - ContentType: 'application/json' - }) - ) - } catch (error) { - console.error(`Failed to save metadata for ${id}:`, error) - throw new Error(`Failed to save metadata for ${id}: ${error}`) - } - } - - /** - * Get metadata from storage - */ - public async getMetadata(id: string): Promise { - await this.ensureInitialized() - - try { - // Import the GetObjectCommand only when needed - const { GetObjectCommand } = await import('@aws-sdk/client-s3') - - try { - // Try to get the metadata from S3-compatible storage - const response = await this.s3Client.send( - new GetObjectCommand({ - Bucket: this.bucketName, - Key: `${METADATA_PREFIX}${id}.json` - }) - ) - - // Convert the response body to a string - const bodyContents = await response.Body.transformToString() - return JSON.parse(bodyContents) - } catch { - return null // Metadata not found - } - } catch (error) { - console.error(`Failed to get metadata for ${id}:`, error) - return null - } - } - - /** - * Clear all data from storage - */ - public async clear(): Promise { - await this.ensureInitialized() - - try { - // Import the ListObjectsV2Command and DeleteObjectCommand only when needed - const { ListObjectsV2Command, DeleteObjectCommand } = await import( - '@aws-sdk/client-s3' - ) - - // List all objects in the bucket - const listResponse = await this.s3Client.send( - new ListObjectsV2Command({ - Bucket: this.bucketName - }) - ) - - // If there are no objects, return - if (!listResponse.Contents || listResponse.Contents.length === 0) { - return - } - - // Delete each object - const deletePromises = listResponse.Contents.map( - async (object: { Key: string }) => { - try { - await this.s3Client.send( - new DeleteObjectCommand({ - Bucket: this.bucketName, - Key: object.Key - }) - ) - } catch (error) { - console.error(`Failed to delete object ${object.Key}:`, error) - } - } - ) - - await Promise.all(deletePromises) - } catch (error) { - console.error('Failed to clear storage:', error) - throw new Error(`Failed to clear storage: ${error}`) - } - } - - /** - * Get information about storage usage and capacity - */ - public async getStorageStatus(): Promise<{ - type: string - used: number - quota: number | null - details?: Record - }> { - await this.ensureInitialized() - - try { - // Import the ListObjectsV2Command only when needed - const { ListObjectsV2Command } = await import('@aws-sdk/client-s3') - - // List all objects in the bucket to calculate total size - const listResponse = await this.s3Client.send( - new ListObjectsV2Command({ - Bucket: this.bucketName - }) - ) - - let totalSize = 0 - let nodeCount = 0 - let edgeCount = 0 - let metadataCount = 0 - - // Calculate total size and counts - if (listResponse.Contents) { - for (const object of listResponse.Contents) { - totalSize += object.Size || 0 - - const key = object.Key || '' - if (key.startsWith(NOUNS_PREFIX)) { - nodeCount++ - } else if (key.startsWith(VERBS_PREFIX)) { - edgeCount++ - } else if (key.startsWith(METADATA_PREFIX)) { - metadataCount++ - } - } - } - - // Count nodes by noun type by examining metadata - const nounTypeCounts: Record = { - person: 0, - place: 0, - thing: 0, - event: 0, - concept: 0, - content: 0, - group: 0, - list: 0, - category: 0, - default: 0 - } - - // List all noun objects and count by type using metadata - const nounsListResponse = await this.s3Client.send( - new ListObjectsV2Command({ - Bucket: this.bucketName, - Prefix: NOUN_PREFIX - }) - ) - - if (nounsListResponse.Contents) { - for (const object of nounsListResponse.Contents) { - try { - // Extract node ID from the key - const nodeId = object.Key?.replace(NOUN_PREFIX, '').replace('.json', '') - if (nodeId) { - // Get metadata to determine noun type - const metadata = await this.getMetadata(nodeId) - const nounType = metadata?.noun || 'default' - - if (nounType in nounTypeCounts) { - nounTypeCounts[nounType]++ - } else { - nounTypeCounts.default++ - } - } - } catch (error) { - // If we can't get metadata, count as default - nounTypeCounts.default++ - } - } - } - - return { - type: this.serviceType, - used: totalSize, - quota: null, // S3-compatible services typically don't provide quota information through the API - details: { - nodeCount, - edgeCount, - metadataCount, - nounTypes: { - person: { count: nounTypeCounts.person }, - place: { count: nounTypeCounts.place }, - thing: { count: nounTypeCounts.thing }, - event: { count: nounTypeCounts.event }, - concept: { count: nounTypeCounts.concept }, - content: { count: nounTypeCounts.content }, - group: { count: nounTypeCounts.group }, - list: { count: nounTypeCounts.list }, - category: { count: nounTypeCounts.category }, - default: { count: nounTypeCounts.default } - } - } - } - } catch (error) { - console.error('Failed to get storage status:', error) - return { - type: this.serviceType, - used: 0, - quota: null, - details: { error: String(error) } - } - } - } - - /** - * Ensure the storage adapter is initialized - */ - private async ensureInitialized(): Promise { - if (!this.isInitialized) { - await this.init() - } - } - - /** - * Get the appropriate prefix for a node - now all nouns use the same prefix - */ - private async getNodePrefix(id: string): Promise { - // All nouns now use the same prefix regardless of type - return NOUN_PREFIX - } - - /** - * Convert a Map to a plain object for serialization - */ - private mapToObject( - map: Map, - valueTransformer: (value: V) => any = (v) => v - ): Record { - const obj: Record = {} - for (const [key, value] of map.entries()) { - obj[key.toString()] = valueTransformer(value) - } - return obj - } -}