/** * @ignore * implement Promise specification by KISSY * @author yiminghe@gmail.com */ (function (S, undefined) { var PROMISE_VALUE = '__promise_value', processImmediate = S.setImmediate, logger = S.getLogger('s/promise'), PROMISE_PROGRESS_LISTENERS = '__promise_progress_listeners', PROMISE_PENDINGS = '__promise_pendings'; function logError(str) { if (typeof console !== 'undefined' && console.error) { console.error(str); } } /* two effects: 1. call fulfilled with immediate value 2. push fulfilled in right promise */ function promiseWhen(promise, fulfilled, rejected) { // simply call rejected if (promise instanceof Reject) { // if there is a rejected , should always has! see when() processImmediate(function () { rejected.call(promise, promise[PROMISE_VALUE]); }); } else { var v = promise[PROMISE_VALUE], pendings = promise[PROMISE_PENDINGS]; // unresolved // pushed to pending list if (pendings) { pendings.push([fulfilled, rejected]); } // rejected or nested promise else if (isPromise(v)) { promiseWhen(v, fulfilled, rejected); } else { // fulfilled value // normal value represents ok // need return user's return value // if return promise then forward if (fulfilled) { processImmediate(function () { fulfilled.call(promise, v); }); } } } } /** * @class KISSY.Defer * Defer constructor For KISSY, implement Promise specification. */ function Defer(promise) { var self = this; if (!(self instanceof Defer)) { return new Defer(promise); } // http://en.wikipedia.org/wiki/Object-capability_model // principal of least authority /** * defer object's promise * @type {KISSY.Promise} */ self.promise = promise || new Promise(); self.promise.defer = self; } Defer.prototype = { constructor: Defer, /** * fulfill defer object's promise * note: can only be called once * @param value defer object's value * @return {KISSY.Promise} defer object's promise */ resolve: function (value) { var promise = this.promise, pendings; if (!(pendings = promise[PROMISE_PENDINGS])) { return null; } // set current promise 's resolved value // maybe a promise or instant value promise[PROMISE_VALUE] = value; pendings = [].concat(pendings); promise[PROMISE_PENDINGS] = undefined; promise[PROMISE_PROGRESS_LISTENERS] = undefined; S.each(pendings, function (p) { promiseWhen(promise, p[0], p[1]); }); return value; }, /** * reject defer object's promise * @param reason * @return {KISSY.Promise} defer object's promise */ reject: function (reason) { return this.resolve(new Reject(reason)); }, /** * notify promise 's progress listeners * @param message */ notify: function (message) { S.each(this.promise[PROMISE_PROGRESS_LISTENERS], function (listener) { processImmediate(function () { listener(message); }); }); } }; function isPromise(obj) { return obj && obj instanceof Promise; } /** * @class KISSY.Promise * Promise constructor. * This class should not be instantiated manually. * Instances will be created and returned as needed by {@link KISSY.Defer#promise} * @param [v] promise 's resolved value */ function Promise(v) { var self = this; // maybe internal value is also a promise self[PROMISE_VALUE] = v; if (v === undefined) { // unresolved self[PROMISE_PENDINGS] = []; self[PROMISE_PROGRESS_LISTENERS] = []; } } Promise.prototype = { constructor: Promise, /** * register callbacks when this promise object is resolved * @param {Function} fulfilled called when resolved successfully,pass a resolved value to this function and * return a value (could be promise object) for the new promise 's resolved value. * @param {Function} [rejected] called when error occurs,pass error reason to this function and * return a new reason for the new promise 's error reason * @param {Function} [progressListener] progress listener * @return {KISSY.Promise} a new promise object */ then: function (fulfilled, rejected, progressListener) { if (progressListener) { this.progress(progressListener); } return when(this, fulfilled, rejected); }, /** * call progress listener when defer.notify is called * @param {Function} [progressListener] progress listener */ progress: function (progressListener) { if (this[PROMISE_PROGRESS_LISTENERS]) { this[PROMISE_PROGRESS_LISTENERS].push(progressListener); } return this; }, /** * call rejected callback when this promise object is rejected * @param {Function} rejected called with rejected reason * @return {KISSY.Promise} a new promise object */ fail: function (rejected) { return when(this, 0, rejected); }, /** * call callback when this promise object is rejected or resolved * @param {Function} callback the second parameter is * true when resolved and false when rejected * @@return {KISSY.Promise} a new promise object */ fin: function (callback) { return when(this, function (value) { return callback(value, true); }, function (reason) { return callback(reason, false); }); }, /** * register callbacks when this promise object is resolved, * and throw error at next event loop if promise * (current instance if no fulfilled and rejected parameter or * new instance caused by call this.then(fulfilled, rejected)) * fails. * @param {Function} [fulfilled] called when resolved successfully,pass a resolved value to this function and * return a value (could be promise object) for the new promise 's resolved value. * @param {Function} [rejected] called when error occurs,pass error reason to this function and * return a new reason for the new promise 's error reason */ done: function (fulfilled, rejected) { var self = this, onUnhandledError = function (e) { setTimeout(function () { throw e; }, 0); }, promiseToHandle = fulfilled || rejected ? self.then(fulfilled, rejected) : self; promiseToHandle.fail(onUnhandledError); }, /** * whether the given object is a resolved promise * if it is resolved with another promise, * then that promise needs to be resolved as well. * @member KISSY.Promise */ isResolved: function () { return isResolved(this); }, /** * whether the given object is a rejected promise */ isRejected: function () { return isRejected(this); } }; /** * Reject promise * @param {String|KISSY.Promise.Reject} reason reject reason * @class KISSY.Promise.Reject * @extend KISSY.Promise * @private */ function Reject(reason) { if (reason instanceof Reject) { return reason; } var self = this; Promise.apply(self, arguments); if (self[PROMISE_VALUE] instanceof Promise) { logger.error('assert.not(this.__promise_value instanceof promise) in Reject constructor'); } return self; } S.extend(Reject, Promise); // wrap for promiseWhen function when(value, fulfilled, rejected) { var defer = new Defer(), done = 0; // wrap user's callback to catch exception function _fulfilled(value) { try { return fulfilled ? fulfilled.call(this, value) : // propagate value; } catch (e) { // can not use logger.error // must expose to user // print stack info for firefox/chrome logError(e.stack || e); return new Reject(e); } } function _rejected(reason) { try { return rejected ? // error recovery rejected.call(this, reason) : // propagate new Reject(reason); } catch (e) { // print stack info for firefox/chrome logError(e.stack || e); return new Reject(e); } } function finalFulfill(value) { if (done) { logger.error('already done at fulfilled'); return; } if (value instanceof Promise) { logger.error('assert.not(value instanceof Promise) in when'); return; } done = 1; defer.resolve(_fulfilled.call(this, value)); } if (value instanceof Promise) { promiseWhen(value, finalFulfill, function (reason) { if (done) { logger.error('already done at rejected'); return; } done = 1; // _reject may return non-Reject object for error recovery defer.resolve(_rejected.call(this, reason)); }); } else { finalFulfill(value); } // chained and leveled // wait for value's resolve return defer.promise; } function isResolved(obj) { // exclude Reject at first return !isRejected(obj) && isPromise(obj) && // self is resolved (obj[PROMISE_PENDINGS] === undefined) && // value is a resolved promise or value is immediate value ( // immediate value !isPromise(obj[PROMISE_VALUE]) || // resolved with a resolved promise !!! :) // Reject.__promise_value is string isResolved(obj[PROMISE_VALUE]) ); } function isRejected(obj) { return isPromise(obj) && (obj[PROMISE_PENDINGS] === undefined) && (obj[PROMISE_VALUE] instanceof Reject); } KISSY.Defer = Defer; KISSY.Promise = Promise; Promise.Defer = Defer; S.mix(Promise, { /** * register callbacks when obj as a promise is resolved * or call fulfilled callback directly when obj is not a promise object * @param {KISSY.Promise|*} obj a promise object or value of any type * @param {Function} fulfilled called when obj resolved successfully,pass a resolved value to this function and * return a value (could be promise object) for the new promise 's resolved value. * @param {Function} [rejected] called when error occurs in obj,pass error reason to this function and * return a new reason for the new promise 's error reason * @return {KISSY.Promise} a new promise object * * for example: * @example * function check(p) { * S.Promise.when(p, function(v){ * alert(v === 1); * }); * } * * var defer = S.Defer(); * defer.resolve(1); * * check(1); // => alert(true) * * check(defer.promise); //=> alert(true); * * @static * @method * @member KISSY.Promise */ when: when, /** * whether the given object is a promise * @method * @static * @param obj the tested object * @return {Boolean} * @member KISSY.Promise */ isPromise: isPromise, /** * whether the given object is a resolved promise * @method * @static * @param obj the tested object * @return {Boolean} * @member KISSY.Promise */ isResolved: isResolved, /** * whether the given object is a rejected promise * @method * @static * @param obj the tested object * @return {Boolean} * @member KISSY.Promise */ isRejected: isRejected, /** * return a new promise * which is resolved when all promises is resolved * and rejected when any one of promises is rejected * @param {KISSY.Promise[]} promises list of promises * @static * @return {KISSY.Promise} * @member KISSY.Promise */ all: function (promises) { var count = promises.length; if (!count) { return null; } var defer = Defer(); for (var i = 0; i < promises.length; i++) { (function (promise, i) { when(promise, function (value) { promises[i] = value; if (--count === 0) { // if all is resolved // then resolve final returned promise with all value defer.resolve(promises); } }, function (r) { // if any one is rejected // then reject final return promise with first reason defer.reject(r); }); })(promises[i], i); } return defer.promise; }, /** * provide es6 generator * @param generatorFunc es6 generator function which has yielded promise */ async: function (generatorFunc) { return function () { var generator = generatorFunc.apply(this, arguments); function doAction(action, arg) { var result; // in case error on first try { result = generator[action](arg); } catch (e) { return new Reject(e); } if (result.done) { return result.value; } return when(result.value, next, throwEx); } function next(v) { return doAction('next', v); } function throwEx(e) { return doAction('throw', e); } return next(); }; } }); })(KISSY); /* refer: - http://wiki.commonjs.org/wiki/Promises - http://en.wikipedia.org/wiki/Futures_and_promises#Read-only_views - http://en.wikipedia.org/wiki/Object-capability_model - https://github.com/kriskowal/q - http://www.sitepen.com/blog/2010/05/03/robust-promises-with-dojo-deferred-1-5/ - http://dojotoolkit.org/documentation/tutorials/1.6/deferreds/ */