UNPKG

@killiandvcz/buffer

Version:

Autonomous message buffer system with intelligent batching and self-monitoring capabilities

269 lines (234 loc) 8.27 kB
/** * @fileoverview Enhanced message buffer system using train station metaphor * @version 1.1.0 */ import { Entry } from "./entry"; /** * @typedef {Object} BufferConfig * @property {number} [maxSize=1000] Maximum size of the buffer * @property {number} [targetBatchSize=50] Target number of messages per batch * @property {number} [minBatchSize=10] Minimum batch size to trigger processing * @property {number} [maxBatchDelay=1000] Maximum delay before processing an incomplete batch (ms) * @property {boolean} [preserveOrder=true] Whether to preserve message order * @property {number} [maxRetries=3] Maximum number of retries for failed batches * @property {number} [baseRetryDelay=1000] Base delay for exponential backoff (ms) * @property {number} [processingCheckInterval=1000] Interval to check if processing is possible when blocked (ms) * @property {function} [canProcess] Custom function to check if processing is possible */ export class MegaBuffer { /** * Creates a new MessageBuffer instance * @param {function(Array<*>): Promise<void>} executor Batch execution function * @param {BufferConfig} [config={}] Buffer configuration */ constructor(executor, config = {}) { // Core configuration this.config = { maxSize: config.maxSize ?? 1000, targetBatchSize: config.targetBatchSize ?? 50, minBatchSize: config.minBatchSize ?? 10, maxBatchDelay: config.maxBatchDelay ?? 1000, preserveOrder: config.preserveOrder ?? true, maxRetries: config.maxRetries ?? 3, baseRetryDelay: config.baseRetryDelay ?? 1000, processingCheckInterval: config.processingCheckInterval ?? 1000, canProcess: config.canProcess ?? (async () => true) }; // Validate executor if (typeof executor !== 'function') { throw new Error('Executor must be a function'); } this.executor = executor; // Internal state this._queue = new Map(); this._flushTimeout = null; this._canProcessInterval = null; this._isProcessing = false; // Flag to prevent concurrent processing this._retryMap = new Map(); // Track retry attempts per message // Statistics this._stats = { processed: 0, dropped: 0, errors: 0, lastProcessingTime: 0 }; } /** * Checks if processing is currently possible * @private * @returns {Promise<boolean>} */ async _canProcess() { try { const canProcess = !!(await this.config.canProcess()); // Clear or set up monitoring interval based on processing status if (canProcess) { this._clearProcessingCheck(); } else { this._setupProcessingCheck(); } return canProcess; } catch (error) { console.error('Error in canProcess function:', error); return false; } } /** * Sets up the processing check interval * @private */ _setupProcessingCheck() { if (!this._canProcessInterval) { this._canProcessInterval = setInterval(() => { if (this._queue.size === 0 || this._canProcess()) { this._clearProcessingCheck(); this._startFlushing(); } }, this.config.processingCheckInterval); } } /** * Clears the processing check interval * @private */ _clearProcessingCheck() { if (this._canProcessInterval) { clearInterval(this._canProcessInterval); this._canProcessInterval = null; } } /** * Adds a new message to the buffer * @param {*} content Message content * @param {Object} [options={}] Message options * @param {number} [options.priority=0] Message priority * @param {Object} [options.metadata={}] Additional metadata * @returns {Promise<boolean>} Whether the message was successfully added */ async add(content, options = {}) { // Check buffer capacity if (this._queue.size >= this.config.maxSize) { this._stats.dropped++; return false; } const entry = new Entry(content, options); this._queue.set(entry.id, entry); // Start the departure countdown if we have messages if (this._queue.size > 0) { this._startFlushing(); } // Immediate departure if train is full if (this._queue.size >= this.config.targetBatchSize) { await this._flush(); } return true; } /** * Starts the flush countdown timer * @private */ _startFlushing() { // Don't set a new timeout if one already exists if (this._flushTimeout) { return; } this._flushTimeout = setTimeout(async () => { this._flushTimeout = null; await this._flush(); }, this.config.maxBatchDelay); } /** * Processes the current batch of messages * @private * @returns {Promise<void>} */ async _flush() { // Guard conditions if (this._queue.size === 0 || this._isProcessing || !(await this._canProcess())) { return; } // Clear any pending flush timeout if (this._flushTimeout) { clearTimeout(this._flushTimeout); this._flushTimeout = null; } this._isProcessing = true; const processingStart = Date.now(); try { // Prepare the batch (like passengers boarding the train) const batch = Array.from(this._queue.values()) .sort((a, b) => b.priority - a.priority) // Higher priority first .slice(0, this.config.targetBatchSize) .map(entry => { entry.attempts++; this._retryMap.set(entry.id, entry.attempts); return entry; }); // Process the batch this.executor(batch.map(entry => entry.content)); // Success handling this._stats.processed += batch.length; this._stats.lastProcessingTime = Date.now() - processingStart; // Remove processed messages and their retry counts batch.forEach(entry => { this._queue.delete(entry.id); this._retryMap.delete(entry.id); }); } catch (error) { this._stats.errors++; console.error('Error processing batch:', error); // Handle retries for failed messages this._handleBatchError(); } finally { this._isProcessing = false; // Check if we need to schedule the next departure if (this._queue.size > 0) { this._startFlushing(); } } } /** * Handles batch processing errors * @private */ _handleBatchError() { // Implement exponential backoff for retries for (const [id, attempts] of this._retryMap.entries()) { if (attempts >= this.config.maxRetries) { this._queue.delete(id); this._retryMap.delete(id); this._stats.dropped++; } } } /** * Returns current buffer statistics * @returns {Object} Current buffer statistics */ getStats() { return { ...this._stats, currentSize: this._queue.size, isProcessing: this._isProcessing }; } /** * Destroys the buffer and cleans up resources */ destroy() { this._clearProcessingCheck(); if (this._flushTimeout) { clearTimeout(this._flushTimeout); this._flushTimeout = null; } this._queue.clear(); this._retryMap.clear(); } /** * Force flush the buffer and restart the timer */ flush() { this._flush(); } } export default MegaBuffer;