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
JavaScript
;
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