UNPKG

tiny-essentials

Version:

Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.

387 lines 11.8 kB
export default TinyRateLimiter; /** * Callback triggered when a group's stored hit history exceeds * the configured memory limit. * * This callback is purely informational and does not block execution. */ export type OnMemoryExceeded = (groupId: string) => void; /** * Callback triggered when a group is considered expired and removed * during the cleanup process. * * This usually happens when the group remains inactive longer than * its configured TTL or the global maxIdle value. */ export type OnGroupExpired = (groupId: string) => void; /** * Callback triggered when a group's stored hit history exceeds * the configured memory limit. * * This callback is purely informational and does not block execution. * * @typedef {(groupId: string) => void} OnMemoryExceeded */ /** * Callback triggered when a group is considered expired and removed * during the cleanup process. * * This usually happens when the group remains inactive longer than * its configured TTL or the global maxIdle value. * * @typedef {(groupId: string) => void} OnGroupExpired */ /** * A lightweight and flexible rate limiter supporting both user-based * and group-based throttling. * * ## Core Concepts * - Every user belongs to a group. * - If no explicit group is assigned, the user acts as their own group. * - All users within the same group share the same hit history and limits. * * ## Supported Limiting Strategies * - Max number of hits * - Time-based sliding window * - Combination of both * * ## Extra Features * - Per-group TTL (time-to-live) * - Automatic cleanup of inactive groups * - Optional memory cap per group * - Runtime metrics and statistics * * ## Interval Window (Sliding Window) Behavior * * This rate limiter uses a **sliding time window** strategy when `interval` * is configured. * * ### How it works * - Each hit is stored as a timestamp (Date.now()). * - On every new hit and rate check, timestamps older than: * * `now - interval` * * are discarded. * * - Only hits that occurred within the last `interval` milliseconds * are considered valid. * * ### Practical example * If: * - interval = 10_000 (10 seconds) * - maxHits = 5 * * Then: * - Any group may perform **up to 5 hits in any rolling 10-second window**. * - The window moves continuously with time. * * This avoids burst issues common in fixed-window rate limiters. * * This class is designed to be deterministic, predictable, * and safe to use in long-running Node.js processes. */ declare class TinyRateLimiter { /** * Creates a new TinyRateLimiter instance. * * At least one of `maxHits` or `interval` must be provided. * * @param {Object} options * @param {number|null} [options.maxMemory=100000] * Maximum timestamps stored per group (memory cap). * @param {number} [options.maxHits] * Maximum number of allowed hits. * @param {number} [options.interval] * Sliding time window in milliseconds. * @param {number} [options.cleanupInterval] * Interval (ms) for automatic cleanup execution. * @param {number} [options.maxIdle=300000] * Maximum inactivity time (ms) before a group expires. */ constructor({ maxHits, interval, cleanupInterval, maxIdle, maxMemory }: { maxMemory?: number | null | undefined; maxHits?: number | undefined; interval?: number | undefined; cleanupInterval?: number | undefined; maxIdle?: number | undefined; }); /** * Stores hit timestamps per group. * * Key: groupId * Value: array of timestamps (ms) * * @type {Map<string, number[]>} */ groupData: Map<string, number[]>; /** * Stores the timestamp of the most recent activity per group. * * Used for expiration and cleanup logic. * * @type {Map<string, number>} */ lastSeen: Map<string, number>; /** * Maps user IDs to their assigned group IDs. * * @type {Map<string, string>} */ userToGroup: Map<string, string>; /** * Flags whether an ID represents a true group or an implicit user group. * * `true` → explicit group * `false` → implicit (user acting as group) * * @type {Map<string, boolean>} */ groupFlags: Map<string, boolean>; /** * Per-group TTL (time-to-live) overrides. * * If not defined for a group, `maxIdle` is used instead. * * @type {Map<string, number>} */ groupTTL: Map<string, number>; /** * Assign a callback to be notified when a group's stored * hit history exceeds the configured memory limit. * * @param {OnMemoryExceeded} callback */ setOnMemoryExceeded(callback: OnMemoryExceeded): void; /** * Removes the memory-exceeded callback. */ clearOnMemoryExceeded(): void; /** * Set the callback to be triggered when a group expires and is removed. * * This callback is called automatically during cleanup when a group * becomes inactive for longer than its TTL. * * @param {OnGroupExpired} callback - A function that receives the expired groupId. */ setOnGroupExpired(callback: OnGroupExpired): void; /** * Removes the group-expiration callback. */ clearOnGroupExpired(): void; /** * Check if a given ID is a groupId (not a userId) * @param {string} id * @returns {boolean} */ isGroupId(id: string): boolean; /** * Get all user IDs that belong to a given group. * @param {string} groupId * @returns {string[]} */ getUsersInGroup(groupId: string): string[]; /** * Set TTL (in milliseconds) for a specific group * @param {string} groupId * @param {number} ttl */ setGroupTTL(groupId: string, ttl: number): void; /** * Get TTL (in ms) for a specific group. * @param {string} groupId * @returns {number|null} */ getGroupTTL(groupId: string): number | null; /** * Delete the TTL setting for a specific group * @param {string} groupId */ deleteGroupTTL(groupId: string): void; /** * Assigns a user to a group. * * If the user already has recorded hits, those hits * are merged into the target group. * * @param {string} userId * @param {string} groupId * @throws {Error} If the user belongs to another group */ assignToGroup(userId: string, groupId: string): void; /** * Resolves the effective group ID for a user. * * If the user is not explicitly assigned to a group, * the user ID itself is treated as the group ID. * * @param {string} userId * @returns {string} */ getGroupId(userId: string): string; /** * Registers a hit for a user and applies the sliding window logic. * * ⚠️ **Important usage notice** * This method **must be called before** `isRateLimited(userId)` * in order for rate limit checks to work correctly. * * ### Sliding window cleanup * When `interval` is configured: * - The current timestamp is added to the group's history. * - All timestamps older than `now - interval` are immediately removed. * * This ensures that the stored history always represents * the **current active window**. * * ### Important notes * - Cleanup happens on every hit, not on a fixed schedule. * - The window continuously moves forward in time. * - Memory usage is naturally bounded by time, and optionally by `maxMemory`. * * @param {string} userId */ hit(userId: string): void; /** * Checks whether a user is currently rate limited. * * ### Evaluation process * When `interval` is defined: * 1. The cutoff time is calculated as: * * `Date.now() - interval` * * 2. Only hits newer than this cutoff are counted. * 3. If `maxHits` is defined: * - The group is limited when the count exceeds `maxHits`. * 4. If `maxHits` is not defined: * - Any hit within the window causes a limited state. * * This guarantees consistent behavior regardless of when hits occur. * * @param {string} userId * @returns {boolean} */ isRateLimited(userId: string): boolean; /** * Manually reset group data * @param {string} groupId */ resetGroup(groupId: string): void; /** * Manually reset a user mapping * @param {string} userId */ resetUserGroup(userId: string): void; /** * Set custom timestamps to a group * @param {string} groupId * @param {number[]} timestamps */ setData(groupId: string, timestamps: number[]): void; /** * Check if a group has data * @param {string} groupId * @returns {boolean} */ hasData(groupId: string): boolean; /** * Get timestamps from a group * @param {string} groupId * @returns {number[]} */ getData(groupId: string): number[]; /** * Get the maximum idle time (in milliseconds) before a group is considered expired. * @returns {number} */ getMaxIdle(): number; /** * Set the maximum idle time (in milliseconds) before a group is considered expired. * @param {number} ms */ setMaxIdle(ms: number): void; /** * Cleanup old/inactive groups with individual TTLs * @private */ private _cleanup; /** * Get list of active group IDs * @returns {string[]} */ getActiveGroups(): string[]; /** * Get a shallow copy of all user-to-group mappings as a plain object * @returns {Record<string, string>} */ getAllUserMappings(): Record<string, string>; /** * Returns the configured sliding window size in milliseconds. * * This value represents how far back in time hits are considered * valid for rate limiting decisions. * * @returns {number} */ getInterval(): number; /** * Get the maximum number of allowed hits. * @returns {number} */ getMaxHits(): number; /** * Get the total number of hits recorded for a group. * @param {string} groupId * @returns {number} */ getTotalHits(groupId: string): number; /** * Get the timestamp of the last hit for a group. * @param {string} groupId * @returns {number|null} */ getLastHit(groupId: string): number | null; /** * Get milliseconds since the last hit for a group. * @param {string} groupId * @returns {number|null} */ getTimeSinceLastHit(groupId: string): number | null; /** * Internal utility to compute average spacing * @private * @param {number[]|undefined} history * @returns {number|null} */ private _calculateAverageSpacing; /** * Get average time between hits for a group (ms). * @param {string} groupId * @returns {number|null} */ getAverageHitSpacing(groupId: string): number | null; /** * Get metrics about a group's activity. * @param {string} groupId * @returns {{ * totalHits: number, * lastHit: number|null, * timeSinceLastHit: number|null, * averageHitSpacing: number|null * }} */ getMetrics(groupId: string): { totalHits: number; lastHit: number | null; timeSinceLastHit: number | null; averageHitSpacing: number | null; }; /** * Destroy the rate limiter, stopping cleanup and clearing data */ destroy(): void; #private; } //# sourceMappingURL=TinyRateLimiter.d.mts.map