jexidb
Version:
JexiDB is a pure JS NPM library for managing data on disk efficiently, without the need for a server.
280 lines (234 loc) • 8.51 kB
JavaScript
/**
* OperationQueue - Queue system for database operations
* Resolves race conditions between concurrent operations
*/
export class OperationQueue {
constructor(debugMode = false) {
this.queue = []
this.processing = false
this.operationId = 0
this.debugMode = debugMode
this.stats = {
totalOperations: 0,
completedOperations: 0,
failedOperations: 0,
averageProcessingTime: 0,
maxProcessingTime: 0,
totalProcessingTime: 0
}
}
/**
* Adds an operation to the queue
* @param {Function} operation - Asynchronous function to be executed
* @returns {Promise} - Promise that resolves when the operation is completed
*/
async enqueue(operation) {
const id = ++this.operationId
const startTime = Date.now()
if (this.debugMode) {
console.log(`🔄 Queue: Enqueuing operation ${id}, queue length: ${this.queue.length}`)
}
this.stats.totalOperations++
return new Promise((resolve, reject) => {
// Capture stack trace for debugging stuck operations
const stackTrace = new Error().stack
this.queue.push({
id,
operation,
resolve,
reject,
timestamp: startTime,
stackTrace: stackTrace,
startTime: Date.now()
})
// Process immediately if not already processing
this.process().catch(reject)
})
}
/**
* Processes all operations in the queue sequentially
*/
async process() {
if (this.processing || this.queue.length === 0) {
return
}
this.processing = true
if (this.debugMode) {
console.log(`🔄 Queue: Starting to process ${this.queue.length} operations`)
}
try {
while (this.queue.length > 0) {
const { id, operation, resolve, reject, timestamp } = this.queue.shift()
if (this.debugMode) {
console.log(`🔄 Queue: Processing operation ${id}`)
}
try {
const result = await operation()
const processingTime = Date.now() - timestamp
// Atualizar estatísticas
this.stats.completedOperations++
this.stats.totalProcessingTime += processingTime
this.stats.averageProcessingTime = this.stats.totalProcessingTime / this.stats.completedOperations
this.stats.maxProcessingTime = Math.max(this.stats.maxProcessingTime, processingTime)
resolve(result)
if (this.debugMode) {
console.log(`✅ Queue: Operation ${id} completed in ${processingTime}ms`)
}
} catch (error) {
const processingTime = Date.now() - timestamp
// Atualizar estatísticas
this.stats.failedOperations++
this.stats.totalProcessingTime += processingTime
this.stats.averageProcessingTime = this.stats.totalProcessingTime / (this.stats.completedOperations + this.stats.failedOperations)
this.stats.maxProcessingTime = Math.max(this.stats.maxProcessingTime, processingTime)
reject(error)
if (this.debugMode) {
console.error(`❌ Queue: Operation ${id} failed in ${processingTime}ms:`, error)
}
}
}
} finally {
this.processing = false
if (this.debugMode) {
console.log(`🔄 Queue: Finished processing, remaining: ${this.queue.length}`)
}
}
}
/**
* Waits for all pending operations to be processed
* @param {number|null} maxWaitTime - Maximum wait time in ms (null = wait indefinitely)
* @returns {Promise<boolean>} - true if all operations were processed, false if a timeout occurred
*/
async waitForCompletion(maxWaitTime = 5000) {
const startTime = Date.now()
// CRITICAL FIX: Support infinite wait when maxWaitTime is null
const hasTimeout = maxWaitTime !== null && maxWaitTime !== undefined
while (this.queue.length > 0) {
// Check timeout only if we have one
if (hasTimeout && (Date.now() - startTime) >= maxWaitTime) {
break
}
await new Promise(resolve => setTimeout(resolve, 1))
}
const completed = this.queue.length === 0
if (!completed && hasTimeout) {
// CRITICAL: Don't leave operations hanging - fail fast with detailed error
const pendingOperations = this.queue.map(op => ({
id: op.id,
stackTrace: op.stackTrace,
startTime: op.startTime,
waitTime: Date.now() - op.startTime
}))
// Clear the queue to prevent memory leaks
this.queue = []
const error = new Error(`OperationQueue: Operations timed out after ${maxWaitTime}ms. ${pendingOperations.length} operations were stuck and have been cleared.`)
error.pendingOperations = pendingOperations
error.queueStats = this.getStats()
if (this.debugMode) {
console.error(`❌ Queue: Operations timed out, clearing ${pendingOperations.length} stuck operations:`)
pendingOperations.forEach(op => {
console.error(` - Operation ${op.id} (waiting ${op.waitTime}ms):`)
console.error(` Stack: ${op.stackTrace}`)
})
}
throw error
}
return completed
}
/**
* Returns the current queue length
*/
getQueueLength() {
return this.queue.length
}
/**
* Checks whether operations are currently being processed
*/
isProcessing() {
return this.processing
}
/**
* Returns queue statistics
*/
getStats() {
return {
...this.stats,
queueLength: this.queue.length,
isProcessing: this.processing,
successRate: this.stats.totalOperations > 0 ?
(this.stats.completedOperations / this.stats.totalOperations) * 100 : 0
}
}
/**
* Clears the queue (for emergency situations)
*/
clear() {
const clearedCount = this.queue.length
this.queue = []
if (this.debugMode) {
console.log(`🧹 Queue: Cleared ${clearedCount} pending operations`)
}
return clearedCount
}
/**
* Detects stuck operations and returns detailed information
* @param {number} stuckThreshold - Time in ms to consider an operation stuck
* @returns {Array} - List of stuck operations with stack traces
*/
detectStuckOperations(stuckThreshold = 10000) {
const now = Date.now()
const stuckOperations = this.queue.filter(op => (now - op.startTime) > stuckThreshold)
return stuckOperations.map(op => ({
id: op.id,
waitTime: now - op.startTime,
stackTrace: op.stackTrace,
timestamp: op.timestamp
}))
}
/**
* Force-cleans stuck operations (last resort)
* @param {number} stuckThreshold - Time in ms to consider an operation stuck
* @returns {number} - Number of operations removed
*/
forceCleanupStuckOperations(stuckThreshold = 10000) {
const stuckOps = this.detectStuckOperations(stuckThreshold)
if (stuckOps.length > 0) {
// Reject all stuck operations
stuckOps.forEach(stuckOp => {
const opIndex = this.queue.findIndex(op => op.id === stuckOp.id)
if (opIndex !== -1) {
const op = this.queue[opIndex]
op.reject(new Error(`Operation ${op.id} was stuck for ${stuckOp.waitTime}ms and has been force-cleaned. Stack: ${stuckOp.stackTrace}`))
this.queue.splice(opIndex, 1)
}
})
if (this.debugMode) {
console.error(`🧹 Queue: Force-cleaned ${stuckOps.length} stuck operations`)
stuckOps.forEach(op => {
console.error(` - Operation ${op.id} (stuck for ${op.waitTime}ms)`)
})
}
}
return stuckOps.length
}
/**
* Checks whether the queue is empty
*/
isEmpty() {
return this.queue.length === 0
}
/**
* Returns information about the next operation in the queue
*/
peekNext() {
if (this.queue.length === 0) {
return null
}
const next = this.queue[0]
return {
id: next.id,
timestamp: next.timestamp,
waitTime: Date.now() - next.timestamp
}
}
}