swaf/src/Throttler.ts

82 lines
2.8 KiB
TypeScript
Raw Normal View History

import {TooManyRequestsHttpError} from "./HttpError";
export default class Throttler {
private static readonly throttles: { [throttleName: string]: Throttle } = {};
/**
* Throttle function; will throw a TooManyRequestsHttpError when the threshold is reached.
*
* This throttle is adaptive: it will slowly decrease (linear) until it reaches 0 after {@param resetPeriod} ms.
* Threshold will hold for {@param holdPeriod} ms.
*
* @param action a unique action name (can be used multiple times, but it'll account for a single action).
* @param max how many times this action can be triggered per id.
* @param resetPeriod after how much time in ms the throttle will reach 0.
* @param id an identifier of who triggered the action.
* @param holdPeriod time in ms after each call before the threshold begins to decrease.
* @param jailPeriod time in ms for which the throttle will throw when it is triggered.
*/
public static throttle(action: string, max: number, resetPeriod: number, id: string, holdPeriod: number = 100, jailPeriod: number = 30 * 1000) {
let throttle = this.throttles[action];
if (!throttle) throttle = this.throttles[action] = new Throttle(max, resetPeriod, holdPeriod, jailPeriod);
throttle.trigger(id);
}
private constructor() {
}
}
class Throttle {
private readonly max: number;
private readonly resetPeriod: number;
private readonly holdPeriod: number;
private readonly jailPeriod: number;
private readonly triggers: {
[id: string]: {
count: number,
lastTrigger?: number,
jailed?: number;
}
} = {};
constructor(max: number, resetPeriod: number, holdPeriod: number, jailPeriod: number) {
this.max = max;
this.resetPeriod = resetPeriod;
this.holdPeriod = holdPeriod;
this.jailPeriod = jailPeriod;
}
public trigger(id: string) {
let trigger = this.triggers[id];
if (!trigger) trigger = this.triggers[id] = {count: 0};
let currentDate = new Date().getTime();
if (trigger.jailed && currentDate - trigger.jailed > 0) {
this.throw();
return;
}
if (trigger.lastTrigger) {
let timeDiff = currentDate - trigger.lastTrigger;
if (timeDiff > this.holdPeriod) {
timeDiff -= this.holdPeriod;
trigger.count = Math.floor(Math.min(trigger.count, this.max * (1 - timeDiff / this.resetPeriod)));
}
}
trigger.count++;
trigger.lastTrigger = currentDate;
if (trigger.count > this.max) {
trigger.jailed = currentDate;
this.throw();
return;
}
}
private throw() {
throw new TooManyRequestsHttpError();
}
}