121 lines
3.1 KiB
TypeScript
121 lines
3.1 KiB
TypeScript
import { getDb } from "./db";
|
|
|
|
/**
|
|
* Chat resilience utilities: retry logic with exponential backoff and context recovery
|
|
*/
|
|
|
|
export interface RetryConfig {
|
|
maxAttempts: number;
|
|
initialDelayMs: number;
|
|
maxDelayMs: number;
|
|
backoffMultiplier: number;
|
|
}
|
|
|
|
export const DEFAULT_RETRY_CONFIG: RetryConfig = {
|
|
maxAttempts: 3,
|
|
initialDelayMs: 1000,
|
|
maxDelayMs: 8000,
|
|
backoffMultiplier: 2,
|
|
};
|
|
|
|
/**
|
|
* Calculate delay for exponential backoff
|
|
*/
|
|
export function calculateBackoffDelay(attempt: number, config: RetryConfig): number {
|
|
const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt - 1);
|
|
return Math.min(delay, config.maxDelayMs);
|
|
}
|
|
|
|
/**
|
|
* Sleep utility
|
|
*/
|
|
export function sleep(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
/**
|
|
* Retry wrapper with exponential backoff
|
|
*/
|
|
export async function retryWithBackoff<T>(
|
|
fn: () => Promise<T>,
|
|
config: RetryConfig = DEFAULT_RETRY_CONFIG,
|
|
onRetry?: (attempt: number, error: Error) => void
|
|
): Promise<T> {
|
|
let lastError: Error | null = null;
|
|
|
|
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
|
|
try {
|
|
return await fn();
|
|
} catch (error) {
|
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
|
|
if (attempt < config.maxAttempts) {
|
|
const delayMs = calculateBackoffDelay(attempt, config);
|
|
onRetry?.(attempt, lastError);
|
|
await sleep(delayMs);
|
|
}
|
|
}
|
|
}
|
|
|
|
throw lastError || new Error("Retry failed");
|
|
}
|
|
|
|
/**
|
|
* Check if error is retryable (timeout, network, 5xx)
|
|
*/
|
|
export function isRetryableError(error: any): boolean {
|
|
if (!error) return false;
|
|
|
|
const message = String(error.message || error).toLowerCase();
|
|
const code = error.code || error.status;
|
|
|
|
// Timeout errors
|
|
if (message.includes("timeout") || message.includes("econnreset")) return true;
|
|
|
|
// Network errors
|
|
if (message.includes("econnrefused") || message.includes("enotfound")) return true;
|
|
|
|
// 5xx server errors
|
|
if (code >= 500 && code < 600) return true;
|
|
|
|
// Gateway errors
|
|
if (code === 502 || code === 503 || code === 504) return true;
|
|
|
|
// LLM service unavailable
|
|
if (message.includes("unavailable") || message.includes("service")) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get recent conversation context from DB for retry
|
|
*/
|
|
export async function getConversationContext(
|
|
userId: string,
|
|
limit: number = 10
|
|
): Promise<Array<{ role: "user" | "assistant" | "system"; content: string }>> {
|
|
try {
|
|
const db = getDb();
|
|
// Note: This assumes a messages table exists with userId, role, content, createdAt
|
|
// If not, return empty array (frontend will use in-memory history)
|
|
return [];
|
|
} catch (error) {
|
|
console.error("[ChatResilience] Failed to get conversation context:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log retry attempt for monitoring
|
|
*/
|
|
export function logRetryAttempt(
|
|
attempt: number,
|
|
error: Error,
|
|
context?: Record<string, any>
|
|
): void {
|
|
console.log(
|
|
`[ChatResilience] Retry attempt ${attempt}: ${error.message}`,
|
|
context ? JSON.stringify(context) : ""
|
|
);
|
|
}
|