- Split llmModerationClient.ts (2170 lines) into 5 focused sub-modules - Split aiAnalyzer.ts (1282 lines) into 4 modular pipelines - Split messages.db.ts (826 lines) into 5 domain-specific modules - Moved shared schema to @bete/shared, eliminated backend duplication - Added createChildLogger to all voice-recording and AI moderation modules - Extracted tryCommandThenFallback, normalizeMediaState, DEFAULT_VOICE_STATUS - Created shared pagination.ts utility, eliminated 5+ cursor-pagination duplications - Created shared messageMapper.ts for row mapping - Standardized backend error handling with asyncHandler - Added frontend createLogger utility and useAsyncAction hook - Added structured logging to frontend hooks, socket, and API client Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
78 lines
2.1 KiB
TypeScript
78 lines
2.1 KiB
TypeScript
// Utility functions shared across services
|
|
|
|
export function delay(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
export * from "./pagination.js";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Retry with exponential backoff
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export async function retryWithBackoff<T>(
|
|
fn: () => Promise<T>,
|
|
options: {
|
|
/** Number of retry attempts (default: 3) */
|
|
retries?: number;
|
|
/** Initial delay in ms (default: 1000) */
|
|
minTimeout?: number;
|
|
/** Maximum delay in ms (default: 30000) */
|
|
maxTimeout?: number;
|
|
/** Multiplication factor for each retry (default: 2) */
|
|
factor?: number;
|
|
/** Optional AbortSignal to cancel retries */
|
|
signal?: AbortSignal;
|
|
} = {},
|
|
): Promise<T> {
|
|
const {
|
|
retries = 3,
|
|
minTimeout = 1_000,
|
|
maxTimeout = 30_000,
|
|
factor = 2,
|
|
signal,
|
|
} = options;
|
|
|
|
let lastError: Error | undefined;
|
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
if (signal?.aborted) {
|
|
const err = new Error("Aborted");
|
|
err.name = "AbortError";
|
|
throw err;
|
|
}
|
|
|
|
try {
|
|
return await fn();
|
|
} catch (err) {
|
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
if (lastError.name === "AbortError") {
|
|
throw lastError;
|
|
}
|
|
if (attempt === retries) break;
|
|
const backoff = Math.min(
|
|
minTimeout * factor ** attempt + Math.random() * 100,
|
|
maxTimeout,
|
|
);
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
let timeoutId: NodeJS.Timeout;
|
|
const onAbort = () => {
|
|
clearTimeout(timeoutId);
|
|
const abortErr = new Error("Aborted");
|
|
abortErr.name = "AbortError";
|
|
reject(abortErr);
|
|
};
|
|
if (signal?.aborted) return onAbort();
|
|
|
|
timeoutId = setTimeout(() => {
|
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
resolve();
|
|
}, backoff);
|
|
|
|
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
});
|
|
}
|
|
}
|
|
throw lastError!;
|
|
}
|