TokenBucket limiter with lazy refill

TypeScript
/** Token bucket: allows short bursts while enforcing a sustained refill rate. */
export class TokenBucket {
  private tokens: number;
  private updatedAt: number;

  constructor(
    private readonly capacity: number,
    private readonly refillPerSecond: number,
    private readonly clock: () => number = Date.now,
  ) {
    if (capacity < 1 || refillPerSecond <= 0) throw new RangeError('capacity and rate must be positive');
    this.tokens = capacity;
    this.updatedAt = clock();
  }

  /** Consume tokens if available, else report how long the caller must wait. */
  tryTake(count = 1): { ok: true } | { ok: false; retryAfterMs: number } {
    const now = this.clock();
    const gained = ((now - this.updatedAt) / 1_000) * this.refillPerSecond;
    this.tokens = Math.min(this.capacity, this.tokens + gained);
    this.updatedAt = now;
    if (this.tokens >= count) {
      this.tokens -= count;
      return { ok: true };
    }
    const missing = count - this.tokens;
    return { ok: false, retryAfterMs: Math.ceil((missing / this.refillPerSecond) * 1_000) };
  }

  /** Wait until `count` tokens are available, then consume them. */
  async take(count = 1): Promise<void> {
    for (;;) {
      const result = this.tryTake(count);
      if (result.ok) return;
      await new Promise((resolve) => setTimeout(resolve, result.retryAfterMs));
    }
  }
}