UNPKG

rate-keeper

Version:

A lightweight utility for easily adding rate limiting to functions, ideal for preventing API rate limit violations.

156 lines 5.35 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DropPolicy = void 0; exports.default = RateKeeper; const globalRateData = new Map(); /** @internal Error messages used throughout the library. */ const ERRORS = { CANCELLED: "Cancelled by user.", QUEUE_FULL: "Queue is at max capacity." }; /** * @enum {DropPolicy} Defines the behavior of the queue when the maximum size has been reached. */ var DropPolicy; (function (DropPolicy) { DropPolicy[DropPolicy["Reject"] = 0] = "Reject"; DropPolicy[DropPolicy["DropOldest"] = 1] = "DropOldest"; })(DropPolicy || (exports.DropPolicy = DropPolicy = {})); /** * @internal * Efficient O(1) amortized queue implementation. * Avoids O(n) shift() operations on standard arrays. * @template T - The type of elements in the queue. */ class Deque { #items = []; #head = 0; /** * Adds an item to the end of the queue. * @param {T} item - The item to add. */ push(item) { this.#items.push(item); } /** * Removes and returns the first item from the queue. * @returns {T | undefined} The first item, or undefined if empty. */ shift() { if (this.#head >= this.#items.length) { return undefined; } const item = this.#items[this.#head]; this.#items[this.#head++] = undefined; // Allow GC // Compact when over half empty and reasonably sized if (this.#head > 64 && this.#head > this.#items.length / 2) { this.#items = this.#items.slice(this.#head); this.#head = 0; } return item; } /** * Removes a specific item from the queue. * @param {T} item - The item to remove. * @returns {boolean} True if the item was found and removed, false otherwise. */ remove(item) { const index = this.#items.indexOf(item, this.#head); if (index !== -1) { this.#items.splice(index, 1); return true; } return false; } /** * The number of items currently in the queue. * @returns {number} The queue length. */ get length() { return this.#items.length - this.#head; } } /** * @internal * Holds the state for a rate-limited queue. * @property {Deque<Action>} queue - The queue of pending actions. * @property {ReturnType<typeof setInterval> | null} timer - The interval timer for processing the queue. * @property {QueueSettings} settings - The configuration for this queue. */ class LimitData { queue = new Deque(); timer = null; settings; constructor(settings) { this.settings = settings; } } /** * @internal * Retrieves or creates the LimitData for a given queue ID. * @param {QueueSettings} settings - The queue settings containing the ID. * @returns {LimitData} The existing or newly created LimitData instance. */ function getRateData(settings) { const { id } = settings; if (globalRateData.has(id)) { return globalRateData.get(id); } const newLimitData = new LimitData(settings); globalRateData.set(id, newLimitData); return newLimitData; } /** * @param {(...args: Args) => Result} action The action to be rate-limited. * @param {number} rateLimit The minimum interval in milliseconds between each execution. * @param {QueueSettings} settings Optional. Queue settings for rate limiting and execution. * @returns {(...args: Args) => CancelablePromise<Result>} An asynchronous function that executes the action and returns a promise with the result and a cancel method. */ function RateKeeper(action, rateLimit, settings = { id: 0 }) { const limitData = settings.id === 0 ? new LimitData(settings) : getRateData(settings); function processQueue() { const next = limitData.queue.shift(); if (next) { next.action(); } if (limitData.queue.length === 0 && limitData.timer !== null) { clearInterval(limitData.timer); limitData.timer = null; } } function publicFunc(...args) { const { maxQueueSize, dropPolicy } = limitData.settings; let resolve; let reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); const actionEntry = { action: () => resolve(action(...args)), reject: (reason) => { reject(reason); }, id: settings.id }; promise.cancel = (reason) => { if (limitData.queue.remove(actionEntry)) { actionEntry.reject(reason || new Error(ERRORS.CANCELLED)); } }; if (maxQueueSize !== undefined && limitData.queue.length >= maxQueueSize) { if (dropPolicy === DropPolicy.Reject) { return Promise.reject(new Error(ERRORS.QUEUE_FULL)); } else if (dropPolicy === DropPolicy.DropOldest) { limitData.queue.shift()?.reject(new Error(ERRORS.QUEUE_FULL)); } } limitData.queue.push(actionEntry); if (limitData.timer === null) { processQueue(); limitData.timer = setInterval(processQueue, rateLimit); } return promise; } return publicFunc; } //# sourceMappingURL=index.js.map