UNPKG

@sailboat-computer/data-storage

Version:

Shared data storage library for sailboat computer v3

238 lines (209 loc) 6.07 kB
/** * Error handling and resilience utilities * * This file contains error classes and utility functions for error handling * and resilience patterns like circuit breakers, retries, and timeouts. */ import { CircuitBreakerState, CircuitBreakerStateType, StorageErrorCode, StorageErrorCodeType } from '../types'; // Re-export StorageErrorCode for backward compatibility export { StorageErrorCode, StorageErrorCodeType } from '../types'; /** * Storage error class */ export class StorageError extends Error { /** * Error code */ public readonly code: StorageErrorCodeType; /** * Error details */ public readonly details: Record<string, any>; /** * Create a new storage error * * @param code - Error code * @param message - Error message * @param details - Error details */ constructor(code: StorageErrorCodeType, message: string, details: Record<string, any> = {}) { super(message); this.name = 'StorageError'; this.code = code; this.details = details; // Capture stack trace if (Error.captureStackTrace) { Error.captureStackTrace(this, StorageError); } } /** * Convert error to string * * @returns String representation of error */ toString(): string { return `${this.name}[${this.code}]: ${this.message}`; } /** * Convert error to JSON * * @returns JSON representation of error */ toJSON(): Record<string, any> { return { name: this.name, code: this.code, message: this.message, details: this.details, stack: this.stack }; } } /** * Circuit breaker implementation * * Implements the circuit breaker pattern to prevent cascading failures * by failing fast when a service is unavailable. */ export class CircuitBreaker { private state: CircuitBreakerStateType = CircuitBreakerState.CLOSED; private failureCount = 0; private lastFailureTime: number = 0; private readonly failureThreshold: number; private readonly resetTimeoutMs: number; /** * Create a new circuit breaker * * @param failureThreshold - Number of failures before opening circuit * @param resetTimeoutMs - Reset timeout in milliseconds */ constructor(failureThreshold: number = 3, resetTimeoutMs: number = 30000) { this.failureThreshold = failureThreshold; this.resetTimeoutMs = resetTimeoutMs; } /** * Execute a function with circuit breaker protection * * @param fn - Function to execute * @returns Function result * @throws Error if circuit is open */ async execute<T>(fn: () => Promise<T>): Promise<T> { if (this.state === CircuitBreakerState.OPEN) { // Check if reset timeout has elapsed if (Date.now() - this.lastFailureTime >= this.resetTimeoutMs) { this.state = CircuitBreakerState.HALF_OPEN; } else { throw new Error('Circuit is open'); } } try { // Execute function const result = await fn(); // Reset failure count on success if (this.state === CircuitBreakerState.HALF_OPEN) { this.state = CircuitBreakerState.CLOSED; } this.failureCount = 0; return result; } catch (error) { // Increment failure count this.failureCount++; this.lastFailureTime = Date.now(); // Open circuit if failure threshold is reached if (this.failureCount >= this.failureThreshold) { this.state = CircuitBreakerState.OPEN; } throw error; } } /** * Get circuit breaker state * * @returns Circuit breaker state */ getState(): CircuitBreakerStateType { return this.state; } /** * Reset circuit breaker */ reset(): void { this.state = CircuitBreakerState.CLOSED; this.failureCount = 0; this.lastFailureTime = 0; } } /** * Execute a function with retry logic * * @param fn - Function to execute * @param maxRetries - Maximum number of retries * @param baseDelayMs - Base delay in milliseconds * @returns Function result * @throws Error if all retries fail */ export async function withRetry<T>( fn: () => Promise<T>, maxRetries: number = 3, baseDelayMs: number = 1000 ): Promise<T> { let lastError: Error | undefined; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (attempt < maxRetries) { // Exponential backoff with jitter const jitter = Math.random() * 0.3 + 0.85; // 0.85-1.15 const delay = baseDelayMs * Math.pow(2, attempt) * jitter; await new Promise(resolve => setTimeout(resolve, delay)); } } } throw lastError; } /** * Execute a function with timeout * * @param promise - Promise to execute * @param timeoutMs - Timeout in milliseconds * @returns Promise result * @throws Error if timeout is reached */ export async function withTimeout<T>( promise: Promise<T>, timeoutMs: number = 5000 ): Promise<T> { let timeoutId: NodeJS.Timeout; const timeoutPromise = new Promise<never>((_, reject) => { timeoutId = setTimeout(() => { reject(new Error(`Operation timed out after ${timeoutMs}ms`)); }, timeoutMs); }); try { return await Promise.race([promise, timeoutPromise]); } finally { clearTimeout(timeoutId!); } } /** * Execute a function with bulkhead protection * * @param fn - Function to execute * @param maxConcurrent - Maximum number of concurrent executions * @param maxQueue - Maximum queue size * @returns Function result * @throws Error if queue is full */ export async function withBulkhead<T>( fn: () => Promise<T>, maxConcurrent: number = 10, maxQueue: number = 100 ): Promise<T> { // Implementation of bulkhead pattern // This is a simplified version that doesn't actually limit concurrency // A real implementation would use a semaphore or similar mechanism return await fn(); }