UNPKG

@felixgeelhaar/govee-api-client

Version:

Enterprise-grade TypeScript client library for the Govee Developer REST API

248 lines 8.53 kB
import { GoveeApiClientError, NetworkError } from '../../errors'; import { RetryPolicy } from './RetryPolicy'; /** * Enterprise-grade retry executor with comprehensive logging and metrics */ export class RetryExecutor { constructor(retryPolicy, config = {}) { this.retryPolicy = retryPolicy; this.logger = config.logger; this.enableRequestLogging = config.enableRequestLogging ?? true; this.enablePerformanceTracking = config.enablePerformanceTracking ?? true; } /** * Executes a request with retry logic */ async execute(request) { const result = await this.executeWithResult(request); if (result.success) { return result.data; } throw result.error || new Error('Request failed without specific error'); } /** * Executes a request with detailed retry result information */ async executeWithResult(request) { const startTime = Date.now(); const attempts = []; let attemptNumber = 0; let lastError; this.logRequestStart(request); while (true) { attemptNumber++; const attemptStartTime = new Date(); const delayMs = attemptNumber === 1 ? 0 : this.retryPolicy.calculateDelay(attemptNumber - 1, lastError); // Apply delay before retry attempts if (delayMs > 0) { this.logger?.debug({ requestId: request.id, attemptNumber, delayMs }, 'Waiting before retry attempt'); await this.sleep(delayMs); } const attemptResult = await this.executeAttempt(request, attemptNumber); attempts.push({ attemptNumber, startTime: attemptStartTime, durationMs: attemptResult.durationMs, success: attemptResult.success, error: attemptResult.error, delayBeforeAttemptMs: delayMs, }); if (attemptResult.success) { // Success - record and return this.retryPolicy.recordSuccess(); const totalTimeMs = Date.now() - startTime; this.logRequestSuccess(request, attemptNumber, totalTimeMs); return { success: true, data: attemptResult.data, totalAttempts: attemptNumber, totalTimeMs, attempts, }; } // Failure - record each failed attempt lastError = attemptResult.error; this.retryPolicy.recordFailure(lastError); const elapsedTimeMs = Date.now() - startTime; if (!this.retryPolicy.shouldRetry(lastError, attemptNumber, elapsedTimeMs)) { // No more retries - return failure const totalTimeMs = Date.now() - startTime; this.logRequestFailure(request, attemptNumber, totalTimeMs, lastError); return { success: false, error: lastError, totalAttempts: attemptNumber, totalTimeMs, attempts, }; } // Continue retrying this.logRetryAttempt(request, attemptNumber, lastError); } } /** * Executes a single attempt of the request */ async executeAttempt(request, attemptNumber) { const startTime = Date.now(); try { const data = await request.execute(); const durationMs = Date.now() - startTime; this.logAttemptSuccess(request, attemptNumber, durationMs); return { success: true, data, durationMs, }; } catch (error) { const durationMs = Date.now() - startTime; const goveeError = this.normalizeError(error); this.logAttemptFailure(request, attemptNumber, durationMs, goveeError); return { success: false, error: goveeError, durationMs, }; } } /** * Normalizes any error to a GoveeApiClientError */ normalizeError(error) { if (error instanceof GoveeApiClientError) { return error; } if (error instanceof Error) { return new NetworkError(`Unexpected error: ${error.message}`, 'unknown', error); } return new NetworkError('Unknown error occurred', 'unknown'); } /** * Utility function for sleeping */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Logging methods for different stages of retry execution */ logRequestStart(request) { if (!this.enableRequestLogging || !this.logger) return; this.logger.info({ requestId: request.id, description: request.description, context: request.context, }, 'Starting retryable request'); } logRequestSuccess(request, totalAttempts, totalTimeMs) { if (!this.enableRequestLogging || !this.logger) return; this.logger.info({ requestId: request.id, totalAttempts, totalTimeMs, retriesNeeded: totalAttempts - 1, }, 'Retryable request completed successfully'); } logRequestFailure(request, totalAttempts, totalTimeMs, finalError) { if (!this.logger) return; this.logger.error({ requestId: request.id, totalAttempts, totalTimeMs, finalError: finalError.toObject(), }, 'Retryable request failed after all attempts'); } logRetryAttempt(request, attemptNumber, error) { if (!this.enableRequestLogging || !this.logger) return; this.logger.warn({ requestId: request.id, attemptNumber, error: error.toObject(), }, 'Request attempt failed, will retry'); } logAttemptSuccess(request, attemptNumber, durationMs) { if (!this.enableRequestLogging || !this.logger) return; this.logger.debug({ requestId: request.id, attemptNumber, durationMs, }, 'Request attempt succeeded'); } logAttemptFailure(request, attemptNumber, durationMs, error) { if (!this.enableRequestLogging || !this.logger) return; this.logger.debug({ requestId: request.id, attemptNumber, durationMs, error: error.toObject(), }, 'Request attempt failed'); } /** * Gets current retry policy metrics */ getMetrics() { return this.retryPolicy.getMetrics(); } /** * Resets retry policy metrics */ resetMetrics() { this.retryPolicy.reset(); } } /** * Factory for creating common retry executors */ export class RetryExecutorFactory { /** * Creates a retry executor optimized for Govee API operations */ static createForGoveeApi(logger) { const retryPolicy = RetryPolicy.createGoveeOptimized(logger); return new RetryExecutor(retryPolicy, { logger, enableRequestLogging: true, enablePerformanceTracking: true, }); } /** * Creates a conservative retry executor for production environments */ static createConservative(logger) { const retryPolicy = RetryPolicy.createConservative(logger); return new RetryExecutor(retryPolicy, { logger, enableRequestLogging: true, enablePerformanceTracking: true, }); } /** * Creates an aggressive retry executor for development/testing */ static createAggressive(logger) { const retryPolicy = RetryPolicy.createAggressive(logger); return new RetryExecutor(retryPolicy, { logger, enableRequestLogging: true, enablePerformanceTracking: true, }); } /** * Creates a custom retry executor with specific policy */ static createCustom(retryPolicy, logger) { return new RetryExecutor(retryPolicy, { logger, enableRequestLogging: true, enablePerformanceTracking: true, }); } } //# sourceMappingURL=RetryableRequest.js.map