UNPKG

roam-research-mcp

Version:
56 lines (55 loc) • 1.92 kB
/** * Shared rate-limit retry. * * Roam throttles bursts of writes ("Too many requests, try again in a minute"). * Every write path needs the same response — wait and retry with backoff — and * when one path lacks it the failure is expensive: a multi-level batch that dies * midway leaves a half-written page, and Roam has no undo to reverse it. * * This lived as a private method on BatchOperations, which is exactly why * executeStagedBatch was written without it. It belongs here so a new write path * can reach for it. */ import { isRateLimitError } from './errors.js'; export const DEFAULT_RATE_LIMIT_RETRY = { maxRetries: 3, initialDelayMs: 1000, maxDelayMs: 60000, backoffMultiplier: 2, }; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Run `fn`, retrying with exponential backoff while it fails with a rate-limit * error. Any other error is rethrown immediately — retrying a malformed request * just burns quota. * * Rethrows the last rate-limit error once retries are exhausted, so callers keep * whatever context they wrap it in. */ export async function withRateLimitRetry(fn, config = {}, onRetry) { const { maxRetries, initialDelayMs, maxDelayMs, backoffMultiplier } = { ...DEFAULT_RATE_LIMIT_RETRY, ...config, }; let delay = initialDelayMs; let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { if (!isRateLimitError(error)) throw error; lastError = error; if (attempt < maxRetries) { const waitMs = Math.min(delay, maxDelayMs); onRetry?.(attempt + 1, waitMs, error); await sleep(waitMs); delay *= backoffMultiplier; } } } throw lastError; }