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
text/typescript
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