homebridge-pentair-intellicenter-ai
Version:
Homebridge plugin for the Pentair IntelliCenter pool control system (maintained with AI assistance)
227 lines • 8.08 kB
JavaScript
"use strict";
/**
* Error handling and resilience utilities for Pentair Platform
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.RateLimiter = exports.DeadLetterQueue = exports.HealthMonitor = exports.RetryManager = exports.CircuitBreaker = exports.CircuitBreakerState = void 0;
var CircuitBreakerState;
(function (CircuitBreakerState) {
CircuitBreakerState["CLOSED"] = "closed";
CircuitBreakerState["OPEN"] = "open";
CircuitBreakerState["HALF_OPEN"] = "half-open";
})(CircuitBreakerState || (exports.CircuitBreakerState = CircuitBreakerState = {}));
class CircuitBreaker {
constructor(options) {
this.options = options;
this.state = CircuitBreakerState.CLOSED;
this.failureCount = 0;
this.lastFailureTime = 0;
this.successCount = 0;
}
async execute(operation) {
if (this.state === CircuitBreakerState.OPEN) {
if (Date.now() - this.lastFailureTime > this.options.resetTimeout) {
this.state = CircuitBreakerState.HALF_OPEN;
this.successCount = 0;
}
else {
throw new Error('Circuit breaker is OPEN - operation rejected');
}
}
try {
const result = await operation();
this.onSuccess();
return result;
}
catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
if (this.state === CircuitBreakerState.HALF_OPEN) {
this.successCount++;
if (this.successCount >= 3) {
// Require 3 successes to close
this.state = CircuitBreakerState.CLOSED;
}
}
}
onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.options.failureThreshold) {
this.state = CircuitBreakerState.OPEN;
}
}
getState() {
return this.state;
}
getStats() {
return {
state: this.state,
failureCount: this.failureCount,
lastFailureTime: this.lastFailureTime,
successCount: this.successCount,
};
}
reset() {
this.state = CircuitBreakerState.CLOSED;
this.failureCount = 0;
this.lastFailureTime = 0;
this.successCount = 0;
}
}
exports.CircuitBreaker = CircuitBreaker;
class RetryManager {
static async withRetry(operation, options, logger) {
let lastError;
for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
try {
return await operation();
}
catch (error) {
lastError = error;
if (attempt === options.maxAttempts) {
break; // Don't delay on final attempt
}
// Check if error is retryable
if (options.retryableErrors && options.retryableErrors.length > 0) {
const isRetryable = options.retryableErrors.some(retryableError => lastError.message.includes(retryableError));
if (!isRetryable) {
logger === null || logger === void 0 ? void 0 : logger(`Non-retryable error encountered: ${lastError.message}`);
throw lastError;
}
}
const delay = Math.min(options.baseDelay * Math.pow(options.backoffFactor, attempt - 1), options.maxDelay);
logger === null || logger === void 0 ? void 0 : logger(`Attempt ${attempt}/${options.maxAttempts} failed: ${lastError.message}. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
}
exports.RetryManager = RetryManager;
class HealthMonitor {
constructor() {
this.consecutiveFailures = 0;
this.lastSuccessfulOperation = Date.now();
this.responseTimeHistory = [];
this.maxHistorySize = 10;
}
recordSuccess(responseTime) {
this.consecutiveFailures = 0;
this.lastSuccessfulOperation = Date.now();
this.lastError = undefined;
if (responseTime !== undefined) {
this.responseTimeHistory.push(responseTime);
if (this.responseTimeHistory.length > this.maxHistorySize) {
this.responseTimeHistory.shift();
}
}
}
recordFailure(error) {
this.consecutiveFailures++;
this.lastError = error;
}
getHealth() {
const timeSinceLastSuccess = Date.now() - this.lastSuccessfulOperation;
const avgResponseTime = this.responseTimeHistory.length > 0
? this.responseTimeHistory.reduce((a, b) => a + b, 0) / this.responseTimeHistory.length
: undefined;
return {
isHealthy: this.consecutiveFailures < 3 && timeSinceLastSuccess < 300000, // 5 minutes
lastSuccessfulOperation: this.lastSuccessfulOperation,
consecutiveFailures: this.consecutiveFailures,
lastError: this.lastError,
responseTime: avgResponseTime,
};
}
reset() {
this.consecutiveFailures = 0;
this.lastSuccessfulOperation = Date.now();
this.lastError = undefined;
this.responseTimeHistory = [];
}
}
exports.HealthMonitor = HealthMonitor;
class DeadLetterQueue {
constructor(maxSize = 100, maxRetentionMs = 24 * 60 * 60 * 1000) {
this.queue = [];
// 24 hours default
this.maxSize = maxSize;
this.maxRetentionMs = maxRetentionMs;
}
add(command, attempts, error, originalMessageId) {
const item = {
command,
timestamp: Date.now(),
attempts,
lastError: error,
originalMessageId,
};
this.queue.push(item);
// Maintain size limit
if (this.queue.length > this.maxSize) {
this.queue.shift(); // Remove oldest
}
}
getFailedCommands() {
this.cleanup();
return [...this.queue];
}
getStats() {
var _a, _b, _c, _d;
this.cleanup();
return {
queueSize: this.queue.length,
maxSize: this.maxSize,
oldestTimestamp: this.queue.length > 0 ? ((_b = (_a = this.queue[0]) === null || _a === void 0 ? void 0 : _a.timestamp) !== null && _b !== void 0 ? _b : null) : null,
newestTimestamp: this.queue.length > 0 ? ((_d = (_c = this.queue[this.queue.length - 1]) === null || _c === void 0 ? void 0 : _c.timestamp) !== null && _d !== void 0 ? _d : null) : null,
};
}
clear() {
this.queue = [];
}
cleanup() {
const now = Date.now();
this.queue = this.queue.filter(item => now - item.timestamp < this.maxRetentionMs);
}
}
exports.DeadLetterQueue = DeadLetterQueue;
class RateLimiter {
constructor(maxRequests, windowMs) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.requests = [];
}
getNow() {
return Date.now();
}
canMakeRequest() {
const now = this.getNow();
// Remove expired requests
this.requests = this.requests.filter(timestamp => now - timestamp < this.windowMs);
return this.requests.length < this.maxRequests;
}
recordRequest() {
if (!this.canMakeRequest()) {
return false;
}
this.requests.push(this.getNow());
return true;
}
getStats() {
const now = this.getNow();
const activeRequests = this.requests.filter(timestamp => now - timestamp < this.windowMs);
return {
currentRequests: activeRequests.length,
maxRequests: this.maxRequests,
windowMs: this.windowMs,
canMakeRequest: this.canMakeRequest(),
};
}
}
exports.RateLimiter = RateLimiter;
//# sourceMappingURL=errorHandling.js.map