Yet Another Promise/A+ Library
Summary
This library defaults to exporting a symbol Y
(just like the Q
module).
This library implements the promise/A+ specfication and passes
the Promise/A+ test suite.
The goals were, in order of priority, to:
- for me to understand promises better ;)
- implement the Promise/A+ Spec and pass the tests
- using the deferred pattern.
- defaulting to
setImmediate
due to Node.js v0.10+ warning about recursive
calls to process.nextTick
. And I needed to use VERY deep promise
chains/sequences.. - allow for overriding this nextTick-like behaviour as needed.
- speed.
- make it as Q-like as possible.
The advatages of this library to you that other libraries may or may not have:
- Complete data hiding.
- There is no way to access a promises' the internal queue of pending functions
- There are no special/undocumented arguments to
.resolve
, .reject
,
.then
, or .spread
functions.
- User settable
Y.nextTick
for your own optimizations or usage patterns. Y.nextTick
comes with reasonable default.- Additional helper functions are implemented that do not impact performance.
Quick Review of the deferred pattern
A deferred
is an object coupled with a promise
object. The deferred
object is responsible for resolving (also known as fulfilling) and rejecting
the promise
.
The promise
is the object with the then
method. (It also has the spread
method which is the same as the then
method but handles the onFulfilled
callback slightly differently.)
The two objects are coupled together by a queue of (onFulfilled, onResolved)
tuples. The promise.then
and promise.spread
methods build up the queue.
The deferred.resolve
and deferred.reject
methods dispatch the queue once
and only once.
Here is an example in the form of the V.promisify
function:
function promisify(nodeFn, thisObj){
return function(){
var args = Array.prototype.slice.call(arguments)
, d = Y.defer()
args.push(function(err){
if (err) { d.reject(err); return }
if (arguments.length > 2)
d.resolve(Array.prototype.slice.call(arguments, 1))
else
d.resolve(arguments[1])
})
nodeFn.apply(thisObj, args)
return d.promise
}
}
API
Load Module
var Y = require("ya-promise")
Load the library.
Create a Deferred & Promise
Q-alike: Q.defer()
deferred = Y.defer()
deferred = Y.deferred()
promise = deferred.promise
Promise then
promise.then(onFulfililled, onRejected)
This library does NOT support onProgress
. You can have a function as the
third argument to promise.then()
but it will never be called.
Promise spread
promise.spread(onFulfilled, onRejected)
When onFulfilled
is called, and value
is an Array
, value
will be spread
as arguments to the function via onFulfilled.apply(undefined, value)
rather than onFulfilled(value)
.
Resolve a Deferred
deferred.resolve(value)
Causes:
- all
onFulfilled
functions to be called with value
via Y.nextTick
. - the
promise
to change to a fulfilled
state as the Promise/A+ spec requires. - further calls to
deferred.resolve()
or deferred.reject()
to be ignored.
Reject a Deferred
Q-alike: Q.reject()
deferred.reject(value)
Causes:
- all
onRejected
functions to be called with value
via Y.nextTick
. - the
promise
to change to a rejected
state as the Promise/A+ spec requires. - further calls to
deferred.resolve()
or deferred.reject()
to be ignored.
Convert a value or a foreign Promise (thenable) to a Y Promise
Q-alike: Q()
Q-alike: Q.when()
Y(value_or_thanable)
Y.when(value_or_thenable)
Returns a ya-promise
promise given a straight value or thenable.
If a ya-promise
promise is passed in, it is returned unchanged.
If a value is passed in a fulfilled ya-promise
promise is returned.
If a foreign thenable is passed in it is wrapped in a deferred
and a ya-promise
promise is returned.
Create a Promise from an Array of Promises
Q-alike: Q.all()
Y.all([promA, promB, promC]).then( function([resA, resB, resC]){ ... }
, function(reason){ ... } )
When all the promises in the array passed to Y.all(array)
are resolved
the returned promise is resolved. It value is an array of the results of
each of the original promises in the same order.
If ANY of the promises in the array are rejected then the returned promise
is immediately rejected.
Example:
var Y = require('./')
function timeout(n) {
var d = Y.defer(), t = n * Math.random()
setTimeout(function(){ d.resolve(t) }, t*1000 )
return d.promise
}
var t0 = Date.now()
Y.all([ timeout(10, "one")
, timeout(10, "two")
, timeout(10, "three")
])
.then(function(a){
a.forEach(function(r, i){
console.log("%d: %d sec", i, r)
})
console.log("now-t0: %d sec", (Date.now()-t0)/1000)
})
Timeout a Promise
promise.timeout(ms).then(onFulfilled, onRejected)
If promise
is resolved or rejected in less than ms
milliseconds then
onFulfilled
or onRejected
(respectively) will be called with the value
or reason
given.
If promise
is not resolved or rejected within that time limit, then
the promise
will be rejected with the reason set to
"Timed out after " + ms + " ms"
.
In node.js the timeoutId
returned by setTimeout
has a unref
method that
will prevent this timer from allowing the node.js event-loop to end. If
timeoutId
has a unref
method, it is called.
Delay a Promise
Q-alike: promise.delay()
delayed_promise = promise.delay(ms)
From the time where delayed_promise
is created a timer is started for ms
milliseconds. If promise
is fulfilled or rejected within that timer then
delayed_promise
will not be resolved/rejected till the timer expires. If
the timer has already expired delayed_promise
will be resolved/rejected
immediately. delayed_promise
will always be resolved/rejected with the same
value/reason promise
was.
Create a promise with only an onRejected
Q-alike: promise.catch()
Q-alike: promise.fail()
another_promise = promise.fail(onRejected)
I prefer the promise.fail
version but I included the promise.catch
as an
alias.
Convert any onRejected
or a throw error from a callback into a throw
Q-alike: promise.done()
This is really not exactly like Q's promise.done()
. Unlike Q's
promise.done()
it takes NO arguments, but like Q's promise.done()
it
catches any rejected promise and throws the reason
in the nextTick
.
Q's promise.done()
is just like a promise.then()
but the execution is
slightly different in that any rejection is thrown as above.
It is meant to be use as such:
doSomething()
.then(...)
.then(...)
.done()
It still returns a promise, so more thens can follow it, but any rejection
that gets to it will throw an exception on the nextTick
.
Create a Promise whos Resolution is delayed
Q-alike: Q.delay()
delayed = Y.delay(ms)
This is a promise-like version of setTimeout()
but looks nicer.
Y.delay(1000).then(doSomthing)
Create a Fulfilled or Rejected Promise
Q-alike: Q.reject()
fulfilled_promise = Y.resolved(value)
rejected_promise = Y.rejected(reason)
Examples:
Y.reolved(42).then( function(value){ value == 42 }
, function(reason){})
Y.rejected("oops").then( function(value){}
, function(reason){ reason == "oops" })
Detect if an object ISA ya-promise
Deferred or Promise.
Q-alike: Q.isPromise()
var d = Y.defer()
, p = d.promise
Y.isDeferred( d )
Y.isPromise( p )
Convert a node-style async function to a promise-style async function.
Q-alike: Q.denodeify
Q-alike: Q.nfbind
promiseFn = Y.promisify(nodeFn)
promiseFn = Y.nfbind(nodeFn)
promiseFn = Y.denodeify(nodeFn)
A node-style async function looks like this
nodeFn(arg0, arg1, function(err, res0, res1){ ... })
where the return value of nodeFn
is usually undefined
.
The corresponding promise-style async function look like this
promise = promiseFn(arg0, arg1)
promise.then(function([res0, res1]){ ... }, function(err){ ... })
However, for a node-style async function that returns a single result,
Y.promisify(nodeFn)
does NOT return an single element array. For example:
nodeFn(arg0, arg1, function(err, res0){ ... })
is converted to:
promise = promiseFn(arg0, arg1)
promise.then(function(res0){ ... }, function(err){ ... })
Notice, res0
is not wrapped in an array.
Benchmarks
ya-promise
was just tested with the following simple script against a few
other Promise/A+ libraries. (My results also included.)
Remember "Lies, Statistics, and Benchmarks".
var Y = require('ya-promise')
, Q = require('q')
, Vow = require('vow')
, P = require('p-promise')
, promiscuous = require('promiscuous')
Y.nextTick = process.nextTick
exports.compare = {
'ya-promise' : function(done){
var d = Y.defer()
, p = d.promise
p.then(function(v){ return v+1 })
p.then(function(v){ done() })
d.resolve(0)
}
, 'Q' : function(done){
var d = Q.defer()
, p = d.promise
p.then(function(v){ return v+1 })
p.then(function(v){ done() })
d.resolve(0)
}
, 'Vow' : function(done){
var p = Vow.promise()
p.then(function(v){ return v+1 })
p.then(function(v){ done() })
p.fulfill(0)
}
, 'p-promise' : function(done){
var d = P.defer()
, p = d.promise
p.then(function(v){ return v+1 })
p.then(function(v){ done() })
d.resolve(0)
}
, 'promiscuous': function(done){
var d = promiscuous.deferred()
, p = d.promise
p.then(function(v){ return v+1 })
p.then(function(v){ done() })
d.resolve(0)
}
}
require('bench').runMain()
My Benchmark Results
{ http_parser: '1.0',
node: '0.10.4',
v8: '3.14.5.8',
ares: '1.9.0-DEV',
uv: '0.10.4',
zlib: '1.2.3',
modules: '11',
openssl: '1.0.1e' }
Scores: (bigger is better)
Vow
Raw:
> 593.063936063936
> 597.1928071928072
> 607.999000999001
> 604.5444555444556
Average (mean) 600.70004995005
promiscuous
Raw:
> 402.68431568431566
> 398.86013986013984
> 398.8851148851149
> 401.8061938061938
Average (mean) 400.55894105894106
ya-promise
Raw:
> 399.93806193806194
> 396.82917082917083
> 387.72427572427574
> 396.3046953046953
Average (mean) 395.19905094905096
p-promise
Raw:
> 133.1098901098901
> 134.56043956043956
> 134.16683316683316
> 133.2067932067932
Average (mean) 133.76098901098902
Q
Raw:
> 3.3366533864541834
> 3.3716283716283715
> 3.3846153846153846
> 3.3506493506493507
Average (mean) 3.3608866233368224
Winner: Vow
Compared with next highest (promiscuous), it's:
33.32% faster
1.5 times as fast
0.18 order(s) of magnitude faster
A LITTLE FASTER
Compared with the slowest (Q), it's:
99.44% faster
178.73 times as fast
2.25 order(s) of magnitude faster
This is not fair to p-promise
because it uses setImmediate
if avalable.
So here is the fair comparison:
var Y = require('ya-promise')
, P = require('p-promise')
exports.compare = {
'ya-promise' : function(done){
var d = Y.defer()
, p = d.promise
p.then(function(v){ return v+1 })
p.then(function(v){ done() })
d.resolve(0)
}
, 'p-promise' : function(done){
var d = P.defer()
, p = d.promise
p.then(function(v){ return v+1 })
p.then(function(v){ done() })
d.resolve(0)
}
}
require('bench').runMain()
{ http_parser: '1.0',
node: '0.10.4',
v8: '3.14.5.8',
ares: '1.9.0-DEV',
uv: '0.10.4',
zlib: '1.2.3',
modules: '11',
openssl: '1.0.1e' }
Scores: (bigger is better)
p-promise
Raw:
> 133.78121878121877
> 136.0979020979021
> 137.86713286713288
> 139.1988011988012
Average (mean) 136.73626373626374
ya-promise
Raw:
> 108.32167832167832
> 98.51548451548452
> 106.22477522477523
> 106.47152847152847
Average (mean) 104.88336663336663
Winner: p-promise
Compared with next highest (ya-promise), it's:
23.3% faster
1.3 times as fast
0.12 order(s) of magnitude faster
A LITTLE FASTER
Implementation
Performance Lessons Learned
Constructors do not HAVE to be more expensive then Plain-Ole-Objects
IE new Promise(thenFn)
does not have to be more expensive than
{ then: thenFn }
.
then
, reject
, & resolve
are closures not methods
This is total tl;dr. ("To Long Don't Read" for non-internet-hipsters, like me:).
This is a cute fact about the implementation that has a few implications.
For
var deferred = Y.defer()
deferred.resolve
and deferred.reject
are closures not methods. That
means that you could separate the function foo = deferred.resolve
from
the deferred
object and calling foo(value)
will still work.
Basically, deferred
is just a plain javascript object {}
with three
named values promise
, resolve
, and reject
.
For that matter, promise.then
is a closure not a method. If you look at
it promise
only contains a then
entry.
This turns out to be a good thing for two reasons, and bad for one reason:
- Converting a foreign promise to a
ya-promise
promise is easy.
function convert(foreign_promise){
var deferred = Y.defer()
foreign_promise.then(deferred.resolve, deferred.reject)
return deferred.promise
}
- There is no way to access to the internals of the
deferred
or promise
mechanisms. They are truely private.
This could be bad when the initial deferred.resolve
is called, it replaces
deferred.resolve
with a new function. So, if you copy the original function
to a new variable AND that function gets called twice it will call the
previous queued up then
functions twice as well. Simple don't do what I did
above in 1.
do the following instead:
function convert(foreign_promise){
var deferred = Y.defer()
foreign_promise.then( function(value) { deferred.resolve(value) }
, function(reason){ deferred.reject(reasone) })
return deferred.promise
}
Put in terms of code the folowing function returns true
:
function compareResolves(){
var deferred = Y.defer()
, resolveFnBefore = deferred.resolve
deferred.resolve("whatever")
return deferred.resolve !== resolveFnBefore
}
This applys to the promise's then
function as well:
function compareThens(){
var deferred = Y.defer()
, thenFnBefore = deferred.promise.then
deferred.resolve("whatever")
return deferred.promise.then !== thenFnBefore
}
Advice: Screw Nike comercials, "Just DON'T Do It". Don't try to be too clever
by half and take advantage of the fact that deferred.resolve
,
deferred.reject
, and promise.then
are closures not methods because they
"close over" deffered
and promise
as well.
Links
Promise/A+ Specification
Promise/A+ Test Suite
p-promise NPM module
[promiscuous NPM mdulepromiscuous
Q NPM module
bench NPM module
Promise/A+ terminology
tl;dr definition