mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-10 16:43:31 +01:00
rewrite ratelimiting and api route builder (#1667)
* rewrite ratelimiting and api route builder * more stuff * let people pass their own handlers * Update burst.js * Update RequestHandler.js * Update burst.js * Update sequential.js * Update RequestHandler.js
This commit is contained in:
@@ -332,7 +332,7 @@ class Client extends EventEmitter {
|
||||
*/
|
||||
fetchUser(id, cache = true) {
|
||||
if (this.users.has(id)) return Promise.resolve(this.users.get(id));
|
||||
return this.api.users[id].get().then(data =>
|
||||
return this.api.users(id).get().then(data =>
|
||||
cache ? this.dataManager.newUser(data) : new User(this, data)
|
||||
);
|
||||
}
|
||||
@@ -344,7 +344,7 @@ class Client extends EventEmitter {
|
||||
*/
|
||||
fetchInvite(invite) {
|
||||
const code = this.resolver.resolveInviteCode(invite);
|
||||
return this.api.invites[code].get({ query: { with_counts: true } })
|
||||
return this.api.invites(code).get({ query: { with_counts: true } })
|
||||
.then(data => new Invite(this, data));
|
||||
}
|
||||
|
||||
@@ -355,7 +355,7 @@ class Client extends EventEmitter {
|
||||
* @returns {Promise<Webhook>}
|
||||
*/
|
||||
fetchWebhook(id, token) {
|
||||
return this.api.webhooks.opts(id, token).get().then(data => new Webhook(this, data));
|
||||
return this.api.webhooks(id, token).get().then(data => new Webhook(this, data));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -414,7 +414,7 @@ class Client extends EventEmitter {
|
||||
* @returns {Promise<OAuth2Application>}
|
||||
*/
|
||||
fetchApplication(id = '@me') {
|
||||
return this.api.oauth2.applications[id].get()
|
||||
return this.api.oauth2.applications(id).get()
|
||||
.then(app => new OAuth2Application(this, app));
|
||||
}
|
||||
|
||||
|
||||
@@ -8,20 +8,10 @@ class APIRequest {
|
||||
this.client = rest.client;
|
||||
this.method = method;
|
||||
this.path = path.toString();
|
||||
this.route = this.getRoute(this.path);
|
||||
this.route = options.route;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
getRoute(url) {
|
||||
let route = url.split('?')[0];
|
||||
if (route.includes('/channels/') || route.includes('/guilds/')) {
|
||||
const startInd = route.includes('/channels/') ? route.indexOf('/channels/') : route.indexOf('/guilds/');
|
||||
const majorID = route.substring(startInd).split('/')[2];
|
||||
route = route.replace(/(\d{8,})/g, ':id').replace(':id', majorID);
|
||||
}
|
||||
return route;
|
||||
}
|
||||
|
||||
getAuth() {
|
||||
if (this.client.token && this.client.user && this.client.user.bot) {
|
||||
return `Bot ${this.client.token}`;
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
const util = require('util');
|
||||
|
||||
const noop = () => {}; // eslint-disable-line no-empty-function
|
||||
const methods = ['get', 'post', 'delete', 'patch', 'put'];
|
||||
const reflectors = [
|
||||
'toString', 'valueOf', 'inspect', 'constructor',
|
||||
Symbol.toPrimitive, util.inspect.custom,
|
||||
];
|
||||
|
||||
module.exports = restManager => {
|
||||
function buildRoute(manager) {
|
||||
const route = [''];
|
||||
const handler = {
|
||||
get(list, name) {
|
||||
if (name === 'opts') {
|
||||
function toReturn(...args) { // eslint-disable-line no-inner-declarations
|
||||
list.push(...args.filter(x => x !== null && typeof x !== 'undefined'));
|
||||
return new Proxy(list, handler);
|
||||
}
|
||||
const directJoin = () => `${list.join('/')}/${name}`;
|
||||
for (const r of reflectors) toReturn[r] = directJoin;
|
||||
for (const method of methods) {
|
||||
toReturn[method] = options => restManager.request(method, directJoin(), options);
|
||||
}
|
||||
return toReturn;
|
||||
get(target, name) {
|
||||
if (reflectors.includes(name)) return () => route.join('/');
|
||||
if (methods.includes(name)) {
|
||||
return options => manager.request(name, route.join('/'), Object.assign({
|
||||
route: route.map((r, i) => {
|
||||
if (/\d{16,19}/g.test(r)) return /channels|guilds/.test(route[i - 1]) ? r : ':id';
|
||||
return r;
|
||||
}).join('/'),
|
||||
}, options));
|
||||
}
|
||||
if (reflectors.includes(name)) return () => list.join('/');
|
||||
if (methods.includes(name)) return options => restManager.request(name, list.join('/'), options);
|
||||
list.push(name);
|
||||
return new Proxy(list, handler);
|
||||
route.push(name);
|
||||
return new Proxy(noop, handler);
|
||||
},
|
||||
apply(target, _, args) {
|
||||
route.push(...args.filter(x => x != null)); // eslint-disable-line eqeqeq
|
||||
return new Proxy(noop, handler);
|
||||
},
|
||||
};
|
||||
return new Proxy(noop, handler);
|
||||
}
|
||||
|
||||
return new Proxy([''], handler);
|
||||
};
|
||||
module.exports = buildRoute;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
const UserAgentManager = require('./UserAgentManager');
|
||||
const SequentialRequestHandler = require('./RequestHandlers/Sequential');
|
||||
const BurstRequestHandler = require('./RequestHandlers/Burst');
|
||||
const handlers = require('./handlers');
|
||||
const APIRequest = require('./APIRequest');
|
||||
const mountApi = require('./APIRouter');
|
||||
const routeBuilder = require('./APIRouter');
|
||||
const { Error } = require('../../errors');
|
||||
|
||||
class RESTManager {
|
||||
@@ -15,7 +14,7 @@ class RESTManager {
|
||||
}
|
||||
|
||||
get api() {
|
||||
return mountApi(this);
|
||||
return routeBuilder(this);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@@ -35,21 +34,17 @@ class RESTManager {
|
||||
}
|
||||
|
||||
getRequestHandler() {
|
||||
switch (this.client.options.apiRequestMethod) {
|
||||
case 'sequential':
|
||||
return SequentialRequestHandler;
|
||||
case 'burst':
|
||||
return BurstRequestHandler;
|
||||
default:
|
||||
throw new Error('RATELIMIT_INVALID_METHOD');
|
||||
}
|
||||
const method = this.client.options.apiRequestMethod;
|
||||
if (typeof method === 'function') return method;
|
||||
const handler = handlers[method];
|
||||
if (!handler) throw new Error('RATELIMIT_INVALID_METHOD');
|
||||
return handler;
|
||||
}
|
||||
|
||||
request(method, url, options = {}) {
|
||||
const apiRequest = new APIRequest(this, method, url, options);
|
||||
if (!this.handlers[apiRequest.route]) {
|
||||
const RequestHandlerType = this.getRequestHandler();
|
||||
this.handlers[apiRequest.route] = new RequestHandlerType(this, apiRequest.route);
|
||||
this.handlers[apiRequest.route] = new handlers.RequestHandler(this, this.getRequestHandler());
|
||||
}
|
||||
|
||||
return this.push(this.handlers[apiRequest.route], apiRequest);
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
const RequestHandler = require('./RequestHandler');
|
||||
const DiscordAPIError = require('../DiscordAPIError');
|
||||
|
||||
class BurstRequestHandler extends RequestHandler {
|
||||
constructor(restManager, endpoint) {
|
||||
super(restManager, endpoint);
|
||||
|
||||
this.client = restManager.client;
|
||||
|
||||
this.limit = Infinity;
|
||||
this.resetTime = null;
|
||||
this.remaining = 1;
|
||||
this.timeDifference = 0;
|
||||
|
||||
this.resetTimeout = null;
|
||||
}
|
||||
|
||||
push(request) {
|
||||
super.push(request);
|
||||
this.handle();
|
||||
}
|
||||
|
||||
execute(item) {
|
||||
if (!item) return;
|
||||
item.request.gen().end((err, res) => {
|
||||
if (res && res.headers) {
|
||||
this.limit = Number(res.headers['x-ratelimit-limit']);
|
||||
this.resetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
|
||||
this.remaining = Number(res.headers['x-ratelimit-remaining']);
|
||||
this.timeDifference = Date.now() - new Date(res.headers.date).getTime();
|
||||
}
|
||||
if (err) {
|
||||
if (err.status === 429) {
|
||||
this.queue.unshift(item);
|
||||
if (res.headers['x-ratelimit-global']) this.globalLimit = true;
|
||||
if (this.resetTimeout) return;
|
||||
this.resetTimeout = this.client.setTimeout(() => {
|
||||
this.remaining = this.limit;
|
||||
this.globalLimit = false;
|
||||
this.handle();
|
||||
this.resetTimeout = null;
|
||||
}, Number(res.headers['retry-after']) + this.client.options.restTimeOffset);
|
||||
} else {
|
||||
item.reject(err.status === 400 ? new DiscordAPIError(res.request.path, res.body) : err);
|
||||
this.handle();
|
||||
}
|
||||
} else {
|
||||
this.globalLimit = false;
|
||||
const data = res && res.body ? res.body : {};
|
||||
item.resolve(data);
|
||||
this.handle();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handle() {
|
||||
super.handle();
|
||||
if (this.remaining <= 0 || this.queue.length === 0 || this.globalLimit) return;
|
||||
this.execute(this.queue.shift());
|
||||
this.remaining--;
|
||||
this.handle();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BurstRequestHandler;
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* A base class for different types of rate limiting handlers for the REST API.
|
||||
* @private
|
||||
*/
|
||||
class RequestHandler {
|
||||
/**
|
||||
* @param {RESTManager} restManager The REST manager to use
|
||||
*/
|
||||
constructor(restManager) {
|
||||
/**
|
||||
* The RESTManager that instantiated this RequestHandler
|
||||
* @type {RESTManager}
|
||||
*/
|
||||
this.restManager = restManager;
|
||||
|
||||
/**
|
||||
* A list of requests that have yet to be processed
|
||||
* @type {APIRequest[]}
|
||||
*/
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the client is being rate limited on every endpoint
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get globalLimit() {
|
||||
return this.restManager.globallyRateLimited;
|
||||
}
|
||||
|
||||
set globalLimit(value) {
|
||||
this.restManager.globallyRateLimited = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a new API request into this bucket.
|
||||
* @param {APIRequest} request The new request to push into the queue
|
||||
*/
|
||||
push(request) {
|
||||
this.queue.push(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get this RequestHandler to process its current queue.
|
||||
*/
|
||||
handle() {} // eslint-disable-line no-empty-function
|
||||
|
||||
destroy() {
|
||||
this.queue = [];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RequestHandler;
|
||||
@@ -1,98 +0,0 @@
|
||||
const RequestHandler = require('./RequestHandler');
|
||||
const DiscordAPIError = require('../DiscordAPIError');
|
||||
|
||||
/**
|
||||
* Handles API Requests sequentially, i.e. we wait until the current request is finished before moving onto
|
||||
* the next. This plays a _lot_ nicer in terms of avoiding 429's when there is more than one session of the account,
|
||||
* but it can be slower.
|
||||
* @extends {RequestHandler}
|
||||
* @private
|
||||
*/
|
||||
class SequentialRequestHandler extends RequestHandler {
|
||||
/**
|
||||
* @param {RESTManager} restManager The REST manager to use
|
||||
* @param {string} endpoint The endpoint to handle
|
||||
*/
|
||||
constructor(restManager, endpoint) {
|
||||
super(restManager, endpoint);
|
||||
|
||||
/**
|
||||
* The endpoint that this handler is handling
|
||||
* @type {string}
|
||||
*/
|
||||
this.endpoint = endpoint;
|
||||
|
||||
/**
|
||||
* The time difference between Discord's Dates and the local computer's Dates. A positive number means the local
|
||||
* computer's time is ahead of Discord's
|
||||
* @type {number}
|
||||
*/
|
||||
this.timeDifference = 0;
|
||||
|
||||
/**
|
||||
* Whether the queue is being processed or not
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.busy = false;
|
||||
}
|
||||
|
||||
push(request) {
|
||||
super.push(request);
|
||||
this.handle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a request then resolves a promise to indicate its readiness for a new request.
|
||||
* @param {APIRequest} item The item to execute
|
||||
* @returns {Promise<?Object|Error>}
|
||||
*/
|
||||
execute(item) {
|
||||
this.busy = true;
|
||||
return new Promise(resolve => {
|
||||
item.request.gen().end((err, res) => {
|
||||
if (res && res.headers) {
|
||||
this.requestLimit = Number(res.headers['x-ratelimit-limit']);
|
||||
this.requestResetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
|
||||
this.requestRemaining = Number(res.headers['x-ratelimit-remaining']);
|
||||
this.timeDifference = Date.now() - new Date(res.headers.date).getTime();
|
||||
}
|
||||
if (err) {
|
||||
if (err.status === 429) {
|
||||
this.queue.unshift(item);
|
||||
this.restManager.client.setTimeout(() => {
|
||||
this.globalLimit = false;
|
||||
resolve();
|
||||
}, Number(res.headers['retry-after']) + this.restManager.client.options.restTimeOffset);
|
||||
if (res.headers['x-ratelimit-global']) this.globalLimit = true;
|
||||
} else {
|
||||
item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.request.path, res.body) : err);
|
||||
resolve(err);
|
||||
}
|
||||
} else {
|
||||
this.globalLimit = false;
|
||||
const data = res && res.body ? res.body : {};
|
||||
item.resolve(data);
|
||||
if (this.requestRemaining === 0) {
|
||||
this.restManager.client.setTimeout(
|
||||
() => resolve(data),
|
||||
this.requestResetTime - Date.now() + this.timeDifference + this.restManager.client.options.restTimeOffset
|
||||
);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handle() {
|
||||
super.handle();
|
||||
if (this.busy || this.remaining === 0 || this.queue.length === 0 || this.globalLimit) return;
|
||||
this.execute(this.queue.shift()).then(() => {
|
||||
this.busy = false;
|
||||
this.handle();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SequentialRequestHandler;
|
||||
67
src/client/rest/handlers/RequestHandler.js
Normal file
67
src/client/rest/handlers/RequestHandler.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const DiscordAPIError = require('../DiscordAPIError');
|
||||
|
||||
class RequestHandler {
|
||||
constructor(manager, handler) {
|
||||
this.manager = manager;
|
||||
this.client = this.manager.client;
|
||||
this.handle = handler.bind(this);
|
||||
this.limit = Infinity;
|
||||
this.resetTime = null;
|
||||
this.remaining = 1;
|
||||
this.timeDifference = 0;
|
||||
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
get limited() {
|
||||
return this.queue.length === 0 || this.manager.globallyRateLimited || this.remaining <= 0;
|
||||
}
|
||||
|
||||
set globallyLimited(limited) {
|
||||
this.manager.globallyRateLimited = limited;
|
||||
}
|
||||
|
||||
push(request) {
|
||||
this.queue.push(request);
|
||||
this.handle();
|
||||
}
|
||||
|
||||
execute(item) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const finish = timeout => {
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
if (timeout || this.limited) reject({ timeout, limited: this.limited });
|
||||
else resolve();
|
||||
};
|
||||
item.request.gen().end((err, res) => {
|
||||
if (res && res.headers) {
|
||||
if (res.headers['x-ratelimit-global']) this.globallyLimited = true;
|
||||
this.limit = Number(res.headers['x-ratelimit-limit']);
|
||||
this.resetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
|
||||
this.remaining = Number(res.headers['x-ratelimit-remaining']);
|
||||
this.timeDifference = Date.now() - new Date(res.headers.date).getTime();
|
||||
}
|
||||
if (err) {
|
||||
if (err.status === 429) {
|
||||
this.queue.unshift(item);
|
||||
finish(Number(res.headers['retry-after']) + this.client.options.restTimeOffset);
|
||||
} else {
|
||||
item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.request.path, res.body) : err);
|
||||
finish();
|
||||
}
|
||||
} else {
|
||||
const data = res && res.body ? res.body : {};
|
||||
item.resolve(data);
|
||||
finish();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.globallyLimited = false;
|
||||
this.remaining = 1;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RequestHandler;
|
||||
13
src/client/rest/handlers/burst.js
Normal file
13
src/client/rest/handlers/burst.js
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = function burst() {
|
||||
if (this.limited) return;
|
||||
this.execute(this.queue.shift())
|
||||
.then(this.handle.bind(this))
|
||||
.catch(({ timeout }) => {
|
||||
this.client.setTimeout(() => {
|
||||
this.reset();
|
||||
this.handle();
|
||||
}, timeout || (this.resetTime - Date.now() + this.timeDifference + this.client.options.restTimeOffset));
|
||||
});
|
||||
this.remaining--;
|
||||
this.handle();
|
||||
};
|
||||
5
src/client/rest/handlers/index.js
Normal file
5
src/client/rest/handlers/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
sequential: require('./sequential'),
|
||||
burst: require('./burst'),
|
||||
RequestHandler: require('./RequestHandler'),
|
||||
};
|
||||
16
src/client/rest/handlers/sequential.js
Normal file
16
src/client/rest/handlers/sequential.js
Normal file
@@ -0,0 +1,16 @@
|
||||
module.exports = function sequential() {
|
||||
if (this.busy || this.limited) return;
|
||||
this.busy = true;
|
||||
this.execute(this.queue.shift())
|
||||
.then(() => {
|
||||
this.busy = false;
|
||||
this.handle();
|
||||
})
|
||||
.catch(({ timeout }) => {
|
||||
this.client.setTimeout(() => {
|
||||
this.reset();
|
||||
this.busy = false;
|
||||
this.handle();
|
||||
}, timeout || (this.resetTime - Date.now() + this.timeDifference + this.client.options.restTimeOffset));
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user