devoir
Version:
Lightweight Javascript library adding functionality used in everyday tasks
563 lines (479 loc) • 18.5 kB
JavaScript
(function(factory) {
module.exports = function(_root) {
var root = _root || {};
return factory(root);
};
})(function(root) {
'use strict';
/**
* @namespace devoir
* @class {Deferred} Devoir Deferreds
**/
/**
* @constructor Create a new Deferred. If a callback is provided it will be called as soon as the deferred is fully initialized (on nextTick), or immediately if immediate mode is true.
* This callback will be provided three arguments which are all functions: resolve, reject, and notify<br>
* * resolve: is called to resolve this deferred, optionally passing any arguments you wish to resolve the deferred with
* * reject: is called to reject this deferred, optionally passing any arguments you wish to reject the deferred with
* * notify: is called to update the progress of this deferred. Normally there would only be one numerical argument in the range 0-1, but any arguments are valid (progress will be set to arguments provided)
* @param {Function} {callback} Callback function. If specified call as soon as the deferred is created. If the "immediate" flag is specified, call the callback immediately upon construction, otherwise call on "nextTick"
* @param {[options]} Options object
* @type {Object}
* @property {raw=false} if true, deferred resolution arguments will be passed as a single array to any bound resolution / rejection listeners. By default resolution arguments will be passed directly to any bound listeners in the order provided to "resolve"
* @property {immediate=false} if true, this deferred will call its callback immediately, and also call any listeners immediately upon resolution / rejection. Default is to call callback and listeners on "nextTick"
* @return {Deferred} A new Deferred instance
* @example {javascript}
function loadFile(fileName) {
var fs = require('fs');
return new devoir.Deferred(function(resolve, reject) {
fs.loadFile(fileName, function(err, contents) {
if (err) {
reject(err);
return;
}
resolve(contents);
});
});
}
@example {javascript}
function longTask() {
var def = new devoir.Deferred();
setTimeout(function() {
def.resolve();
}, 6000);
return def.promise();
}
@example {javascript}
function loadFile(fileName) {
var fs = require('fs');
return new devoir.Deferred(function(resolve, reject) {
fs.loadFile(fileName, function(err, contents) {
if (err) {
reject(err);
return;
}
resolve(contents);
});
});
}
function loadMultipleFiles(fileNames) {
//Create an array of deferreds
var deferreds = [];
for (var i = 0, il = fileNames.length; i < il; i++) {
deferreds.push(loadFile(fileNames[i]));
}
//Create a new deferred that will resolve when all other deferreds have resolved
//If anyone of the deferreds fail the entire deferred chain will fail
return devoir.Deferred.all(deferreds);
}
**/
function Deferred(cb, _opts) {
function newROProperty(name, func, _parent) {
var parent = _parent || self;
Object.defineProperty(parent, name, {
writable: false,
enumerable: false,
configurable: false,
value: func
});
}
function doAllCallbacks(type, callbackObj, args) {
function doIt() {
var callbackFunc = callbackObj[type], ret;
if (callbackFunc instanceof Function) {
try {
ret = callbackFunc.apply(self, (raw) ? [args] : args);
} catch(e) {
if (callbackObj.reject instanceof Function)
callbackObj.reject.apply(self, (raw) ? [[e]] : [e]);
return;
}
}
if (ret instanceof Deferred) {
ret.proxy(callbackObj.deferred);
} else {
if (ret !== undefined)
callbackObj.deferred[type](ret);
else
callbackObj.deferred[type].apply(callbackObj.deferred, args);
}
}
if (immediate)
doIt();
else
process.nextTick(doIt);
}
function then(resolve, reject, notify) {
var def = new Deferred(),
callbackObj = {
resolve: resolve,
reject: reject,
notify: notify,
deferred: def
};
if (state === 'pending') {
callbacks.push(callbackObj);
} else {
doAllCallbacks((state === 'rejected') ? 'reject' : 'resolve', callbackObj, result);
}
return def;
}
function statusUpdate(type, currentProgress) {
if (state !== 'pending')
return;
var args = [];
if (raw) {
args = currentProgress;
if (!(args instanceof Array))
args = [args];
} else {
for (var i = 1, len = arguments.length; i < len; i++)
args.push(arguments[i]);
}
if (type === 'notify') {
progress = currentProgress;
} else {
result = args;
state = (type === 'resolve') ? 'fulfilled' : 'rejected';
}
for (var i = 0, len = callbacks.length; i < len; i++) {
doAllCallbacks.call(self, type, callbacks[i], args);
}
}
function doIntanceCallback(cb) {
var self = this;
try {
cb.call(self, self.resolve, self.reject, self.notify);
} catch(e) {
console.error(e);
self.reject(e);
}
}
if (!(this instanceof Deferred)) {
var args = [Object.create(Deferred.prototype)];
for (var j = 0, l = arguments.length; j < l; j++)
args.push(arguments[j]);
return Deferred.bind.apply(Deferred, args);
}
var self = this,
opts = (_opts !== undefined) ? _opts : {
raw: false,
immediate: false
},
state = "pending",
immediate = opts.immediate,
raw = opts.raw,
progress = 0,
callbacks = [],
result;
/**
* @function {then} Call *successCallback* when deferred resolves (or *errorCallback* if deferred is rejected), passing arguments as provided to @@@devoir.Deferred.resolve (unless the "raw" option is set, and then all arguments will be provided in a single array)
* @param {Function} {successCallback} Callback function to call when deferred has resolved
* @param {Function} {[errorCallback]} Callback function to call when deferred is rejected
* @param {Function} {[notificationCallback]} Callback function to call when deferred gets a notification update
* @return {Deferred} Return a new deferred that can be used for chaining (if *successCallback* returns a deferred, this deferred wont resolve until the one returned by *successCallback* resolves)
**/
newROProperty('then', then);
/**
* @alias {done} function:devoir.Deferred.then
**/
newROProperty('done', then);
/**
* @function {fail} Call *errorCallback* if deferred is rejected
* @param {Function} {errorCallback} Callback function to call if deferred is rejected
* @return {Deferred} A new deferred that can be used for chaining. See @@@devoir.Deferred.then
**/
newROProperty('fail', then.bind(self, undefined));
/**
* @alias {catch} function:devoir.Deferred.fail
**/
newROProperty('catch', then.bind(self, undefined));
/**
* @function {notification} Call *notificationCallback* if deferred gets a notification event
* @param {Function} {notificationCallback} Callback function to call if deferred gets a progress notification event
* @return {Deferred} A new deferred that can be used for chaining. See @@@devoir.Deferred.then
**/
newROProperty('notification', then.bind(self, undefined, undefined));
/**
* @function {always} Call *callback* if deferred gets resolved or rejected. Call *notificationCallback* if deferred gets a progress notification
* @param {Function} {callback} Callback function to call if deferred gets resolved or rejected
* @param {Function} {[notificationCallback]} Callback function to call if deferred gets a progress notification event
* @return {Deferred} A new deferred that can be used for chaining. See @@@devoir.Deferred.then
**/
newROProperty('always', function(func, notify) {
return then.call(this, func, func, notify);
});
/**
* @function {resolve} Resolve this deferred with the arguments provided
* @param {*} {[args...]} Arguments to resolve this deferred with. These will be passed on to any bound resolve callbacks
* @return {undefined} None
**/
newROProperty('resolve', statusUpdate.bind(self, 'resolve'));
/**
* @function {reject} Reject this deferred with the arguments provided
* @param {*} {[args...]} Arguments to reject this deferred with. These will be passed on to any bound reject callbacks
* @return {undefined} None
**/
newROProperty('reject', statusUpdate.bind(self, 'reject'));
/**
* @function {notify} Notify this deferred with a progress update (usually a Number between 0 and 1, but is not required to be a number)
* @param {*} {[args...]} Arguments to notify this deferred with. These will be passed on to any bound notification callbacks
* @return {undefined} None
**/
newROProperty('notify', statusUpdate.bind(self, 'notify'));
/**
* @function {promise} Return a special clone deferred object that can be used as a normal "view" into this deferred, but can not be updated / modified (read only)
* @return {Deferred} A read-only clone of this deferred
**/
newROProperty('promise', function() {
function nullFunc(){}
var p = Object.create(self);
newROProperty('resolve', nullFunc, p);
newROProperty('reject', nullFunc, p);
newROProperty('notify', nullFunc, p);
return p;
});
/**
* @function {status} Get the status of this deferred
* @return {String} **"pending"** if the deferred is still pending, **"fulfilled"** if the deferred has been resolved, or **"rejected"** if the deferred has been rejected
**/
newROProperty('status', function() {
return state;
});
/**
* @alias {state} function:devoir.Deferred.status
**/
newROProperty('state', function() {
return state;
});
/**
* @function {progress} Get the progress for this deferred as set by notifications
* @return {*} The current progress (usually a Number) of this deferred
* @see function:devoir.Deferred.notify
**/
newROProperty('progress', function() {
return progress;
});
/**
* @function {proxy} Proxy the resolution / rejection / notifications of this deferred to the deferred specified by *deferred* argument
* @param {Deferred} {deferred} The deferred to proxy events to
* @return {Deferred} A new Deferred that can be used for chaining
* @see function:devoir.Deferred.resolve
* @see function:devoir.Deferred.reject
* @see function:devoir.Deferred.notify
**/
newROProperty('proxy', function(deferred) {
function proxyAction(type) {
var args = [];
for (var i = 1, len = arguments.length; i < len; i++)
args.push(arguments[i]);
return deferred[type].apply(deferred, args);
}
return self.then(function() {
return proxyAction.bind(self, 'resolve').apply(self, arguments);
}, function() {
return proxyAction.bind(self, 'reject').apply(self, arguments);
}, function() {
return proxyAction.bind(self, 'notify').apply(self, arguments);
});
});
/**
* @function {immediate} Get/Set this deferred's **"immediate"** mode. In **"immediate"** mode all callbacks are called immediately, instead of "nextTick", which is the default
* @param {Boolean} {[set]} If specified, set **"immediate"** mode according to boolean value. If not specified, return "immediate" mode state
* @return {Deferred} If *set* argument is specified, *this* is returned for chaining.
* @return {Boolean} If *set* is not specified, return the current **"immediate"** mode state
**/
newROProperty('immediate', function(set) {
if (arguments.length === 0)
return immediate;
immediate = set;
return self;
});
/**
* @function {raw} Get/Set this deferred's **"raw"** mode. In **"raw"** mode all callbacks are called with a single array of arguments as the argument to any callbacks instead of passing arguments as supplied to resolve/reject/notify
* @param {Boolean} {[set]} If specified, set "raw" mode according to boolean value. If not specified, return "raw" mode state
* @return {Deferred} If *set* argument is specified, *this* is returned for chaining.
* @return {Boolean} If *set* is not specified, return the current **"raw"** mode state
**/
newROProperty('raw', function(set) {
if (arguments.length === 0)
return raw;
raw = set;
return self;
});
if (cb instanceof Function) {
if (immediate) {
doIntanceCallback.call(self, cb);
} else {
process.nextTick(function() {
doIntanceCallback.call(self, cb);
});
}
} else if (cb instanceof Object) {
if (cb.hasOwnProperty('immediate'))
immediate = cb.immediate;
if (cb.hasOwnProperty('raw'))
raw = cb.raw;
}
return this;
}
/**
* @function {all} This will create a new deferred that waits on the status of ALL deferreds passed to it. If any one of the deferreds is rejected the entire chain is rejected
* @static
* @public
* @param {Array} {deferreds} Array if deferreds to wait on
* @return {Deferred} A new deferred wrapping all deferreds provided by the argument *deferreds*
**/
Deferred.all = function(promises, opts) {
var resolvedValues = new Array(promises.length),
resolvedCount = 0;
return new Deferred(function(resolve, reject, notify) {
var self = this;
for (var i = 0, len = promises.length; i < len; i++) {
(function(_promise, i) {
var promise = _promise;
if (!(promise instanceof Deferred))
promise = Deferred.resolve(promise);
promise.then(function(val) {
if (self.status() !== 'pending')
return;
var args = [];
for (var j = 0, l = arguments.length; j < l; j++)
args.push(arguments[j]);
resolvedValues[i] = (args.length > 1) ? args : args[0];
resolvedCount++;
notify(resolvedCount / len);
if (resolvedCount >= len)
resolve.apply(self, (self.raw()) ? [resolvedValues] : resolvedValues);
}, function() {
if (self.status() !== 'pending')
return;
notify(1);
reject.apply(self, arguments);
});
})(promises[i], i);
}
}, opts);
};
/**
* @function {race} This will create a new deferred that waits on the status of ALL deferreds passed to it. This deferred will be resolved/rejected as soon as any one of the deferreds is resolved or rejected
* @static
* @public
* @param {Array} {deferreds} Array if deferreds to wait on
* @return {Deferred} A new deferred wrapping all deferreds provided by the argument *deferreds*
**/
Deferred.race = function(promises, opts) {
return new Deferred(function(resolve, reject, notify) {
var self = this;
for (var i = 0, len = promises.length; i < len; i++) {
(function(_promise) {
var promise = _promise;
if (!(promise instanceof Deferred))
promise = Deferred.resolve(promise);
promise.then(function() {
if (self.status() !== 'pending')
return;
notify(1);
resolve.apply(self, arguments);
}, function() {
if (self.status() !== 'pending')
return;
notify(1);
reject.apply(self, arguments);
});
})(promises[i]);
}
}, opts);
};
/**
* @function {every} This will create a new deferred that waits on the status of ALL deferreds passed to it. This deferred will not resolve / reject until ALL deferreds have resolved / rejected. If anyone of the deferreds has been rejected than the whole chain is rejected
* @static
* @public
* @param {Array} {deferreds} Array if deferreds to wait on
* @return {Deferred} A new deferred wrapping all deferreds provided by the argument *deferreds*
**/
Deferred.every = function(promises, opts) {
var resolvedValues = new Array(promises.length),
resolvedCount = 0;
return new Deferred(function(resolve, reject, notify) {
var self = this;
function doCallback(resolvedValues) {
var isRejected = false;
for (var i = 0, len = promises.length; i < len; i++) {
if (promises[i].status() === 'rejected') {
isRejected = true;
break;
}
}
if (isRejected)
reject.apply(self, (self.raw()) ? [resolvedValues] : resolvedValues);
else
resolve.apply(self, (self.raw()) ? [resolvedValues] : resolvedValues);
}
for (var i = 0, len = promises.length; i < len; i++) {
(function(_promise, i) {
var promise = _promise;
if (!(promise instanceof Deferred))
promise = Deferred.resolve(promise);
promise.then(function() {
if (self.status() !== 'pending')
return;
var args = [];
for (var j = 0, l = arguments.length; j < l; j++)
args.push(arguments[j]);
resolvedValues[i] = (args.length > 1) ? args : args[0];
resolvedCount++;
notify(resolvedCount / len);
if (resolvedCount >= len)
doCallback(resolvedValues);
}, function() {
if (self.status() !== 'pending')
return;
var args = [];
for (var j = 0, l = arguments.length; j < l; j++)
args.push(arguments[j]);
var err = resolvedValues[i] = new Error((args.length > 1) ? args : args[0]);
err.value = args;
resolvedCount++;
notify(resolvedCount / len);
if (resolvedCount >= len)
doCallback(resolvedValues);
});
})(promises[i], i);
}
}, opts);
};
/**
* @function {resolve} Create an immediately resolved deferred with any arguments passed to resolve. This is the same as: `var def = new devoir.Deferred(undefined, {immediate: true});def.resolve(args...);`
* @static
* @public
* @param {*} {[args...]} Any arguments you wish to resolve the deferred with
* @return {Deferred} A new resolved deferred
**/
Deferred.resolve = function() {
var args = [];
for (var j = 0, l = arguments.length; j < l; j++)
args.push(arguments[j]);
return new Deferred(function(resolve, reject, notify) {
notify(1);
resolve.apply(this, args);
}, {immediate: true});
};
/**
* @function {reject} Create an immediately rejected deferred with any arguments passed to reject. This is the same as: `var def = new devoir.Deferred(undefined, {immediate: true});def.reject(args...);`
* @static
* @public
* @param {*} {[args...]} Any arguments you wish to reject the deferred with
* @return {Deferred} A new rejected deferred
**/
Deferred.reject = function() {
var args = [];
for (var j = 0, l = arguments.length; j < l; j++)
args.push(arguments[j]);
return new Deferred(function(resolve, reject, notify) {
notify(1);
reject.apply(this, args);
}, {immediate: true});
};
root.Deferred = Deferred;
return Deferred;
});