Node.js Return a function that is not invoked instantly

Viewed 36

I'm working on a library to interact with the discord API - and hence I need a ratelimiting system.

I of course need to be able to make REST requests, which I've seperated into a standalone part of my app, for example I might run:

await this.client.rest.get('hello/hello') (Naming unrelated)

I then make a request to my redis cache to fetch the current rate limit, and should we currently be passed it, I place the request in an array (my queue), until the ratelimit resets.

The issue I am having is working out how to return a value to the above function, even if it's been placed in a queue etc? I've attached my code below.

import axios, { AxiosRequestHeaders, AxiosResponse } from "axios";
import { InternalRequest, REST } from ".";
import { BucketHandler } from "./BucketHandler.js";

export class RequestManager {
  REST: REST;
  ratelimit: any;
  handlers: BucketHandler[];
  constructor(REST: REST) {
    this.REST = REST;

    this.ratelimit = {
      queue: [],
      timer: null,
      time: null,
    };

    this.handlers = [];
  }

  private async manageQueue() {
    if (this.ratelimit.queue.length < 1) return;
    if (this.ratelimit.timer) return;
    this.ratelimit.time = await this.REST.redisClient.ttl("ratelimit:global");
    this.ratelimit.timer = setTimeout(() => {
      this.ratelimit.timer = null;
      this.processQueue();
    }, this.ratelimit.time * 1500);
  }

  private async processQueue() {
    const globalRateLimit = Number(await this.REST.redisClient.incr("ratelimit:global"));
    this.request(this.ratelimit.queue.shift());
    if (globalRateLimit < 2 && this.ratelimit.queue.length > 0) this.processQueue();
    else this.manageQueue();
  }

  private async parseResponse(res: AxiosResponse) {
    return res.data;
  }

  private formatRequest(options: InternalRequest) {
    const url: string = `${this.REST.api}/v${this.REST.version}${options.fullRoute}`;
    const headers: AxiosRequestHeaders = {};
    if (options.requestOptions.authorization) headers.Authorization = `${options.requestOptions.authorizationPrefix} ${this.REST.token}`;

    return { url, headers };
  }

  public async raw(options: InternalRequest) {
    const globalRateLimit = Number(await this.REST.redisClient.incr("ratelimit:global"));
    if (globalRateLimit > 2) {
      await this.REST.redisClient.decr("ratelimit:global");
      const queue = this.ratelimit.queue.push(options);
      this.manageQueue();
      return queue
    } else {
      return this.request(options);
    }
  }

  private async request(options: InternalRequest) {
    console.log("REQUEST RUN!");
    const request = this.formatRequest(options);

    const res = await axios(request.url, {
      method: options.requestMethod,
      headers: request.headers,
    }).catch((err) => {
      console.log("ERROR!");
    });

    return res;
  }
}
1 Answers

I think you are looking to return a promise that resolves when the request has been made.

You should also make sure to handle rate limit errors received from the API.

The logic should look like this:

manager.request(whatever) {
    // basically a deferred with resolve and reject
    let resolver = { res: null, rej: null, request: whatever };
    const promise = new Promise((res, rej) => {   
         resolve.res = res; resolver.rej = rej;
    });
    this.queue.push(resolver);
    this.startQueueProcessor();
    return promise;
}
startQueueProcessor() {
    // start timeout if not started already, call the real processor ->
    // limit the number of items you are processing
    // process items from queue
    const item = queue.pop();
    // make sure to re-throw error, otherwise the .then part will be run
    this.makeRequest(item).catch(e => item.rej(e)).then(res => item.res(e));
    // start new timeout. 
    // I hate intervals but you can do the same with intervals
}
Related