75 lines
1.9 KiB
TypeScript
75 lines
1.9 KiB
TypeScript
import StatusCode from './HTTP/StatusCode'
|
|
import { buildRFC7807 } from './RFCs/RFC7807'
|
|
import ResponseBuilder from './ResponseBuilder'
|
|
|
|
interface StorageItem {
|
|
pointsRemaining: number
|
|
timeReset: number
|
|
}
|
|
|
|
export interface RateLimitHeaders {
|
|
"Retry-After"?: string,
|
|
"X-RateLimit-Limit": string,
|
|
"X-RateLimit-Remaining": string,
|
|
"X-RateLimit-Reset": string
|
|
}
|
|
|
|
export default class RateLimiter {
|
|
|
|
/**
|
|
* number of points that can be used per {timeSpan}
|
|
*/
|
|
public static points = 10
|
|
|
|
/**
|
|
* timeSpan in seconds
|
|
*/
|
|
public static timeSpan = 60
|
|
|
|
private static instance: RateLimiter = new RateLimiter()
|
|
public static getInstance(): RateLimiter {
|
|
return this.instance
|
|
}
|
|
|
|
private storage: Record<string, StorageItem> = {}
|
|
|
|
public constructor(
|
|
private points = RateLimiter.points,
|
|
private timeSpan = RateLimiter.timeSpan
|
|
) {}
|
|
|
|
public consume(key: string, value: number = 1): Response | RateLimitHeaders {
|
|
let item = this.storage[key]
|
|
const now = (new Date().getTime() / 1000)
|
|
if (!item) {
|
|
item = {
|
|
pointsRemaining: this.points,
|
|
timeReset: now + this.timeSpan
|
|
}
|
|
}
|
|
if (item.timeReset <= now) {
|
|
item.timeReset = now + this.timeSpan
|
|
item.pointsRemaining = this.points
|
|
}
|
|
item.pointsRemaining -= value
|
|
this.storage[key] = item
|
|
const headers: RateLimitHeaders = {
|
|
"X-RateLimit-Limit": this.points.toFixed(0),
|
|
"X-RateLimit-Remaining": Math.max(item.pointsRemaining, 0).toFixed(0),
|
|
"X-RateLimit-Reset": item.timeReset.toFixed(0)
|
|
}
|
|
if (item.pointsRemaining < 0) {
|
|
const res = new ResponseBuilder()
|
|
const resp = buildRFC7807({
|
|
type: '/docs/error/rate-limited',
|
|
status: StatusCode.TOO_MANY_REQUESTS,
|
|
title: 'You are being rate limited as you have done too many requests to the server'
|
|
}, res)
|
|
res.addHeader('Retry-After', (item.timeReset - now).toFixed(0))
|
|
res.addHeaders(headers as any)
|
|
return resp
|
|
}
|
|
return headers
|
|
}
|
|
}
|