@message-in-the-middle/core
Version:
Framework-agnostic middleware pattern for message queue processing. Core package with all middlewares.
136 lines • 4.25 kB
JavaScript
"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