Skip to content

Commit

Permalink
Merge branch 'main' into 20-use-prepare-workflow-instead-of-publishin…
Browse files Browse the repository at this point in the history
…g-built-js
  • Loading branch information
zefir-git authored Sep 7, 2023
2 parents bb5d089 + 716289a commit d84f951
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 104 deletions.
6 changes: 0 additions & 6 deletions src/AttemptResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
148 changes: 50 additions & 98 deletions src/RateLimit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,71 +2,44 @@ import {AttemptResult} from "./AttemptResult";

/**
* Rate limit
* @class
*/
export class RateLimit {
/**
* Rate limit instances
* @private
* @static
* @type {Map<string, RateLimit>}
* @internal
*/
static #instances = new Map<string, RateLimit>();
static readonly #instances = new Map<string, RateLimit>();

/**
* Whether this rate limit is deleted
* @private
* @type {boolean}
* @internal
*/
#deleted = false;

/**
* Attempts memory
* @private
* @type {Map<string, [number, number]>}
* Attempts memory. First number is attempts, second number is timestamp
* @internal
*/
#attempts = new Map<string, [number, number]>();

/**
* 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;
readonly #attempts = new Map<string, [number, 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];
Expand All @@ -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
Expand All @@ -105,22 +77,20 @@ 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);
}

/**
* 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;
Expand All @@ -129,73 +99,63 @@ 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);
}

/**
* 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);
}

/**
* 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);
}

/**
* 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);
Expand All @@ -204,54 +164,46 @@ 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);
}

/**
* 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();
}

/**
* 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();
}

/**
* 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);
Expand Down

0 comments on commit d84f951

Please sign in to comment.