import Lacuna from '../lacuna'; import { RPC_LIMIT_ERROR_CODE } from './constants'; import { sleep } from './util'; import type { ResponseHandler } from './responses'; export interface RpcLimitHandlerOptions { /** How many times to retry a single call before giving up. Default 5. */ maxRetries?: number; /** Delay before the first retry, doubling each attempt. Default 1000ms. */ baseDelayMs?: number; /** Ceiling on the backoff delay. Default 60000ms. */ maxDelayMs?: number; } const DEFAULTS = { maxRetries: 5, baseDelayMs: 1000, maxDelayMs: 60000, }; /** * A ready-made response handler for the server's RPC limit error ("slow * down", code 1010). Backs off progressively and retries until the call * succeeds or maxRetries is reached, so a long-running script isn't stopped by * hitting the per-minute request limit. * * Retrying re-publishes the response as a new event with an incremented * attempt, so this handler is called again if the limit is still in force - * the backoff loop is the event flow, not a loop in here. * * Enable it with Lacuna#enableRpcLimitHandler. */ export function rpcLimitHandler( lacuna: Lacuna, options: RpcLimitHandlerOptions = {} ): ResponseHandler { const { maxRetries, baseDelayMs, maxDelayMs } = { ...DEFAULTS, ...options }; return async (event) => { if (event.response.error?.code !== RPC_LIMIT_ERROR_CODE) return; const { module, method } = event.request; if (event.attempt >= maxRetries) { lacuna.log.warn( `RPC limit still in force on ${module}/${method} after ${maxRetries} ` + `retries - giving up and returning the error.` ); return; } const delay = Math.min(baseDelayMs * 2 ** event.attempt, maxDelayMs); lacuna.log.warn( `RPC limit hit on ${module}/${method}, retrying in ${delay}ms ` + `(${event.attempt + 1}/${maxRetries}).` ); await sleep(delay); return event.retry(); }; }