/** * Which address an outbound request leaves by, and how many it may carry. * * Venues rate-limit per IP. One address gets a fixed allowance, so placing more * orders than that means more addresses — and choosing between them has to be * accounted centrally or every caller spends the same allowance independently. * * ## This adds no counting * * `@forgezero/providers/http` already reserves before a call and settles from * the response, and its budget is keyed by an arbitrary string rather than a * real hostname. So the per-address cap IS that budget with the address as the * key. What this module adds is the CHOICE of address and the health of each * one, which genuinely did not exist. * * Writing a second counter here would mean two answers to "how much is left", * and they would disagree the first time a response settled differently from * its reservation. * * ## Sticky by key, not round-robin * * Orders for one account should leave by one address where possible. A venue * sees a coherent client rather than an account whose requests arrive from four * places, and — more practically — a per-address weight limit is easier to * reason about when one account's burst lands in one bucket rather than * smeared across all of them. * * Stickiness is a preference, never a guarantee. When the preferred address is * exhausted or unhealthy the request moves, because refusing an order to * preserve affinity is the wrong trade. */ export declare class PoolExhausted extends Error { readonly addresses: number; readonly retryAfterMs: number; constructor(addresses: number, retryAfterMs: number); } export declare class PoolEmpty extends Error { constructor(); } export interface PoolAddress { /** The outbound address, or a label for one. Also the budget key. */ id: string; /** Local address to bind to, when the host can. Absent means the default route. */ bind?: string; enabled: boolean; /** Set when a venue has banned this address. Cleared by hand or by expiry. */ blockedUntilMs?: number; } export interface PoolOptions { addresses: readonly PoolAddress[]; /** Requests per address per window. The owner's figure is 50. */ perAddress: number; windowMs: number; /** Stop at a fraction of the limit. The venue's window boundary is not ours. */ headroom?: number; now?: () => number; /** * Off means every request takes the default route and no accounting happens. * * Correct for a single-node install, and being able to turn it off is what * makes a routing problem debuggable: an operator can remove the routing. */ enabled?: boolean; } export interface Lease { /** The address chosen, or null when the pool is off. */ address: PoolAddress | null; /** Call with the true cost once the response is in. */ settle: (actualCost: number) => void; } /** * A pool over the shared budget. * * Deliberately not a class: the state that matters lives in the budget ledger * in `http.ts`, which is module-level and shared by every client in the * process. A pool holding its own counters would be a per-instance view of a * per-process limit, which is the bug this exists to avoid. */ export declare function createAddressPool(options: PoolOptions): { /** * Take an address for one request. * * Reserves BEFORE the request, like the underlying budget: counting on the * way back lets a burst of concurrent orders all pass the check and * collectively exceed the limit — which is precisely what happens when a * strategy wakes up. */ take(args?: { key?: string; cost?: number; }): Lease; /** * Mark an address banned by the venue. * * A 418 from Binance means this address is banned and hammering makes it * longer, so it leaves the rotation entirely rather than merely running out * of budget — a budget resets on its own and a ban does not. */ block(id: string, forMs: number): void; /** What each address has spent. For an admin screen and for a decision to scale. */ state(): { id: string; spent: number; limit: number; blocked: boolean; enabled: boolean; }[]; /** Total capacity per window, so a screen can say "you can place N". */ capacity: () => number; enabled: boolean; }; export type AddressPool = ReturnType;