A tool for tailing a labelers' firehose, rehydrating, and storing records for future analysis of moderation decisions.
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}