UNPKG

@bitrix24/b24jssdk

Version:

Bitrix24 REST API JavaScript SDK

310 lines (307 loc) 9.71 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 { 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