SPB Git forge

spb/market-atlas

Public
12commits 1branches 0releases
1.1 MBsize
maindefault branch
10 days agolast push
TypeScript 96.7% SQL 1.6% CSS 0.8% JavaScript 0.5%
2.4 KB · 88 lines typescript
Raw Blame History
1/** Token bucket with async acquisition. Never busy-waits. */2export class TokenBucket {3  private tokens: number;4  private last: number;5  private queue: Array<() => void> = [];6  private timer: NodeJS.Timeout | null = null;78  constructor(9    public ratePerSec: number,10    public burst: number = Math.max(1, Math.ceil(ratePerSec)),11  ) {12    this.tokens = burst;13    this.last = Date.now();14  }1516  private refill() {17    const now = Date.now();18    const delta = (now - this.last) / 1000;19    this.tokens = Math.min(this.burst, this.tokens + delta * this.ratePerSec);20    this.last = now;21  }2223  tryTake(): boolean {24    this.refill();25    if (this.tokens >= 1) {26      this.tokens -= 1;27      return true;28    }29    return false;30  }3132  acquire(): Promise<void> {33    if (this.tryTake()) return Promise.resolve();34    return new Promise((resolve) => {35      this.queue.push(resolve);36      this.schedule();37    });38  }3940  private schedule() {41    if (this.timer) return;42    const waitMs = Math.max(5, ((1 - this.tokens) / this.ratePerSec) * 1000);43    this.timer = setTimeout(() => {44      this.timer = null;45      while (this.queue.length && this.tryTake()) this.queue.shift()!();46      if (this.queue.length) this.schedule();47    }, waitMs);48  }4950  pending(): number {51    return this.queue.length;52  }53}5455/** Central limiter keyed by host. Connectors never sleep on their own. */56export class RateLimiter {57  private buckets = new Map<string, TokenBucket>();58  constructor(private defaults: { ratePerSec: number; burst?: number } = { ratePerSec: 2 }) {}5960  configure(host: string, ratePerSec: number, burst?: number) {61    this.buckets.set(host.toLowerCase(), new TokenBucket(ratePerSec, burst));62  }6364  bucket(host: string): TokenBucket {65    const key = host.toLowerCase();66    let b = this.buckets.get(key);67    if (!b) {68      b = new TokenBucket(this.defaults.ratePerSec, this.defaults.burst);69      this.buckets.set(key, b);70    }71    return b;72  }7374  acquire(host: string): Promise<void> {75    return this.bucket(host).acquire();76  }7778  snapshot(): Array<{ host: string; ratePerSec: number; pending: number }> {79    return [...this.buckets.entries()].map(([host, b]) => ({ host, ratePerSec: b.ratePerSec, pending: b.pending() }));80  }81}8283/** Exponential backoff with full jitter. */84export function backoffMs(attempt: number, baseMs = 1000, maxMs = 5 * 60_000): number {85  const exp = Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));86  return Math.floor(Math.random() * exp);87}88