A tool for tailing a labelers' firehose, rehydrating, and storing records for future analysis of moderation decisions.
at main 2.4 kB view raw
1import { logger } from "../logger/index.js"; 2 3export interface RetryConfig { 4 maxAttempts: number; 5 initialDelay: number; 6 maxDelay: number; 7 backoffMultiplier: number; 8 retryableErrors?: ((error: any) => boolean)[]; 9} 10 11export class RetryError extends Error { 12 constructor( 13 message: string, 14 public attempts: number, 15 public lastError: Error 16 ) { 17 super(message); 18 this.name = "RetryError"; 19 } 20} 21 22export async function withRetry<T>( 23 fn: () => Promise<T>, 24 config: Partial<RetryConfig> = {} 25): Promise<T> { 26 const { 27 maxAttempts = 3, 28 initialDelay = 1000, 29 maxDelay = 30000, 30 backoffMultiplier = 2, 31 retryableErrors = [(error) => true], 32 } = config; 33 34 let lastError: Error; 35 let delay = initialDelay; 36 37 for (let attempt = 1; attempt <= maxAttempts; attempt++) { 38 try { 39 return await fn(); 40 } catch (error) { 41 lastError = error instanceof Error ? error : new Error(String(error)); 42 43 const isRetryable = retryableErrors.some((check) => check(lastError)); 44 45 if (!isRetryable || attempt >= maxAttempts) { 46 throw new RetryError( 47 `Operation failed after ${attempt} attempts`, 48 attempt, 49 lastError 50 ); 51 } 52 53 logger.warn( 54 { 55 attempt, 56 maxAttempts, 57 delay, 58 error: lastError.message, 59 }, 60 "Retrying after error" 61 ); 62 63 await new Promise((resolve) => setTimeout(resolve, delay)); 64 delay = Math.min(delay * backoffMultiplier, maxDelay); 65 } 66 } 67 68 throw new RetryError( 69 `Operation failed after ${maxAttempts} attempts`, 70 maxAttempts, 71 lastError! 72 ); 73} 74 75export function isRateLimitError(error: any): boolean { 76 return ( 77 error?.status === 429 || 78 error?.message?.toLowerCase().includes("rate limit") || 79 error?.message?.toLowerCase().includes("too many requests") 80 ); 81} 82 83export function isNetworkError(error: any): boolean { 84 return ( 85 error?.code === "ECONNRESET" || 86 error?.code === "ENOTFOUND" || 87 error?.code === "ETIMEDOUT" || 88 error?.message?.toLowerCase().includes("network") || 89 error?.message?.toLowerCase().includes("timeout") 90 ); 91} 92 93export function isServerError(error: any): boolean { 94 return error?.status >= 500 && error?.status < 600; 95} 96 97export function isRecordNotFoundError(error: any): boolean { 98 return ( 99 error?.error === "RecordNotFound" || 100 error?.message?.includes("RecordNotFound") 101 ); 102}