jexidb
Version:
JexiDB is a pure JS NPM library for managing data on disk efficiently, without the need for a server.
430 lines (373 loc) • 11.7 kB
JavaScript
import { EventEmitter } from 'events'
import fs from 'fs'
import readline from 'readline'
/**
* StreamingProcessor - Efficient streaming processing for large datasets
*
* Features:
* - Memory-efficient processing of large files
* - Configurable batch sizes
* - Progress tracking
* - Error handling and recovery
* - Transform pipelines
* - Backpressure control
*/
export class StreamingProcessor extends EventEmitter {
constructor(opts = {}) {
super()
this.opts = {
batchSize: opts.batchSize || 1000,
maxConcurrency: opts.maxConcurrency || 5,
bufferSize: opts.bufferSize || 64 * 1024, // 64KB
enableProgress: opts.enableProgress !== false,
progressInterval: opts.progressInterval || 1000, // 1 second
enableBackpressure: opts.enableBackpressure !== false,
maxPendingBatches: opts.maxPendingBatches || 10,
...opts
}
this.isProcessing = false
this.currentBatch = 0
this.totalBatches = 0
this.processedItems = 0
this.totalItems = 0
this.pendingBatches = 0
this.stats = {
startTime: 0,
endTime: 0,
totalProcessingTime: 0,
averageBatchTime: 0,
itemsPerSecond: 0,
memoryUsage: 0
}
this.progressTimer = null
this.transformPipeline = []
}
/**
* Add a transform function to the pipeline
*/
addTransform(transformFn) {
this.transformPipeline.push(transformFn)
return this
}
/**
* Process a file stream
*/
async processFileStream(filePath, processorFn) {
if (this.isProcessing) {
throw new Error('Streaming processor is already running')
}
this.isProcessing = true
this.stats.startTime = Date.now()
this.currentBatch = 0
this.processedItems = 0
try {
// Get file size for progress tracking
const stats = await fs.promises.stat(filePath)
this.totalItems = Math.ceil(stats.size / this.opts.bufferSize)
// Start progress tracking
if (this.opts.enableProgress) {
this._startProgressTracking()
}
// Create read stream
const fileStream = fs.createReadStream(filePath, {
encoding: 'utf8',
highWaterMark: this.opts.bufferSize
})
// Create readline interface
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
})
let batch = []
let lineCount = 0
// Process lines in batches
for await (const line of rl) {
if (line.trim()) {
batch.push(line)
lineCount++
// Process batch when it reaches the configured size
if (batch.length >= this.opts.batchSize) {
await this._processBatch(batch, processorFn)
batch = []
}
}
}
// Process remaining items in the last batch
if (batch.length > 0) {
await this._processBatch(batch, processorFn)
}
this.stats.endTime = Date.now()
this.stats.totalProcessingTime = this.stats.endTime - this.stats.startTime
this.stats.itemsPerSecond = this.processedItems / (this.stats.totalProcessingTime / 1000)
this.emit('complete', {
totalItems: this.processedItems,
totalBatches: this.currentBatch,
processingTime: this.stats.totalProcessingTime,
itemsPerSecond: this.stats.itemsPerSecond
})
} catch (error) {
this.emit('error', error)
throw error
} finally {
this.isProcessing = false
this._stopProgressTracking()
}
}
/**
* Process an array of items in streaming fashion
*/
async processArray(items, processorFn) {
if (this.isProcessing) {
throw new Error('Streaming processor is already running')
}
this.isProcessing = true
this.stats.startTime = Date.now()
this.currentBatch = 0
this.processedItems = 0
this.totalItems = items.length
this.totalBatches = Math.ceil(items.length / this.opts.batchSize)
try {
// Start progress tracking
if (this.opts.enableProgress) {
this._startProgressTracking()
}
// Process items in batches
for (let i = 0; i < items.length; i += this.opts.batchSize) {
const batch = items.slice(i, i + this.opts.batchSize)
await this._processBatch(batch, processorFn)
}
this.stats.endTime = Date.now()
this.stats.totalProcessingTime = this.stats.endTime - this.stats.startTime
this.stats.itemsPerSecond = this.processedItems / (this.stats.totalProcessingTime / 1000)
this.emit('complete', {
totalItems: this.processedItems,
totalBatches: this.currentBatch,
processingTime: this.stats.totalProcessingTime,
itemsPerSecond: this.stats.itemsPerSecond
})
} catch (error) {
this.emit('error', error)
throw error
} finally {
this.isProcessing = false
this._stopProgressTracking()
}
}
/**
* Process a generator function
*/
async processGenerator(generatorFn, processorFn) {
if (this.isProcessing) {
throw new Error('Streaming processor is already running')
}
this.isProcessing = true
this.stats.startTime = Date.now()
this.currentBatch = 0
this.processedItems = 0
this.totalItems = 0 // Unknown for generators
try {
// Start progress tracking
if (this.opts.enableProgress) {
this._startProgressTracking()
}
const generator = generatorFn()
let batch = []
for await (const item of generator) {
batch.push(item)
this.totalItems++
// Process batch when it reaches the configured size
if (batch.length >= this.opts.batchSize) {
await this._processBatch(batch, processorFn)
batch = []
}
}
// Process remaining items in the last batch
if (batch.length > 0) {
await this._processBatch(batch, processorFn)
}
this.stats.endTime = Date.now()
this.stats.totalProcessingTime = this.stats.endTime - this.stats.startTime
this.stats.itemsPerSecond = this.processedItems / (this.stats.totalProcessingTime / 1000)
this.emit('complete', {
totalItems: this.processedItems,
totalBatches: this.currentBatch,
processingTime: this.stats.totalProcessingTime,
itemsPerSecond: this.stats.itemsPerSecond
})
} catch (error) {
this.emit('error', error)
throw error
} finally {
this.isProcessing = false
this._stopProgressTracking()
}
}
/**
* Process a single batch
*/
async _processBatch(batch, processorFn) {
if (this.opts.enableBackpressure && this.pendingBatches >= this.opts.maxPendingBatches) {
// Wait for backpressure to reduce
await this._waitForBackpressure()
}
this.pendingBatches++
this.currentBatch++
try {
const startTime = Date.now()
// Apply transform pipeline
let transformedBatch = batch
for (const transform of this.transformPipeline) {
transformedBatch = await transform(transformedBatch)
}
// Process the batch
const result = await processorFn(transformedBatch, this.currentBatch)
const batchTime = Date.now() - startTime
this.stats.averageBatchTime =
(this.stats.averageBatchTime + batchTime) / 2
this.processedItems += batch.length
this.emit('batchComplete', {
batchNumber: this.currentBatch,
batchSize: batch.length,
processingTime: batchTime,
result
})
} catch (error) {
this.emit('batchError', {
batchNumber: this.currentBatch,
batchSize: batch.length,
error
})
throw error
} finally {
this.pendingBatches--
}
}
/**
* Wait for backpressure to reduce
*/
async _waitForBackpressure() {
return new Promise((resolve) => {
const checkBackpressure = () => {
if (this.pendingBatches < this.opts.maxPendingBatches) {
resolve()
} else {
setTimeout(checkBackpressure, 10)
}
}
checkBackpressure()
})
}
/**
* Start progress tracking
*/
_startProgressTracking() {
this.progressTimer = setInterval(() => {
const progress = {
currentBatch: this.currentBatch,
totalBatches: this.totalBatches,
processedItems: this.processedItems,
totalItems: this.totalItems,
percentage: this.totalItems > 0 ? (this.processedItems / this.totalItems) * 100 : 0,
itemsPerSecond: this.stats.itemsPerSecond,
averageBatchTime: this.stats.averageBatchTime,
memoryUsage: process.memoryUsage().heapUsed / 1024 / 1024 // MB
}
this.emit('progress', progress)
}, this.opts.progressInterval)
this.progressTimer.unref(); // Allow process to exit without waiting for this timer
}
/**
* Stop progress tracking
*/
_stopProgressTracking() {
if (this.progressTimer) {
clearInterval(this.progressTimer)
this.progressTimer = null
}
}
/**
* Get current statistics
*/
getStats() {
return {
...this.stats,
isProcessing: this.isProcessing,
currentBatch: this.currentBatch,
totalBatches: this.totalBatches,
processedItems: this.processedItems,
totalItems: this.totalItems,
pendingBatches: this.pendingBatches,
transformPipelineLength: this.transformPipeline.length
}
}
/**
* Stop processing
*/
stop() {
this.isProcessing = false
this._stopProgressTracking()
this.emit('stopped')
}
/**
* Reset the processor
*/
reset() {
this.stop()
this.currentBatch = 0
this.totalBatches = 0
this.processedItems = 0
this.totalItems = 0
this.pendingBatches = 0
this.stats = {
startTime: 0,
endTime: 0,
totalProcessingTime: 0,
averageBatchTime: 0,
itemsPerSecond: 0,
memoryUsage: 0
}
this.transformPipeline = []
}
}
/**
* Predefined transform functions
*/
export const Transforms = {
// Parse JSON lines
parseJSON: (batch) => {
return batch.map(line => {
try {
return JSON.parse(line)
} catch (error) {
console.warn('Failed to parse JSON line:', line)
return null
}
}).filter(item => item !== null)
},
// Filter out null/undefined values
filterNull: (batch) => {
return batch.filter(item => item !== null && item !== undefined)
},
// Transform to specific format
toFormat: (format) => (batch) => {
switch (format) {
case 'string':
return batch.map(item => String(item))
case 'number':
return batch.map(item => Number(item))
case 'object':
return batch.map(item => typeof item === 'object' ? item : { value: item })
default:
return batch
}
},
// Add metadata
addMetadata: (metadata) => (batch) => {
return batch.map(item => ({
...item,
...metadata,
processedAt: Date.now()
}))
}
}
export default StreamingProcessor