diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c5d7fca..d525d8f 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -28,5 +28,7 @@ jobs:
${{ runner.os }}-node-v${{ matrix.node }}-
- name: Install Dependencies
run: npm install
+ - name: Build
+ run: npm run build
- name: Run All Node.js Tests
run: npm run test
diff --git a/.gitignore b/.gitignore
index 6704566..1560d95 100644
--- a/.gitignore
+++ b/.gitignore
@@ -102,3 +102,5 @@ dist
# TernJS port file
.tern-port
+
+lib/
diff --git a/README.md b/README.md
index 4ee040d..beffb68 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# Rate limiting utility
-
+



@@ -42,6 +42,7 @@ If you want to reset the rate limit after a successful login, call [`rateLimit.r
- [Static method: `RateLimit.attempt(name, source, [attempts], [callback])`](#static-method-ratelimitattemptname-source-attempts-callback)
- [Static method: `RateLimit.check(name, source, [callback])`](#static-method-ratelimitcheckname-source-callback)
- [Static method: `RateLimit.clear(name)`](#static-method-ratelimitclearname)
+ - [Static method: `RateLimit.cleanup([name])`](#static-method-ratelimitcleanupname)
- [Static method: `RateLimit.create(name, limit, timeWindow)`](#static-method-ratelimitcreatename-limit-timewindow)
- [Static method: `RateLimit.delete(name)`](#static-method-ratelimitdeletename)
- [Static method: `RateLimit.get(name)`](#static-method-ratelimitgetname)
@@ -51,6 +52,7 @@ If you want to reset the rate limit after a successful login, call [`rateLimit.r
- [`rateLimit.attempt(source, [attempts], [callback])`](#ratelimitattemptsource-attempts-callback)
- [`rateLimit.check(source, [callback])`](#ratelimitchecksource-callback)
- [`rateLimit.clear()`](#ratelimitclear)
+ - [`rateLimit.cleanup()`](#ratelimitcleanup)
- [`rateLimit.delete()`](#ratelimitdelete)
- [`rateLimit.limit`](#ratelimitlimit)
- [`rateLimit.name`](#ratelimitname)
@@ -102,6 +104,13 @@ Clear rate limit attempts storage. This is equivalent to resetting all rate limi
- Returns: [`void`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Undefined_type)
- Throws: [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) If the rate limit does not exist
+
+
+### Static method: `RateLimit.cleanup([name])`
+Clean up rate limit attempts storage. This will remove expired entries.
+
+- `name` [`string`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) The name of the rate limit. Default: `undefined`
+
### Static method: `RateLimit.create(name, limit, timeWindow)`
Create a new rate limit
@@ -154,6 +163,7 @@ Create a new rate limit
- `name` [`string`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) The name of the rate limit
- `limit` [`number`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) The number of attempts allowed per time window (e.g. 60)
- `timeWindow` [`number`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) The time window in seconds (e.g. 60)
+- `cleanupInterval` [`number`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) Cleanup interval in seconds (see [`rateLimit.cleanup()`](#ratelimitcleanup)). Set to `-1` to disable periodic cleanup. Defaults to `timeWindow`
- Throws: [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) If the rate limit already exists
@@ -183,6 +193,12 @@ Clear rate limit attempts storage. This is equivalent to resetting all rate limi
- Returns: [`void`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Undefined_type)
+
+### `rateLimit.cleanup()`
+Clean up rate limit attempts storage. This will remove expired entries.
+
+- Returns: [`void`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Undefined_type)
+
### `rateLimit.delete()`
Delete the rate limit instance. After it is deleted, it should not be used any further without constructing a new instance.
diff --git a/README.template.md b/README.template.md
index eec210b..ee66692 100644
--- a/README.template.md
+++ b/README.template.md
@@ -42,6 +42,7 @@ If you want to reset the rate limit after a successful login, call [`rateLimit.r
- [Static method: `RateLimit.attempt(name, source, [attempts], [callback])`](#static-method-ratelimitattemptname-source-attempts-callback)
- [Static method: `RateLimit.check(name, source, [callback])`](#static-method-ratelimitcheckname-source-callback)
- [Static method: `RateLimit.clear(name)`](#static-method-ratelimitclearname)
+ - [Static method: `RateLimit.cleanup([name])`](#static-method-ratelimitcleanupname)
- [Static method: `RateLimit.create(name, limit, timeWindow)`](#static-method-ratelimitcreatename-limit-timewindow)
- [Static method: `RateLimit.delete(name)`](#static-method-ratelimitdeletename)
- [Static method: `RateLimit.get(name)`](#static-method-ratelimitgetname)
@@ -51,6 +52,7 @@ If you want to reset the rate limit after a successful login, call [`rateLimit.r
- [`rateLimit.attempt(source, [attempts], [callback])`](#ratelimitattemptsource-attempts-callback)
- [`rateLimit.check(source, [callback])`](#ratelimitchecksource-callback)
- [`rateLimit.clear()`](#ratelimitclear)
+ - [`rateLimit.cleanup()`](#ratelimitcleanup)
- [`rateLimit.delete()`](#ratelimitdelete)
- [`rateLimit.limit`](#ratelimitlimit)
- [`rateLimit.name`](#ratelimitname)
@@ -102,6 +104,13 @@ Clear rate limit attempts storage. This is equivalent to resetting all rate limi
- Returns: [`void`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Undefined_type)
- Throws: [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) If the rate limit does not exist
+
+
+### Static method: `RateLimit.cleanup([name])`
+Clean up rate limit attempts storage. This will remove expired entries.
+
+- `name` [`string`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) The name of the rate limit. Default: `undefined`
+
### Static method: `RateLimit.create(name, limit, timeWindow)`
Create a new rate limit
@@ -154,6 +163,7 @@ Create a new rate limit
- `name` [`string`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) The name of the rate limit
- `limit` [`number`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) The number of attempts allowed per time window (e.g. 60)
- `timeWindow` [`number`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) The time window in seconds (e.g. 60)
+- `cleanupInterval` [`number`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) Cleanup interval in seconds (see [`rateLimit.cleanup()`](#ratelimitcleanup)). Set to `-1` to disable periodic cleanup. Defaults to `timeWindow`
- Throws: [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) If the rate limit already exists
@@ -183,6 +193,12 @@ Clear rate limit attempts storage. This is equivalent to resetting all rate limi
- Returns: [`void`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Undefined_type)
+
+### `rateLimit.cleanup()`
+Clean up rate limit attempts storage. This will remove expired entries.
+
+- Returns: [`void`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Undefined_type)
+
### `rateLimit.delete()`
Delete the rate limit instance. After it is deleted, it should not be used any further without constructing a new instance.
diff --git a/lib/AttemptResult.d.ts b/lib/AttemptResult.d.ts
deleted file mode 100644
index 9c31b35..0000000
--- a/lib/AttemptResult.d.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { RateLimit } from "./RateLimit";
-/**
- * The result from a rate limit attempt
- * @interface
- */
-export interface AttemptResult {
- /**
- * The number of requests this rate limit allows per time window
- * @readonly
- */
- readonly limit: number;
- /**
- * The number of requests remaining in the current time window
- * @readonly
- */
- readonly remaining: number;
- /**
- * The number of seconds until the current time window resets
- * @readonly
- */
- readonly reset: number;
- /**
- * The rate limit that this attempt was made on
- * @readonly
- */
- readonly rateLimit: RateLimit;
- /**
- * Whether this attempt should be allowed to proceed. If false, the attempt is rate limited.
- * @readonly
- */
- readonly allow: boolean;
-}
-//# sourceMappingURL=AttemptResult.d.ts.map
\ No newline at end of file
diff --git a/lib/AttemptResult.d.ts.map b/lib/AttemptResult.d.ts.map
deleted file mode 100644
index 27ffbc4..0000000
--- a/lib/AttemptResult.d.ts.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"AttemptResult.d.ts","sourceRoot":"","sources":["../src/AttemptResult.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AAEtC;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC1B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAE9B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CAC3B"}
\ No newline at end of file
diff --git a/lib/AttemptResult.js b/lib/AttemptResult.js
deleted file mode 100644
index cb0ff5c..0000000
--- a/lib/AttemptResult.js
+++ /dev/null
@@ -1 +0,0 @@
-export {};
diff --git a/lib/RateLimit.d.ts b/lib/RateLimit.d.ts
deleted file mode 100644
index b422dfe..0000000
--- a/lib/RateLimit.d.ts
+++ /dev/null
@@ -1,146 +0,0 @@
-import { AttemptResult } from "./AttemptResult";
-/**
- * Rate limit
- * @class
- */
-export declare class RateLimit {
- #private;
- /**
- * Name of the rate limit
- * @readonly
- * @type {string}
- */
- readonly name: string;
- /**
- * The number of requests allowed per time window
- * @type {number}
- */
- limit: number;
- /**
- * The time window in seconds (e.g. 60)
- * @type {number}
- */
- timeWindow: number;
- /**
- * Create a new rate limit
- * @param {string} name - The name of the rate limit
- * @param {number} limit - The number of requests allowed per time window (e.g. 60)
- * @param {number} timeWindow - The time window in seconds (e.g. 60)
- * @returns {RateLimit}
- * @throws {Error} - If the rate limit already exists
- */
- constructor(name: string, limit: number, timeWindow: number);
- /**
- * Check the attempt state for a source ID without decrementing the remaining attempts
- * @param {string} source - Unique source identifier (e.g. username, IP, etc.)
- * @param {function(AttemptResult): void} [callback] - Return data in a callback
- * @returns {AttemptResult}
- */
- check(source: string, callback?: (result: AttemptResult) => void): AttemptResult;
- /**
- * Make an attempt with a source ID
- * @param {string} source - Unique source identifier (e.g. username, IP, etc.)
- * @param {number} [attempts=1] - The number of attempts to make
- * @param {function(AttemptResult): void} [callback] - Return data in a callback
- * @returns {AttemptResult}
- */
- attempt(source: string, attempts?: number, callback?: (result: AttemptResult) => void): AttemptResult;
- /**
- * Reset limit for a source ID. The storage entry will be deleted and a new one will be created on the next attempt.
- * @param {string} source - Unique source identifier (e.g. username, IP, etc.)
- * @returns {void}
- */
- reset(source: string): void;
- /**
- * Set the remaining attempts for a source ID.
- * > **Warning**: This is not recommended as the remaining attempts depend on the limit of the instance.
- * @param {string} source - Unique source identifier (e.g. username, IP, etc.)
- * @param {number} remaining - The number of remaining attempts
- * @returns {void}
- */
- setRemaining(source: string, remaining: number): void;
- /**
- * Clear rate limit attempts storage. This is equivalent to resetting all rate limits.
- * @returns {void}
- */
- clear(): void;
- /**
- * Delete the rate limit instance. After it is deleted, it should not be used any further without constructing a new instance.
- * @returns {void}
- */
- delete(): void;
- /**
- * Get a rate limit instance
- * @param {string} name - The name of the rate limit
- * @returns {RateLimit | null}
- * @static
- */
- static get(name: string): RateLimit | null;
- /**
- * Check the attempt state for a source ID without decrementing the remaining attempts
- * @param {string} name - The name of the rate limit
- * @param {string} source - Unique source identifier (e.g. username, IP, etc.)
- * @param {function(AttemptResult): void} [callback] - Return data in a callback
- * @returns {AttemptResult}
- * @throws {Error} - If the rate limit does not exist
- * @static
- */
- static check(name: string, source: string, callback?: (result: AttemptResult) => void): AttemptResult;
- /**
- * Make an attempt with a source ID
- * @param {string} name - The name of the rate limit
- * @param {string} source - Unique source identifier (e.g. username, IP, etc.)
- * @param {number} [attempts=1] - The number of attempts to make
- * @param {function(AttemptResult): void} [callback] - Return data in a callback
- * @returns {AttemptResult}
- * @throws {Error} - If the rate limit does not exist
- * @static
- */
- static attempt(name: string, source: string, attempts?: number, callback?: (result: AttemptResult) => void): AttemptResult;
- /**
- * Reset limit for a source ID. The storage entry will be deleted and a new one will be created on the next attempt.
- * @param {string} name - The name of the rate limit
- * @param {string} source - Unique source identifier (e.g. username, IP, etc.)
- * @returns {void}
- * @throws {Error} - If the rate limit does not exist
- * @static
- */
- static reset(name: string, source: string): void;
- /**
- * Set the remaining attempts for a source ID.
- * > **Warning**: This is not recommended as the remaining attempts depend on the limit of the instance.
- * @param {string} name - The name of the rate limit
- * @param {string} source - Unique source identifier (e.g. username, IP, etc.)
- * @param {number} remaining - The number of remaining attempts
- * @returns {void}
- * @throws {Error} - If the rate limit does not exist
- * @static
- */
- static setRemaining(name: string, source: string, remaining: number): void;
- /**
- * Clear rate limit attempts storage. This is equivalent to resetting all rate limits.
- * @param {string} name - The name of the rate limit
- * @returns {void}
- * @throws {Error} - If the rate limit does not exist
- * @static
- */
- static clear(name: string): void;
- /**
- * Delete the rate limit instance. After it is deleted, it should not be used any further without constructing a new instance.
- * @param {string} name - The name of the rate limit
- * @returns {void}
- * @throws {Error} - If the rate limit does not exist
- * @static
- */
- static delete(name: string): void;
- /**
- * Create a new rate limit
- * @param {string} name - The name of the rate limit
- * @param {number} limit - The number of attempts allowed per time window (e.g. 60)
- * @param {number} timeWindow - The time window in seconds (e.g. 60)
- * @returns {RateLimit}
- * @static
- */
- static create(name: string, limit: number, timeWindow: number): RateLimit;
-}
-//# sourceMappingURL=RateLimit.d.ts.map
\ No newline at end of file
diff --git a/lib/RateLimit.d.ts.map b/lib/RateLimit.d.ts.map
deleted file mode 100644
index a45ca67..0000000
--- a/lib/RateLimit.d.ts.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"RateLimit.d.ts","sourceRoot":"","sources":["../src/RateLimit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAE9C;;;GAGG;AACH,qBAAa,SAAS;;IAuBlB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;;;;;;OAOG;gBACS,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;IAQ3D;;;;;OAKG;IACH,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,GAAG,aAAa;IAgBhF;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAU,EAAE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,GAAG,aAAa;IAcxG;;;;OAIG;IACH,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAK3B;;;;;;OAMG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAOrD;;;OAGG;IACH,KAAK,IAAI,IAAI;IAKb;;;OAGG;IACH,MAAM,IAAI,IAAI;IAMd;;;;;OAKG;IACH,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAI1C;;;;;;;;OAQG;IACH,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,GAAG,aAAa;IAMrG;;;;;;;;;OASG;IACH,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAU,EAAE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,GAAG,aAAa;IAM7H;;;;;;;OAOG;IACH,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAMhD;;;;;;;;;OASG;IACH,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAM1E;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAMhC;;;;;;OAMG;IACH,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAMjC;;;;;;;OAOG;IACH,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS;CAK5E"}
\ No newline at end of file
diff --git a/lib/RateLimit.js b/lib/RateLimit.js
deleted file mode 100644
index 71620f2..0000000
--- a/lib/RateLimit.js
+++ /dev/null
@@ -1,253 +0,0 @@
-/**
- * Rate limit
- * @class
- */
-export class RateLimit {
- /**
- * Rate limit instances
- * @private
- * @static
- * @type {Map}
- */
- static #instances = new Map();
- /**
- * Whether this rate limit is deleted
- * @private
- * @type {boolean}
- */
- #deleted = false;
- /**
- * Attempts memory
- * @private
- * @type {Map}
- */
- #attempts = new Map();
- /**
- * Name of the rate limit
- * @readonly
- * @type {string}
- */
- name;
- /**
- * The number of requests allowed per time window
- * @type {number}
- */
- limit;
- /**
- * The time window in seconds (e.g. 60)
- * @type {number}
- */
- timeWindow;
- /**
- * Create a new rate limit
- * @param {string} name - The name of the rate limit
- * @param {number} limit - The number of requests allowed per time window (e.g. 60)
- * @param {number} timeWindow - The time window in seconds (e.g. 60)
- * @returns {RateLimit}
- * @throws {Error} - If the rate limit already exists
- */
- constructor(name, limit, timeWindow) {
- if (RateLimit.#instances.has(name))
- throw new Error(`Rate limit with name "${name}" already exists`);
- this.name = name;
- this.limit = limit;
- this.timeWindow = timeWindow;
- RateLimit.#instances.set(name, this);
- }
- /**
- * Check the attempt state for a source ID without decrementing the remaining attempts
- * @param {string} source - Unique source identifier (e.g. username, IP, etc.)
- * @param {function(AttemptResult): void} [callback] - Return data in a callback
- * @returns {AttemptResult}
- */
- check(source, callback) {
- if (this.#deleted)
- throw new Error(`Rate limit "${this.name}" has been deleted. Construct a new instance`);
- const attempts = this.#attempts.get(source) ?? [0, Date.now()];
- const remaining = this.limit - attempts[0];
- const reset = Math.ceil((attempts[1] + (this.timeWindow * 1000) - Date.now()) / 1000);
- const result = {
- limit: this.limit,
- remaining,
- reset,
- rateLimit: this,
- allow: remaining > 0
- };
- if (callback)
- callback(result);
- return result;
- }
- /**
- * Make an attempt with a source ID
- * @param {string} source - Unique source identifier (e.g. username, IP, etc.)
- * @param {number} [attempts=1] - The number of attempts to make
- * @param {function(AttemptResult): void} [callback] - Return data in a callback
- * @returns {AttemptResult}
- */
- attempt(source, attempts = 1, callback) {
- if (this.#deleted)
- throw new Error(`Rate limit "${this.name}" has been deleted. Construct a new instance`);
- const data = this.#attempts.get(source) ?? [0, Date.now()];
- // if the time window has expired, reset the attempts
- if (data[1] + (this.timeWindow * 1000) < Date.now()) {
- data[0] = 0;
- data[1] = Date.now();
- }
- // increment the attempts
- data[0] += attempts;
- this.#attempts.set(source, data);
- return this.check(source, callback);
- }
- /**
- * Reset limit for a source ID. The storage entry will be deleted and a new one will be created on the next attempt.
- * @param {string} source - Unique source identifier (e.g. username, IP, etc.)
- * @returns {void}
- */
- reset(source) {
- if (this.#deleted)
- throw new Error(`Rate limit "${this.name}" has been deleted. Construct a new instance`);
- this.#attempts.delete(source);
- }
- /**
- * Set the remaining attempts for a source ID.
- * > **Warning**: This is not recommended as the remaining attempts depend on the limit of the instance.
- * @param {string} source - Unique source identifier (e.g. username, IP, etc.)
- * @param {number} remaining - The number of remaining attempts
- * @returns {void}
- */
- setRemaining(source, remaining) {
- if (this.#deleted)
- throw new Error(`Rate limit "${this.name}" has been deleted. Construct a new instance`);
- const data = this.#attempts.get(source) ?? [0, Date.now()];
- data[0] = this.limit - remaining;
- this.#attempts.set(source, data);
- }
- /**
- * Clear rate limit attempts storage. This is equivalent to resetting all rate limits.
- * @returns {void}
- */
- clear() {
- if (this.#deleted)
- throw new Error(`Rate limit "${this.name}" has been deleted. Construct a new instance`);
- this.#attempts.clear();
- }
- /**
- * Delete the rate limit instance. After it is deleted, it should not be used any further without constructing a new instance.
- * @returns {void}
- */
- delete() {
- this.clear();
- this.#deleted = true;
- RateLimit.#instances.delete(this.name);
- }
- /**
- * Get a rate limit instance
- * @param {string} name - The name of the rate limit
- * @returns {RateLimit | null}
- * @static
- */
- static get(name) {
- return RateLimit.#instances.get(name) ?? null;
- }
- /**
- * Check the attempt state for a source ID without decrementing the remaining attempts
- * @param {string} name - The name of the rate limit
- * @param {string} source - Unique source identifier (e.g. username, IP, etc.)
- * @param {function(AttemptResult): void} [callback] - Return data in a callback
- * @returns {AttemptResult}
- * @throws {Error} - If the rate limit does not exist
- * @static
- */
- static check(name, source, callback) {
- const rateLimit = RateLimit.get(name);
- if (!rateLimit)
- throw new Error(`Rate limit with name "${name}" does not exist`);
- return rateLimit.check(source, callback);
- }
- /**
- * Make an attempt with a source ID
- * @param {string} name - The name of the rate limit
- * @param {string} source - Unique source identifier (e.g. username, IP, etc.)
- * @param {number} [attempts=1] - The number of attempts to make
- * @param {function(AttemptResult): void} [callback] - Return data in a callback
- * @returns {AttemptResult}
- * @throws {Error} - If the rate limit does not exist
- * @static
- */
- static attempt(name, source, attempts = 1, callback) {
- const rateLimit = RateLimit.get(name);
- if (!rateLimit)
- throw new Error(`Rate limit with name "${name}" does not exist`);
- return rateLimit.attempt(source, attempts, callback);
- }
- /**
- * Reset limit for a source ID. The storage entry will be deleted and a new one will be created on the next attempt.
- * @param {string} name - The name of the rate limit
- * @param {string} source - Unique source identifier (e.g. username, IP, etc.)
- * @returns {void}
- * @throws {Error} - If the rate limit does not exist
- * @static
- */
- static reset(name, source) {
- const rateLimit = RateLimit.get(name);
- if (!rateLimit)
- throw new Error(`Rate limit with name "${name}" does not exist`);
- return rateLimit.reset(source);
- }
- /**
- * Set the remaining attempts for a source ID.
- * > **Warning**: This is not recommended as the remaining attempts depend on the limit of the instance.
- * @param {string} name - The name of the rate limit
- * @param {string} source - Unique source identifier (e.g. username, IP, etc.)
- * @param {number} remaining - The number of remaining attempts
- * @returns {void}
- * @throws {Error} - If the rate limit does not exist
- * @static
- */
- static setRemaining(name, source, remaining) {
- const rateLimit = RateLimit.get(name);
- if (!rateLimit)
- throw new Error(`Rate limit with name "${name}" does not exist`);
- return rateLimit.setRemaining(source, remaining);
- }
- /**
- * Clear rate limit attempts storage. This is equivalent to resetting all rate limits.
- * @param {string} name - The name of the rate limit
- * @returns {void}
- * @throws {Error} - If the rate limit does not exist
- * @static
- */
- static clear(name) {
- const rateLimit = RateLimit.get(name);
- if (!rateLimit)
- throw new Error(`Rate limit with name "${name}" does not exist`);
- return rateLimit.clear();
- }
- /**
- * Delete the rate limit instance. After it is deleted, it should not be used any further without constructing a new instance.
- * @param {string} name - The name of the rate limit
- * @returns {void}
- * @throws {Error} - If the rate limit does not exist
- * @static
- */
- static delete(name) {
- const rateLimit = RateLimit.get(name);
- if (!rateLimit)
- throw new Error(`Rate limit with name "${name}" does not exist`);
- return rateLimit.delete();
- }
- /**
- * Create a new rate limit
- * @param {string} name - The name of the rate limit
- * @param {number} limit - The number of attempts allowed per time window (e.g. 60)
- * @param {number} timeWindow - The time window in seconds (e.g. 60)
- * @returns {RateLimit}
- * @static
- */
- static create(name, limit, timeWindow) {
- const existing = RateLimit.get(name);
- if (existing)
- return existing;
- return new RateLimit(name, limit, timeWindow);
- }
-}
diff --git a/package-lock.json b/package-lock.json
index 3c2cb39..658c21a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,8 @@
"license": "GPL-3.0",
"devDependencies": {
"c8": "^8.0.1",
- "mocha": "^10.0.0"
+ "mocha": "^10.0.0",
+ "typescript": "^4.9.5"
}
},
"node_modules/@bcoe/v8-coverage": {
@@ -1152,6 +1153,19 @@
"node": ">=8.0"
}
},
+ "node_modules/typescript": {
+ "version": "4.9.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
"node_modules/v8-to-istanbul": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz",
@@ -2120,6 +2134,12 @@
"is-number": "^7.0.0"
}
},
+ "typescript": {
+ "version": "4.9.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+ "dev": true
+ },
"v8-to-istanbul": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz",
diff --git a/package.json b/package.json
index bad904a..d23a6a3 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"test": "c8 npm run _test",
"coverage": "c8 --reporter=html npm run _test",
"build": "npm ci && tsc",
+ "prepack": "npm run build",
"readme": "node scripts/generateReadme.js"
},
"repository": {
@@ -31,7 +32,7 @@
"homepage": "https://github.com/cloudnode-pro/ratelimit#readme",
"devDependencies": {
"c8": "^8.0.1",
- "mocha": "^10.0.0"
- },
- "dependencies": {}
+ "mocha": "^10.0.0",
+ "typescript": "^4.9.5"
+ }
}
diff --git a/src/RateLimit.ts b/src/RateLimit.ts
index e8b69ac..56adbbc 100644
--- a/src/RateLimit.ts
+++ b/src/RateLimit.ts
@@ -22,16 +22,24 @@ export class RateLimit {
*/
readonly #attempts = new Map();
+ /**
+ * Cleanup timer
+ * @internal
+ */
+ readonly #cleanupTimer?: number;
+
/**
* Create a new rate limit
* @param name - The name of the rate limit
* @param limit - The number of requests allowed per time window (e.g. 60)
* @param timeWindow - The time window in seconds (e.g. 60)
+ * @param [cleanupInterval=timeWindow] - Cleanup interval in seconds (see {@link RateLimit#cleanup}). Set to `-1` to disable periodic cleanup. Defaults to `timeWindow`
* @throws {Error} - If the rate limit already exists
*/
- public constructor(public readonly name: string, public readonly limit: number, public readonly timeWindow: number) {
+ public constructor(public readonly name: string, public readonly limit: number, public readonly timeWindow: number, cleanupInterval: number = timeWindow) {
if (RateLimit.#instances.has(name)) throw new Error(`Rate limit with name "${name}" already exists`);
RateLimit.#instances.set(name, this);
+ if (cleanupInterval > 0) this.#cleanupTimer = setInterval(() => this.cleanup(), cleanupInterval * 1000);
}
/**
@@ -105,6 +113,18 @@ export class RateLimit {
this.#attempts.clear();
}
+ /**
+ * Clean up rate limit attempts storage. This will remove expired entries.
+ * @throws {Error} - If the rate limit has been deleted
+ */
+ public cleanup(): void {
+ if (this.#deleted) throw new Error(`Rate limit "${this.name}" has been deleted. Construct a new instance`);
+ const now = Date.now();
+ for (const [source, [attempts]] of this.#attempts) {
+ if (attempts + (this.timeWindow * 1000) < now) this.#attempts.delete(source);
+ }
+ }
+
/**
* Delete the rate limit instance. After it is deleted, it should not be used any further without constructing a new instance.
*/
@@ -112,6 +132,7 @@ export class RateLimit {
this.clear();
this.#deleted = true;
RateLimit.#instances.delete(this.name);
+ clearInterval(this.#cleanupTimer);
}
/**
@@ -186,6 +207,20 @@ export class RateLimit {
return rateLimit.clear();
}
+ /**
+ * Clean up rate limit attempts storage. This will remove expired entries.
+ * @param [name] - The name of the rate limit. If not provided, all rate limits will be cleaned up.
+ * @throws {Error} - If the rate limit does not exist
+ */
+ public static cleanup(name?: string): void {
+ if (name) {
+ const rateLimit = RateLimit.get(name);
+ if (!rateLimit) throw new Error(`Rate limit with name "${name}" does not exist`);
+ return rateLimit.cleanup();
+ }
+ else for (const rateLimit of RateLimit.#instances.values()) rateLimit.cleanup();
+ }
+
/**
* Delete the rate limit instance. After it is deleted, it should not be used any further without constructing a new instance.
* @param name - The name of the rate limit
@@ -194,7 +229,8 @@ export class RateLimit {
public static delete(name: string): void {
const rateLimit = RateLimit.get(name);
if (!rateLimit) throw new Error(`Rate limit with name "${name}" does not exist`);
- return rateLimit.delete();
+ rateLimit.delete();
+ RateLimit.#instances.delete(name);
}
/**
diff --git a/test/test.js b/test/test.js
index 595b9d1..5a6c5d4 100644
--- a/test/test.js
+++ b/test/test.js
@@ -72,6 +72,7 @@ describe("RateLimit", () => {
assert.throws(() => rateLimit.reset("source1"), {message: "Rate limit \"test\" has been deleted. Construct a new instance"});
assert.throws(() => rateLimit.setRemaining("source1", 3), {message: "Rate limit \"test\" has been deleted. Construct a new instance"});
assert.throws(() => rateLimit.clear(), {message: "Rate limit \"test\" has been deleted. Construct a new instance"});
+ assert.throws(() => rateLimit.cleanup(), {message: "Rate limit \"test\" has been deleted. Construct a new instance"});
assert.throws(() => rateLimit.delete(), {message: "Rate limit \"test\" has been deleted. Construct a new instance"});
});
});
@@ -123,6 +124,21 @@ describe("RateLimit", () => {
assert.strictEqual(RateLimit.check("test", "source2").remaining, 5);
assert.strictEqual(RateLimit.check("test", "source3").remaining, 5);
});
+ it("should clear expired attempts", async () => {
+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
+ RateLimit.attempt("test", "source1");
+ await sleep(1500);
+ RateLimit.cleanup("test");
+ assert.strictEqual(RateLimit.check("test", "source1").remaining, 5);
+ });
+ it("should clear expired attempts from all rate limits", async () => {
+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
+ RateLimit.attempt("test", "source1");
+ new RateLimit("test2", 5, 1).attempt("source1");
+ await sleep(1500);
+ RateLimit.cleanup();
+ RateLimit.delete("test2");
+ });
it("should delete the RateLimit instance", () => {
RateLimit.attempt("test", "source1");
RateLimit.delete("test");
@@ -132,6 +148,7 @@ describe("RateLimit", () => {
assert.throws(() => RateLimit.reset("test", "source1"), {message: "Rate limit with name \"test\" does not exist"});
assert.throws(() => RateLimit.setRemaining("test", "source1", 3), {message: "Rate limit with name \"test\" does not exist"});
assert.throws(() => RateLimit.clear("test"), {message: "Rate limit with name \"test\" does not exist"});
+ assert.throws(() => RateLimit.cleanup("test"), {message: "Rate limit with name \"test\" does not exist"});
assert.throws(() => RateLimit.delete("test"), {message: "Rate limit with name \"test\" does not exist"});
});
});