UNPKG

riot-ratelimiter

Version:

A rate limiter handling rate-limits enforced by the riot-games api

363 lines (362 loc) 14 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const RateLimit_1 = require("../RateLimit"); const RiotRateLimiterParameterError_1 = require("../errors/RiotRateLimiterParameterError"); var STRATEGY; (function (STRATEGY) { STRATEGY[STRATEGY["BURST"] = 0] = "BURST"; STRATEGY[STRATEGY["SPREAD"] = 1] = "SPREAD"; })(STRATEGY = exports.STRATEGY || (exports.STRATEGY = {})); exports.RATELIMIT_BACKOFF_DURATION_MS_DEFAULT = 1000; class RateLimiter { constructor({ limits, strategy = RateLimiter.STRATEGY.BURST, debug = false }) { this.backoffDurationMS = exports.RATELIMIT_BACKOFF_DURATION_MS_DEFAULT; this.intervalProcessQueue = null; this.intervalNextSpreadExecution = null; this.queue = []; if (!limits || !Array.isArray(limits) || limits.length === 0) { throw new RiotRateLimiterParameterError_1.RiotRateLimiterParameterError('At least one RateLimit has to be provided!'); } this.limits = limits; this.strategy = strategy; this.debug = debug; limits.forEach(limit => limit.addLimiter(this)); } addOrUpdateLimit(limit) { if (this.debug && (limit.type === RateLimit_1.RATELIMIT_TYPE.BACKOFF || limit.type === RateLimit_1.RATELIMIT_TYPE.SYNC)) { console.log('adding ' + RateLimit_1.RATELIMIT_TYPE_STRINGS[limit.type] + ' limit', limit.toString()); } const limitIndex = this.indexOfLimit(limit); if (limitIndex === -1) { limit.addLimiter(this); this.limits.push(limit); return limit; } else if (limit.type === RateLimit_1.RATELIMIT_TYPE.BACKOFF || limit.type === RateLimit_1.RATELIMIT_TYPE.SYNC) { const foundLimit = this.limits[limitIndex]; foundLimit.updateSilently(limit); foundLimit.restartTimeout(); if (limit.type === RateLimit_1.RATELIMIT_TYPE.SYNC) { this.clearTimeoutAndInterval(); } return foundLimit; } return null; } removeLimit(limit) { const index = this.indexOfLimit(limit); if (index !== -1) { const removedLimit = this.limits.splice(index, 1)[0]; removedLimit.reloadLimiters(); return removedLimit; } return null; } updateLimits(limitsOptions) { if (this.debug) { console.log('limits before update: ' + this.getLimitStrings()); console.log('options to update from: ' + JSON.stringify(limitsOptions, null, 2)); } this.pause(); this.limits.filter(limit => !limitsOptions.find(options => limit.equals(options))) .forEach(options => this.removeLimit(options)); if (this.isInitializing()) { this.limits.forEach(limit => { const update = limitsOptions.find(options => limit.equals(options)); if (update) { limit.update(update); } }); } limitsOptions.filter(options => this.indexOfLimit(options) === -1) .forEach(options => { this.addOrUpdateLimit(new RateLimit_1.RateLimit(options, { debug: this.debug })); }); if (this.debug) { console.log('updated limits: ' + this.getLimitStrings()); } this.unpause(); } indexOfLimit(limit) { let index = -1; this.limits.find((_limit, i) => { if (_limit.equals(limit)) { index = i; return true; } else return false; }); return index; } notifyAboutBackoffFinished(limit) { if (this.debug && this.indexOfLimit(limit) === -1) { console.warn(this.toString() + ' got notified from ' + limit.toString() + ' but is not attached to it!'); } this.backoffUntilTimestamp = null; this.addOrUpdateLimit(RateLimiter.createSyncRateLimit()); } notifyAboutLimitUpdate(limit) { if (this.debug && this.indexOfLimit(limit) === -1) { console.warn(this.toString() + ' got notified from ' + limit.toString() + ' but is not attached to it!'); } if (this.isStrategySpread()) { this.refresh(); } } notifyAboutExceededLimitReset() { this.addOrUpdateLimit(RateLimiter.createSyncRateLimit()); } notifyAboutLimitReached(limit) { if (this.debug && this.indexOfLimit(limit) === -1) { console.warn(this.toString() + ' got notified from ' + limit.toString() + ' but is not attached to it!'); } console.warn('rate limit reached ' + limit.toString()); } notifyAboutRemovedLimit(rateLimit) { this.removeLimit(rateLimit); } isStrategyBurst() { return this.strategy === STRATEGY.BURST; } isStrategySpread() { return this.strategy === STRATEGY.SPREAD; } get isPaused() { return this._isPaused; } checkBurstRateLimit() { const exceededLimit = this.limits.find(limit => !limit.check(this.strategy)); return !exceededLimit; } checkSpreadRateLimit() { return this.queue.length === 0 && !this.intervalNextSpreadExecution && this.limits.length > 0; } getLimits() { return this.limits; } getLimitStrings() { return this.limits.map((limit) => limit.toString() + '\r\n'); } toString() { let rateLimiterSetupInfo = `RateLimiter with ${this.getStrategyString()} - Limits: \r\n${this.getLimitStrings()}`; let spreadLimitExecutionInfo = `${this.isStrategySpread() ? `next execution in ${this.getSpreadInterval() / 1000} seconds` : ''}`; let backoffInfo = `${this.backoffUntilTimestamp ? `| backing off until ${new Date(this.backoffUntilTimestamp)}` : ''}`; return `${rateLimiterSetupInfo} | ${spreadLimitExecutionInfo} | ${backoffInfo}`; } getQueueSize() { return this.queue.length; } getStrategy() { return this.strategy; } getStrategyString() { switch (this.strategy) { case STRATEGY.SPREAD: return 'SPREAD Strategy'; case STRATEGY.BURST: return 'BURST Strategy'; default: return 'UNKNOWN Strategy'; } } pause() { if (this.debug) { console.log('pausing limiter ' + this.toString()); } this._isPaused = true; this.clearTimeoutAndInterval(); } setStrategy(strategy) { this.strategy = strategy; this.refresh(); } scheduling(fn, isReschedule = false) { if (this.isStrategyBurst()) { return this.schedulingWithBurst(fn, isReschedule); } if (this.isStrategySpread()) { return this.schedulingWithSpread(fn, isReschedule); } } rescheduling(fn) { return this.scheduling(fn, true); } backoff({ retryAfterMS = void 0 } = {}) { if (retryAfterMS === void 0) { if (this.debug) { console.log('429 from underlying system, backing off generically'); } retryAfterMS = this.backoffDurationMS; this.backoffDurationMS *= 2; } else { this.backoffDurationMS = exports.RATELIMIT_BACKOFF_DURATION_MS_DEFAULT; } if (retryAfterMS <= 1000) retryAfterMS = 2000; this.backoffUntilTimestamp = Date.now() + retryAfterMS; if (this.debug) { console.log('Backing off for ' + retryAfterMS / 1000 + 'seconds'); } this.addOrUpdateLimit(RateLimiter.createBackoffRateLimit((retryAfterMS / 1000), this.debug)); this.addOrUpdateLimit(RateLimiter.createSyncRateLimit(this.debug)); } resetBackoff() { this.backoffDurationMS = exports.RATELIMIT_BACKOFF_DURATION_MS_DEFAULT; this.backoffUntilTimestamp = null; } schedulingWithBurst(fn, isReschedule = false) { return new Promise((resolve, reject) => { if (this.debug) { console.log('scheduling request, limit not exceeded: ' + this.checkBurstRateLimit() + ' rescheduled:' + ' ' + isReschedule); } if (!this.isPaused && this.checkBurstRateLimit()) { if (this.debug) console.log('executing function'); this.execute(fn, resolve, reject); } else { this.addToQueue(fn, resolve, reject, isReschedule); } }); } schedulingWithSpread(fn, isReschedule = false) { return new Promise((resolve, reject) => { if (!this.isPaused && this.checkSpreadRateLimit()) { this.refresh(); this.execute(fn, resolve, reject); } else { this.addToQueue(fn, resolve, reject, isReschedule); } }); } addToQueue(fn, resolve, reject, isReschedule = false) { if ((this.isStrategySpread() && !this.intervalNextSpreadExecution) || (this.isStrategyBurst() && !this.intervalProcessQueue)) { this.refresh(); } if (isReschedule) { this.queue.unshift({ fn, resolve, reject }); } else { this.queue.push({ fn, resolve, reject }); } return this.queue; } processSpreadLimitInterval() { if (this.queue.length !== 0) { const { fn, resolve, reject } = this.queue.shift(); this.execute(fn, resolve, reject); } else { this.pause(); } } refresh() { if (this.isStrategyBurst()) { this.refreshBurstLimiter(); } else if (this.isStrategySpread()) { this.refreshSpreadLimiter(); } } clearTimeoutAndInterval() { clearInterval(this.intervalProcessQueue); this.intervalProcessQueue = null; clearInterval(this.intervalNextSpreadExecution); this.intervalNextSpreadExecution = null; } refreshBurstLimiter() { this.clearTimeoutAndInterval(); this.processBurstQueue(); if (this.queue.length !== 0) { const factorForEqualRights = Math.floor(Math.random() * 100); this.intervalProcessQueue = setInterval(() => { this.processBurstQueue(); }, 1000 + factorForEqualRights); this.intervalProcessQueue.unref(); } } refreshSpreadLimiter() { this.clearTimeoutAndInterval(); this.intervalNextSpreadExecution = setInterval(() => { this.processSpreadLimitInterval(); }, this.getSpreadInterval()); this.intervalNextSpreadExecution.unref(); } execute(fn, onSuccess, onError) { try { this.limits.forEach(limit => limit.increment()); onSuccess(fn(this)); } catch (e) { onError(e); } } processBurstQueue() { if (this.checkBurstRateLimit()) { const limitWithLowestRequestsRemaining = this.limits.reduce((foundLimit, limit) => { if (foundLimit === null) { return limit; } return (foundLimit.getRemainingRequests(STRATEGY.BURST) < limit.getRemainingRequests(STRATEGY.BURST)) ? foundLimit : limit; }, null); const queueSplice = this.queue.splice(0, limitWithLowestRequestsRemaining.getRemainingRequests(STRATEGY.BURST)); if (this.debug && limitWithLowestRequestsRemaining.type === RateLimit_1.RATELIMIT_TYPE.SYNC) { console.log('processing single item to sync with headers'); queueSplice.forEach(({ fn, resolve, reject }) => { this.scheduling(fn, true).then(resolve).catch((err) => { this.backoff(); reject(err); }); }); } queueSplice.forEach(({ fn, resolve, reject }) => { this.scheduling(fn, true).then(resolve).catch(reject); }); } } isBackoffWithoutRetryAfter() { return this.backoffDurationMS !== exports.RATELIMIT_BACKOFF_DURATION_MS_DEFAULT; } isStrategy(strategy) { return this.strategy === strategy; } getQueue() { return this.queue; } unpause() { if (this.debug) { console.log('unpausing limiter ' + this.toString()); } this._isPaused = false; this.refresh(); } getSpreadInterval() { return this.limits.reduce((longestInterval, limit) => { const interval = limit.getSpreadInterval(); if (longestInterval === null) return interval; return (longestInterval > interval) ? longestInterval : interval; }, null); } isInitializing() { return !!this.limits.find(limit => limit.type === RateLimit_1.RATELIMIT_TYPE.SYNC); } static createSyncRateLimit(debug = false) { return new RateLimit_1.RateLimit({ requests: 1, seconds: RateLimit_1.RATELIMIT_INIT_SECONDS, type: RateLimit_1.RATELIMIT_TYPE.SYNC }, { debug }); } static createBackoffRateLimit(seconds, debug) { return new RateLimit_1.RateLimit({ requests: 0, seconds: seconds, type: RateLimit_1.RATELIMIT_TYPE.BACKOFF }, { debug }); } } RateLimiter.STRATEGY = STRATEGY; exports.RateLimiter = RateLimiter;