export type OnMemoryExceeded = (groupId: string) => void; 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 { #private; /** * Gets the current destruction status of the instance. * * @returns {boolean} True if the instance is destroyed, false otherwise. */ get destroyed(): boolean; /** * Stores hit timestamps per group. * * Key: groupId * Value: array of timestamps (ms) * * @type {Map} */ groupData: Map; /** * Stores the timestamp of the most recent activity per group. * * Used for expiration and cleanup logic. * * @type {Map} */ lastSeen: Map; /** * Maps user IDs to their assigned group IDs. * * @type {Map} */ userToGroup: Map; /** * Flags whether an ID represents a true group or an implicit user group. * * `true` → explicit group * `false` → implicit (user acting as group) * * @type {Map} */ groupFlags: Map; /** * Per-group TTL (time-to-live) overrides. * * If not defined for a group, `maxIdle` is used instead. * * @type {Map} */ groupTTL: Map; /** * 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; /** * 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; maxHits?: number; interval?: number; cleanupInterval?: number; maxIdle?: number; }); /** * 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} */ getAllUserMappings(): Record; /** * 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; } export default TinyRateLimiter; //# sourceMappingURL=TinyRateLimiter.d.mts.map