bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
568 lines (496 loc) • 17.5 kB
JavaScript
/**
* 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;