90 lines
3.4 KiB
JavaScript
Raw Permalink Normal View History

2023-11-29 18:23:54 +03:00
"use strict";
const util = require("util");
const Base = require("../structures/Base");
/**
* Handle ratelimiting something
* @prop {Number} interval How long (in ms) to wait between clearing used tokens
* @prop {Number} lastReset Timestamp of last token clearing
* @prop {Number} lastSend Timestamp of last token consumption
* @prop {Number} tokenLimit The max number tokens the bucket can consume per interval
* @prop {Number} tokens How many tokens the bucket has consumed in this interval
*/
class Bucket {
/**
* Construct a Bucket
* @arg {Number} tokenLimit The max number of tokens the bucket can consume per interval
* @arg {Number} interval How long (in ms) to wait between clearing used tokens
* @arg {Object} [options] Optional parameters
* @arg {Object} options.latencyRef A latency reference object
* @arg {Number} options.latencyRef.latency Interval between consuming tokens
* @arg {Number} options.reservedTokens How many tokens to reserve for priority operations
*/
constructor(tokenLimit, interval, options = {}) {
this.tokenLimit = tokenLimit;
this.interval = interval;
this.latencyRef = options.latencyRef || {latency: 0};
this.lastReset = this.tokens = this.lastSend = 0;
this.reservedTokens = options.reservedTokens || 0;
this._queue = [];
}
check() {
if(this.timeout || this._queue.length === 0) {
return;
}
if(this.lastReset + this.interval + this.tokenLimit * this.latencyRef.latency < Date.now()) {
this.lastReset = Date.now();
this.tokens = Math.max(0, this.tokens - this.tokenLimit);
}
let val;
let tokensAvailable = this.tokens < this.tokenLimit;
let unreservedTokensAvailable = this.tokens < (this.tokenLimit - this.reservedTokens);
while(this._queue.length > 0 && (unreservedTokensAvailable || (tokensAvailable && this._queue[0].priority))) {
this.tokens++;
tokensAvailable = this.tokens < this.tokenLimit;
unreservedTokensAvailable = this.tokens < (this.tokenLimit - this.reservedTokens);
const item = this._queue.shift();
val = this.latencyRef.latency - Date.now() + this.lastSend;
if(this.latencyRef.latency === 0 || val <= 0) {
item.func();
this.lastSend = Date.now();
} else {
setTimeout(() => {
item.func();
}, val);
this.lastSend = Date.now() + val;
}
}
if(this._queue.length > 0 && !this.timeout) {
this.timeout = setTimeout(() => {
this.timeout = null;
this.check();
}, this.tokens < this.tokenLimit ? this.latencyRef.latency : Math.max(0, this.lastReset + this.interval + this.tokenLimit * this.latencyRef.latency - Date.now()));
}
}
/**
* Queue something in the Bucket
* @arg {Function} func A callback to call when a token can be consumed
* @arg {Boolean} [priority=false] Whether or not the callback should use reserved tokens
*/
queue(func, priority=false) {
if(priority) {
this._queue.unshift({func, priority});
} else {
this._queue.push({func, priority});
}
this.check();
}
[util.inspect.custom]() {
return Base.prototype[util.inspect.custom].call(this);
}
}
module.exports = Bucket;