brainy/src/storage/enhancedClearOperations.ts
David Snelling fcb7197fb0 feat: add node: protocol to all Node.js built-in imports for bundler compatibility
- Updated all fs, path, crypto, os, url, util, events, http, https, net, child_process, stream, and zlib imports
- Changed both static imports and dynamic imports to use node: protocol
- This makes Brainy more bundler-friendly by explicitly marking Node.js built-ins
- Prevents bundlers from attempting to polyfill or bundle these modules
- Reduces bundle size for web applications using Brainy
- Improves tree-shaking and dead code elimination

Benefits for external bundlers:
- Clear distinction between Node.js built-ins and external dependencies
- No ambiguity about what needs polyfilling
- Smaller bundles for browser builds
- Better compatibility with modern bundlers (Webpack 5, Vite, Rollup, esbuild)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-17 14:20:21 -07:00

493 lines
No EOL
15 KiB
TypeScript

/**
* 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<ClearResult> {
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<ClearProgress, 'stage'>) => void
): Promise<number> {
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<string> {
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<ClearResult> {
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<number> => {
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<ClearResult> {
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<ClearProgress, 'stage'>) => void
): Promise<number> {
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<ClearResult> {
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<number> => {
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
}
}