UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and

270 lines (269 loc) 8.84 kB
/** * Retry and resilience utilities for NeuroLink * Part of Sub-phase 3.3.3 - Edge Case Handling */ import { logger } from "./logger.js"; import { SYSTEM_LIMITS } from "../core/constants.js"; /** * Calculate exponential backoff delay with jitter * @param attempt - Current attempt number (1-based) * @param initialDelay - Initial delay in milliseconds * @param multiplier - Backoff multiplier for exponential growth * @param maxDelay - Maximum delay cap in milliseconds * @param addJitter - Whether to add random jitter to prevent thundering herd * @returns Calculated delay in milliseconds */ export function calculateBackoffDelay(attempt, initialDelay = SYSTEM_LIMITS.DEFAULT_INITIAL_DELAY, multiplier = SYSTEM_LIMITS.DEFAULT_BACKOFF_MULTIPLIER, maxDelay = SYSTEM_LIMITS.DEFAULT_MAX_DELAY, addJitter = true) { // Calculate exponential backoff const exponentialDelay = initialDelay * Math.pow(multiplier, attempt - 1); // Apply maximum delay cap const cappedDelay = Math.min(exponentialDelay, maxDelay); // Add jitter to avoid thundering herd (up to 10% of delay, max 1 second) const jitter = addJitter ? Math.random() * Math.min(cappedDelay * 0.1, 1000) : 0; return cappedDelay + jitter; } /** * Error types that are typically retryable */ export class NetworkError extends Error { cause; constructor(message, cause) { super(message); this.cause = cause; this.name = "NetworkError"; } } export class TemporaryError extends Error { cause; constructor(message, cause) { super(message); this.cause = cause; this.name = "TemporaryError"; } } /** * Default retry configuration */ export const DEFAULT_RETRY_CONFIG = { maxAttempts: SYSTEM_LIMITS.DEFAULT_RETRY_ATTEMPTS, initialDelay: SYSTEM_LIMITS.DEFAULT_INITIAL_DELAY, maxDelay: SYSTEM_LIMITS.DEFAULT_MAX_DELAY, backoffMultiplier: SYSTEM_LIMITS.DEFAULT_BACKOFF_MULTIPLIER, retryCondition: (error) => { // Retry on network errors, timeouts, and specific HTTP errors if (error instanceof NetworkError || error instanceof TemporaryError) { return true; } // Retry on timeout errors if (error && typeof error === "object" && (error.name === "TimeoutError" || error.code === "TIMEOUT")) { return true; } // Retry on network-related errors if (error && typeof error === "object" && (error.code === "ECONNRESET" || error.code === "ENOTFOUND" || error.code === "ECONNREFUSED" || error.code === "ETIMEDOUT")) { return true; } // Retry on HTTP 5xx errors and some 4xx errors if (error && typeof error === "object" && error.status) { const status = Number(error.status); return status >= 500 || status === 429 || status === 408; } // Don't retry by default return false; }, onRetry: (attempt, error) => { const message = error instanceof Error ? error.message : String(error); logger.warn(`⚠️ Retry attempt ${attempt}: ${message}`); }, }; /** * Sleep utility for retry delays */ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Execute an operation with retry logic */ export async function withRetry(operation, options = {}) { const config = { ...DEFAULT_RETRY_CONFIG, ...options }; let lastError; for (let attempt = 1; attempt <= config.maxAttempts; attempt++) { try { return await operation(); } catch (error) { lastError = error; // Don't retry if it's the last attempt if (attempt === config.maxAttempts) { break; } // Check if we should retry this error if (!config.retryCondition(error)) { break; } // Call retry callback config.onRetry(attempt, error); // Calculate delay with exponential backoff and jitter const jitteredDelay = calculateBackoffDelay(attempt, config.initialDelay, config.backoffMultiplier, config.maxDelay, true); await sleep(jitteredDelay); } } throw lastError; } /** * Enhanced timeout with retry for network operations */ export async function withTimeoutAndRetry(operation, timeoutMs, retryOptions = {}) { return withRetry(async () => { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new NetworkError(`Operation timed out after ${timeoutMs}ms`)); }, timeoutMs); operation() .then((result) => { clearTimeout(timeout); resolve(result); }) .catch((error) => { clearTimeout(timeout); reject(error); }); }); }, retryOptions); } /** * Circuit breaker pattern for preventing cascading failures */ export class CircuitBreaker { threshold; timeout; monitorWindow; failures = 0; lastFailureTime = 0; state = "closed"; constructor(threshold = 5, timeout = 60000, // 1 minute monitorWindow = 600000) { this.threshold = threshold; this.timeout = timeout; this.monitorWindow = monitorWindow; } async execute(operation) { if (this.state === "open") { if (Date.now() - this.lastFailureTime > this.timeout) { this.state = "half-open"; } 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.failures = 0; this.state = "closed"; } onFailure() { this.failures++; this.lastFailureTime = Date.now(); if (this.failures >= this.threshold) { this.state = "open"; } } getState() { return this.state; } reset() { this.failures = 0; this.lastFailureTime = 0; this.state = "closed"; } } /** * Rate limiter to prevent overwhelming APIs */ export class RateLimiter { maxRequests; windowMs; requests = []; constructor(maxRequests, windowMs) { this.maxRequests = maxRequests; this.windowMs = windowMs; } async acquire() { const now = Date.now(); // Remove old requests outside the window this.requests = this.requests.filter((time) => now - time < this.windowMs); if (this.requests.length >= this.maxRequests) { // Calculate delay until next available slot const oldestRequest = Math.min(...this.requests); const delay = this.windowMs - (now - oldestRequest); if (delay > 0) { await sleep(delay); return this.acquire(); // Try again after delay } } this.requests.push(now); } } /** * Utility for graceful shutdown handling */ export class GracefulShutdown { operations = new Set(); shutdownPromise = null; track(operation) { this.operations.add(operation); operation.finally(() => { this.operations.delete(operation); }); return operation; } async shutdown(timeoutMs = 30000) { if (this.shutdownPromise) { return this.shutdownPromise; } this.shutdownPromise = this.performShutdown(timeoutMs); return this.shutdownPromise; } async performShutdown(timeoutMs) { logger.debug(`🔄 Graceful shutdown: waiting for ${this.operations.size} operations...`); try { await Promise.race([ Promise.all(this.operations), sleep(timeoutMs).then(() => { throw new Error(`Shutdown timeout: ${this.operations.size} operations still running`); }), ]); logger.debug("✅ Graceful shutdown completed"); } catch (error) { logger.warn(`⚠️ Shutdown warning: ${error instanceof Error ? error.message : String(error)}`); } } } /** * Global instances for convenience */ export const globalShutdown = new GracefulShutdown(); export const providerCircuitBreaker = new CircuitBreaker(3, 30000); // 3 failures, 30s timeout export const apiRateLimiter = new RateLimiter(100, 60000); // 100 requests per minute