- 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>
493 lines
No EOL
15 KiB
TypeScript
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
|
|
}
|
|
} |