burstFriendlyTokenBucketLimiter

JavaScript
// Token bucket allowing bursts up to capacity while enforcing an average rate.
export class TokenBucket {
  constructor(capacity, refillPerSecond, now = () => performance.now()) {
    if (!(capacity > 0) || !(refillPerSecond > 0)) throw new RangeError('capacity and rate must be positive');
    this.capacity = capacity;
    this.tokens = capacity;
    this.ratePerMs = refillPerSecond / 1000;
    this.lastRefill = now();
    this.now = now;
  }

  #refill() {
    const current = this.now();
    const elapsed = Math.max(0, current - this.lastRefill);
    this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.ratePerMs);
    this.lastRefill = current;
  }

  tryTake(count = 1) {
    if (!(count > 0) || count > this.capacity) throw new RangeError('count must be within capacity');
    this.#refill();
    if (this.tokens < count) return false;
    this.tokens -= count;
    return true;
  }

  async acquire(count = 1) {
    while (!this.tryTake(count)) {
      this.#refill();
      const waitMs = Math.ceil((count - this.tokens) / this.ratePerMs);
      await new Promise((resolve) => setTimeout(resolve, Math.max(1, waitMs)));
    }
  }
}