Commit 085af518 authored by Tristan Cavelier's avatar Tristan Cavelier

Promy - the promises library - added

parent 6113c5a2
/*
* Promy: Promises library
* Copyright (C) 2013 Nexedi SA
*
* This library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global setInterval, setTimeout, clearInterval, clearTimeout */
(function (dependencies, module) {
"use strict";
/*global define, exports, window */
if (typeof define === 'function' && define.amd) {
return define(dependencies, module);
}
if (typeof exports === 'object') {
module(exports);
}
if (typeof window === 'object') {
window.promy = {};
module(window.promy);
}
}(['exports'], function (exports) {
"use strict";
var UNRESOLVED = 0, RESOLVED = 1, REJECTED = 2, CANCELLED = 3;
/**
* thenItem(item, [onSucess], [onError], [onProgress]): any
*
* Execute one of the given callback when the item is fulfilled. If the item
* is not a promise, then onSuccess is called with the item as first
* parameter.
*
* @param {Any} item A promise, deferred or a simple value
* @param {Function} [onSuccess] The callback to call on resolve
* @param {Function} [onError] The callback to call on reject
* @param {Function} [onProgress] The callback to call on notify
*/
function thenItem(item, onSuccess, onError, onProgress) {
if (typeof item === 'object' && item !== null) {
if (typeof item.promise === 'object' && item.promise !== null &&
typeof item.promise.then === 'function') {
// item seams to be a Deferred
return item.promise.then(
onSuccess,
onError,
onProgress
);
}
if (typeof item.then === 'function') {
// item seams to be a Promise
return item.then(
onSuccess,
onError,
onProgress
);
}
}
return onSuccess(item);
}
/**
* promiseResolve(promise, answers): any
*
* Resolve the promise with the given answers.
*
* @param {Promise} promise The promise to resolve
* @param {Array} answers The arguments to give
*/
function promiseResolve(promise, answers) {
var array;
if (promise._state === UNRESOLVED) {
promise._state = RESOLVED;
promise._answers = answers;
array = promise._onResolve.slice();
setTimeout(function () {
var i;
for (i = 0; i < array.length; i += 1) {
try {
array[i].apply(promise, promise._answers);
} catch (ignore) {} // errors will never be retrieved by global
}
});
// free the memory
promise._onResolve = undefined;
promise._onReject = undefined;
promise._onProgress = undefined;
}
}
/**
* promiseReject(promise, answers): any
*
* Reject the promise with the given answers.
*
* @param {Promise} promise The promise to reject
* @param {Array} answers The arguments to give
*/
function promiseReject(promise, answers) {
var array;
if (promise._state === UNRESOLVED) {
promise._state = REJECTED;
promise._answers = answers;
array = promise._onReject.slice();
setTimeout(function () {
var i;
for (i = 0; i < array.length; i += 1) {
try {
array[i].apply(promise, promise._answers);
} catch (ignore) {} // errors will never be retrieved by global
}
});
// free the memory
promise._onResolve = undefined;
promise._onReject = undefined;
promise._onProgress = undefined;
}
}
/**
* promiseNotify(promise, answers): any
*
* Notify the promise with the given answers.
*
* @param {Promise} promise The promise to notify
* @param {Array} answers The arguments to give
*/
function promiseNotify(promise, answers) {
var i;
if (promise._onProgress) {
for (i = 0; i < promise._onProgress.length; i += 1) {
try {
promise._onProgress[i].apply(promise, answers);
} catch (ignore) {} // errors will never be retrieved by global
}
}
}
/**
* Promise(cancel)
*
* @class Promise
* @constructor
*/
function Promise(cancel) {
this._onReject = [];
this._onResolve = [];
this._onProgress = [];
this._state = UNRESOLVED;
if (typeof cancel === 'function') {
this.promise._cancel = cancel;
}
}
////////////////////////////////////////////////////////////
// http://wiki.commonjs.org/wiki/Promises/A
// then(fulfilledHandler, errorHandler, progressHandler)
/**
* then([onSuccess], [onError], [onProgress]): Promise
*
* Returns a new Promise with the return value of the `onSuccess` or `onError`
* callback as first parameter. If the pervious promise is resolved, the
* `onSuccess` callback is called. If rejected, the `onError` callback is
* called. If notified, `onProgress` is called.
*
* Deferred.when(1).
* then(function (one) { return one + 1; }).
* then(console.log); // shows 2
*
* @method then
* @param {Function} [onSuccess] The callback to call on resolve
* @param {Function} [onError] The callback to call on reject
* @param {Function} [onProgress] The callback to call on notify
* @return {Promise} The new promise
*/
Promise.prototype.then = function (onSuccess, onError, onProgress) {
/*global Deferred*/
var defer, next = new this.constructor(this._cancel), that = this;
defer = new Deferred();
defer.promise = next;
switch (this._state) {
case RESOLVED:
if (typeof onSuccess === 'function') {
setTimeout(function () {
try {
thenItem(
onSuccess.apply(that, that._answers),
defer.resolve.bind(defer),
defer.reject.bind(defer)
);
} catch (e) {
defer.reject(e);
}
});
} else {
setTimeout(function () {
defer.resolve.apply(defer, that._answers);
});
}
break;
case REJECTED:
if (typeof onError === 'function') {
setTimeout(function () {
var result;
try {
result = onError.apply(that, that._answers);
if (result === undefined) {
return defer.reject.apply(defer, that._answers);
}
thenItem(
result,
defer.reject.bind(defer),
defer.reject.bind(defer)
);
} catch (e) {
defer.reject(e);
}
});
} else {
setTimeout(function () {
defer.reject.apply(defer, that._answers);
});
}
break;
case UNRESOLVED:
if (typeof onSuccess === 'function') {
this._onResolve.push(function () {
try {
thenItem(
onSuccess.apply(that, arguments),
defer.resolve.bind(defer),
defer.reject.bind(defer),
defer.notify.bind(defer)
);
} catch (e) {
defer.reject(e);
}
});
} else {
this._onResolve.push(function () {
defer.resolve.apply(defer, arguments);
});
}
if (typeof onError === 'function') {
this._onReject.push(function () {
try {
thenItem(
onError.apply(that, that._answers),
defer.reject.bind(defer),
defer.reject.bind(defer)
);
} catch (e) {
defer.reject(e);
}
});
} else {
this._onReject.push(function () {
defer.reject.apply(defer, that._answers);
});
}
if (typeof onProgress === 'function') {
this._onProgress.push(function () {
var result;
try {
result = onProgress.apply(that, arguments);
if (result === undefined) {
defer.notify.apply(defer, arguments);
} else {
defer.notify(result);
}
} catch (e) {
defer.notify.apply(defer, arguments);
}
});
} else {
this._onProgress.push(function () {
defer.notify.apply(defer, arguments);
});
}
break;
default:
break;
}
return next;
};
// p.resolve() ?
// p.reject() ?
// p.notify() ?
/**
* p.cancel(): p
*
* Cancels the operation by calling promise._cancel().
*
* @method cancel
* @return {Promise} this
*/
Promise.prototype.cancel = function () {
if (this._state === UNRESOLVED) {
this._state = CANCELLED;
if (typeof this._cancel === 'function') {
this._cancel();
}
this._onResolve = undefined;
this._onReject = undefined;
this._onProgress = undefined;
}
return this;
};
/**
* p.timeout(delay): p
*
* Reject the promise with an Error("Timeout") and cancel the operation.
*
* @method timeout
* @param {Number} delay The delay before rejection
* @return {Promise} this
*/
Promise.prototype.timeout = function (delay) {
return exports.choose(this, exports.delay(delay).then(function () {
throw new Error("Timeout (" + delay + ")");
}));
};
////////////////////////////////////////////////////////////
// http://wiki.commonjs.org/wiki/Promises/A
// get(propertyName)
/**
* get(property): Promise
*
* Get the property of the promise response as first parameter of the new
* Promise.
*
* Deferred.when({'a': 'b'}).get('a').then(console.log); // shows 'b'
*
* @method get
* @param {String} property The object property name
* @return {Promise} The promise
*/
Promise.prototype.get = function (property) {
return this.then(function (dict) {
return dict[property];
});
};
////////////////////////////////////////////////////////////
// http://wiki.commonjs.org/wiki/Promises/A
// call(functionName, arg1, arg2, ...)
/**
* call(function_name, *args): Promise
*
* Deferred.when({'a': console.log}).call('a', 'b'); // shows 'b'
*
* @method call
* @param {String} function_name The function to call
* @param {Any} *[args] The function arguments
* @return {Promise} A new promise
*/
Promise.prototype.call = function (function_name) {
var args = Array.prototype.slice.call(arguments, 1);
return this.then(function (dict) {
return dict[function_name].apply(dict, args);
});
};
/**
* put(property, value): Promise
*
* Put a property value from a promise response and return the registered
* value as first parameter of the new Promise.
*
* Deferred.when({'a': 'b'}).put('a', 'c').then(console.log); // shows 'c'
*
* @method put
* @param {String} property The object property name
* @param {String} value The value to put
* @return {Promise} A new promise
*/
Promise.prototype.put = function (property, value) {
return this.then(function (dict) {
dict[property] = value;
return dict[property];
});
};
/**
* p.done(callback): p
*
* Call the callback on resolve.
*
* Deferred.when(1).
* done(function (one) { return one + 1; }).
* done(console.log); // shows 1
*
* @method done
* @param {Function} callback The callback to call on resolve
* @return {Promise} This promise
*/
Promise.prototype.done = function (callback) {
var that = this;
if (typeof callback !== 'function') {
return this;
}
switch (this._state) {
case RESOLVED:
setTimeout(function () {
try {
callback.apply(that, that._answers);
} catch (ignore) {} // errors will never be retrieved by global
});
break;
case UNRESOLVED:
this._onResolve.push(callback);
break;
default:
break;
}
return this;
};
/**
* p.fail(callback): p
*
* Call the callback on reject.
*
* promisedTypeError().
* fail(function (e) { name_error(); }).
* fail(console.log); // shows TypeError
*
* @method fail
* @param {Function} callback The callback to call on reject
* @return {Promise} This promise
*/
Promise.prototype.fail = function (callback) {
var that = this;
if (typeof callback !== 'function') {
return this;
}
switch (this._state) {
case REJECTED:
setTimeout(function () {
try {
callback.apply(that, that._answers);
} catch (ignore) {} // errors will never be retrieved by global
});
break;
case UNRESOLVED:
this._onReject.push(callback);
break;
default:
break;
}
return this;
};
/**
* p.progress(callback): p
*
* Call the callback on notify.
*
* Promise.delay(100, 10).
* progress(function () { return null; }).
* progress(console.log); // does not show null
*
* @method progress
* @param {Function} callback The callback to call on notify
* @return {Promise} This promise
*/
Promise.prototype.progress = function (callback) {
if (typeof callback !== 'function') {
return this;
}
switch (this._state) {
case UNRESOLVED:
this._onProgress.push(callback);
break;
default:
break;
}
return this;
};
/**
* p.always(callback): p
*
* Call the callback on resolve or on reject.
*
* sayHello().
* done(iAnswer).
* fail(iHeardNothing).
* always(iKeepWalkingAnyway);
*
* @method always
* @param {Function} callback The callback to call on resolve or on reject
* @return {Promise} This promise
*/
Promise.prototype.always = function (callback) {
var that = this;
if (typeof callback !== 'function') {
return this;
}
switch (this._state) {
case RESOLVED:
case REJECTED:
setTimeout(function () {
try {
callback.apply(that, that._answers);
} catch (ignore) {} // errors will never be retrieved by global
});
break;
case UNRESOLVED:
that._onReject.push(callback);
that._onResolve.push(callback);
break;
default:
break;
}
return this;
};
exports.Promise = Promise;
/**
* Deferred(cancel)
*
* @class Deferred
* @constructor
*/
function Deferred(cancel) {
this.promise = new Promise(cancel);
}
/**
* resolve(*args): any
*
* Resolves the promise with the given arguments.
*
* @method resolve
* @param {Any} *[args] The arguments to give
*/
Deferred.prototype.resolve = function () {
return promiseResolve(this.promise, arguments);
};
/**
* reject(*args): any
*
* Rejects the promise with the given arguments.
*
* @method reject
* @param {Any} *[args] The arguments to give
*/
Deferred.prototype.reject = function () {
return promiseReject(this.promise, arguments);
};
/**
* notify(*args): any
*
* Notifies the promise with the given arguments.
*
* @method notify
* @param {Any} *[args] The arguments to give
*/
Deferred.prototype.notify = function () {
return promiseNotify(this.promise, arguments);
};
exports.Deferred = Deferred;
//////////////////////////////////////////////////////////////////////
// Inspired by Task.js
/**
* now(value): Promise
*
* Converts an ordinary value into a fulfilled promise.
*
* @param {Any} value The value to use
* @return {Promise} The resolved promise
*/
exports.now = function (value) {
var deferred = new Deferred();
deferred.resolve(value);
return deferred.promise;
};
/**
* join(*promises): Promise
*
* Produces a promise that is resolved when all the given promises are
* resolved. The resolved value is an array of each of the resolved values of
* the given promises.
*
* If any of the promises is rejected, the joined promise is rejected with the
* same error, and any remaining unfulfilled promises are cancelled.
*
* @param {Promise} *[promises] The promises to join
* @return {Promise} A new promise
*/
exports.join = function () {
var promises, results = [], i, count = 0, deferred;
promises = Array.prototype.slice.call(arguments);
function cancel() {
var j;
for (j = 0; j < promises.length; j += 1) {
promises[j].cancel();
}
}
deferred = new Deferred(cancel);
function succeed(j) {
return function (answer) {
results[j] = answer;
count += 1;
if (count !== promises.length) {
return;
}
deferred.resolve(results);
};
}
function failed(answer) {
cancel();
deferred.reject(answer);
}
function notify(j) {
return function (answer) {
deferred.notify({
"promise": this,
"index": j,
"answer": answer
});
};
}
for (i = 0; i < promises.length; i += 1) {
promises[i].then(succeed(i), failed, notify(i));
}
return deferred.promise;
};
/**
* choose(*promises): Promise
*
* Produces a promise that is fulfilled when any one of the given promises is
* fulfilled. As soon as one of the promises is fulfilled, whether by being
* resolved or rejected, all the other promises are cancelled.
*
* @param {Promise} *[promises] The promises to use
* @return {Promise} A new promise
*/
exports.choose = function () {
var promises, i, deferred;
promises = Array.prototype.slice.call(arguments);
function cancel() {
var j;
for (j = 0; j < promises.length; j += 1) {
promises[j].cancel();
}
}
deferred = new Deferred(cancel);
function succeed(answer) {
cancel();
deferred.resolve(answer);
}
function failed(answer) {
cancel();
deferred.reject(answer);
}
function notify(j) {
return function (answer) {
deferred.notify({
"promise": this,
"index": j,
"answer": answer
});
};
}
for (i = 0; i < promises.length; i += 1) {
promises[i].then(succeed, failed, notify(i));
}
return deferred.promise;
};
/**
* sleep(delay[, every]): Promise
*
* Resolve the promise after `timeout` milliseconds and notfies us every
* `every` milliseconds.
*
* Deferred.delay(50, 10).then(console.log, console.error, console.log);
* // // shows
* // 10 // from progress
* // 20 // from progress
* // 30 // from progress
* // 40 // from progress
* // 50 // from progress
* // 50 // from success
*
* @param {Number} delay In milliseconds
* @param {Number} [every] In milliseconds
* @return {Promise} A new promise
*/
exports.sleep = function (delay, every) {
var deferred, timeout, interval, now = 0;
function cancel() {
clearTimeout(timeout);
clearInterval(interval);
}
deferred = new Deferred(cancel);
if (typeof every === 'number' && isFinite(every)) {
interval = setInterval(function () {
now += every;
deferred.notify(now);
}, every);
}
timeout = setTimeout(function () {
clearInterval(interval);
deferred.notify(delay);
deferred.resolve(delay);
}, delay);
return deferred.promise;
};
////////////////////////////////////////////////////////////
// http://wiki.commonjs.org/wiki/Promises/B
// when(value, callback, errback_opt)
/**
* when(item, [onSuccess], [onError], [onProgress]): Promise
*
* Return an item as first parameter of the promise answer. If item is of
* type Promise, the method will just return the promise. If item is of type
* Deferred, the method will return the deferred promise.
*
* Deferred.when('a').then(console.log); // shows 'a'
*
* @param {Any} item The item to use
* @param {Function} [onSuccess] The callback called on success
* @param {Function} [onError] the callback called on error
* @param {Function} [onProgress] the callback called on progress
* @return {Promise} The promise
*/
exports.when = function (item, onSuccess, onError, onProgress) {
if (typeof item === 'object' && item !== null) {
if (typeof item.promise === 'object' && item.promise !== null &&
typeof item.promise.then === 'function') {
// item seams to be a Deferred
return item.promise.then(onSuccess, onError, onProgress);
}
if (typeof item.then === 'function') {
// item seams to be a Promise
return item.then(onSuccess, onError, onProgress);
}
}
// item is just a value, convert into fulfilled promise
var deferred = new Deferred(), promise;
if (typeof onSuccess === 'function') {
promise = deferred.promise.then(onSuccess);
} else {
promise = deferred.promise;
}
deferred.resolve(item);
return promise;
};
////////////////////////////////////////////////////////////
// http://wiki.commonjs.org/wiki/Promises/B
// get(object, name)
/**
* get(dict, property): Promise
*
* Return the dict property as first parameter of the promise answer.
*
* Deferred.get({'a': 'b'}, 'a').then(console.log); // shows 'b'
*
* @param {Object} dict The object to use
* @param {String} property The object property name
* @return {Promise} The promise
*/
exports.get = function (dict, property) {
var p = new Deferred();
try {
p.resolve(dict[property]);
} catch (e) {
p.reject(e);
}
return p;
};
////////////////////////////////////////////////////////////
// http://wiki.commonjs.org/wiki/Promises/B
// put(object, name, value)
/**
* put(dict, property, value): Promise
*
* Set and return the dict property as first parameter of the promise answer.
*
* Deferred.put({'a': 'b'}, 'a', 'c').then(console.log); // shows 'c'
*
* @param {Object} dict The object to use
* @param {String} property The object property name
* @param {Any} value The value
* @return {Promise} The promise
*/
exports.put = function (dict, property, value) {
var p = new Deferred();
try {
dict[property] = value;
p.resolve(dict[property]);
} catch (e) {
p.reject(e);
}
return p;
};
}));
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment