UNPKG

s3db.js

Version:

Use AWS S3, the world's most reliable document storage, as a database with this ORM.

285 lines (243 loc) 7.81 kB
import { PromisePool } from '@supercharge/promise-pool'; import { tryFn } from './try-fn.js'; /** * High-performance bulk inserter for S3DB * Optimized for continuous high-volume inserts with partitions */ export class HighPerformanceInserter { constructor(resource, options = {}) { this.resource = resource; // Performance tuning this.batchSize = options.batchSize || 100; this.concurrency = options.concurrency || 50; // Parallel S3 operations this.flushInterval = options.flushInterval || 1000; // ms this.disablePartitions = options.disablePartitions || false; this.useStreamMode = options.useStreamMode || false; // Buffers this.insertBuffer = []; this.partitionBuffer = new Map(); // Deferred partition operations this.stats = { inserted: 0, failed: 0, partitionsPending: 0, avgInsertTime: 0 }; // Auto-flush timer this.flushTimer = null; this.isProcessing = false; // Partition processing queue this.partitionQueue = []; this.partitionProcessor = null; } /** * Add item to insert buffer (non-blocking) */ async add(data) { this.insertBuffer.push({ data, timestamp: Date.now(), promise: null }); // Auto-flush when buffer is full if (this.insertBuffer.length >= this.batchSize) { setImmediate(() => this.flush()); } else if (!this.flushTimer) { // Set flush timer if not already set this.flushTimer = setTimeout(() => this.flush(), this.flushInterval); } return { queued: true, position: this.insertBuffer.length }; } /** * Bulk add items */ async bulkAdd(items) { for (const item of items) { await this.add(item); } return { queued: items.length }; } /** * Process buffered inserts in parallel */ async flush() { if (this.isProcessing || this.insertBuffer.length === 0) return; this.isProcessing = true; clearTimeout(this.flushTimer); this.flushTimer = null; // Take current buffer and reset const batch = this.insertBuffer.splice(0, this.batchSize); const startTime = Date.now(); try { // Process inserts in parallel with connection pooling const { results, errors } = await PromisePool .for(batch) .withConcurrency(this.concurrency) .process(async (item) => { return await this.performInsert(item); }); // Update stats const duration = Date.now() - startTime; this.stats.inserted += results.filter(r => r.success).length; this.stats.failed += errors.length; this.stats.avgInsertTime = duration / batch.length; // Process partition queue separately (non-blocking) if (!this.disablePartitions && this.partitionQueue.length > 0) { this.processPartitionsAsync(); } } finally { this.isProcessing = false; // Continue processing if more items if (this.insertBuffer.length > 0) { setImmediate(() => this.flush()); } } } /** * Perform single insert with optimizations */ async performInsert(item) { const { data } = item; try { // Temporarily disable partitions for the insert const originalAsyncPartitions = this.resource.config.asyncPartitions; const originalPartitions = this.resource.config.partitions; if (this.disablePartitions) { // Completely bypass partitions during insert this.resource.config.partitions = {}; } // Perform insert const [ok, err, result] = await tryFn(() => this.resource.insert(data)); if (!ok) { return { success: false, error: err }; } // Queue partition creation for later (if not disabled) if (!this.disablePartitions && originalPartitions && Object.keys(originalPartitions).length > 0) { this.partitionQueue.push({ operation: 'create', data: result, partitions: originalPartitions }); this.stats.partitionsPending++; } // Restore original config this.resource.config.partitions = originalPartitions; this.resource.config.asyncPartitions = originalAsyncPartitions; return { success: true, data: result }; } catch (error) { return { success: false, error }; } } /** * Process partitions asynchronously in background */ async processPartitionsAsync() { if (this.partitionProcessor) return; // Already processing this.partitionProcessor = setImmediate(async () => { const batch = this.partitionQueue.splice(0, 100); // Process 100 at a time if (batch.length === 0) { this.partitionProcessor = null; return; } // Create partitions in parallel with lower priority await PromisePool .for(batch) .withConcurrency(10) // Lower concurrency for partitions .process(async (item) => { try { await this.resource.createPartitionReferences(item.data); this.stats.partitionsPending--; } catch (err) { // Silently handle partition errors this.resource.emit('partitionIndexError', { operation: 'bulk-insert', error: err }); } }); // Continue processing if more partitions if (this.partitionQueue.length > 0) { this.processPartitionsAsync(); } else { this.partitionProcessor = null; } }); } /** * Force flush all pending operations */ async forceFlush() { while (this.insertBuffer.length > 0 || this.isProcessing) { await this.flush(); await new Promise(resolve => setTimeout(resolve, 10)); } } /** * Get current statistics */ getStats() { return { ...this.stats, bufferSize: this.insertBuffer.length, isProcessing: this.isProcessing, throughput: this.stats.avgInsertTime > 0 ? Math.round(1000 / this.stats.avgInsertTime) : 0 // inserts per second }; } /** * Destroy and cleanup */ destroy() { clearTimeout(this.flushTimer); this.insertBuffer = []; this.partitionQueue = []; } } /** * Stream-based inserter for maximum performance */ export class StreamInserter { constructor(resource, options = {}) { this.resource = resource; this.concurrency = options.concurrency || 100; this.skipPartitions = options.skipPartitions !== false; this.skipHooks = options.skipHooks || false; this.skipValidation = options.skipValidation || false; } /** * Direct S3 write bypassing most S3DB overhead */ async fastInsert(data) { const id = data.id || this.resource.generateId(); const key = this.resource.getResourceKey(id); // Minimal processing const metadata = this.skipValidation ? { id, ...data } : await this.resource.schema.mapper({ id, ...data }); // Direct S3 put const command = { Bucket: this.resource.client.config.bucket, Key: key, Metadata: metadata, Body: '' // Empty body for speed }; await this.resource.client.client.send(new PutObjectCommand(command)); return { id, inserted: true }; } /** * Bulk insert with maximum parallelism */ async bulkInsert(items) { const { results, errors } = await PromisePool .for(items) .withConcurrency(this.concurrency) .process(async (item) => { return await this.fastInsert(item); }); return { success: results.length, failed: errors.length, errors: errors.slice(0, 10) // First 10 errors }; } }