@bitrix24/b24jssdk
Version:
Bitrix24 REST API JavaScript SDK
310 lines (307 loc) • 9.71 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 { RateLimiter } from './rate-limiter.mjs';
import { OperatingLimiter } from './operating-limiter.mjs';
import { AdaptiveDelayer } from './adaptive-delayer.mjs';
import { LoggerFactory } from '../../../logger/logger-factory.mjs';
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
class RestrictionManager {
static {
__name(this, "RestrictionManager");
}
#rateLimiter;
#operatingLimiter;
#adaptiveDelayer;
#config;
#stats = {
/** Retry attempts */
retries: 0,
/** Consecutive errors */
consecutiveErrors: 0,
/** Limit triggers */
limitHits: 0
};
#errorCounts = /* @__PURE__ */ new Map();
_logger;
constructor(params) {
this._logger = LoggerFactory.createNullLogger();
this.#config = params;
this.#rateLimiter = new RateLimiter(params.rateLimit);
this.#operatingLimiter = new OperatingLimiter(params.operatingLimit);
this.#adaptiveDelayer = new AdaptiveDelayer(params.adaptiveConfig, this.#operatingLimiter);
}
// region Logger ////
setLogger(logger) {
this._logger = logger;
this.#rateLimiter.setLogger(this._logger);
this.#operatingLimiter.setLogger(this._logger);
this.#adaptiveDelayer.setLogger(this._logger);
}
getLogger() {
return this._logger;
}
// endregion ////
async applyOperatingLimits(requestId, method, params) {
const operatingWait = await this.#operatingLimiter.waitIfNeeded(requestId, method, params);
if (operatingWait > 0) {
this.incrementStats("limitHits");
this.#logMethodBlocked(this.#operatingLimiter.getTitle(), requestId, method, operatingWait);
await this.#delay(operatingWait);
} else {
const adaptiveDelay = await this.#adaptiveDelayer.waitIfNeeded(requestId, method, params);
if (adaptiveDelay > 0) {
this.incrementStats("limitHits");
this.#logMethodBlocked(this.#adaptiveDelayer.getTitle(), requestId, method, adaptiveDelay);
await this.#delay(adaptiveDelay);
}
}
}
/**
* Checks and waits for the rate limit
* The loop is needed for parallel requests (Promise.all())
*/
async checkRateLimit(requestId, method) {
let waitTime;
let times = 1;
do {
waitTime = await this.#rateLimiter.waitIfNeeded(requestId, method);
if (waitTime > 0) {
this.incrementStats("limitHits");
this.#logMethodBlockedWithTimes(this.#rateLimiter.getTitle(), requestId, method, waitTime, times);
await this.#delay(waitTime);
times++;
}
} while (waitTime > 0);
}
async updateStats(requestId, method, timeData) {
await this.#operatingLimiter.updateStats(requestId, method, timeData);
await this.#adaptiveDelayer.updateStats(requestId, method, timeData);
await this.#rateLimiter.updateStats(requestId, method, timeData);
}
async handleError(requestId, method, params, error, attempt) {
if (this.#isRateLimitError(error)) {
const wait = await this.#handleRateLimitExceeded(requestId) * Math.pow(1.5, attempt);
this.#logError(this.#rateLimiter.getTitle(), requestId, "QUERY_LIMIT_EXCEEDED", error.message, method, wait);
return wait;
}
if (this.#isOperatingLimitError(error)) {
const wait = Math.max(1e4, await this.#handleOperatingLimitError(requestId, method, params, error));
this.#logError(this.#operatingLimiter.getTitle(), requestId, "OPERATION_TIME_LIMIT", error.message, method, wait);
return wait;
}
if (!this.#isNeedThrowError(error)) {
const baseDelay = await this.#getErrorBackoff(requestId);
const maxDelay = Math.max(3e4, baseDelay);
const delay = Math.min(maxDelay, baseDelay * Math.pow(2, attempt));
const jitter = delay * 0.1 * (Math.random() * 2 - 1);
const wait = Math.max(100, delay + jitter);
this.#logSomeError(requestId, error?.code ? `${error.code}` : "?", error.message, method, wait);
return wait;
}
return 0;
}
/**
* Checks if the error is a rate limit
*/
#isRateLimitError(error) {
return error.status === 503 || error.code === "QUERY_LIMIT_EXCEEDED";
}
/**
* Delay when exceeding the rate limit
*/
async #handleRateLimitExceeded(requestId) {
return this.#rateLimiter.handleExceeded(requestId);
}
/**
* Checks if the error is an operating limit
*
* @memo `OPERATION_TIME_LIMIT` && `429` - obtained through practical means
* @memo This doesn't work for `batch` queries.
*/
#isOperatingLimitError(error) {
return error.status === 429 || error.code === "OPERATION_TIME_LIMIT";
}
/**
* Operating limit error delay
*
* @memo Currently, the errors don't include timings for operations.
* For this reason, we will take data from the previous request
*/
async #handleOperatingLimitError(requestId, method, params, _error) {
return this.#operatingLimiter.getTimeToFree(requestId, method, params, _error);
}
/**
* Checks whether attempts should be stopped if errors are encountered that are unclear.
*/
#isNeedThrowError(error) {
const answerError = {
code: error?.code ?? "-1",
description: error?.message ?? ""
};
return [
...this.exceptionCodeForHard,
...this.exceptionCodeForSoft
].includes(answerError.code) || (answerError.description ?? "").includes("Could not find value for parameter");
}
/**
* These exceptions will be thrown
*/
get exceptionCodeForHard() {
return [
"ERR_BAD_REQUEST",
"JSSDK_UNKNOWN_ERROR",
// 'REQUEST_TIMEOUT', 'NETWORK_ERROR',
"100",
"INTERNAL_SERVER_ERROR",
"ERROR_UNEXPECTED_ANSWER",
"PORTAL_DELETED",
"ERROR_BATCH_METHOD_NOT_ALLOWED",
"ERROR_BATCH_LENGTH_EXCEEDED",
"NO_AUTH_FOUND",
"INVALID_REQUEST",
"OVERLOAD_LIMIT",
"expired_token",
"ACCESS_DENIED",
"INVALID_CREDENTIALS",
"user_access_error",
"insufficient_scope",
"ERROR_MANIFEST_IS_NOT_AVAILABLE",
"allowed_only_intranet_user",
"NOT_FOUND",
"INVALID_ARG_VALUE"
];
}
/**
* These exceptions will be thrown into `AjaxResult` as `AjaxError`
*/
get exceptionCodeForSoft() {
return [
"ERROR_ENTITY_NOT_FOUND",
"BITRIX_REST_V3_EXCEPTION_ACCESSDENIEDEXCEPTION",
"BITRIX_REST_V3_EXCEPTION_INVALIDJSONEXCEPTION",
"BITRIX_REST_V3_EXCEPTION_INVALIDFILTEREXCEPTION",
"BITRIX_REST_V3_EXCEPTION_INVALIDSELECTEXCEPTION",
"BITRIX_REST_V3_EXCEPTION_ENTITYNOTFOUNDEXCEPTION",
"BITRIX_REST_V3_EXCEPTION_METHODNOTFOUNDEXCEPTION",
"BITRIX_REST_V3_EXCEPTION_UNKNOWNDTOPROPERTYEXCEPTION",
"BITRIX_REST_V3_EXCEPTION_VALIDATION_REQUESTVALIDATIONEXCEPTION",
"BITRIX_REST_V3_EXCEPTION_VALIDATION_DTOVALIDATIONEXCEPTION"
];
}
/**
* Delay due to unknown errors
*/
async #getErrorBackoff(_requestId) {
return this.#config.retryDelay;
}
incrementError(method) {
const current = this.#errorCounts.get(method) || 0;
this.#errorCounts.set(method, current + 1);
this.incrementStats("consecutiveErrors");
}
resetErrors(method) {
this.#errorCounts.delete(method);
this.#stats.consecutiveErrors = 0;
}
incrementStats(stat) {
this.#stats[stat]++;
}
/**
* Returns job statistics
*/
getStats() {
return {
...this.#stats,
...this.#rateLimiter.getStats(),
...this.#adaptiveDelayer.getStats(),
...this.#operatingLimiter.getStats(),
errorCounts: Object.fromEntries(this.#errorCounts)
};
}
/**
* Resets limiters and statistics
*/
async reset() {
await this.#rateLimiter.reset();
await this.#operatingLimiter.reset();
await this.#adaptiveDelayer.reset();
this.#errorCounts.clear();
this.#stats = {
retries: 0,
consecutiveErrors: 0,
limitHits: 0
};
}
async setConfig(params) {
this.#config = params;
await this.#rateLimiter.setConfig(params.rateLimit);
await this.#operatingLimiter.setConfig(params.operatingLimit);
await this.#adaptiveDelayer.setConfig(params.adaptiveConfig);
}
getParams() {
return { ...this.#config };
}
/**
* Delay function
*/
async #delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Public access to the delay function
*/
async waiteDelay(ms) {
return this.#delay(ms);
}
// region Log ////
#logMethodBlocked(limiter, requestId, method, wait) {
this.getLogger().notice(`${limiter} blocked method ${method}`, {
requestId,
method,
wait,
limiter
});
}
#logMethodBlockedWithTimes(limiter, requestId, method, wait, times) {
this.getLogger().notice(`${limiter} blocked method ${method} | ${times} times`, {
requestId,
method,
times,
wait,
limiter
});
}
#logError(limiter, requestId, code, message, method, wait) {
this.getLogger().error(`${limiter} recognized the ${code} error for the ${method} method`, {
requestId,
method,
wait,
limiter,
error: {
code,
message
}
});
}
#logSomeError(requestId, code, message, method, wait) {
this.getLogger().error(`recognized the ${code} error for the ${method} method`, {
requestId,
method,
wait,
error: {
code,
message
}
});
}
// endregion ////
}
export { RestrictionManager };
//# sourceMappingURL=manager.mjs.map