Add a custom RateLimiter implementation

This commit is contained in:
Alex Gleason 2025-01-25 13:36:49 -06:00
parent b8d288868d
commit 12de164a4f
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
3 changed files with 99 additions and 0 deletions

View file

@ -0,0 +1,77 @@
import { RateLimitError } from './RateLimitError.ts';
import { RateLimiter, RateLimiterClient } from './types.ts';
interface MemoryRateLimiterOpts {
limit: number;
window: number;
}
export class MemoryRateLimiter implements RateLimiter {
private iid: number;
private previous = new Map<string, RateLimiterClient>();
private current = new Map<string, RateLimiterClient>();
constructor(private opts: MemoryRateLimiterOpts) {
this.iid = setInterval(() => {
this.previous = this.current;
this.current = new Map();
}, opts.window);
}
get limit(): number {
return this.opts.limit;
}
get window(): number {
return this.opts.window;
}
client(key: string): RateLimiterClient {
const curr = this.current.get(key);
const prev = this.previous.get(key);
if (curr) {
return curr;
}
if (prev) {
this.current.set(key, prev);
this.previous.delete(key);
return prev;
}
const next = new MemoryRateLimiterClient(this);
this.current.set(key, next);
return next;
}
[Symbol.dispose](): void {
clearInterval(this.iid);
}
}
class MemoryRateLimiterClient implements RateLimiterClient {
private _hits: number = 0;
readonly resetAt: Date;
constructor(private limiter: MemoryRateLimiter) {
this.resetAt = new Date(Date.now() + limiter.window);
}
get hits(): number {
return this._hits;
}
get remaining(): number {
return this.limiter.limit - this.hits;
}
hit(n: number = 1): void {
this._hits += n;
if (this.remaining < 0) {
throw new RateLimitError(this.limiter, this);
}
}
}

View file

@ -0,0 +1,10 @@
import { RateLimiter, RateLimiterClient } from './types.ts';
export class RateLimitError extends Error {
constructor(
readonly limiter: RateLimiter,
readonly client: RateLimiterClient,
) {
super('Rate limit exceeded');
}
}

View file

@ -0,0 +1,12 @@
export interface RateLimiter extends Disposable {
readonly limit: number;
readonly window: number;
client(key: string): RateLimiterClient;
}
export interface RateLimiterClient {
readonly hits: number;
readonly resetAt: Date;
readonly remaining: number;
hit(n?: number): void;
}