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));
}
}
}