Concepts to Implementations: Promise Implementation Intro

This article continues the Promise Overview. In this part, we'll prepare to dissect the implementation of a promise library. Specifically, Floatdrop's Pinkie which is described as an "itty bitty little widdle twinkie pinkie ES2015 Promise implementation". Perfect for our purposes but also widely used from my observations.

Floatdrop/Pinkie

Our purposes is to simply learn how a promise library could be implemented. This implementation is not the only way as is so often the case in software.

Of course, if you review something like Bluebird you'll find that the implementation of Promises can become quite complex[1]. This is true of almost anything in software.

Tech Stack

Promises have become quite common but they are not yet native to browser javascript engines. Pinkie is written and intended solely for Node but you can use Promises in browsers -- there are implementations that support browser based deployments.

That said, you'll need Node installed to run this code. You can get it from the download page.

Follow Along!

So that you can follow along with the exact version that I'm referencing, I've forked the main repository and you can clone the git repository at: https://github.com/TheOpenSourceU/pinkie.git

Note that I've inserted any points of interest in the code at branch tOSU/article. Use this branch when following along or referencing line numbers.

Once cloned, run npm install to install the main and dev dependencies.

Then run npm test to run the unit tests. If these commands do not work, [re]install Node.JS. If you continue to have trouble, open an issue at github.com/TheOpenSourceU/pinkie/issues

pinkie-install

Introduction to Pinkie's Source Code

Pinkie is a tiny library; it consists of two js files: index.js and test.js. The former is the implementation and latter are the unit tests.

Given this is a node-based solution, I always start with the index.js (or otherwise defined entry point per the package.json.) In Pinkie's case, this is index.js.

Within that file, I start with what the module exports and go backward from there. This is ultimately the Promise function which, in this context, is a "class". You ultimately use it via the new keyword such as.

new Promise(function resolver(resolve, reject) {
  fs.readFile('foo.json', 'utf8', function (err, data) {
    if (err) {
      reject(err);
      return;
    }
    resolve(data);
  });
});
//=> Promise

Above is the example given on the GitHub page; I've augmented this code to use the function name resolver which is how it's referred to in the Pinkie source code. We pass this resolver to invokeResolver (below) which simply runs the resolver callback method.

//Sourced from https://github.com/TheOpenSourceU/pinkie/blob/tOSU/article/index.js#L170
function Promise(resolver) {
  if (typeof resolver !== 'function') {
    throw new TypeError('Promise resolver ' + resolver + ' is not a function');
  }

  if (this instanceof Promise === false) {
    throw new TypeError('Failed to construct \'Promise\': Please use the \'new\' operator, this object constructor cannot be called as a function.');
  }

  this._then = [];

  invokeResolver(resolver, this); //tOSU: Invoked the promise callbacke 'resolver'
}

It is this method (below) that then either invokes resolve or reject; the latter which is invoked if an exception/error is thrown.

Bottom line, this is where the action defined in resolver (above) is actually executed; the methods defined above are passed in which become the resolve and reject methods that resolver ultimately calls.

// Sourced from https://github.com/TheOpenSourceU/pinkie/blob/tOSU/article/index.js#L34
function invokeResolver(resolver, promise) {
  function resolvePromise(value) {
    resolve(promise, value);
  }

  function rejectPromise(reason) {
    reject(promise, reason);
  }

  try {
    resolver(resolvePromise, rejectPromise);
  } catch (e) {
    rejectPromise(e);
  }
}

The resolve and reject method wrapped in methods in order to create a closure on the promise variable.


  1. This holds true for the implementation of any concept in software engineering. ↩︎