buddy

node MVC discord bot
Log | Files | Refs | README

RequestHandler.js (6002B)


      1 'use strict';
      2 
      3 const DiscordAPIError = require('./DiscordAPIError');
      4 const HTTPError = require('./HTTPError');
      5 const {
      6   Events: { RATE_LIMIT },
      7   browser,
      8 } = require('../util/Constants');
      9 const Util = require('../util/Util');
     10 
     11 function parseResponse(res) {
     12   if (res.headers.get('content-type').startsWith('application/json')) return res.json();
     13   if (browser) return res.blob();
     14   return res.buffer();
     15 }
     16 
     17 function getAPIOffset(serverDate) {
     18   return new Date(serverDate).getTime() - Date.now();
     19 }
     20 
     21 function calculateReset(reset, serverDate) {
     22   return new Date(Number(reset) * 1000).getTime() - getAPIOffset(serverDate);
     23 }
     24 
     25 class RequestHandler {
     26   constructor(manager) {
     27     this.manager = manager;
     28     this.busy = false;
     29     this.queue = [];
     30     this.reset = -1;
     31     this.remaining = -1;
     32     this.limit = -1;
     33     this.retryAfter = -1;
     34   }
     35 
     36   push(request) {
     37     if (this.busy) {
     38       this.queue.push(request);
     39       return this.run();
     40     } else {
     41       return this.execute(request);
     42     }
     43   }
     44 
     45   run() {
     46     if (this.queue.length === 0) return Promise.resolve();
     47     return this.execute(this.queue.shift());
     48   }
     49 
     50   get limited() {
     51     return Boolean(this.manager.globalTimeout) || (this.remaining <= 0 && Date.now() < this.reset);
     52   }
     53 
     54   get _inactive() {
     55     return this.queue.length === 0 && !this.limited && this.busy !== true;
     56   }
     57 
     58   async execute(item) {
     59     // Insert item back to the beginning if currently busy
     60     if (this.busy) {
     61       this.queue.unshift(item);
     62       return null;
     63     }
     64 
     65     this.busy = true;
     66     const { reject, request, resolve } = item;
     67 
     68     // After calculations and requests have been done, pre-emptively stop further requests
     69     if (this.limited) {
     70       const timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now();
     71 
     72       if (this.manager.client.listenerCount(RATE_LIMIT)) {
     73         /**
     74          * Emitted when the client hits a rate limit while making a request
     75          * @event Client#rateLimit
     76          * @param {Object} rateLimitInfo Object containing the rate limit info
     77          * @param {number} rateLimitInfo.timeout Timeout in ms
     78          * @param {number} rateLimitInfo.limit Number of requests that can be made to this endpoint
     79          * @param {string} rateLimitInfo.method HTTP method used for request that triggered this event
     80          * @param {string} rateLimitInfo.path Path used for request that triggered this event
     81          * @param {string} rateLimitInfo.route Route used for request that triggered this event
     82          */
     83         this.manager.client.emit(RATE_LIMIT, {
     84           timeout,
     85           limit: this.limit,
     86           method: request.method,
     87           path: request.path,
     88           route: request.route,
     89         });
     90       }
     91 
     92       if (this.manager.globalTimeout) {
     93         await this.manager.globalTimeout;
     94       } else {
     95         // Wait for the timeout to expire in order to avoid an actual 429
     96         await Util.delayFor(timeout);
     97       }
     98     }
     99 
    100     // Perform the request
    101     let res;
    102     try {
    103       res = await request.make();
    104     } catch (error) {
    105       // NodeFetch error expected for all "operational" errors, such as 500 status code
    106       this.busy = false;
    107       return reject(new HTTPError(error.message, error.constructor.name, error.status, request.method, request.path));
    108     }
    109 
    110     if (res && res.headers) {
    111       const serverDate = res.headers.get('date');
    112       const limit = res.headers.get('x-ratelimit-limit');
    113       const remaining = res.headers.get('x-ratelimit-remaining');
    114       const reset = res.headers.get('x-ratelimit-reset');
    115       const retryAfter = res.headers.get('retry-after');
    116 
    117       this.limit = limit ? Number(limit) : Infinity;
    118       this.remaining = remaining ? Number(remaining) : 1;
    119       this.reset = reset ? calculateReset(reset, serverDate) : Date.now();
    120       this.retryAfter = retryAfter ? Number(retryAfter) : -1;
    121 
    122       // https://github.com/discordapp/discord-api-docs/issues/182
    123       if (item.request.route.includes('reactions')) {
    124         this.reset = new Date(serverDate).getTime() - getAPIOffset(serverDate) + 250;
    125       }
    126 
    127       // Handle global ratelimit
    128       if (res.headers.get('x-ratelimit-global')) {
    129         // Set the manager's global timeout as the promise for other requests to "wait"
    130         this.manager.globalTimeout = Util.delayFor(this.retryAfter);
    131 
    132         // Wait for the global timeout to resolve before continuing
    133         await this.manager.globalTimeout;
    134 
    135         // Clean up global timeout
    136         this.manager.globalTimeout = null;
    137       }
    138     }
    139 
    140     // Finished handling headers, safe to unlock manager
    141     this.busy = false;
    142 
    143     if (res.ok) {
    144       const success = await parseResponse(res);
    145       // Nothing wrong with the request, proceed with the next one
    146       resolve(success);
    147       return this.run();
    148     } else if (res.status === 429) {
    149       // A ratelimit was hit - this should never happen
    150       this.queue.unshift(item);
    151       this.manager.client.emit('debug', `429 hit on route ${item.request.route}`);
    152       await Util.delayFor(this.retryAfter);
    153       return this.run();
    154     } else if (res.status >= 500 && res.status < 600) {
    155       // Retry the specified number of times for possible serverside issues
    156       if (item.retries === this.manager.client.options.retryLimit) {
    157         return reject(
    158           new HTTPError(res.statusText, res.constructor.name, res.status, item.request.method, request.path),
    159         );
    160       } else {
    161         item.retries++;
    162         this.queue.unshift(item);
    163         return this.run();
    164       }
    165     } else {
    166       // Handle possible malformed requests
    167       try {
    168         const data = await parseResponse(res);
    169         if (res.status >= 400 && res.status < 500) {
    170           return reject(new DiscordAPIError(request.path, data, request.method, res.status));
    171         }
    172         return null;
    173       } catch (err) {
    174         return reject(new HTTPError(err.message, err.constructor.name, err.status, request.method, request.path));
    175       }
    176     }
    177   }
    178 }
    179 
    180 module.exports = RequestHandler;