UNPKG

@bitrix24/b24jssdk

Version:

Bitrix24 REST API JavaScript SDK

190 lines (186 loc) • 6.29 kB
/** * @package @bitrix24/b24jssdk * @version 3.0.0 * @copyright (c) 2026 Bitrix24 * @license MIT * @see https://github.com/bitrix24/b24jssdk * @see https://bitrix24.github.io/b24jssdk/ */ 'use strict'; const loggerFactory = require('../../../logger/logger-factory.cjs'); var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); class OperatingLimiter { static { __name(this, "OperatingLimiter"); } #config; #methodStats = /* @__PURE__ */ new Map(); #stats = { /** Heavy requests */ heavyRequestCount: 0 }; _logger; getTitle() { return "operatingLimiter"; } constructor(config) { this._logger = loggerFactory.LoggerFactory.createNullLogger(); this.#config = config; } // region Logger //// setLogger(logger) { this._logger = logger; } getLogger() { return this._logger; } // endregion //// get limitMs() { return this.#config.limitMs; } getMethodStat(method) { const stats = this.#methodStats.get(method); if (!stats) { return void 0; } return stats; } async canProceed(requestId, method, params) { const timeToFree = await this.getTimeToFree(requestId, method, params); return timeToFree === 0; } async waitIfNeeded(requestId, method, params) { return this.getTimeToFree(requestId, method, params); } /** * Returns the time until the method's operating limit is released (in ms) * The analysis is based on the previous function call. * It's important to understand that we're talking about locks of up to 10 minutes. * This is a fairly strict lock based on the limit: * - not reached - no lock * - reached - lock until the unlock time + 1 second * * `_requestId` and `_params` went unused when the batch budget started being * read under its own `batch` key: the special case that recursed per * sub-command was what needed them. They stay in the signature because * `RestrictionManager.getTimeToFree` and the public `Http.getTimeToFree` * forward all four arguments through. */ async getTimeToFree(_requestId, method, _params, _error) { this.#cleanupOldStats(); const stats = this.#methodStats.get(method); if (!stats) { return 0; } const limitWithBuffer = Math.max(1e3, this.#config.limitMs - 5e3); if (stats.operating >= limitWithBuffer) { const now = Date.now(); if (stats.operating_reset_at > now) { return stats.operating_reset_at - now + 1e3; } return 5e3; } return 0; } /** * Updates operating time statistics for the method. * * `data` is optional because **a successful response without a `time` block at * all is normal**: `rest.documentation.openapi` answers with the OpenAPI * document at the top level — no `result` envelope and no `time` — on every * portal tried (an on-premise build, a cloud portal and a cloud sandbox). * Destructuring the absent block threw `Cannot destructure property * 'operating' of 'data' as it is undefined` and turned a fine HTTP 200 into an * exception. Same shape as #338, one level over: there the missing key was * `result`, here it is `time`. * * A `time` block that arrives *without* the counters is a second, separate * case and was never the crash — the `operating === undefined` check below has * always caught it. It is the normal self-hosted state, because the portal's * own limiter is off by default there (the `rest` module's * `load_limiter_active` option, default `N`, which nothing in the product ever * sets), so the counters are missing from every response on such a portal — * which is also a portal that is not enforcing anything. * * No counters are synthesised when the block is absent. A fabricated * `operating: 0` is indistinguishable from a real "nothing consumed yet" and * would make the limiter confidently wrong — on-premise that exact value is * what an enabled-but-unconfigured limiter reports. */ async updateStats(requestId, method, data) { if (!data) { return; } this.#cleanupOldStats(); const { operating, operating_reset_at } = data; if (operating === void 0 || operating === null) { return; } if (!this.#methodStats.has(method)) { this.#methodStats.set(method, { operating: 0, operating_reset_at: 0, lastUpdated: Date.now() }); } const stats = this.#methodStats.get(method); stats.operating = operating * 1e3; if (operating_reset_at !== void 0) { stats.operating_reset_at = operating_reset_at * 1e3; } stats.lastUpdated = Date.now(); const usagePercent = stats.operating / this.#config.limitMs * 100; if (usagePercent > this.#config.heavyPercent) { this.#stats.heavyRequestCount++; this.#logStat(requestId, method, usagePercent, stats.operating); } } /** * Clearing outdated operating limit data */ #cleanupOldStats() { const now = Date.now(); const maxAge = this.#config.windowMs + 1e4; for (const [method, stats] of this.#methodStats.entries()) { if (now - stats.lastUpdated > maxAge) { this.#methodStats.delete(method); } } } async reset() { this.#methodStats.clear(); this.#stats = { heavyRequestCount: 0 }; } getStats() { const operatingStats = {}; for (const [method, stats] of this.#methodStats.entries()) { operatingStats[method] = Number.parseFloat((stats.operating / 1e3).toFixed(2)); } return { ...this.#stats, operatingStats }; } async setConfig(config) { this.#config = config; } // region Log //// #logStat(requestId, method, percent, operating) { this.getLogger().debug(`${this.getTitle()} detected limit for method ${method}`, { requestId, method, operating: { percent: Number.parseFloat(percent.toFixed(2)), current: Number.parseFloat((operating / 1e3).toFixed(0)), max: Number.parseFloat((this.#config.limitMs / 1e3).toFixed(0)) } }).catch(() => { }); } // endregion //// } exports.OperatingLimiter = OperatingLimiter; //# sourceMappingURL=operating-limiter.cjs.map