UNPKG

@bitrix24/b24jssdk

Version:

Bitrix24 REST API JavaScript SDK

403 lines (400 loc) 12.6 kB
/** * @package @bitrix24/b24jssdk * @version 1.1.0 * @copyright (c) 2026 Bitrix24 * @license MIT * @see https://github.com/bitrix24/b24jssdk * @see https://bitrix24.github.io/b24jssdk/ */ import { LoggerFactory } from '../../../logger/logger-factory.mjs'; var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); class RateLimiter { static { __name(this, "RateLimiter"); } #tokens; #lastRefill; #refillIntervalMs; #config; #lockQueue = []; #originalConfig; // Original configuration for recovery #errorThreshold = 5; // 60-second error threshold to reduce limits #successThreshold = 20; // Consecutive success threshold for restoring limits #minDrainRate = 0.5; // Minimum drain rate #minBurstLimit = 5; // Minimum burst limit #errorTimestamps = []; // Error timestamps (last 60 seconds) #successTimestamps = []; // Timestamps of successful requests _logger; constructor(config) { this._logger = LoggerFactory.createNullLogger(); this.#config = config; this.#originalConfig = { ...config }; this.#tokens = config.burstLimit; this.#lastRefill = Date.now(); this.#refillIntervalMs = 1e3 / config.drainRate; } getTitle() { return "rateLimiter"; } // region Logger //// setLogger(logger) { this._logger = logger; } getLogger() { return this._logger; } // endregion //// /** * @inheritDoc */ async canProceed(requestId, _method, _params) { await this.#acquireLock(requestId); try { const now = Date.now(); const timePassed = now - this.#lastRefill; const refillAmount = timePassed * this.#config.drainRate / 1e3; this.#tokens = Math.min( this.#config.burstLimit, this.#tokens + refillAmount ); this.#lastRefill = now; return this.#tokens >= 1; } finally { this.#releaseLock(); } } /** * @inheritDoc */ async waitIfNeeded(requestId, _method, _params) { await this.#acquireLock(requestId); try { const now = Date.now(); const timePassed = now - this.#lastRefill; const refillAmount = timePassed * this.#config.drainRate / 1e3; this.#tokens = Math.min( this.#config.burstLimit, this.#tokens + refillAmount ); this.#lastRefill = now; if (this.#tokens >= 1) { this.#tokens -= 1; return 0; } const deficit = 1 - this.#tokens; return Math.ceil(deficit * this.#refillIntervalMs); } finally { this.#releaseLock(); } } /** * Error handler. * If there are a lot of errors, we'll lower the limits. */ async handleExceeded(requestId) { await this.#acquireLock(requestId); try { this.#recordError(); if (this.#config.adaptiveEnabled && this.#shouldReduceLimits()) { this.#reduceLimits(requestId); } this.#tokens = 0; return this.#refillIntervalMs + 1e3; } finally { this.#releaseLock(); } } /** * Successful request handler. * If everything is OK, we'll restore the limits. */ async updateStats(requestId, method, _data) { if (method.startsWith("batch::")) { return; } await this.#acquireLock(requestId); try { this.#recordSuccess(); if (this.#config.adaptiveEnabled) { this.#logStat(requestId); } if (this.#config.adaptiveEnabled && this.#shouldRestoreLimits()) { this.#restoreLimits(requestId); } } finally { this.#releaseLock(); } } /** * @inheritDoc */ async reset() { await this.#acquireLock("reset"); try { this.#tokens = this.#config.burstLimit; this.#lastRefill = Date.now(); this.#errorTimestamps = []; this.#successTimestamps = []; this.#config.drainRate = this.#originalConfig.drainRate; this.#config.burstLimit = this.#originalConfig.burstLimit; this.#refillIntervalMs = 1e3 / this.#config.drainRate; } finally { this.#releaseLock(); } } /** * @inheritDoc */ getStats() { return { tokens: this.#tokens, burstLimit: this.#config.burstLimit, originalBurstLimit: this.#originalConfig.burstLimit, drainRate: this.#config.drainRate, originalDrainRate: this.#originalConfig.drainRate, refillIntervalMs: this.#refillIntervalMs, lastRefill: this.#lastRefill, pendingRequests: this.#lockQueue.length, recentErrors: this.#errorTimestamps.length, recentSuccesses: this.#successTimestamps.length }; } /** * @inheritDoc */ async setConfig(config) { await this.#acquireLock("setConfig"); try { this.#config = config; this.#originalConfig = { ...config }; this.#refillIntervalMs = 1e3 / this.#config.drainRate; if (config.burstLimit > this.#tokens) { this.#tokens = Math.min(config.burstLimit, this.#tokens); } this.#errorTimestamps = []; this.#successTimestamps = []; } finally { this.#releaseLock(); } } /** * Acquire a lock for the critical section * Uses a promise queue */ async #acquireLock(requestId) { return new Promise((resolve) => { const queueLength = this.#lockQueue.push(resolve); if (queueLength > 1) { this.#logAcquireQueue(requestId, queueLength); } if (this.#lockQueue.length === 1) { resolve(); } }); } /** * Releases the lock and allows the next person in the queue to proceed */ #releaseLock() { this.#lockQueue.shift(); if (this.#lockQueue.length > 0) { const nextResolve = this.#lockQueue[0]; nextResolve(); } } /** * Checks whether the limits need to be reduced */ #shouldReduceLimits() { return this.#errorTimestamps.length >= this.#errorThreshold; } /** * Checks whether limits need to be restored * Restore if: * 1. Many successful requests (more than the threshold) * 2. Few errors (less than half the threshold) * 3. Current limits are lower than the original ones */ #shouldRestoreLimits() { return this.#successTimestamps.length >= this.#successThreshold && this.#errorTimestamps.length < this.#errorThreshold / 2 && (this.#config.drainRate < this.#originalConfig.drainRate || this.#config.burstLimit < this.#originalConfig.burstLimit); } /** * Reduces limits for frequent errors */ #reduceLimits(requestId) { const newDrainRate = Math.max( this.#minDrainRate, Number.parseFloat((this.#config.drainRate * 0.8).toFixed(2)) ); const newBurstLimit = Math.max( this.#minBurstLimit, Number.parseFloat((this.#config.burstLimit * 0.8).toFixed(2)) ); this.#config.drainRate = newDrainRate; this.#config.burstLimit = newBurstLimit; this.#refillIntervalMs = 1e3 / newDrainRate; this.#logReduceLimits(requestId, newDrainRate, newBurstLimit); this.#errorTimestamps = []; this.#successTimestamps = []; } /** * Restores limits during stable operation */ #restoreLimits(requestId) { if (this.#config.drainRate === this.#originalConfig.drainRate && this.#config.burstLimit === this.#originalConfig.burstLimit) { return; } const newDrainRate = Math.min( this.#originalConfig.drainRate, Number.parseFloat((this.#config.drainRate * 1.1).toFixed(2)) ); const newBurstLimit = Math.min( this.#originalConfig.burstLimit, Number.parseFloat((this.#config.burstLimit * 1.1).toFixed(2)) ); this.#config.drainRate = newDrainRate; this.#config.burstLimit = newBurstLimit; this.#refillIntervalMs = 1e3 / newDrainRate; this.#logRestoreLimits(requestId, newDrainRate, newBurstLimit); this.#errorTimestamps = []; this.#successTimestamps = []; } /** * Writes an error to the temporary history */ #recordError() { const now = Date.now(); this.#errorTimestamps.push(now); this.#successTimestamps = []; this.#cleanupOldErrors(now); } /** * Clears old errors (older than 60 seconds) */ #cleanupOldErrors(now) { const cutoff = now - 6e4; this.#errorTimestamps = this.#errorTimestamps.filter((timestamp) => timestamp > cutoff); } /** * Writes a successful request to the temporary history */ #recordSuccess() { const now = Date.now(); this.#successTimestamps.push(now); this.#cleanupOldSuccesses(); this.#cleanupOldErrors(now); } /** * Clears old progress */ #cleanupOldSuccesses() { this.#successTimestamps = this.#successTimestamps.slice(-1 * this.#successThreshold); } // region Log //// #logReduceLimits(requestId, currentDrainRate, currentBurstLimit) { const originalDrainRate = this.#originalConfig.drainRate; const drainRateCondition = currentDrainRate < originalDrainRate; const originalBurstLimit = this.#originalConfig.burstLimit; const burstLimitCondition = currentBurstLimit < originalBurstLimit; this.getLogger().warning( `${this.getTitle()} is lowering limits due to frequent errors`, { requestId, drainRate: { current: currentDrainRate, original: originalDrainRate, condition: drainRateCondition, formatted: `(${currentDrainRate} < ${originalDrainRate}) ${drainRateCondition}` }, burstLimit: { current: currentBurstLimit, original: originalBurstLimit, condition: burstLimitCondition, formatted: `(${currentBurstLimit} < ${originalBurstLimit}) ${burstLimitCondition}` } } ); } #logRestoreLimits(requestId, currentDrainRate, currentBurstLimit) { const originalDrainRate = this.#originalConfig.drainRate; const drainRateCondition = currentDrainRate < originalDrainRate; const originalBurstLimit = this.#originalConfig.burstLimit; const burstLimitCondition = currentBurstLimit < originalBurstLimit; this.getLogger().warning( `${this.getTitle()} increases limits during stable operation`, { requestId, drainRate: { current: currentDrainRate, original: originalDrainRate, condition: drainRateCondition, formatted: `(${currentDrainRate} < ${originalDrainRate}) ${drainRateCondition}` }, burstLimit: { current: currentBurstLimit, original: originalBurstLimit, condition: burstLimitCondition, formatted: `(${currentBurstLimit} < ${originalBurstLimit}) ${burstLimitCondition}` } } ); } #logAcquireQueue(requestId, queueLength) { this.getLogger().debug(`${this.getTitle()} request in queue`, { requestId, queueLength }); } #logStat(requestId) { const successCount = this.#successTimestamps.length; const successThreshold = this.#successThreshold; const successCondition = successCount >= successThreshold; const errorCount = this.#errorTimestamps.length; const errorThreshold = this.#errorThreshold; const failCondition = errorCount < errorThreshold / 2; const currentDrainRate = this.#config.drainRate; const originalDrainRate = this.#originalConfig.drainRate; const drainRateCondition = currentDrainRate < originalDrainRate; const currentBurstLimit = this.#config.burstLimit; const originalBurstLimit = this.#originalConfig.burstLimit; const burstLimitCondition = currentBurstLimit < originalBurstLimit; this.getLogger().debug(`${this.getTitle()} state`, { requestId, success: { count: successCount, threshold: successThreshold, condition: successCondition, formatted: `(${successCount} >= ${successThreshold}) ${successCondition}` }, fail: { count: errorCount, threshold: errorThreshold / 2, condition: failCondition, formatted: `(${errorCount} < ${errorThreshold / 2}) ${failCondition}` }, drainRate: { current: currentDrainRate, original: originalDrainRate, condition: drainRateCondition, formatted: `(${currentDrainRate} < ${originalDrainRate}) ${drainRateCondition}` }, burstLimit: { current: currentBurstLimit, original: originalBurstLimit, condition: burstLimitCondition, formatted: `(${currentBurstLimit} < ${originalBurstLimit}) ${burstLimitCondition}` } }); } // endregion //// } export { RateLimiter }; //# sourceMappingURL=rate-limiter.mjs.map