promised-redmine
Version:
Redmine Rest API Client for node.js with Promises/A+ compliance
450 lines (413 loc) • 14.6 kB
JavaScript
/**
* attempt of a simple defer/promise library for mobile development
* @author Jonathan Gotti < jgotti at jgotti dot net>
* @since 2012-10
* @version 0.7.5
*/
(function(undef){
"use strict";
var nextTick
, isFunc = function(f){ return ( typeof f === 'function' ); }
, isArray = function(a){ return Array.isArray ? Array.isArray(a) : (a instanceof Array); }
, isObjOrFunc = function(o){ return !!(o && (typeof o).match(/function|object/)); }
, isNotVal = function(v){ return (v === false || v === undef || v === null); }
, slice = function(a, offset){ return [].slice.call(a, offset); }
, undefStr = 'undefined'
, tErr = typeof TypeError === undefStr ? Error : TypeError
;
if ( (typeof process !== undefStr) && process.nextTick ) {
nextTick = process.nextTick;
} else if ( typeof MessageChannel !== undefStr ) {
var ntickChannel = new MessageChannel(), queue = [];
ntickChannel.port1.onmessage = function(){ queue.length && (queue.shift())(); };
nextTick = function(cb){
queue.push(cb);
ntickChannel.port2.postMessage(0);
};
} else {
nextTick = function(cb){ setTimeout(cb, 0); };
}
function rethrow(e){ nextTick(function(){ throw e;}); }
/**
* a function called on fulfilled promise resolution
* @typedef {function} fulfilled
* @param {*} value promise resolved value
* @returns {*} next promise resolution value
*/
/**
* a function called on failed promise resolution
* @typedef {function} failed
* @param {*} reason promise rejection reason
* @returns {*} next promise resolution value or rethrow the reason
*/
//-- defining unenclosed promise methods --//
/**
* same as then without failed callback
* @param {fulfilled} fulfilled callback
* @returns {promise} a new promise
*/
function promise_success(fulfilled){ return this.then(fulfilled, undef); }
/**
* same as then with only a failed callback
* @param {failed} failed callback
* @returns {promise} a new promise
*/
function promise_error(failed){ return this.then(undef, failed); }
/**
* same as then but fulfilled callback will receive multiple parameters when promise is fulfilled with an Array
* @param {fulfilled} fulfilled callback
* @param {failed} failed callback
* @returns {promise} a new promise
*/
function promise_apply(fulfilled, failed){
return this.then(
function(a){
return isFunc(fulfilled) ? fulfilled.apply(null, isArray(a) ? a : [a]) : (defer.onlyFuncs ? a : fulfilled);
}
, failed || undef
);
}
/**
* cleanup method which will be always executed regardless fulfillment or rejection
* @param {function} cb a callback called regardless of the fulfillment or rejection of the promise which will be called
* when the promise is not pending anymore
* @returns {promise} the same promise untouched
*/
function promise_ensure(cb){
function _cb(){ cb(); }
this.then(_cb, _cb);
return this;
}
/**
* take a single callback which wait for an error as first parameter. other resolution values are passed as with the apply/spread method
* @param {function} cb a callback called regardless of the fulfillment or rejection of the promise which will be called
* when the promise is not pending anymore with error as first parameter if any as in node style
* callback. Rest of parameters will be applied as with the apply method.
* @returns {promise} a new promise
*/
function promise_nodify(cb){
return this.then(
function(a){
return isFunc(cb) ? cb.apply(null, isArray(a) ? a.splice(0,0,undefined) && a : [undefined,a]) : (defer.onlyFuncs ? a : cb);
}
, function(e){
return cb(e);
}
);
}
/**
*
* @param {function} [failed] without parameter will only rethrow promise rejection reason outside of the promise library on next tick
* if passed a failed method then will call failed on rejection and throw the error again if failed didn't
* @returns {promise} a new promise
*/
function promise_rethrow(failed){
return this.then(
undef
, failed ? function(e){ failed(e); throw e; } : rethrow
);
}
/**
* return a defer object
* @param {boolean} [alwaysAsync] if set force the async resolution for this promise independantly of the D.alwaysAsync option
* @returns {deferred} defered object with property 'promise' and methods reject,fulfill,resolve (fulfill being an alias for resolve)
*/
var defer = function (alwaysAsync){
var alwaysAsyncFn = (undef !== alwaysAsync ? alwaysAsync : defer.alwaysAsync) ? nextTick : function(fn){fn();}
, status = 0 // -1 failed | 1 fulfilled
, pendings = []
, value
/**
* @typedef promise
*/
, _promise = {
/**
* @param {fulfilled|function} fulfilled callback
* @param {failed|function} failed callback
* @returns {promise} a new promise
*/
then: function(fulfilled, failed){
var d = defer();
pendings.push([
function(value){
try{
if( isNotVal(fulfilled)){
d.resolve(value);
} else {
d.resolve(isFunc(fulfilled) ? fulfilled(value) : (defer.onlyFuncs ? value : fulfilled));
}
}catch(e){
d.reject(e);
}
}
, function(err){
if ( isNotVal(failed) || ((!isFunc(failed)) && defer.onlyFuncs) ) {
d.reject(err);
}
if ( failed ) {
try{ d.resolve(isFunc(failed) ? failed(err) : failed); }catch(e){ d.reject(e);}
}
}
]);
status !== 0 && alwaysAsyncFn(execCallbacks);
return d.promise;
}
, success: promise_success
, error: promise_error
, otherwise: promise_error
, apply: promise_apply
, spread: promise_apply
, ensure: promise_ensure
, nodify: promise_nodify
, rethrow: promise_rethrow
, isPending: function(){ return status === 0; }
, getStatus: function(){ return status; }
}
;
_promise.toSource = _promise.toString = _promise.valueOf = function(){return value === undef ? this : value; };
function execCallbacks(){
/*jshint bitwise:false*/
if ( status === 0 ) {
return;
}
var cbs = pendings, i = 0, l = cbs.length, cbIndex = ~status ? 0 : 1, cb;
pendings = [];
for( ; i < l; i++ ){
(cb = cbs[i][cbIndex]) && cb(value);
}
}
/**
* fulfill deferred with given value
* @param {*} val
* @returns {deferred} this for method chaining
*/
function _resolve(val){
var done = false;
function once(f){
return function(x){
if (done) {
return undefined;
} else {
done = true;
return f(x);
}
};
}
if ( status ) {
return this;
}
try {
var then = isObjOrFunc(val) && val.then;
if ( isFunc(then) ) { // managing a promise
if( val === _promise ){
throw new tErr("Promise can't resolve itself");
}
then.call(val, once(_resolve), once(_reject));
return this;
}
} catch (e) {
once(_reject)(e);
return this;
}
alwaysAsyncFn(function(){
value = val;
status = 1;
execCallbacks();
});
return this;
}
/**
* reject deferred with given reason
* @param {*} Err
* @returns {deferred} this for method chaining
*/
function _reject(Err){
status || alwaysAsyncFn(function(){
try{ throw(Err); }catch(e){ value = e; }
status = -1;
execCallbacks();
});
return this;
}
return /**@type deferred */ {
promise:_promise
,resolve:_resolve
,fulfill:_resolve // alias
,reject:_reject
};
};
defer.deferred = defer.defer = defer;
defer.nextTick = nextTick;
defer.alwaysAsync = true; // setting this will change default behaviour. use it only if necessary as asynchronicity will force some delay between your promise resolutions and is not always what you want.
/**
* setting onlyFuncs to false will break promises/A+ conformity by allowing you to pass non undefined/null values instead of callbacks
* instead of just ignoring any non function parameters to then,success,error... it will accept non null|undefined values.
* this will allow you shortcuts like promise.then('val','handled error'')
* to be equivalent of promise.then(function(){ return 'val';},function(){ return 'handled error'})
*/
defer.onlyFuncs = true;
/**
* return a fulfilled promise of given value (always async resolution)
* @param {*} value
* @returns {promise}
*/
defer.resolve = defer.resolved = defer.fulfilled = function(value){ return defer(true).resolve(value).promise; };
/**
* return a rejected promise with given reason of rejection (always async rejection)
* @param {*} reason
* @returns {promise}
*/
defer.reject = defer.rejected = function(reason){ return defer(true).reject(reason).promise; };
/**
* return a promise with no resolution value which will be resolved in time ms (using setTimeout)
* @param {int} [time] in ms default to 0
* @returns {promise}
*/
defer.wait = function(time){
var d = defer();
setTimeout(d.resolve, time || 0);
return d.promise;
};
/**
* return a promise for the return value of function call which will be fulfilled in delay ms or rejected if given fn throw an error
* @param {*} fn to execute or value to return after given delay
* @param {int} [delay] in ms default to 0
* @returns {promise}
*/
defer.delay = function(fn, delay){
var d = defer();
setTimeout(function(){ try{ d.resolve(isFunc(fn) ? fn.apply(null) : fn); }catch(e){ d.reject(e); } }, delay || 0);
return d.promise;
};
/**
* if given value is not a promise return a fulfilled promise resolved to given value
* @param {*} promise a value or a promise
* @returns {promise}
*/
defer.promisify = function(promise){
if ( promise && isFunc(promise.then) ) { return promise;}
return defer.resolved(promise);
};
function multiPromiseResolver(callerArguments, returnPromises){
var promises = slice(callerArguments);
if ( promises.length === 1 && isArray(promises[0]) ) {
if(! promises[0].length ){
return defer.fulfilled([]);
}
promises = promises[0];
}
var args = []
, d = defer()
, c = promises.length
;
if ( !c ) {
d.resolve(args);
} else {
var resolver = function(i){
promises[i] = defer.promisify(promises[i]);
promises[i].then(
function(v){
args[i] = returnPromises ? promises[i] : v;
(--c) || d.resolve(args);
}
, function(e){
if( ! returnPromises ){
d.reject(e);
} else {
args[i] = promises[i];
(--c) || d.resolve(args);
}
}
);
};
for( var i = 0, l = c; i < l; i++ ){
resolver(i);
}
}
return d.promise;
}
function sequenceZenifier(promise, zenValue){
return promise.then(isFunc(zenValue) ? zenValue : function(){return zenValue;});
}
function sequencePromiseResolver(callerArguments){
var funcs = slice(callerArguments);
if ( funcs.length === 1 && isArray(funcs[0]) ) {
funcs = funcs[0];
}
var d = defer(), i=0, l=funcs.length, promise = defer.resolved();
for(; i<l; i++){
promise = sequenceZenifier(promise, funcs[i]);
}
d.resolve(promise);
return d.promise;
}
/**
* return a promise for all given promises / values.
* the returned promises will be fulfilled with a list of resolved value.
* if any given promise is rejected then on the first rejection the returned promised will be rejected with the same reason
* @param {array|...*} [promise] can be a single array of promise/values as first parameter or a list of direct parameters promise/value
* @returns {promise} of a list of given promise resolution value
*/
defer.all = function(){ return multiPromiseResolver(arguments,false); };
/**
* return an always fulfilled promise of array<promise> list of promises/values regardless they resolve fulfilled or rejected
* @param {array|...*} [promise] can be a single array of promise/values as first parameter or a list of direct parameters promise/value
* (non promise values will be promisified)
* @returns {promise} of the list of given promises
*/
defer.resolveAll = function(){ return multiPromiseResolver(arguments,true); };
/**
* execute given function in sequence passing their returned values to the next one in sequence.
* You can pass values or promise instead of functions they will be passed in the sequence as if a function returned them.
* if any function throw an error or a rejected promise the final returned promise will be rejected with that reason.
* @param {array|...*} [function] list of function to call in sequence receiving previous one as a parameter
* (non function values will be treated as if returned by a function)
* @returns {promise} of the list of given promises
*/
defer.sequence = function(){ return sequencePromiseResolver(arguments); };
/**
* transform a typical nodejs async method awaiting a callback as last parameter, receiving error as first parameter to a function that
* will return a promise instead. the returned promise will resolve with normal callback value minus the first error parameter on
* fulfill and will be rejected with that error as reason in case of error.
* @param {object} [subject] optional subject of the method to encapsulate
* @param {function} fn the function to encapsulate if the normal callback should receive more than a single parameter (minus the error)
* the promise will resolve with the list or parameters as fulfillment value. If only one parameter is sent to the
* callback then it will be used as the resolution value.
* @returns {Function}
*/
defer.nodeCapsule = function(subject, fn){
if ( !fn ) {
fn = subject;
subject = void(0);
}
return function(){
var d = defer(), args = slice(arguments);
args.push(function(err, res){
err ? d.reject(err) : d.resolve(arguments.length > 2 ? slice(arguments, 1) : res);
});
try{
fn.apply(subject, args);
}catch(e){
d.reject(e);
}
return d.promise;
};
};
/*global define*/
if ( typeof define === 'function' && define.amd ) {
define('D.js', [], function(){ return defer; });
} else if ( typeof module !== undefStr && module.exports ) {
module.exports = defer;
} else if ( typeof window !== undefStr ) {
var oldD = window.D;
/**
* restore global D variable to its previous value and return D to the user
* @returns {Function}
*/
defer.noConflict = function(){
window.D = oldD;
return defer;
};
window.D = defer;
}
})();