/** * Enhanced Clear/Delete Operations for Brainy Storage * Provides safe, efficient, and production-ready bulk deletion methods */ export interface ClearOptions { /** * Safety confirmation - must match database instance name * Prevents accidental deletion of wrong databases */ confirmInstanceName?: string /** * Performance optimization settings */ batchSize?: number maxConcurrency?: number /** * Safety mechanisms */ dryRun?: boolean createBackup?: boolean /** * Progress callback for large operations */ onProgress?: (progress: ClearProgress) => void } export interface ClearProgress { stage: 'backup' | 'nouns' | 'verbs' | 'metadata' | 'system' | 'cache' | 'complete' totalItems: number processedItems: number errors: number estimatedTimeRemaining?: number } export interface ClearResult { success: boolean itemsDeleted: { nouns: number verbs: number metadata: number system: number } duration: number errors: Error[] backupLocation?: string } /** * Enhanced FileSystem bulk delete operations */ export class EnhancedFileSystemClear { constructor( private rootDir: string, private fs: any, private path: any ) {} /** * Optimized bulk delete for filesystem storage * Uses parallel deletion with controlled concurrency */ async clear(options: ClearOptions = {}): Promise { const startTime = Date.now() const result: ClearResult = { success: false, itemsDeleted: { nouns: 0, verbs: 0, metadata: 0, system: 0 }, duration: 0, errors: [] } try { // Safety checks if (options.confirmInstanceName) { const actualName = this.path.basename(this.rootDir) if (actualName !== options.confirmInstanceName) { throw new Error( `Instance name mismatch: expected '${options.confirmInstanceName}', got '${actualName}'` ) } } // Create backup if requested if (options.createBackup) { result.backupLocation = await this.createBackup() options.onProgress?.({ stage: 'backup', totalItems: 1, processedItems: 1, errors: 0 }) } // Dry run - just count items if (options.dryRun) { return await this.performDryRun(options) } // Optimized deletion with batching const batchSize = options.batchSize || 100 const maxConcurrency = options.maxConcurrency || 10 // Delete nouns directory with optimization result.itemsDeleted.nouns = await this.clearDirectoryOptimized( this.path.join(this.rootDir, 'nouns'), batchSize, maxConcurrency, (progress) => options.onProgress?.({ ...progress, stage: 'nouns' }) ) // Delete verbs directory with optimization result.itemsDeleted.verbs = await this.clearDirectoryOptimized( this.path.join(this.rootDir, 'verbs'), batchSize, maxConcurrency, (progress) => options.onProgress?.({ ...progress, stage: 'verbs' }) ) // Delete metadata directories const metadataDirs = ['metadata', 'noun-metadata', 'verb-metadata'] for (const dir of metadataDirs) { result.itemsDeleted.metadata += await this.clearDirectoryOptimized( this.path.join(this.rootDir, dir), batchSize, maxConcurrency, (progress) => options.onProgress?.({ ...progress, stage: 'metadata' }) ) } // Delete system directories const systemDirs = ['system', 'index'] for (const dir of systemDirs) { result.itemsDeleted.system += await this.clearDirectoryOptimized( this.path.join(this.rootDir, dir), batchSize, maxConcurrency, (progress) => options.onProgress?.({ ...progress, stage: 'system' }) ) } result.success = true result.duration = Date.now() - startTime options.onProgress?.({ stage: 'complete', totalItems: Object.values(result.itemsDeleted).reduce((a, b) => a + b, 0), processedItems: Object.values(result.itemsDeleted).reduce((a, b) => a + b, 0), errors: result.errors.length }) } catch (error) { result.errors.push(error as Error) result.duration = Date.now() - startTime } return result } /** * High-performance directory clearing with controlled concurrency */ private async clearDirectoryOptimized( dirPath: string, batchSize: number, maxConcurrency: number, onProgress?: (progress: Omit) => void ): Promise { try { // Check if directory exists const stats = await this.fs.promises.stat(dirPath) if (!stats.isDirectory()) return 0 // Get all files in the directory const files = await this.fs.promises.readdir(dirPath) const totalFiles = files.length if (totalFiles === 0) return 0 let processedFiles = 0 let errors = 0 // Process files in batches with controlled concurrency for (let i = 0; i < files.length; i += batchSize) { const batch = files.slice(i, i + batchSize) // Create semaphore for concurrency control const semaphore = new Array(Math.min(maxConcurrency, batch.length)).fill(0) await Promise.all( batch.map(async (file: string, index: number) => { // Wait for semaphore slot await new Promise(resolve => { const slotIndex = index % semaphore.length semaphore[slotIndex] = performance.now() resolve(undefined) }) try { const filePath = this.path.join(dirPath, file) await this.fs.promises.unlink(filePath) processedFiles++ } catch (error) { errors++ console.warn(`Failed to delete file ${file}:`, error) } // Report progress every 50 files or at end of batch if (processedFiles % 50 === 0 || processedFiles === totalFiles) { onProgress?.({ totalItems: totalFiles, processedItems: processedFiles, errors }) } }) ) // Small yield between batches to prevent blocking await new Promise(resolve => setImmediate(resolve)) } return processedFiles } catch (error: any) { if (error.code === 'ENOENT') { return 0 // Directory doesn't exist, that's fine } throw error } } private async createBackup(): Promise { const timestamp = new Date().toISOString().replace(/[:.]/g, '-') const backupDir = `${this.rootDir}-backup-${timestamp}` // Use cp -r for efficient directory copying const { spawn } = await import('node:child_process') return new Promise((resolve, reject) => { const cp = spawn('cp', ['-r', this.rootDir, backupDir]) cp.on('close', (code) => { if (code === 0) { resolve(backupDir) } else { reject(new Error(`Backup failed with code ${code}`)) } }) cp.on('error', reject) }) } private async performDryRun(options: ClearOptions): Promise { const startTime = Date.now() const result: ClearResult = { success: true, itemsDeleted: { nouns: 0, verbs: 0, metadata: 0, system: 0 }, duration: 0, errors: [] } const countFiles = async (dirPath: string): Promise => { try { const files = await this.fs.promises.readdir(dirPath) return files.filter((f: string) => f.endsWith('.json')).length } catch (error: any) { if (error.code === 'ENOENT') return 0 throw error } } result.itemsDeleted.nouns = await countFiles(this.path.join(this.rootDir, 'nouns')) result.itemsDeleted.verbs = await countFiles(this.path.join(this.rootDir, 'verbs')) result.itemsDeleted.metadata = await countFiles(this.path.join(this.rootDir, 'metadata')) + await countFiles(this.path.join(this.rootDir, 'noun-metadata')) + await countFiles(this.path.join(this.rootDir, 'verb-metadata')) result.itemsDeleted.system = await countFiles(this.path.join(this.rootDir, 'system')) + await countFiles(this.path.join(this.rootDir, 'index')) result.duration = Date.now() - startTime return result } } /** * Enhanced S3 bulk delete operations */ export class EnhancedS3Clear { constructor( private s3Client: any, private bucketName: string ) {} /** * Optimized bulk delete for S3 storage * Uses batch delete operations for maximum efficiency */ async clear(options: ClearOptions = {}): Promise { const startTime = Date.now() const result: ClearResult = { success: false, itemsDeleted: { nouns: 0, verbs: 0, metadata: 0, system: 0 }, duration: 0, errors: [] } try { // Safety checks if (options.confirmInstanceName) { // Extract instance name from bucket structure or prefix const bucketInfo = await this.getBucketInfo() if (bucketInfo.instanceName !== options.confirmInstanceName) { throw new Error( `Instance name mismatch: expected '${options.confirmInstanceName}', got '${bucketInfo.instanceName}'` ) } } // Dry run - just count objects if (options.dryRun) { return await this.performDryRun(options) } // AWS S3 batch delete supports up to 1000 objects per request const batchSize = Math.min(options.batchSize || 1000, 1000) // Delete with optimized batching const prefixes = [ { prefix: 'nouns/', key: 'nouns' as keyof typeof result.itemsDeleted }, { prefix: 'verbs/', key: 'verbs' as keyof typeof result.itemsDeleted }, { prefix: 'metadata/', key: 'metadata' as keyof typeof result.itemsDeleted }, { prefix: 'noun-metadata/', key: 'metadata' as keyof typeof result.itemsDeleted }, { prefix: 'verb-metadata/', key: 'metadata' as keyof typeof result.itemsDeleted }, { prefix: 'system/', key: 'system' as keyof typeof result.itemsDeleted }, { prefix: 'index/', key: 'system' as keyof typeof result.itemsDeleted } ] for (const { prefix, key } of prefixes) { const deleted = await this.clearPrefixOptimized( prefix, batchSize, (progress) => options.onProgress?.({ ...progress, stage: key === 'nouns' ? 'nouns' : key === 'verbs' ? 'verbs' : key === 'metadata' ? 'metadata' : 'system' }) ) result.itemsDeleted[key] += deleted } result.success = true result.duration = Date.now() - startTime } catch (error) { result.errors.push(error as Error) result.duration = Date.now() - startTime } return result } /** * High-performance prefix clearing using S3 batch delete */ private async clearPrefixOptimized( prefix: string, batchSize: number, onProgress?: (progress: Omit) => void ): Promise { const { ListObjectsV2Command, DeleteObjectsCommand } = await import('@aws-sdk/client-s3') let totalDeleted = 0 let continuationToken: string | undefined do { // List objects with the prefix const listResponse = await this.s3Client.send( new ListObjectsV2Command({ Bucket: this.bucketName, Prefix: prefix, MaxKeys: batchSize, ContinuationToken: continuationToken }) ) if (!listResponse.Contents || listResponse.Contents.length === 0) { break } // Prepare batch delete request const objectsToDelete = listResponse.Contents .filter((obj: any) => obj.Key) .map((obj: any) => ({ Key: obj.Key! })) if (objectsToDelete.length > 0) { // Perform batch delete const deleteResponse = await this.s3Client.send( new DeleteObjectsCommand({ Bucket: this.bucketName, Delete: { Objects: objectsToDelete, Quiet: false // Get detailed response } }) ) const deletedCount = deleteResponse.Deleted?.length || 0 totalDeleted += deletedCount // Report any errors if (deleteResponse.Errors && deleteResponse.Errors.length > 0) { for (const error of deleteResponse.Errors) { console.warn(`Failed to delete ${error.Key}: ${error.Message}`) } } // Report progress onProgress?.({ totalItems: totalDeleted + (listResponse.IsTruncated ? 1000 : 0), // Estimate processedItems: totalDeleted, errors: deleteResponse.Errors?.length || 0 }) } continuationToken = listResponse.NextContinuationToken // Small delay to respect AWS rate limits await new Promise(resolve => setTimeout(resolve, 10)) } while (continuationToken) return totalDeleted } private async getBucketInfo(): Promise<{ instanceName: string }> { // Each Brainy instance has its own bucket with the same name as the instance // The bucket name IS the instance name return { instanceName: this.bucketName } } private async performDryRun(options: ClearOptions): Promise { const startTime = Date.now() const { ListObjectsV2Command } = await import('@aws-sdk/client-s3') const result: ClearResult = { success: true, itemsDeleted: { nouns: 0, verbs: 0, metadata: 0, system: 0 }, duration: 0, errors: [] } const countObjects = async (prefix: string): Promise => { let count = 0 let continuationToken: string | undefined do { const response = await this.s3Client.send( new ListObjectsV2Command({ Bucket: this.bucketName, Prefix: prefix, MaxKeys: 1000, ContinuationToken: continuationToken }) ) count += response.KeyCount || 0 continuationToken = response.NextContinuationToken } while (continuationToken) return count } result.itemsDeleted.nouns = await countObjects('nouns/') result.itemsDeleted.verbs = await countObjects('verbs/') result.itemsDeleted.metadata = await countObjects('metadata/') + await countObjects('noun-metadata/') + await countObjects('verb-metadata/') result.itemsDeleted.system = await countObjects('system/') + await countObjects('index/') result.duration = Date.now() - startTime return result } }