2026-06-01 21:44:29 +07:00
|
|
|
// Utility functions shared across services
|
|
|
|
|
|
|
|
|
|
export function delay(ms: number): Promise<void> {
|
|
|
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 19:46:08 +07:00
|
|
|
export * from "./pagination.js";
|
|
|
|
|
|
2026-06-02 21:06:42 +07:00
|
|
|
// ---------------------------------------------------------------------------
|
2026-06-09 11:56:03 +07:00
|
|
|
// Retry with exponential backoff
|
2026-06-02 21:06:42 +07:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
export async function retryWithBackoff<T>(
|
|
|
|
|
fn: () => Promise<T>,
|
2026-06-09 11:56:03 +07:00
|
|
|
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;
|
|
|
|
|
} = {},
|
2026-06-02 21:06:42 +07:00
|
|
|
): Promise<T> {
|
|
|
|
|
const {
|
|
|
|
|
retries = 3,
|
|
|
|
|
minTimeout = 1_000,
|
|
|
|
|
maxTimeout = 30_000,
|
|
|
|
|
factor = 2,
|
2026-06-06 12:04:10 +07:00
|
|
|
signal,
|
2026-06-02 21:06:42 +07:00
|
|
|
} = options;
|
|
|
|
|
|
|
|
|
|
let lastError: Error | undefined;
|
|
|
|
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
2026-06-06 12:04:10 +07:00
|
|
|
if (signal?.aborted) {
|
|
|
|
|
const err = new Error("Aborted");
|
|
|
|
|
err.name = "AbortError";
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
2026-06-09 10:16:04 +07:00
|
|
|
|
2026-06-02 21:06:42 +07:00
|
|
|
try {
|
|
|
|
|
return await fn();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
lastError = err instanceof Error ? err : new Error(String(err));
|
2026-06-06 12:04:10 +07:00
|
|
|
if (lastError.name === "AbortError") {
|
|
|
|
|
throw lastError;
|
|
|
|
|
}
|
2026-06-02 21:06:42 +07:00
|
|
|
if (attempt === retries) break;
|
|
|
|
|
const backoff = Math.min(
|
|
|
|
|
minTimeout * factor ** attempt + Math.random() * 100,
|
|
|
|
|
maxTimeout,
|
|
|
|
|
);
|
2026-06-09 10:16:04 +07:00
|
|
|
|
2026-06-06 12:04:10 +07:00
|
|
|
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();
|
2026-06-09 10:16:04 +07:00
|
|
|
|
2026-06-06 12:04:10 +07:00
|
|
|
timeoutId = setTimeout(() => {
|
|
|
|
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
|
|
|
resolve();
|
|
|
|
|
}, backoff);
|
2026-06-09 10:16:04 +07:00
|
|
|
|
2026-06-06 12:04:10 +07:00
|
|
|
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
|
|
|
});
|
2026-06-02 21:06:42 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
throw lastError!;
|
|
|
|
|
}
|