@felixgeelhaar/govee-api-client
Version:
Enterprise-grade TypeScript client library for the Govee Developer REST API
248 lines • 8.53 kB
JavaScript
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