'use strict'; var util = require('util'); var EventEmitter = require('events').EventEmitter; function toArray(arr, start, end) { return Array.prototype.slice.call(arr, start, end) } function strongUnshift(x, arrLike) { var arr = toArray(arrLike); arr.unshift(x); return arr; } /** * MPromise constructor. * * _NOTE: The success and failure event names can be overridden by setting `Promise.SUCCESS` and `Promise.FAILURE` respectively._ * * @param {Function} back a function that accepts `fn(err, ...){}` as signature * @inherits NodeJS EventEmitter http://nodejs.org/api/events.html#events_class_events_eventemitter * @event `reject`: Emits when the promise is rejected (event name may be overridden) * @event `fulfill`: Emits when the promise is fulfilled (event name may be overridden) * @api public */ function Promise(back) { this.emitter = new EventEmitter(); this.emitted = {}; this.ended = false; if ('function' == typeof back) { this.ended = true; this.onResolve(back); } } /* * Module exports. */ module.exports = Promise; /*! * event names */ Promise.SUCCESS = 'fulfill'; Promise.FAILURE = 'reject'; /** * Adds `listener` to the `event`. * * If `event` is either the success or failure event and the event has already been emitted, the`listener` is called immediately and passed the results of the original emitted event. * * @param {String} event * @param {Function} callback * @return {MPromise} this * @api private */ Promise.prototype.on = function (event, callback) { if (this.emitted[event]) callback.apply(undefined, this.emitted[event]); else this.emitter.on(event, callback); return this; }; /** * Keeps track of emitted events to run them on `on`. * * @api private */ Promise.prototype.safeEmit = function (event) { // ensures a promise can't be fulfill() or reject() more than once if (event == Promise.SUCCESS || event == Promise.FAILURE) { if (this.emitted[Promise.SUCCESS] || this.emitted[Promise.FAILURE]) { return this; } this.emitted[event] = toArray(arguments, 1); } this.emitter.emit.apply(this.emitter, arguments); return this; }; /** * @api private */ Promise.prototype.hasRejectListeners = function () { return EventEmitter.listenerCount(this.emitter, Promise.FAILURE) > 0; }; /** * Fulfills this promise with passed arguments. * * If this promise has already been fulfilled or rejected, no action is taken. * * @api public */ Promise.prototype.fulfill = function () { return this.safeEmit.apply(this, strongUnshift(Promise.SUCCESS, arguments)); }; /** * Rejects this promise with `reason`. * * If this promise has already been fulfilled or rejected, no action is taken. * * @api public * @param {Object|String} reason * @return {MPromise} this */ Promise.prototype.reject = function (reason) { if (this.ended && !this.hasRejectListeners()) throw reason; return this.safeEmit(Promise.FAILURE, reason); }; /** * Resolves this promise to a rejected state if `err` is passed or * fulfilled state if no `err` is passed. * * @param {Error} [err] error or null * @param {Object} [val] value to fulfill the promise with * @api public */ Promise.prototype.resolve = function (err, val) { if (err) return this.reject(err); return this.fulfill(val); }; /** * Adds a listener to the SUCCESS event. * * @return {MPromise} this * @api public */ Promise.prototype.onFulfill = function (fn) { if (!fn) return this; if ('function' != typeof fn) throw new TypeError("fn should be a function"); return this.on(Promise.SUCCESS, fn); }; /** * Adds a listener to the FAILURE event. * * @return {MPromise} this * @api public */ Promise.prototype.onReject = function (fn) { if (!fn) return this; if ('function' != typeof fn) throw new TypeError("fn should be a function"); return this.on(Promise.FAILURE, fn); }; /** * Adds a single function as a listener to both SUCCESS and FAILURE. * * It will be executed with traditional node.js argument position: * function (err, args...) {} * * Also marks the promise as `end`ed, since it's the common use-case, and yet has no * side effects unless `fn` is undefined or null. * * @param {Function} fn * @return {MPromise} this */ Promise.prototype.onResolve = function (fn) { if (!fn) return this; if ('function' != typeof fn) throw new TypeError("fn should be a function"); this.on(Promise.FAILURE, function (err) { fn.call(this, err); }); this.on(Promise.SUCCESS, function () { fn.apply(this, strongUnshift(null, arguments)); }); return this; }; /** * Creates a new promise and returns it. If `onFulfill` or * `onReject` are passed, they are added as SUCCESS/ERROR callbacks * to this promise after the next tick. * * Conforms to [promises/A+](https://github.com/promises-aplus/promises-spec) specification. Read for more detail how to use this method. * * ####Example: * * var p = new Promise; * p.then(function (arg) { * return arg + 1; * }).then(function (arg) { * throw new Error(arg + ' is an error!'); * }).then(null, function (err) { * assert.ok(err instanceof Error); * assert.equal('2 is an error', err.message); * }); * p.complete(1); * * @see promises-A+ https://github.com/promises-aplus/promises-spec * @param {Function} onFulfill * @param {Function} [onReject] * @return {MPromise} newPromise */ Promise.prototype.then = function (onFulfill, onReject) { var newPromise = new Promise; if ('function' == typeof onFulfill) { this.onFulfill(handler(newPromise, onFulfill)); } else { this.onFulfill(newPromise.fulfill.bind(newPromise)); } if ('function' == typeof onReject) { this.onReject(handler(newPromise, onReject)); } else { this.onReject(newPromise.reject.bind(newPromise)); } return newPromise; }; function handler(promise, fn) { function newTickHandler() { var pDomain = promise.emitter.domain; if (pDomain && pDomain !== process.domain) pDomain.enter(); try { var x = fn.apply(undefined, boundHandler.args); } catch (err) { promise.reject(err); return; } resolve(promise, x); } function boundHandler() { boundHandler.args = arguments; process.nextTick(newTickHandler); } return boundHandler; } function resolve(promise, x) { function fulfillOnce() { if (done++) return; resolve.apply(undefined, strongUnshift(promise, arguments)); } function rejectOnce(reason) { if (done++) return; promise.reject(reason); } if (promise === x) { promise.reject(new TypeError("promise and x are the same")); return; } var rest = toArray(arguments, 1); var type = typeof x; if ('undefined' == type || null == x || !('object' == type || 'function' == type)) { promise.fulfill.apply(promise, rest); return; } try { var theThen = x.then; } catch (err) { promise.reject(err); return; } if ('function' != typeof theThen) { promise.fulfill.apply(promise, rest); return; } var done = 0; try { var ret = theThen.call(x, fulfillOnce, rejectOnce); return ret; } catch (err) { if (done++) return; promise.reject(err); } } /** * Signifies that this promise was the last in a chain of `then()s`: if a handler passed to the call to `then` which produced this promise throws, the exception will go uncaught. * * ####Example: * * var p = new Promise; * p.then(function(){ throw new Error('shucks') }); * setTimeout(function () { * p.fulfill(); * // error was caught and swallowed by the promise returned from * // p.then(). we either have to always register handlers on * // the returned promises or we can do the following... * }, 10); * * // this time we use .end() which prevents catching thrown errors * var p = new Promise; * var p2 = p.then(function(){ throw new Error('shucks') }).end(); // <-- * setTimeout(function () { * p.fulfill(); // throws "shucks" * }, 10); * * @api public * @param {Function} [onReject] * @return {MPromise} this */ Promise.prototype.end = Promise.prototype['catch'] = function (onReject) { if (!onReject && !this.hasRejectListeners()) onReject = function idRejector(e) { throw e; }; this.onReject(onReject); this.ended = true; return this; }; /** * A debug utility function that adds handlers to a promise that will log some output to the `console` * * ####Example: * * var p = new Promise; * p.then(function(){ throw new Error('shucks') }); * setTimeout(function () { * p.fulfill(); * // error was caught and swallowed by the promise returned from * // p.then(). we either have to always register handlers on * // the returned promises or we can do the following... * }, 10); * * // this time we use .end() which prevents catching thrown errors * var p = new Promise; * var p2 = p.then(function(){ throw new Error('shucks') }).end(); // <-- * setTimeout(function () { * p.fulfill(); // throws "shucks" * }, 10); * * @api public * @param {MPromise} p * @param {String} name * @return {MPromise} this */ Promise.trace = function (p, name) { p.then( function () { console.log("%s fulfill %j", name, toArray(arguments)); }, function () { console.log("%s reject %j", name, toArray(arguments)); } ) }; Promise.prototype.chain = function (p2) { var p1 = this; p1.onFulfill(p2.fulfill.bind(p2)); p1.onReject(p2.reject.bind(p2)); return p2; }; Promise.prototype.all = function (promiseOfArr) { var pRet = new Promise; this.then(promiseOfArr).then( function (promiseArr) { var count = 0; var ret = []; var errSentinel; if (!promiseArr.length) pRet.resolve(); promiseArr.forEach(function (promise, index) { if (errSentinel) return; count++; promise.then( function (val) { if (errSentinel) return; ret[index] = val; --count; if (count == 0) pRet.fulfill(ret); }, function (err) { if (errSentinel) return; errSentinel = err; pRet.reject(err); } ); }); return pRet; } , pRet.reject.bind(pRet) ); return pRet; }; Promise.hook = function (arr) { var p1 = new Promise; var pFinal = new Promise; var signalP = function () { --count; if (count == 0) pFinal.fulfill(); return pFinal; }; var count = 1; var ps = p1; arr.forEach(function (hook) { ps = ps.then( function () { var p = new Promise; count++; hook(p.resolve.bind(p), signalP); return p; } ) }); ps = ps.then(signalP); p1.resolve(); return ps; }; /* This is for the A+ tests, but it's very useful as well */ Promise.fulfilled = function fulfilled() { var p = new Promise; p.fulfill.apply(p, arguments); return p; }; Promise.rejected = function rejected(reason) { return new Promise().reject(reason); }; Promise.deferred = function deferred() { var p = new Promise; return { promise: p, reject: p.reject.bind(p), resolve: p.fulfill.bind(p), callback: p.resolve.bind(p) } }; /* End A+ tests adapter bit */