Promiz.js
Promiz.js is a promises/A+ compliant library (mostly), which aims to have both a small footprint and have great performance (< 1Kb (625 bytes minified + gzip)). I wont go over why javascript promises are amazing. Instead, I'm going to focus on what goes on behind the scenes and what it takes to create a promise library. But first, some benchmarks (see bench.js for source - server side):
Benchmarks are obviously just that 'benchmarks', and do not necessarily test real-world application usage. However, I feel that they are still quite important for a control flow library, which is why Promiz.js has been optimized for performance. There is however, one thing I should mention: Promiz.js will attempt to execute synchronously if possible. This technically breaks spec, however it allows us to get Async.js levels of performance (note: Async.js is not a promise library and doesn't look as clean).
Alright, lets look at the API that our library has to provide. Here is a basic common use case:
function testPromise(val) {
// An example asyncronous promise function
var deferred = Promiz.defer()
setTimeout(function(){
deferred.resolve(val)
}, 0)
return deferred
}
testPromise(22).then(function(twentyTwo){
// This gets called when the async call finishes
return 33
}).then(function success(thiryThree){
// Values get passed down the chain.
// values can also be promises
return testPromise(99)
}, function error(err) {
// If an error happens, it gets passed here
})
Now, while the usage is simple, the backend can get a little bit complicated and requires a good bit of javascript knowledge. Lets start with the most minimal possible setup.
First we're going to need a generator, that creates the deferred
(promise) objects:
var Promiz = {
// promise factory
defer: function(){
return new defer()
}
}
Now, lets define our promise object. Remember, to be spec compatible, it must have a .then() method, and have a state. In order to be able to chain these calls, we're also going to need to keep track of what we need to call later. This will constitute our stack
(functions that need to be resolved eventually).
function defer(){
// State transitions from pending to either resolved or rejected
this.state = 'pending'
// The current stack of deferred calls that need to be made
this.stack = []
// The heart of the promise
// adding a deferred call to our call stack
this.then = function(fn, er){
this.stack.push([fn, er])
if (this.state !== 'pending') {
// Consume the stack, running the the next function
this.fire()
}
return this
}
}
The .then() simply adds the functions it was called with (a success callback and an optional error callback) to the stack, and then checks to see if it should consume the stack. Note that we return this
which is a reference to our deferred object. This lets us call .then() again, and add to the same deferred stack. Notice, our promise needs to come out of its pending state before we can start consuming the stack. Lets add two methods to our deferred object:
// Resolved the promise to a value
// Only affects the first time it is called
this.resolve = function(val){
if (this.state === 'pending'){
this.state = 'resolved'
this.fire(val)
}
return this
}
// Rejects the promise with a value
// Only affects the first time it is called
this.reject = function(val){
if (this.state === 'pending'){
this.state = 'rejected'
this.fire(val)
}
return this
}
Alright, so this resolve actually does two things. It checks to see if we've already been resolved (by checking our pending state) which is important to be spec compliant, and it fires off our resolved value to start consuming the stack. At this point, were almost done (!). We just need a function that actually consumes our current promise stack (the this.fire()
- the most complicated function).
// This is our main execution thread
// Here is where we consume the stack of promises
this.fire = function (val) {
var self = this
this.val = typeof val !== 'undefined' ? val : this.val
// Iterate through the stack
while(this.stack.length && this.state !== 'pending') {
// Get the next stack item
var entry = this.stack.shift()
// if the entry has a function for the state we're in, call it
var fn = this.state === 'rejected' ? entry[1] : entry[0]
if(fn) {
// wrap in a try/catch to get errors that might be thrown
try {
// call the deferred function
this.val = fn.call(null, this.val)
// If the value returned is a promise, resolve it
if(this.val && typeof this.val.then === 'function') {
// save our state
var prevState = this.state
// Halt stack execution until the promise resolves
this.state = 'pending'
// resolving
this.val.then(function(v){
// success callback
self.resolve(v)
}, function(err){
// error callback
// re-run the stack item if it has an error callback
// but only if we weren't already in a rejected state
if(prevState !== 'rejected' && entry[1]) {
self.stack.unshift(entry)
}
self.reject(err)
})
} else {
this.state = 'resolved'
}
} catch (e) {
// the function call failed, lets reject ourselves
// and re-run the stack item in case it handles errors
// but only if we didn't just do that
// (eg. the error function of on the stack threw)
this.val = e
if(this.state !== 'rejected' && entry[1]) {
this.stack.unshift(entry)
}
this.state = 'rejected'
}
}
}
}
And that's it!