@bitrix24/b24jssdk
Version:
Bitrix24 REST API JavaScript SDK
403 lines (400 loc) • 12.6 kB
JavaScript
/**
* @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