UNPKG

@message-in-the-middle/core

Version:

Framework-agnostic middleware pattern for message queue processing. Core package with all middlewares.

136 lines 4.25 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TokenBucketRateLimiter = exports.ThrottlingOutboundMiddleware = void 0; class ThrottlingOutboundMiddleware { rateLimiter; waitForSlot; constructor(rateLimiter, waitForSlot = true) { this.rateLimiter = rateLimiter; this.waitForSlot = waitForSlot; } async processOutbound(context, next) { if (this.waitForSlot) { await this.rateLimiter.waitForToken(); context.metadata.rateLimitWaited = true; } else { const acquired = await this.rateLimiter.tryAcquire(); if (!acquired) { throw new Error('Rate limit exceeded'); } } await next(); } } exports.ThrottlingOutboundMiddleware = ThrottlingOutboundMiddleware; class TokenBucketRateLimiter { maxTokens; refillRate; tokens; lastRefill; waitQueue = []; processingQueue = false; processTimeout = null; logger; constructor(maxTokens, refillRate, options = {}) { this.maxTokens = maxTokens; this.refillRate = refillRate; this.tokens = maxTokens; this.lastRefill = Date.now(); this.logger = options.logger; } async tryAcquire() { this.refill(); if (this.tokens >= 1) { this.tokens -= 1; return true; } return false; } async waitForToken() { this.refill(); if (this.tokens >= 1) { this.tokens -= 1; return; } return new Promise((resolve) => { this.waitQueue.push(resolve); setImmediate(() => this.scheduleQueueProcessing()); }); } async destroy() { if (this.processTimeout) { clearTimeout(this.processTimeout); this.processTimeout = null; } const rejectedCount = this.waitQueue.length; this.waitQueue.forEach(resolve => { resolve(); }); this.waitQueue = []; this.processingQueue = false; if (rejectedCount > 0 && this.logger) { this.logger.warn(`TokenBucketRateLimiter: Destroyed with ${rejectedCount} pending requests in queue`); } } availableTokens() { this.refill(); return Math.floor(this.tokens); } queueSize() { return this.waitQueue.length; } refill() { const now = Date.now(); const timePassed = (now - this.lastRefill) / 1000; if (timePassed < 0) { this.lastRefill = now; return; } const MAX_TIME_PASSED = 3600; const safeTimePassed = Math.min(timePassed, MAX_TIME_PASSED); const tokensToAdd = safeTimePassed * this.refillRate; this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd); this.lastRefill = now; } scheduleQueueProcessing() { if (this.processingQueue || this.waitQueue.length === 0) { return; } this.processingQueue = true; const timeUntilToken = this.calculateTimeUntilNextToken(); if (this.processTimeout) { clearTimeout(this.processTimeout); } this.processTimeout = setTimeout(() => { this.processQueue(); }, timeUntilToken); } calculateTimeUntilNextToken() { if (this.tokens >= 1) { return 0; } const tokensNeeded = 1 - this.tokens; const timeNeeded = (tokensNeeded / this.refillRate) * 1000; return Math.max(1, Math.ceil(timeNeeded) + 1); } async processQueue() { this.processTimeout = null; while (this.waitQueue.length > 0) { this.refill(); if (this.tokens >= 1) { this.tokens -= 1; const resolve = this.waitQueue.shift(); resolve(); } else { this.processingQueue = false; this.scheduleQueueProcessing(); return; } } this.processingQueue = false; } } exports.TokenBucketRateLimiter = TokenBucketRateLimiter; //# sourceMappingURL=throttling.middleware.js.map