Design Patterns in Action: NodeJS rate limiter and promisification

0

Disclaimer: Here, I am not going to present some real-life example where promisification is used but I am gonna show you how to apply this pattern in a real-life example.

The promisification is a necessity that comes from the asynchronous nature of Javascript whenever we need to make synchronous calls. Let’s assume that we want to use a function, named myFunc . It is usually a function that creates some result (or an error) which in turn needs to be handled by the callback. So, it will probably look like that:

myFunc(inputValue, callback(error, myFuncOutput) {
  if (error) {
    ...
    error-handling code
    ...
  } else {
    if (myFuncOutput.isValid) {
      ...
      extra actions for a special case
      ...
    } else {
      ...
      code that handles the main flow
      ...
    }
  }
});

myFunc includes some asynchronous operations (e.g making HTTP requests, trying (untill successing) to fetch some information through a regular action that has been scheduled by setInterval,….). Now, we want to call this function but sunchronously (for reason that I hope you can understand). We can’t do it as it is but we can create a wrapper:

async function myFuncWrapper(inputValue, myFunc) {
  return new Promise((resolve) => {
    myFunc(inputValue, (error, myFuncOutput) => {
      if (error) {
        resolve({
      case: error
    });
      } else if (myFuncOutput.isValid) {
        resolve({
      case: special
    });
      } else {
        resolve({
      case: main
    });
      }
    });
  });
}

const response = await myFuncWrapper(inputValue, myFunc);

switch(response.case) {
  case 'error':
    ...
    error-handling code
    ...
  break;
  case 'special':
    ...
      extra actions for a special case
      ...
  break;
  case 'main':
    ...
    code that handles the main flow
    ...
  break;
}

Note: You may not need the else if (myFuncOutput.isValid) part. It depends on the kind of output provided by the callback function and whether you need to check something in this output.

The myFuncWrapper function can be called synchronously (with “await”).

Let’s now see an example of how we can apply this pattern. https://github.com/Tabcorp/redis-rate-limiter is a package that help us limit the request rate. According to its documentation, the rate limiter can be used like this:

var redis = require('redis');
var client = redis.createClient(6379, 'localhost', {enable_offline_queue: false});

var rateLimiter = require('redis-rate-limiter');
var limit = rateLimiter.create({
  redis: client,
  key: function(x) { return x.id },
  rate: '100/minute'
});

limit(request, function(err, rate) {
  if (err) {
    console.warn('Rate limiting not available');
  } else {
    console.log('Rate window: '  + rate.window);  // 60
    console.log('Rate limit: '   + rate.limit);   // 100
    console.log('Rate current: ' + rate.current); // 74
    if (rate.over) {
      console.error('Over the limit!');
    }
  }
});

 

So, practically, the limit() is the asynchronous function we want to call. This function is asynchronous because it makes requests to the Redis server. This function cannot be called with “await”. At least, not at the moment I am writing this article. As you may have noticed our limit() function is using a familiar form:

limit(request, function(err, rate) {
  if (err) {
    console.warn('Rate limiting not available');
  } else {
    if (rate.over) {
      console.error('Over the limit!');
    }
    // Here goes the code that needs to be executed
    // when the rate limit has not been exceeded
  }
});

 

Our problem is that limit() function is not returning a promise. We can change this by promisifying the call:

async function checkRequestRate(request, limit) {
  return new Promise((resolve) => {
    limit(request, (err, rate) => {
      if (err) {
        resolve({
          overLimit: false,
          error: err.message,
        });
      } else if (rate.over) {
        resolve({
          overLimit: true,
          error: null,
        });
      } else {
        resolve({
          overLimit: false,
          error: null,
        });
      }
    });
  });
}

 

Now, we can call the checkRequestRate wrapper in a synchronous way:

await checkRequestRate(request, limit);

Tip: have a look at Node8’s util.promisify function which takes this idea further and allows you to promisify a wider range of cases.