diff --git a/src/promy/promy.js b/src/promy/promy.js new file mode 100644 index 0000000000000000000000000000000000000000..8e234135f8debf91930fe5c18f595cc7fc6e0a12 --- /dev/null +++ b/src/promy/promy.js @@ -0,0 +1,835 @@ +/* + * 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; + }; + +}));