You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

445 lines
11 KiB

'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 */