{"version":3,"file":"operating-limiter.mjs","sources":["../../../../../src/core/http/limiters/operating-limiter.ts"],"sourcesContent":["import type { OperatingLimitConfig, ILimiter } from '../../../types/limiters'\nimport type { PayloadTime } from '../../../types/payloads'\nimport type { LoggerInterface } from '../../../types/logger'\nimport { LoggerFactory } from '../../../logger'\n\ninterface OperatingStats {\n  /*\n   * operating time in 10 minutes (in ms)\n   */\n  operating: number\n  /**\n   * reset time (timestamp in ms)\n   */\n  operating_reset_at: number\n  lastUpdated: number\n}\n\n/**\n * Operating limiting\n *\n * @todo docs\n */\nexport class OperatingLimiter implements ILimiter {\n  #config: OperatingLimitConfig\n  #methodStats = new Map<string, OperatingStats>()\n  #stats = {\n    /** Heavy requests */\n    heavyRequestCount: 0\n  }\n\n  private _logger: LoggerInterface\n\n  getTitle(): string {\n    return 'operatingLimiter'\n  }\n\n  constructor(config: OperatingLimitConfig) {\n    this._logger = LoggerFactory.createNullLogger()\n    this.#config = config\n  }\n\n  // region Logger ////\n  setLogger(logger: LoggerInterface): void {\n    this._logger = logger\n  }\n\n  getLogger(): LoggerInterface {\n    return this._logger\n  }\n  // endregion ////\n\n  get limitMs(): number {\n    return this.#config.limitMs\n  }\n\n  getMethodStat(method: string): undefined | OperatingStats {\n    const stats = this.#methodStats.get(method)\n    if (!stats) {\n      return undefined\n    }\n\n    return stats\n  }\n\n  async canProceed(requestId: string, method: string, params?: any): Promise<boolean> {\n    const timeToFree = await this.getTimeToFree(requestId, method, params)\n    return timeToFree === 0\n  }\n\n  async waitIfNeeded(requestId: string, method: string, params?: any): Promise<number> {\n    return this.getTimeToFree(requestId, method, params)\n  }\n\n  /**\n   * Returns the time until the method's operating limit is released (in ms)\n   * The analysis is based on the previous function call.\n   * It's important to understand that we're talking about locks of up to 10 minutes.\n   * This is a fairly strict lock based on the limit:\n   *   - not reached - no lock\n   *   - reached - lock until the unlock time + 1 second\n   */\n  async getTimeToFree(\n    requestId: string,\n    method: string,\n    params?: any,\n    _error?: any\n  ): Promise<number> {\n    this.#cleanupOldStats()\n\n    if (method === 'batch') {\n      return this.#getTimeToFreeBatch(requestId, params)\n    }\n\n    const stats = this.#methodStats.get(method)\n    if (!stats) {\n      return 0\n    }\n\n    // Use limit with buffer. When calculating the operating limit, we will take 5 seconds less\n    const limitWithBuffer = Math.max(1_000, this.#config.limitMs - 5_000)\n    if (stats.operating >= limitWithBuffer) {\n      const now = Date.now()\n      if (stats.operating_reset_at > now) {\n        // Return the time before reset_at + 1 second\n        return (stats.operating_reset_at - now) + 1_000\n      }\n      return 5_000 // 5 seconds by default\n    }\n\n    return 0\n  }\n\n  /**\n   * For `batch` commands, returns the maximum time until the method reaches the operating limit (in ms)\n   */\n  async #getTimeToFreeBatch(requestId: string, params: any): Promise<number> {\n    let maxWait = 0\n\n    if (!params?.cmd || !Array.isArray(params.cmd)) {\n      return maxWait\n    }\n\n    const batchMethods = params.cmd\n      .map((row: string) => row.split('?')[0])\n      .filter(Boolean)\n\n    for (const methodName of batchMethods) {\n      const waitTime = await this.getTimeToFree(requestId, `batch::${methodName}`, {})\n      maxWait = Math.max(maxWait, waitTime)\n    }\n\n    return maxWait\n  }\n\n  /**\n   * Updates operating time statistics for the method\n   */\n  async updateStats(requestId: string, method: string, data: PayloadTime): Promise<void> {\n    this.#cleanupOldStats()\n\n    // all in seconds\n    const { operating, operating_reset_at } = data\n    if (operating === undefined || operating === null) {\n      return\n    }\n\n    if (!this.#methodStats.has(method)) {\n      this.#methodStats.set(method, {\n        operating: 0,\n        operating_reset_at: 0,\n        lastUpdated: Date.now()\n      })\n    }\n\n    const stats = this.#methodStats.get(method)!\n\n    stats.operating = operating * 1000\n    stats.operating_reset_at = operating_reset_at * 1000\n    stats.lastUpdated = Date.now()\n\n    // Check for heavy requests\n    const usagePercent = (stats.operating / this.#config.limitMs) * 100\n    if (usagePercent > this.#config.heavyPercent) {\n      this.#stats.heavyRequestCount++\n\n      // log if close to the limit\n      this.#logStat(requestId, method, usagePercent, stats.operating)\n    }\n  }\n\n  /**\n   * Clearing outdated operating limit data\n   */\n  #cleanupOldStats(): void {\n    const now = Date.now()\n    const maxAge = this.#config.windowMs + 10_000 // 10 seconds extra\n\n    for (const [method, stats] of this.#methodStats.entries()) {\n      if (now - stats.lastUpdated > maxAge) {\n        this.#methodStats.delete(method)\n      }\n    }\n  }\n\n  async reset(): Promise<void> {\n    this.#methodStats.clear()\n    this.#stats = {\n      heavyRequestCount: 0\n    }\n  }\n\n  getStats(): {\n    heavyRequestCount: number\n    operatingStats: { [method: string]: number }\n  } {\n    const operatingStats: Record<string, number> = {}\n\n    for (const [method, stats] of this.#methodStats.entries()) {\n      operatingStats[method] = Number.parseFloat((stats.operating / 1000).toFixed(2))\n    }\n\n    return {\n      ...this.#stats,\n      operatingStats\n    }\n  }\n\n  async setConfig(config: OperatingLimitConfig): Promise<void> {\n    this.#config = config\n  }\n\n  // region Log ////\n  #logStat(requestId: string, method: string, percent: number, operating: number) {\n    this.getLogger().debug(`${this.getTitle()} detected limit for method ${method}`, {\n      requestId,\n      method,\n      operating: {\n        percent: Number.parseFloat(percent.toFixed(2)),\n        current: Number.parseFloat((operating / 1000).toFixed(0)),\n        max: Number.parseFloat((this.#config.limitMs / 1000).toFixed(0))\n      }\n    })\n  }\n  // endregion ////\n}\n"],"names":[],"mappings":"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"}