autotel
Version:
Write Once, Observe Anywhere
99 lines (83 loc) • 2.58 kB
text/typescript
/**
* Token bucket rate limiter for event subscribers
*
* Prevents overwhelming downstream events platforms with too many events.
* Uses token bucket algorithm for smooth rate limiting with burst capacity.
*/
export interface RateLimiterConfig {
/** Maximum events per second (default: 100) */
maxEventsPerSecond: number;
/** Burst capacity - max events in a single burst (default: 2x rate) */
burstCapacity?: number;
}
/**
* Token bucket rate limiter
*
* Allows bursts up to burstCapacity, then smooths to maxEventsPerSecond.
* Thread-safe for async operations.
*/
export class TokenBucketRateLimiter {
private tokens: number;
private readonly maxTokens: number;
private readonly refillRate: number; // tokens per millisecond
private lastRefill: number;
constructor(config: RateLimiterConfig) {
this.maxTokens = config.burstCapacity || config.maxEventsPerSecond * 2;
this.tokens = this.maxTokens; // Start with full bucket
this.refillRate = config.maxEventsPerSecond / 1000; // Convert to per-ms
this.lastRefill = Date.now();
}
/**
* Try to consume a token (allow an event)
* Returns true if allowed, false if rate limit exceeded
*/
tryConsume(count = 1): boolean {
this.refill();
if (this.tokens >= count) {
this.tokens -= count;
return true;
}
return false;
}
/**
* Wait until a token is available (async rate limiting)
* Returns a promise that resolves when the event can be processed
*/
async waitForToken(count = 1): Promise<void> {
this.refill();
if (this.tokens >= count) {
this.tokens -= count;
return;
}
// Calculate wait time until we have enough tokens
const tokensNeeded = count - this.tokens;
const waitMs = Math.ceil(tokensNeeded / this.refillRate);
await new Promise((resolve) => setTimeout(resolve, waitMs));
// After waiting, try again (recursive)
return this.waitForToken(count);
}
/**
* Refill tokens based on elapsed time
*/
private refill(): void {
const now = Date.now();
const elapsed = now - this.lastRefill;
const tokensToAdd = elapsed * this.refillRate;
this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
this.lastRefill = now;
}
/**
* Get current available tokens (for testing/debugging)
*/
getAvailableTokens(): number {
this.refill();
return Math.floor(this.tokens);
}
/**
* Reset the rate limiter (for testing)
*/
reset(): void {
this.tokens = this.maxTokens;
this.lastRefill = Date.now();
}
}