From a67321ba27a3912bda9392d3b992085dc7d42388 Mon Sep 17 00:00:00 2001 From: William Duncan Date: Mon, 6 Mar 2023 11:24:19 +0200 Subject: [PATCH 01/16] add TypeScript as dev dependency --- package-lock.json | 22 +++++++++++++++++++++- package.json | 6 +++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index fa03603..fade76a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "GPL-3.0", "devDependencies": { "c8": "^7.12.0", - "mocha": "^10.0.0" + "mocha": "^10.0.0", + "typescript": "^4.9.5" } }, "node_modules/@bcoe/v8-coverage": { @@ -1093,6 +1094,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", @@ -2009,6 +2023,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 3652c4c..ef1d21f 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "homepage": "https://github.com/cloudnode-pro/ratelimit#readme", "devDependencies": { "c8": "^7.12.0", - "mocha": "^10.0.0" - }, - "dependencies": {} + "mocha": "^10.0.0", + "typescript": "^4.9.5" + } } From 9e35a0729cc5905c948dc7b840ccb5d848b170a8 Mon Sep 17 00:00:00 2001 From: William Duncan Date: Mon, 6 Mar 2023 11:35:31 +0200 Subject: [PATCH 02/16] clean-up expired attempts on a specific rate limit or all rate limits --- lib/RateLimit.d.ts | 11 +++++++++++ lib/RateLimit.d.ts.map | 2 +- lib/RateLimit.js | 29 +++++++++++++++++++++++++++++ src/RateLimit.ts | 26 ++++++++++++++++++++++++++ test/test.js | 16 ++++++++++++++++ 5 files changed, 83 insertions(+), 1 deletion(-) diff --git a/lib/RateLimit.d.ts b/lib/RateLimit.d.ts index b422dfe..c589711 100644 --- a/lib/RateLimit.d.ts +++ b/lib/RateLimit.d.ts @@ -64,6 +64,11 @@ export declare class RateLimit { * @returns {void} */ clear(): void; + /** + * Clean up rate limit attempts storage. This will remove expired entries. + * @throws {Error} - If the rate limit has been deleted + */ + cleanup(): void; /** * Delete the rate limit instance. After it is deleted, it should not be used any further without constructing a new instance. * @returns {void} @@ -125,6 +130,12 @@ export declare class RateLimit { * @static */ static clear(name: string): void; + /** + * 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 + */ + static cleanup(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 diff --git a/lib/RateLimit.d.ts.map b/lib/RateLimit.d.ts.map index a45ca67..b851c3c 100644 --- a/lib/RateLimit.d.ts.map +++ b/lib/RateLimit.d.ts.map @@ -1 +1 @@ -{"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 +{"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;IACI,OAAO,IAAI,IAAI;IAQtB;;;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;;;;OAIG;WACW,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;IAS1C;;;;;;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 index 71620f2..40ccf08 100644 --- a/lib/RateLimit.js +++ b/lib/RateLimit.js @@ -131,6 +131,19 @@ export class RateLimit { throw new Error(`Rate limit "${this.name}" has been deleted. Construct a new instance`); this.#attempts.clear(); } + /** + * Clean up rate limit attempts storage. This will remove expired entries. + * @throws {Error} - If the rate limit has been deleted + */ + cleanup() { + if (this.#deleted) + throw new Error(`Rate limit "${this.name}" has been deleted. Construct a new instance`); + const now = Date.now(); + for (const [source, data] of this.#attempts) { + if (data[1] + (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. * @returns {void} @@ -223,6 +236,22 @@ export class RateLimit { throw new Error(`Rate limit with name "${name}" does not exist`); 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 + */ + static cleanup(name) { + 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 {string} name - The name of the rate limit diff --git a/src/RateLimit.ts b/src/RateLimit.ts index 7d418d2..ee342ff 100644 --- a/src/RateLimit.ts +++ b/src/RateLimit.ts @@ -136,6 +136,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, data] of this.#attempts) { + if (data[1] + (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. * @returns {void} @@ -230,6 +242,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 {string} name - The name of the rate limit diff --git a/test/test.js b/test/test.js index 595b9d1..f8d7b81 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,20 @@ 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(); + }); it("should delete the RateLimit instance", () => { RateLimit.attempt("test", "source1"); RateLimit.delete("test"); @@ -132,6 +147,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"}); }); }); From 1770a3473f30e211d5973455998a2757107601be Mon Sep 17 00:00:00 2001 From: William Duncan Date: Mon, 6 Mar 2023 11:42:58 +0200 Subject: [PATCH 03/16] update docs --- README.md | 19 ++++++++++++++++++- README.template.md | 17 +++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ee040d..b3b6fb1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Rate limiting utility -![version: 1.2.1](https://img.shields.io/badge/version-1.2.1-%233b82f6) +![version: 1.2.2](https://img.shields.io/badge/version-1.2.2-%233b82f6) ![test: passing](https://img.shields.io/badge/tests-passing-%2316a34a) ![coverage: 100%](https://img.shields.io/badge/coverage-100%25-%2316a34a) ![build: passing](https://img.shields.io/badge/build-passing-%2316a34a) @@ -34,6 +34,8 @@ attemptLogin("john.doe", "password123"); //-> "success" If you want to reset the rate limit after a successful login, call [`rateLimit.reset(username)`](#ratelimitresetsource). +To prevent building-up of memory usage, you should call [`RateLimit.cleanup()`](#static-method-ratelimitcleanupname) periodically. This will remove rate limits that have not been used for a while. + # Documentation
Table of contents @@ -42,6 +44,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 +54,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 +106,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 @@ -183,6 +194,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..3e330db 100644 --- a/README.template.md +++ b/README.template.md @@ -34,6 +34,8 @@ attemptLogin("john.doe", "password123"); //-> "success" If you want to reset the rate limit after a successful login, call [`rateLimit.reset(username)`](#ratelimitresetsource). +To prevent building-up of memory usage, you should call [`RateLimit.cleanup()`](#static-method-ratelimitcleanupname) periodically. This will remove rate limits that have not been used for a while. + # Documentation
Table of contents @@ -42,6 +44,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 +54,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 +106,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 @@ -183,6 +194,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. From f5146040dc3eb6c756a40c6b6e1cc49e10e978bf Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Thu, 7 Sep 2023 16:16:19 +0300 Subject: [PATCH 04/16] ignore `lib` dir --- .gitignore | 2 + lib/AttemptResult.d.ts | 33 ----- lib/AttemptResult.d.ts.map | 1 - lib/AttemptResult.js | 1 - lib/RateLimit.d.ts | 146 --------------------- lib/RateLimit.d.ts.map | 1 - lib/RateLimit.js | 253 ------------------------------------- 7 files changed, 2 insertions(+), 435 deletions(-) delete mode 100644 lib/AttemptResult.d.ts delete mode 100644 lib/AttemptResult.d.ts.map delete mode 100644 lib/AttemptResult.js delete mode 100644 lib/RateLimit.d.ts delete mode 100644 lib/RateLimit.d.ts.map delete mode 100644 lib/RateLimit.js 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/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); - } -} From aa91897800805289bcceb72f255d07a9c6addbc2 Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Thu, 7 Sep 2023 16:17:47 +0300 Subject: [PATCH 05/16] prepack script --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index bad904a..6e25aad 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": { From 44f4ee5a805c16ed872d8f4f80d013a7bb7263e3 Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Thu, 7 Sep 2023 16:37:37 +0300 Subject: [PATCH 06/16] revert lines from 1770a3473f30e211d5973455998a2757107601be --- README.md | 2 -- README.template.md | 2 -- 2 files changed, 4 deletions(-) diff --git a/README.md b/README.md index b3b6fb1..5b09d93 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,6 @@ attemptLogin("john.doe", "password123"); //-> "success" If you want to reset the rate limit after a successful login, call [`rateLimit.reset(username)`](#ratelimitresetsource). -To prevent building-up of memory usage, you should call [`RateLimit.cleanup()`](#static-method-ratelimitcleanupname) periodically. This will remove rate limits that have not been used for a while. - # Documentation
Table of contents diff --git a/README.template.md b/README.template.md index 3e330db..720d036 100644 --- a/README.template.md +++ b/README.template.md @@ -34,8 +34,6 @@ attemptLogin("john.doe", "password123"); //-> "success" If you want to reset the rate limit after a successful login, call [`rateLimit.reset(username)`](#ratelimitresetsource). -To prevent building-up of memory usage, you should call [`RateLimit.cleanup()`](#static-method-ratelimitcleanupname) periodically. This will remove rate limits that have not been used for a while. - # Documentation
Table of contents From e66278d0642d856debec89bbeb7841de08ce7a04 Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Thu, 7 Sep 2023 16:50:14 +0300 Subject: [PATCH 07/16] minor improvement --- src/RateLimit.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/RateLimit.ts b/src/RateLimit.ts index ee342ff..2265745 100644 --- a/src/RateLimit.ts +++ b/src/RateLimit.ts @@ -143,8 +143,8 @@ export class RateLimit { 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, data] of this.#attempts) { - if (data[1] + (this.timeWindow * 1000) < now) this.#attempts.delete(source); + for (const [source, [attempts]] of this.#attempts) { + if (attempts + (this.timeWindow * 1000) < now) this.#attempts.delete(source); } } From 13e23201f2b4c9798231abcd13de7f241f3f1f34 Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Thu, 7 Sep 2023 17:06:40 +0300 Subject: [PATCH 08/16] Remove unneeded jsdoc --- src/AttemptResult.ts | 6 -- src/RateLimit.ts | 146 +++++++++++++++---------------------------- 2 files changed, 49 insertions(+), 103 deletions(-) diff --git a/src/AttemptResult.ts b/src/AttemptResult.ts index edf8737..db6bf86 100644 --- a/src/AttemptResult.ts +++ b/src/AttemptResult.ts @@ -2,36 +2,30 @@ 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; } diff --git a/src/RateLimit.ts b/src/RateLimit.ts index 7d418d2..fa15296 100644 --- a/src/RateLimit.ts +++ b/src/RateLimit.ts @@ -2,71 +2,44 @@ import {AttemptResult} from "./AttemptResult"; /** * Rate limit - * @class */ export class RateLimit { /** * Rate limit instances - * @private - * @static - * @type {Map} + * @internal */ - static #instances = new Map(); + static readonly #instances = new Map(); /** * Whether this rate limit is deleted - * @private - * @type {boolean} + * @internal */ #deleted = false; /** - * Attempts memory - * @private - * @type {Map} + * Attempts memory. First number is attempts, second number is timestamp + * @internal */ #attempts = new Map(); - /** - * 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} + * @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) * @throws {Error} - If the rate limit already exists */ - constructor(name: string, limit: number, timeWindow: number) { + public constructor(public readonly name: string, public readonly limit: number, public readonly timeWindow: number) { 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} + * @param source - Unique source identifier (e.g. username, IP, etc.) + * @param [callback] - Return data in a callback */ - check(source: string, callback?: (result: AttemptResult) => void): AttemptResult { + public check(source: string, callback?: (result: AttemptResult) => void): AttemptResult { 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]; @@ -84,12 +57,11 @@ export class RateLimit { /** * 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} + * @param source - Unique source identifier (e.g. username, IP, etc.) + * @param [attempts=1] - The number of attempts to make + * @param [callback] - Return data in a callback */ - attempt(source: string, attempts: number = 1, callback?: (result: AttemptResult) => void): AttemptResult { + public attempt(source: string, attempts: number = 1, callback?: (result: AttemptResult) => void): AttemptResult { 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 @@ -105,10 +77,9 @@ export class RateLimit { /** * 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} + * @param source - Unique source identifier (e.g. username, IP, etc.) */ - reset(source: string): void { + public reset(source: string): void { if (this.#deleted) throw new Error(`Rate limit "${this.name}" has been deleted. Construct a new instance`); this.#attempts.delete(source); } @@ -116,11 +87,10 @@ export class RateLimit { /** * 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} + * @param source - Unique source identifier (e.g. username, IP, etc.) + * @param remaining - The number of remaining attempts */ - setRemaining(source: string, remaining: number): void { + public setRemaining(source: string, remaining: number): void { 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; @@ -129,18 +99,16 @@ export class RateLimit { /** * Clear rate limit attempts storage. This is equivalent to resetting all rate limits. - * @returns {void} */ - clear(): void { + public clear(): void { 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(): void { + public delete(): void { this.clear(); this.#deleted = true; RateLimit.#instances.delete(this.name); @@ -148,24 +116,20 @@ export class RateLimit { /** * Get a rate limit instance - * @param {string} name - The name of the rate limit - * @returns {RateLimit | null} - * @static + * @param name - The name of the rate limit */ - static get(name: string): RateLimit | null { + public static get(name: string): RateLimit | null { 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} + * @param name - The name of the rate limit + * @param source - Unique source identifier (e.g. username, IP, etc.) + * @param [callback] - Return data in a callback * @throws {Error} - If the rate limit does not exist - * @static */ - static check(name: string, source: string, callback?: (result: AttemptResult) => void): AttemptResult { + public static check(name: string, source: string, callback?: (result: AttemptResult) => void): AttemptResult { const rateLimit = RateLimit.get(name); if (!rateLimit) throw new Error(`Rate limit with name "${name}" does not exist`); return rateLimit.check(source, callback); @@ -173,15 +137,13 @@ export class RateLimit { /** * 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} + * @param name - The name of the rate limit + * @param source - Unique source identifier (e.g. username, IP, etc.) + * @param [attempts=1] - The number of attempts to make + * @param [callback] - Return data in a callback * @throws {Error} - If the rate limit does not exist - * @static */ - static attempt(name: string, source: string, attempts: number = 1, callback?: (result: AttemptResult) => void): AttemptResult { + public static attempt(name: string, source: string, attempts: number = 1, callback?: (result: AttemptResult) => void): AttemptResult { const rateLimit = RateLimit.get(name); if (!rateLimit) throw new Error(`Rate limit with name "${name}" does not exist`); return rateLimit.attempt(source, attempts, callback); @@ -189,13 +151,11 @@ export class RateLimit { /** * 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} + * @param name - The name of the rate limit + * @param source - Unique source identifier (e.g. username, IP, etc.) * @throws {Error} - If the rate limit does not exist - * @static */ - static reset(name: string, source: string): void { + public static reset(name: string, source: string): void { const rateLimit = RateLimit.get(name); if (!rateLimit) throw new Error(`Rate limit with name "${name}" does not exist`); return rateLimit.reset(source); @@ -204,14 +164,12 @@ export class RateLimit { /** * 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} + * @param name - The name of the rate limit + * @param source - Unique source identifier (e.g. username, IP, etc.) + * @param remaining - The number of remaining attempts * @throws {Error} - If the rate limit does not exist - * @static */ - static setRemaining(name: string, source: string, remaining: number): void { + public static setRemaining(name: string, source: string, remaining: number): void { const rateLimit = RateLimit.get(name); if (!rateLimit) throw new Error(`Rate limit with name "${name}" does not exist`); return rateLimit.setRemaining(source, remaining); @@ -219,12 +177,10 @@ export class RateLimit { /** * Clear rate limit attempts storage. This is equivalent to resetting all rate limits. - * @param {string} name - The name of the rate limit - * @returns {void} + * @param name - The name of the rate limit * @throws {Error} - If the rate limit does not exist - * @static */ - static clear(name: string): void { + public static clear(name: string): void { const rateLimit = RateLimit.get(name); if (!rateLimit) throw new Error(`Rate limit with name "${name}" does not exist`); return rateLimit.clear(); @@ -232,12 +188,10 @@ export class RateLimit { /** * 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} + * @param name - The name of the rate limit * @throws {Error} - If the rate limit does not exist - * @static */ - static delete(name: string): void { + 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(); @@ -245,13 +199,11 @@ export class RateLimit { /** * 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 + * @param name - The name of the rate limit + * @param limit - The number of attempts allowed per time window (e.g. 60) + * @param timeWindow - The time window in seconds (e.g. 60) */ - static create(name: string, limit: number, timeWindow: number): RateLimit { + public static create(name: string, limit: number, timeWindow: number): RateLimit { const existing = RateLimit.get(name); if (existing) return existing; return new RateLimit(name, limit, timeWindow); From bb5d089a837fe664e2cd3b9a4a38bff5786152d8 Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Thu, 7 Sep 2023 17:09:00 +0300 Subject: [PATCH 09/16] build before running tests --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) 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 From eaf7208cd671a9ae95840afb184c583f37a5ca61 Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Thu, 7 Sep 2023 17:17:23 +0300 Subject: [PATCH 10/16] Revert "Merge branch 'jsdoc-improvements' into cleanup" This reverts commit df5f92322d6cc1fb11491c6327ee44333975b8b6, reversing changes made to e66278d0642d856debec89bbeb7841de08ce7a04. --- src/AttemptResult.ts | 6 ++ src/RateLimit.ts | 146 ++++++++++++++++++++++++++++--------------- 2 files changed, 103 insertions(+), 49 deletions(-) diff --git a/src/AttemptResult.ts b/src/AttemptResult.ts index db6bf86..edf8737 100644 --- a/src/AttemptResult.ts +++ b/src/AttemptResult.ts @@ -2,30 +2,36 @@ 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; } diff --git a/src/RateLimit.ts b/src/RateLimit.ts index 39fd0e3..2265745 100644 --- a/src/RateLimit.ts +++ b/src/RateLimit.ts @@ -2,44 +2,71 @@ import {AttemptResult} from "./AttemptResult"; /** * Rate limit + * @class */ export class RateLimit { /** * Rate limit instances - * @internal + * @private + * @static + * @type {Map} */ - static readonly #instances = new Map(); + static #instances = new Map(); /** * Whether this rate limit is deleted - * @internal + * @private + * @type {boolean} */ #deleted = false; /** - * Attempts memory. First number is attempts, second number is timestamp - * @internal + * Attempts memory + * @private + * @type {Map} */ #attempts = new Map(); + /** + * 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 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 {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 */ - public constructor(public readonly name: string, public readonly limit: number, public readonly timeWindow: number) { + constructor(name: string, limit: number, timeWindow: number) { 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 source - Unique source identifier (e.g. username, IP, etc.) - * @param [callback] - Return data in a callback + * @param {string} source - Unique source identifier (e.g. username, IP, etc.) + * @param {function(AttemptResult): void} [callback] - Return data in a callback + * @returns {AttemptResult} */ - public check(source: string, callback?: (result: AttemptResult) => void): AttemptResult { + check(source: string, callback?: (result: AttemptResult) => void): AttemptResult { 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]; @@ -57,11 +84,12 @@ export class RateLimit { /** * Make an attempt with a source ID - * @param source - Unique source identifier (e.g. username, IP, etc.) - * @param [attempts=1] - The number of attempts to make - * @param [callback] - Return data in a callback + * @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} */ - public attempt(source: string, attempts: number = 1, callback?: (result: AttemptResult) => void): AttemptResult { + attempt(source: string, attempts: number = 1, callback?: (result: AttemptResult) => void): AttemptResult { 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 @@ -77,9 +105,10 @@ export class RateLimit { /** * Reset limit for a source ID. The storage entry will be deleted and a new one will be created on the next attempt. - * @param source - Unique source identifier (e.g. username, IP, etc.) + * @param {string} source - Unique source identifier (e.g. username, IP, etc.) + * @returns {void} */ - public reset(source: string): void { + reset(source: string): void { if (this.#deleted) throw new Error(`Rate limit "${this.name}" has been deleted. Construct a new instance`); this.#attempts.delete(source); } @@ -87,10 +116,11 @@ export class RateLimit { /** * 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 source - Unique source identifier (e.g. username, IP, etc.) - * @param remaining - The number of remaining attempts + * @param {string} source - Unique source identifier (e.g. username, IP, etc.) + * @param {number} remaining - The number of remaining attempts + * @returns {void} */ - public setRemaining(source: string, remaining: number): void { + setRemaining(source: string, remaining: number): void { 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; @@ -99,8 +129,9 @@ export class RateLimit { /** * Clear rate limit attempts storage. This is equivalent to resetting all rate limits. + * @returns {void} */ - public clear(): void { + clear(): void { if (this.#deleted) throw new Error(`Rate limit "${this.name}" has been deleted. Construct a new instance`); this.#attempts.clear(); } @@ -119,8 +150,9 @@ export class RateLimit { /** * Delete the rate limit instance. After it is deleted, it should not be used any further without constructing a new instance. + * @returns {void} */ - public delete(): void { + delete(): void { this.clear(); this.#deleted = true; RateLimit.#instances.delete(this.name); @@ -128,20 +160,24 @@ export class RateLimit { /** * Get a rate limit instance - * @param name - The name of the rate limit + * @param {string} name - The name of the rate limit + * @returns {RateLimit | null} + * @static */ - public static get(name: string): RateLimit | null { + static get(name: string): RateLimit | null { return RateLimit.#instances.get(name) ?? null; } /** * Check the attempt state for a source ID without decrementing the remaining attempts - * @param name - The name of the rate limit - * @param source - Unique source identifier (e.g. username, IP, etc.) - * @param [callback] - Return data in a callback + * @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 */ - public static check(name: string, source: string, callback?: (result: AttemptResult) => void): AttemptResult { + static check(name: string, source: string, callback?: (result: AttemptResult) => void): AttemptResult { const rateLimit = RateLimit.get(name); if (!rateLimit) throw new Error(`Rate limit with name "${name}" does not exist`); return rateLimit.check(source, callback); @@ -149,13 +185,15 @@ export class RateLimit { /** * Make an attempt with a source ID - * @param name - The name of the rate limit - * @param source - Unique source identifier (e.g. username, IP, etc.) - * @param [attempts=1] - The number of attempts to make - * @param [callback] - Return data in a callback + * @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 */ - public static attempt(name: string, source: string, attempts: number = 1, callback?: (result: AttemptResult) => void): AttemptResult { + static attempt(name: string, source: string, attempts: number = 1, callback?: (result: AttemptResult) => void): AttemptResult { const rateLimit = RateLimit.get(name); if (!rateLimit) throw new Error(`Rate limit with name "${name}" does not exist`); return rateLimit.attempt(source, attempts, callback); @@ -163,11 +201,13 @@ export class RateLimit { /** * Reset limit for a source ID. The storage entry will be deleted and a new one will be created on the next attempt. - * @param name - The name of the rate limit - * @param source - Unique source identifier (e.g. username, IP, etc.) + * @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 */ - public static reset(name: string, source: string): void { + static reset(name: string, source: string): void { const rateLimit = RateLimit.get(name); if (!rateLimit) throw new Error(`Rate limit with name "${name}" does not exist`); return rateLimit.reset(source); @@ -176,12 +216,14 @@ export class RateLimit { /** * 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 name - The name of the rate limit - * @param source - Unique source identifier (e.g. username, IP, etc.) - * @param remaining - The number of remaining attempts + * @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 */ - public static setRemaining(name: string, source: string, remaining: number): void { + static setRemaining(name: string, source: string, remaining: number): void { const rateLimit = RateLimit.get(name); if (!rateLimit) throw new Error(`Rate limit with name "${name}" does not exist`); return rateLimit.setRemaining(source, remaining); @@ -189,10 +231,12 @@ export class RateLimit { /** * Clear rate limit attempts storage. This is equivalent to resetting all rate limits. - * @param name - The name of the rate limit + * @param {string} name - The name of the rate limit + * @returns {void} * @throws {Error} - If the rate limit does not exist + * @static */ - public static clear(name: string): void { + static clear(name: string): void { const rateLimit = RateLimit.get(name); if (!rateLimit) throw new Error(`Rate limit with name "${name}" does not exist`); return rateLimit.clear(); @@ -214,10 +258,12 @@ export class RateLimit { /** * 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 + * @param {string} name - The name of the rate limit + * @returns {void} * @throws {Error} - If the rate limit does not exist + * @static */ - public static delete(name: string): void { + 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(); @@ -225,11 +271,13 @@ export class RateLimit { /** * Create a new rate limit - * @param name - The name of the rate limit - * @param limit - The number of attempts allowed per time window (e.g. 60) - * @param timeWindow - The time window in seconds (e.g. 60) + * @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 */ - public static create(name: string, limit: number, timeWindow: number): RateLimit { + static create(name: string, limit: number, timeWindow: number): RateLimit { const existing = RateLimit.get(name); if (existing) return existing; return new RateLimit(name, limit, timeWindow); From e6d4d96ec2d06358fa4cc93a32f153528229f864 Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Thu, 7 Sep 2023 17:24:11 +0300 Subject: [PATCH 11/16] add cleanup interval --- README.md | 1 + README.template.md | 1 + src/RateLimit.ts | 10 +++++++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5b09d93..beffb68 100644 --- a/README.md +++ b/README.md @@ -163,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 diff --git a/README.template.md b/README.template.md index 720d036..ee66692 100644 --- a/README.template.md +++ b/README.template.md @@ -163,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 diff --git a/src/RateLimit.ts b/src/RateLimit.ts index c37e703..44a71e1 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); } /** From 5188a08ef4053b86ded17868b2ca99fff619780a Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Thu, 7 Sep 2023 17:43:01 +0300 Subject: [PATCH 12/16] ignore errors in cleanup interval --- src/RateLimit.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/RateLimit.ts b/src/RateLimit.ts index 44a71e1..c4f1687 100644 --- a/src/RateLimit.ts +++ b/src/RateLimit.ts @@ -39,7 +39,9 @@ export class RateLimit { 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); + if (cleanupInterval > 0) this.#cleanupTimer = setInterval(() => { + try{this.cleanup()} catch{} + }, cleanupInterval * 1000); } /** From 6734b2f307661d319e034b68d6e988d0794e194e Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Thu, 7 Sep 2023 17:43:17 +0300 Subject: [PATCH 13/16] delete cleanup interval on delete --- src/RateLimit.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/RateLimit.ts b/src/RateLimit.ts index c4f1687..a697758 100644 --- a/src/RateLimit.ts +++ b/src/RateLimit.ts @@ -134,6 +134,7 @@ export class RateLimit { this.clear(); this.#deleted = true; RateLimit.#instances.delete(this.name); + clearInterval(this.#cleanupTimer); } /** From fa61b974fcdeccb4406aae7347db8375eb89124c Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Thu, 7 Sep 2023 17:43:32 +0300 Subject: [PATCH 14/16] when deleting, remove from instances --- src/RateLimit.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/RateLimit.ts b/src/RateLimit.ts index a697758..25ec895 100644 --- a/src/RateLimit.ts +++ b/src/RateLimit.ts @@ -231,7 +231,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); } /** From e58150b3dabfa4a2d12011119cf81afda1d682a0 Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Thu, 7 Sep 2023 17:43:51 +0300 Subject: [PATCH 15/16] delete ratelimit after use in tests --- test/test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test.js b/test/test.js index f8d7b81..5a6c5d4 100644 --- a/test/test.js +++ b/test/test.js @@ -137,6 +137,7 @@ describe("RateLimit", () => { 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"); From 805c57871846fd80c5baf37c739f788c82024966 Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Thu, 7 Sep 2023 17:47:44 +0300 Subject: [PATCH 16/16] Revert "ignore errors in cleanup interval" This reverts commit 5188a08ef4053b86ded17868b2ca99fff619780a. --- src/RateLimit.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/RateLimit.ts b/src/RateLimit.ts index 25ec895..56adbbc 100644 --- a/src/RateLimit.ts +++ b/src/RateLimit.ts @@ -39,9 +39,7 @@ export class RateLimit { 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(() => { - try{this.cleanup()} catch{} - }, cleanupInterval * 1000); + if (cleanupInterval > 0) this.#cleanupTimer = setInterval(() => this.cleanup(), cleanupInterval * 1000); } /**