export class EnvelopeThrottler { private buckets = { message: { max: 50, tokens: 0, lastRefill: 0, waiting: [] as Array<() => void>, timer: null as NodeJS.Timeout | null, }, command: { max: 200, tokens: 0, lastRefill: 0, waiting: [] as Array<() => void>, timer: null as NodeJS.Timeout | null, }, } private readonly refillInterval = 1000 // ms private readonly throughputFactor = 0.9 // memo for fast-path (zero allocation) private readonly voidPromise = Promise.resolve() public throttle(type: keyof typeof this.buckets): Promise { const bucket = this.buckets[type] this.refillTokens(type) if (bucket.tokens > 0) { bucket.tokens-- return this.voidPromise } const { resolve, promise } = Promise.withResolvers() bucket.waiting.push(resolve) this.scheduleRefill(type) return promise } private refillTokens(type: keyof typeof this.buckets): void { const bucket = this.buckets[type] const now = Date.now() const elapsed = now - bucket.lastRefill if (elapsed >= this.refillInterval) { bucket.tokens = Math.floor(bucket.max * this.throughputFactor) bucket.lastRefill = now } } private scheduleRefill(type: keyof typeof this.buckets): void { const bucket = this.buckets[type] if (bucket.timer) return const delay = Math.max(0, this.refillInterval - (Date.now() - bucket.lastRefill)) bucket.timer = setTimeout(() => { bucket.timer = null this.refillTokens(type) while (bucket.tokens > 0 && bucket.waiting.length > 0) { const resolve = bucket.waiting.shift() if (resolve) { bucket.tokens-- resolve() } } if (bucket.waiting.length > 0) { this.scheduleRefill(type) } }, delay) } }