@sailboat-computer/data-storage
Version:
Shared data storage library for sailboat computer v3
238 lines (209 loc) • 6.07 kB
text/typescript
/**
* 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();
}