UNPKG

bowling-analysis-system

Version:

A comprehensive system for analyzing bowling techniques using video processing and metrics calculation

568 lines (496 loc) 17.5 kB
/** * Base class for all processors in the pipeline * Provides common functionality and standardized interface * @class ProcessorBase */ class ProcessorBase { /** * Creates an instance of ProcessorBase * @param {Object} options - Processor options * @param {String} options.name - Processor name * @param {Object} [options.logger] - Logger instance * @param {Object} [options.config] - Configuration object * @param {Object} [options.serviceRegistry] - Service registry * @param {Object} [options.middleware] - Middleware configuration * @param {Boolean} [options.middleware.timing=true] - Enable timing middleware * @param {Boolean} [options.middleware.validation=true] - Enable validation middleware * @param {Boolean} [options.middleware.errorHandling=true] - Enable error handling middleware * @param {Boolean} [options.middleware.metrics=true] - Enable metrics recording * @param {Boolean} [options.middleware.caching=false] - Enable result caching * @param {Function} [options.validator] - Custom validation function * @param {Function} [options.transformer] - Custom data transformation function */ constructor(options = {}) { if (!options.name) { throw new Error('Processor name is required'); } this.name = options.name; this.logger = options.logger || console; this.config = options.config || {}; this.serviceRegistry = options.serviceRegistry; this.metrics = {}; this.initialized = false; this.executionCount = 0; this.lastExecutionTime = null; this.totalExecutionTime = 0; this.errors = []; this.maxErrors = options.maxErrors || 10; this.validator = options.validator; this.transformer = options.transformer; this.cache = new Map(); this.cacheEnabled = false; this.cacheMaxSize = options.cacheMaxSize || 100; // Configure middleware this.middleware = { timing: true, validation: true, errorHandling: true, metrics: true, caching: false, ...options.middleware }; // Store additional processor-specific configuration this.processorConfig = { ...options }; } /** * Initialize the processor * @param {Object} [options] - Initialization options * @returns {Promise<void>} */ async initialize(options = {}) { if (this.initialized) { return; } try { await this._initializeInternal(options); // Enable caching if specified if (options.caching || this.middleware.caching) { this.cacheEnabled = true; } this.initialized = true; this.logger.debug(`Processor ${this.name} initialized`); } catch (error) { this.logger.error(`Failed to initialize processor ${this.name}:`, error); this._recordError(error, null, { phase: 'initialization' }); throw error; } } /** * Internal initialization method to be implemented by subclasses * @param {Object} options - Initialization options * @returns {Promise<void>} * @protected */ async _initializeInternal(options) { // Base implementation does nothing // Subclasses should override this method } /** * Process data * @param {*} data - Input data * @param {Object} [context={}] - Processing context * @returns {Promise<*>} - Processing result */ async process(data, context = {}) { if (!this.initialized) { await this.initialize(); } // Start timing if enabled const startTime = Date.now(); let result = null; const sanitizedContext = this._sanitizeContext(context); try { // Validate input if middleware enabled if (this.middleware.validation) { await this.validateInput(data, sanitizedContext); } // Check cache if enabled if (this.middleware.caching && this.cacheEnabled) { const cacheKey = this._generateCacheKey(data, sanitizedContext); const cachedResult = this.cache.get(cacheKey); if (cachedResult) { this.logger.debug(`Cache hit for processor ${this.name}`); return cachedResult; } } // Transform data if transformer provided const processData = this.transformer ? this.transformer(data, sanitizedContext) : data; // Execute processor-specific logic result = await this._processInternal(processData, sanitizedContext); // Validate output if middleware enabled if (this.middleware.validation) { await this.validateOutput(result, sanitizedContext); } // Update metrics this.executionCount++; this.lastExecutionTime = Date.now() - startTime; this.totalExecutionTime += this.lastExecutionTime; // Record metrics if middleware enabled if (this.middleware.metrics) { this._recordMetrics(data, result, this.lastExecutionTime, sanitizedContext); } // Cache result if enabled if (this.middleware.caching && this.cacheEnabled) { const cacheKey = this._generateCacheKey(data, sanitizedContext); this._cacheResult(cacheKey, result); } return result; } catch (error) { if (this.middleware.errorHandling) { this._recordError(error, data, sanitizedContext); } throw error; } } /** * Internal processing method to be implemented by subclasses * @param {*} data - Input data * @param {Object} context - Processing context * @returns {Promise<*>} - Processing result * @protected */ async _processInternal(data, context) { throw new Error(`Processor ${this.name} does not implement _processInternal method`); } /** * Validate input data * @param {*} data - Input data * @param {Object} context - Validation context * @returns {Promise<boolean>} - Validation result */ async validateInput(data, context) { try { // If a custom validator is provided, use it if (typeof this.validator === 'function') { const isValid = await this.validator(data, 'input', context); if (!isValid) { throw new Error(`Input validation failed for processor ${this.name}`); } return true; } // Otherwise use the internal implementation return await this._validateInputInternal(data, context); } catch (error) { this.logger.warn(`Input validation failed for processor ${this.name}:`, error); throw error; } } /** * Internal input validation method to be implemented by subclasses * @param {*} data - Input data * @param {Object} context - Validation context * @returns {Promise<boolean>} - Validation result * @protected */ async _validateInputInternal(data, context) { // Default implementation always passes validation return true; } /** * Validate output data * @param {*} data - Output data * @param {Object} context - Validation context * @returns {Promise<boolean>} - Validation result */ async validateOutput(data, context) { try { // If a custom validator is provided, use it if (typeof this.validator === 'function') { const isValid = await this.validator(data, 'output', context); if (!isValid) { throw new Error(`Output validation failed for processor ${this.name}`); } return true; } // Otherwise use the internal implementation return await this._validateOutputInternal(data, context); } catch (error) { this.logger.warn(`Output validation failed for processor ${this.name}:`, error); throw error; } } /** * Internal output validation method to be implemented by subclasses * @param {*} data - Output data * @param {Object} context - Validation context * @returns {Promise<boolean>} - Validation result * @protected */ async _validateOutputInternal(data, context) { // Default implementation always passes validation return true; } /** * Record metrics for processor execution * @param {*} inputData - Input data * @param {*} outputData - Output data * @param {Number} executionTime - Execution time in milliseconds * @param {Object} context - Processing context * @private */ _recordMetrics(inputData, outputData, executionTime, context) { // Update standard metrics this.metrics.lastExecutionTime = executionTime; this.metrics.averageExecutionTime = this.totalExecutionTime / this.executionCount; this.metrics.executionCount = this.executionCount; this.metrics.errorCount = this.errors.length; // Call processor-specific metrics recording this._recordMetricsInternal(inputData, outputData, executionTime, context); } /** * Internal metrics recording method that can be overridden by subclasses * @param {*} inputData - Input data * @param {*} outputData - Output data * @param {Number} executionTime - Execution time in milliseconds * @param {Object} context - Processing context * @protected */ _recordMetricsInternal(inputData, outputData, executionTime, context) { // Default implementation does nothing // Subclasses can override to add processor-specific metrics } /** * Record an error that occurred during processing * @param {Error} error - Error that occurred * @param {*} data - Data being processed * @param {Object} context - Processing context * @private */ _recordError(error, data, context) { const errorRecord = { timestamp: new Date().toISOString(), message: error.message, stack: error.stack, code: error.code, data: this._sanitizeData(data), context: this._sanitizeContext(context) }; this.errors.unshift(errorRecord); // Limit the number of errors stored if (this.errors.length > this.maxErrors) { this.errors.pop(); } this.logger.error(`Error in processor ${this.name}:`, error); } /** * Sanitize data for error recording * @param {*} data - Data to sanitize * @returns {*} - Sanitized data * @private */ _sanitizeData(data) { if (data === null || data === undefined) { return null; } try { // For objects and arrays, create a shallow copy if (typeof data === 'object') { // For large arrays, just include the length and type if (Array.isArray(data) && data.length > 100) { return { type: 'Array', length: data.length, sample: data.slice(0, 10) }; } // For large objects, just include the keys const keys = Object.keys(data); if (keys.length > 50) { return { type: 'Object', keys: keys.slice(0, 50), keyCount: keys.length }; } // For buffers, just include the length if (Buffer.isBuffer(data)) { return { type: 'Buffer', length: data.length, sample: data.slice(0, 20).toString('hex') }; } // For other objects, just return a copy return { ...data }; } // For large strings, truncate if (typeof data === 'string' && data.length > 1000) { return `${data.substring(0, 1000)}... (truncated, total length: ${data.length})`; } // Otherwise, return as is return data; } catch (error) { return `[Error sanitizing data: ${error.message}]`; } } /** * Sanitize context for error recording * @param {Object} context - Context to sanitize * @returns {Object} - Sanitized context * @private */ _sanitizeContext(context) { if (!context || typeof context !== 'object') { return {}; } try { // Create a shallow copy of the context const sanitized = { ...context }; // Remove sensitive or unnecessary fields delete sanitized.serviceRegistry; delete sanitized.config; delete sanitized.password; delete sanitized.token; delete sanitized.secret; delete sanitized.authorization; // Simplify complex objects for (const [key, value] of Object.entries(sanitized)) { if (typeof value === 'object' && value !== null) { if (value.constructor && value.constructor.name !== 'Object' && value.constructor.name !== 'Array') { sanitized[key] = `[${value.constructor.name}]`; } } } return sanitized; } catch (error) { return { error: `Error sanitizing context: ${error.message}` }; } } /** * Generate a cache key for the input data and context * @param {*} data - Input data * @param {Object} context - Processing context * @returns {String} - Cache key * @private */ _generateCacheKey(data, context) { try { // Extract relevant fields from context for caching const { cacheKeyFields = [] } = context; const contextForKey = {}; if (cacheKeyFields.length > 0) { for (const field of cacheKeyFields) { if (field in context) { contextForKey[field] = context[field]; } } } // Generate key based on stringified data and context const dataStr = typeof data === 'object' ? JSON.stringify(data) : String(data); const contextStr = JSON.stringify(contextForKey); return `${this.name}:${dataStr.length}:${contextStr.length}:${Date.now()}`; } catch (error) { this.logger.warn(`Error generating cache key:`, error); return `${this.name}:${Date.now()}`; } } /** * Cache a processing result * @param {String} key - Cache key * @param {*} result - Processing result * @private */ _cacheResult(key, result) { // Enforce cache size limit if (this.cache.size >= this.cacheMaxSize) { // Remove oldest entry (first key) const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } this.cache.set(key, result); } /** * Get processor metrics * @returns {Object} - Processor metrics */ getMetrics() { return { ...this.metrics }; } /** * Get processor errors * @param {Number} [limit] - Maximum number of errors to return * @returns {Array<Object>} - Error records */ getErrors(limit) { if (limit && limit > 0) { return this.errors.slice(0, limit); } return [...this.errors]; } /** * Reset processor state * @returns {ProcessorBase} - For method chaining */ reset() { this.executionCount = 0; this.lastExecutionTime = null; this.totalExecutionTime = 0; this.errors = []; this.metrics = {}; this.cache.clear(); return this; } /** * Get processor status * @returns {Object} - Status object */ getStatus() { return { name: this.name, initialized: this.initialized, executionCount: this.executionCount, lastExecutionTime: this.lastExecutionTime, averageExecutionTime: this.executionCount > 0 ? this.totalExecutionTime / this.executionCount : null, errorCount: this.errors.length, cacheSize: this.cache.size, cacheEnabled: this.cacheEnabled }; } /** * Get a service from the service registry * @param {String} serviceName - Service name * @returns {Object|null} - Service instance or null if not found * @protected */ _getService(serviceName) { if (!this.serviceRegistry) { this.logger.warn(`Service registry not available in processor ${this.name}`); return null; } try { return this.serviceRegistry.get(serviceName); } catch (error) { this.logger.warn(`Failed to get service ${serviceName}:`, error); return null; } } /** * Get a configuration value * @param {String} path - Configuration path (dot notation) * @param {*} defaultValue - Default value if not found * @returns {*} - Configuration value * @protected */ _getConfig(path, defaultValue) { if (!this.config) { return defaultValue; } // If config has a get method, use it if (typeof this.config.get === 'function') { return this.config.get(path, defaultValue); } // Otherwise navigate the config object const parts = path.split('.'); let current = this.config; for (const part of parts) { if (current === undefined || current === null || typeof current !== 'object') { return defaultValue; } current = current[part]; } return current !== undefined ? current : defaultValue; } } module.exports = ProcessorBase;