diff --git a/Gruntfile.js b/Gruntfile.js index fb934d0b7850..666377e37f9b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -216,7 +216,11 @@ module.exports = function(grunt) { }, "promises-aplus-adapter": { dest:'tmp/promises-aplus-adapter++.js', - src:['src/ng/q.js','lib/promises-aplus/promises-aplus-test-adapter.js'] + src: [ + 'lib/promises-aplus/promises-aplus-test-adapter-prefix.js', + 'src/ng/q.js', + 'lib/promises-aplus/promises-aplus-test-adapter.js' + ] } }, diff --git a/lib/promises-aplus/promises-aplus-test-adapter-prefix.js b/lib/promises-aplus/promises-aplus-test-adapter-prefix.js new file mode 100644 index 000000000000..0c16d80f5a17 --- /dev/null +++ b/lib/promises-aplus/promises-aplus-test-adapter-prefix.js @@ -0,0 +1,35 @@ +"use strict"; +var INVISIBLE = 1; +var CONFIGURABLE = 2; +var WRITABLE = 4; + +var defineProperty = function(target, propertyName, flags, value) { + if (typeof target === 'object' || typeof target === 'function') { + var desc = { + /*jshint bitwise: false */ + enumerable: !(flags & INVISIBLE), + configurable: !!(flags & CONFIGURABLE), + writable: !!(flags & WRITABLE) + }; + if (arguments.length > 3) { + desc.value = value; + } + Object.defineProperty(target, propertyName, desc); + } +}; + +var defineProperties = function(target, flags, properties) { + for (var key in properties) { + if (properties.hasOwnProperty(key)) { + defineProperty(target, key, flags, properties[key]); + } + } +}; + +var createObject = Object.create; + +function bind1(self, fn) { + return function(value) { + return fn.call(self, value); + }; +} diff --git a/src/.jshintrc b/src/.jshintrc index 2e2b32250df6..c9a6f18de610 100644 --- a/src/.jshintrc +++ b/src/.jshintrc @@ -68,6 +68,7 @@ "concat": false, "sliceArgs": false, "bind": false, + "bind1": false, "toJsonReplacer": false, "toJson": false, "fromJson": false, @@ -87,6 +88,12 @@ "getter": false, "getBlockElements": false, "VALIDITY_STATE_PROPERTY": false, + "INVISIBLE": true, + "CONFIGURABLE": true, + "WRITABLE": true, + "defineProperty": false, + "defineProperties": false, + "createObject": false, /* filters.js */ "getFirstThursdayOfYear": false, diff --git a/src/Angular.js b/src/Angular.js index 1caab1545dc0..f228300883ac 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -64,6 +64,7 @@ concat: true, sliceArgs: true, bind: true, + bind1: true, toJsonReplacer: true, toJson: true, fromJson: true, @@ -998,6 +999,18 @@ function bind(self, fn) { } +function bind1(self, fn) { + // For performance reasons, no sanity-checks are performed here. This method is not publicly + // exposed, and so failures here should cause test failures in core if issues are present. + // + // Due to not currying arguments, and not testing for error conditions, this should perform + // somewhat better than angular.bind() for the cases where it is needed. + return function(value) { + return fn.call(self, value); + }; +} + + function toJsonReplacer(key, value) { var val = value; @@ -1572,3 +1585,39 @@ function getBlockElements(nodes) { return jqLite(elements); } + +var INVISIBLE = 1; +var CONFIGURABLE = 2; +var WRITABLE = 4; + +function defineProperty(target, propertyName, flags, value) { + if (isObject(target) || isFunction(target)) { + if (Object.defineProperty) { + var desc = { + /*jshint bitwise: false */ + enumerable: !(flags & INVISIBLE), + configurable: !!(flags & CONFIGURABLE), + writable: !!(flags & WRITABLE) + }; + if (arguments.length > 3) { + desc.value = value; + } + Object.defineProperty(target, propertyName, desc); + } else { + target[propertyName] = value; + } + } +} + +function defineProperties(target, flags, propertyNameAndValues) { + forEach(propertyNameAndValues, function(value, propertyName) { + defineProperty(target, propertyName, flags, value); + }); +} + +function createObject(prototype) { + if (Object.create) return Object.create(prototype); + function C() {} + C.prototype = prototype; + return new C(); +} diff --git a/src/ng/http.js b/src/ng/http.js index 219954930e4e..ab155ba465c3 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -960,7 +960,7 @@ function $HttpProvider() { // normalize internal statuses to 0 status = Math.max(status, 0); - (isSuccess(status) ? deferred.resolve : deferred.reject)({ + deferred[(isSuccess(status) ? 'resolve' : 'reject')]({ data: response, status: status, headers: headersGetter(headers), diff --git a/src/ng/q.js b/src/ng/q.js index e571636f4d64..b0cdf8d905fa 100644 --- a/src/ng/q.js +++ b/src/ng/q.js @@ -1,4 +1,906 @@ 'use strict'; +/*jshint newcap: false */ +/* Layout for Promise$$bitField: + * QQWF NCTR BPHS UDLL LLLL LLLL LLLL LLLL + * + * Q = isGcQueued (unused) + * W = isFollowing (The promise that is being followed is not stored explicitly) + * F = isFulfilled + * N = isRejected + * C = isCancellable (unused) + * T = isFinal + * B = isBound (unused) + * P = isProxied (Optimization when .then listeners on a promise are just respective fate sealers + * on some other promise) + * H = isRejectionUnhandled + * S = isCarryingStackTrace + * U = isUnhandledRejectionNotified + * D = isDisposable + * R = [Reserved] + * L = Length (18 bit unsigned) + */ + +/*jshint bitwise: false */ +var NO_STATE = 0x00000000|0; +var IS_GC_QUEUED = 0xC0000000|0; +var IS_FOLLOWING = 0x20000000|0; +var IS_FULFILLED = 0x10000000|0; +var IS_REJECTED = 0x08000000|0; +// IS_CANCELLABLE = 0x04000000|0; Cancelling not supported +var IS_FINAL = 0x02000000|0; +// RESERVED BIT 0x01000000|0 +var IS_BOUND = 0x00800000|0; +var IS_PROXIED = 0x00400000|0; +var IS_REJECTION_UNHANDLED = 0x00200000|0; +var IS_CARRYING_STACK_TRACE = 0x00100000|0; +var IS_UNHANDLED_REJECTION_NOTIFIED = 0x00080000|0; +var IS_DISPOSABLE = 0x00040000|0; +var LENGTH_MASK = 0x0003FFFF|0; +var IS_REJECTED_OR_FULFILLED = IS_REJECTED | IS_FULFILLED; +var IS_FOLLOWING_OR_REJECTED_OR_FULFILLED = IS_REJECTED_OR_FULFILLED | IS_FOLLOWING; +var MAX_LENGTH = LENGTH_MASK; + +var CALLBACK_FULFILL_OFFSET = 0; +var CALLBACK_REJECT_OFFSET = 1; +var CALLBACK_PROGRESS_OFFSET = 2; +var CALLBACK_PROMISE_OFFSET = 3; +var CALLBACK_RECEIVER_OFFSET = 4; +var CALLBACK_SIZE = 5; + +var unhandledRejectionHandled; + +function internalPromiseResolver() {} + +function thrower(e) { + throw e; +} + +function isError(obj) { + return obj instanceof Error; +} + +var $Deferred = function Deferred(Q) { + this.promise = new Q(internalPromiseResolver); + + // Hacks because $timeout depends on unbound execution of functions + this.resolve = bind1(this, this.resolve); + this.reject = bind1(this, this.reject); + this.notify = bind1(this, this.notify); +}; + +defineProperties($Deferred.prototype, WRITABLE, { + resolve: function Deferred$resolve(value) { + var promise = this.promise; + if (promise.isResolved() || promise.$$tryFollow(value)) { + return; + } + + promise.$$invoke(promise.$$fulfill, promise, value); + }, + + + reject: function Deferred$reject(reason) { + var promise = this.promise; + if (promise.isResolved()) return; + var trace = isError(reason) ? reason : new Error(reason + ''); + promise.$$attachExtraTrace(trace); + promise.$$invoke(promise.$$reject, promise, reason); + }, + + + rejectGently: function Deferred$rejectGently(reason) { + var promise = this.promise; + if (promise.isResolved()) return; + var trace = isError(reason) ? reason : new Error(reason + ''); + promise.$$attachExtraTrace(trace); + promise.$$invoke(promise.$$rejectGently, promise, reason); + }, + + + notify: function Deferred$notify(value) { + this.promise.$$progress(value); + }, + + + isResolved: function Deferred$isResolved() { + return this.promise.isResolved(); + }, + + + isFulfilled: function Deferred$isFulfilled() { + return this.promise.isFulfilled(); + }, + + + isRejected: function Deferred$isRejected() { + return this.promise.isRejected(); + }, + + + isPending: function Deferred$isPending() { + return this.promise.isPending(); + } +}); + + +function $Q(resolver, nextTick) { + if (!isFunction(resolver)) { + // todo(@caitp): minErr this. + throw new TypeError('Cannot instantion $Q Promise: `resolver` must be a function.'); + } + + // Private properties + this.$$bitField = NO_STATE; + + // store nextTick in the prototype, so that types which use $rootScope.$evalAsync and types + // which use $browser.defer() are both instances of the same Promise type. + this.$$nextTick = nextTick; + + // From Bluebird: Typical promise has exactly one parallel handler, + // store the first ones directly on the Promise. + this.$$fulfillmentHandler0 = + this.$$rejectionHandler0 = + + this.$$promise0 = + this.$$receiver0 = + this.$$settledValue = void 0; + + if (resolver !== internalPromiseResolver) this.$$resolveFromResolver(resolver); +} + + +function Promise$$cast(obj, originalPromise, Q) { + if (obj && typeof obj === 'object') { + if (obj instanceof Q) { + return obj; + } else { + var then; + try { + then = obj.then; + } catch (e) { + if (originalPromise !== void 0 && isError(e)) { + originalPromise.$$attachExtraTrace(e); + } + return Q.reject(e); + } + if (typeof then === 'function') { + return Promise$$doThenable(obj, then, originalPromise, Q); + } + } + } + return obj; +} + + +function Promise$$castToPromise(obj, originalPromise, Q) { + obj = Promise$$cast(obj, originalPromise, Q); + if (!(obj instanceof Q)) { + return { + then: function Promise$$castToPromiseThen(callback) { + return Q.resolved(callback(obj)); + } + }; + } + return obj; +} + + +function Promise$$doThenable(x, then, originalPromise, Q) { + var resolver = Q.defer(); + var called = false; + try { + then.call(x, Promise$_resolveFromThenable, Promise$_rejectFromThenable, + Promise$_progressFromThenable); + } catch (e) { + if (!called) { + called = true; + var trace = isError(e) ? e : new Error(e + ''); + if (originalPromise !== void 0) { + originalPromise.$$attachExtraTrace(trace); + } + resolver.promise.$$reject(e, trace); + } + } + return resolver.promise; + + function Promise$_resolveFromThenable(y) { + if (called) return; + called = true; + + if (x === y) { + var e = new Error('self-resolution error'); + if (originalPromise !== void 0) { + originalPromise.$$attachExtraTrace(e); + } + resolver.promise.$$reject(e, void 0); + } + resolver.resolve(y); + } + + function Promise$_rejectFromThenable(r) { + if (called) return; + called = true; + + var trace = isError(r) ? r : new Error(r + ''); + if (originalPromise !== void 0) { + originalPromise.$$attachExtraTrace(r); + } + resolver.promise.$$reject(r, trace); + } + + function Promise$_progressFromThenable(v) { + if (called) return; + var promise = resolver.promise; + if (isFunction(promise.$$progress)) { + promise.$$progress(v); + } + } +} + + +function Promise$makePromise(value, resolved, Q) { + var result = new Q(internalPromiseResolver); + if (resolved) { + result.$$fulfillUnchecked(value); + } else { + result.$$rejectUnchecked(value); + } + return result; +} + + +function Promise$handleFinalCallback(callback, value, isResolved, Q) { + var callbackOutput = null; + try { + callbackOutput = (callback || noop)(); + } catch (e) { + return Promise$makePromise(e, false, Q); + } + if (callbackOutput && isFunction(callbackOutput.then)) { + return callbackOutput.then(function() { + return Promise$makePromise(value, isResolved, Q); + }, function(error) { + return Promise$makePromise(error, false, Q); + }); + } else { + return Promise$makePromise(value, isResolved, Q); + } +} + + +defineProperties($Q.prototype, INVISIBLE|WRITABLE, { + then: function Promise$then(didFulfill, didReject, didProgress) { + var Q = this.constructor; + var ret = new Q(internalPromiseResolver); + var callbackIndex = this.$$addCallbacks(didFulfill, didReject, didProgress, ret, void 0); + if (this.isResolved()) { + this.$$invoke(this.$$queueSettleAt, this, callbackIndex); + } + + return ret; + }, + + + catch: function Promise$catch(handler) { + var Q = this.constructor; + var ret = new Q(internalPromiseResolver); + var callbackIndex = this.$$addCallbacks(null, handler, null, ret, void 0); + if (this.isResolved()) { + this.$$invoke(this.$$queueSettleAt, this, callbackIndex); + } + return ret; + }, + + + 'finally': function Promise$finally(handler) { + var Q = this.constructor; + + return this.then(function(value) { + return Promise$handleFinalCallback(handler, value, true, Q); + }, function(error) { + return Promise$handleFinalCallback(handler, error, false, Q); + }); + }, + + + isResolved: function() { + /*jshint bitwise: false */ + return (this.$$bitField & IS_REJECTED_OR_FULFILLED) > 0; + }, + + + isFulfilled: function() { + /*jshint bitwise: false */ + return (this.$$bitField & IS_FULFILLED) > 0; + }, + + + isRejected: function() { + /*jshint bitwise: false */ + return (this.$$bitField & IS_REJECTED) > 0; + }, + + + isPending: function() { + /*jshint bitwise: false */ + return (this.$$bitField & IS_REJECTED_OR_FULFILLED) === 0; + }, + + + toString: function Promise$$toString() { + return '[object Promise]'; + }, + + + $$addCallbacks: function(fulfill, reject, progress, promise, receiver) { + var index = this.$$length(); + + if (index >= MAX_LENGTH - CALLBACK_SIZE) { + index = 0; + this.$$setLength(0); + } + + if (index === 0) { + this.$$promise0 = promise; + if (receiver !== void 0) { + this.$$receiver0 = receiver; + } + if (isFunction(fulfill) && !this.$$isCarryingStackTrace()) { + this.$$fulfillmentHandler0 = fulfill; + } + if (isFunction(reject)) { + this.$$rejectionHandler0 = reject; + } + if (isFunction(progress)) { + this.$$progressHandler0 = progress; + } + } else { + var base = (index << 2) + index - CALLBACK_SIZE; + this[base + CALLBACK_PROMISE_OFFSET] = promise; + this[base + CALLBACK_RECEIVER_OFFSET] = receiver; + this[base + CALLBACK_FULFILL_OFFSET] = isFunction(fulfill) ? fulfill : void 0; + this[base + CALLBACK_REJECT_OFFSET] = isFunction(reject) ? reject : void 0; + this[base + CALLBACK_PROGRESS_OFFSET] = isFunction(progress) ? progress : void 0; + } + + this.$$setLength(index + 1); + return index; + }, + + + $$fulfill: function Promise$$fulfill(value) { + if (this.$$isFollowingOrFulfilledOrRejected()) return; + this.$$fulfillUnchecked(value); + }, + + + $$fulfillUnchecked: function Promise$$fulfillUnchecked(value) { + if (!this.isPending()) return; + if (value === this) { + var err = new Error('Self-resolution forbidden'); + this.$$attachExtraTrace(err); + return this.$$rejectUnchecked(err); + } + + this.$$setFulfilled(); + this.$$settledValue = value; + + var len = this.$$length(); + if (len > 0) { + this.$$invoke(this.$$settlePromises, this, len); + } + }, + + + $$setTrace: function(trace) { + // TODO: improve error logging + }, + + + $$attachExtraTrace: function Promise$$attachExtraTrace(trace) { + // TODO: improve error logging + }, + + + $$isCarryingStackTrace: function Promise$$isCarryingStackTrace() { + /*jshint bitwise: false */ + return (this.$$bitField & IS_CARRYING_STACK_TRACE) > 0; + }, + + + $$getCarriedStackTrace: function Promise$$getCarriedStackTrace() { + return this.$$isCarryingStackTrace() ? + this.$$fulfillmentHandler0 : + void 0; + }, + + + $$setCarriedStackTrace: function Promise$$setCarriedStackTrace(capturedTrace) { + // ASSERT(this.isRejected()) + /*jshint bitwise: false */ + this.$$bitField = (this.$$bitField | IS_CARRYING_STACK_TRACE); + this.$$fulfillmentHandler0 = capturedTrace; + }, + + + $$unsetCarriedStackTrace: function Promise$$unsetCarriedStackTrace() { + /*jshint bitwise: false */ + this.$$bitField = (this.$$bitField & (~IS_CARRYING_STACK_TRACE)); + this.$$fulfillmentHandler0 = void 0; + }, + + + $$proxyPromise: function Promise$$proxyPromise(promise, slot) { + if (arguments.length === 1) slot = -1; + promise.$$setProxied(); + this.$$setProxyHandlers(promise, slot); + }, + + + $$follow: function Promise$$follow(promise) { + this.$$setFollowing(); + + if (promise.isPending()) { + promise.$$proxyPromise(this); + } else if (promise.isFulfilled()) { + this.$$fulfillUnchecked(promise.$$settledValue); + } else { + this.$$rejectUnchecked(promise.$$settledValue, promise.$$getCarriedStackTrace()); + } + + if (promise.$$isRejectionUnhandled()) promise.$$unsetRejectionIsUnhandled(); + }, + + + $$followResolve: function Promise$$followResolve(promise) { + this.$$setFollowing(); + + if (promise.isPending()) { + promise.proxyPromise(this, 0); + } else { + this.$$fulfillUnchecked(promise.$$settledValue); + } + + if (promise.$$isRejectionUnhandled()) promise.$$unsetRejectionIsUnhandled(); + }, + + + $$isFollowingOrFulfilledOrRejected: function Promise$$isFollowingOrFulfilledOrRejected() { + /*jshint bitwise: false */ + return (this.$$bitField & IS_FOLLOWING_OR_REJECTED_OR_FULFILLED) > 0; + }, + + + $$isFollowing: function() { + /*jshint bitwise: false */ + return (this.$$bitField & IS_FOLLOWING) === IS_FOLLOWING; + }, + + + $$resolveFromResolver: function Promise$$resolveFromResolver(resolver) { + var promise = this; + this.$$setTrace(void 0); + + function Promise$$resolver(val) { + if (promise.$$tryFollow(val)) { + return; + } + promise.$$fulfill(val); + } + + function Promise$$rejecter(val) { + var trace = isError(val) ? val : new Error(val + ''); + promise.$$attachExtraTrace(trace); + promise.$$reject(val, trace === val ? void 0 : trace); + } + + try { + resolver.call(null, Promise$$resolver, Promise$$rejecter); + } catch (e) { + this.$$reject(e, isError(e) ? e : new Error(e + '')); + } + }, + + + $$progress: function Promise$$progress(progressValue) { + if (this.$$isFollowingOrFulfilledOrRejected() || !this.$$length()) return; + this.$$invoke(this.$$progressUnchecked, this, progressValue); + }, + + + $$progressUnchecked: function Promise$$progressUnchecked(progressValue) { + if (!this.isPending()) return; + var len = this.$$length(); + var progress = this.$$progress; + for (var i=0; i 0) { + this.$$invoke(this.$$rejectPromises, this, void 0); + } else if (!gentle) { + this.$$ensurePossibleRejectionHandled(); + } + }, + + + $$rejectPromises: function Promise$$rejectPromise() { + this.$$settlePromises(); + this.$$unsetCarriedStackTrace(); + }, + + + $$settlePromises: function Promise$$settlePromises() { + var len = this.$$length(); + for (var i=0; i= 256) { + // this.$$queueGC(); + // } + }, + + + $$isProxied: function Promise$$isProxied() { + /*jshint bitwise: false */ + return (this.$$bitField & IS_PROXIED) === IS_PROXIED; + }, + + + $$length: function Promise$$length() { + /*jshint bitwise: false */ + return this.$$bitField & LENGTH_MASK; + }, + + + $$setLength: function Promise$$setLength(length) { + /*jshint bitwise: false */ + this.$$bitField = ((this.$$bitField & (~LENGTH_MASK)) | (length & LENGTH_MASK)); + }, + + + $$setRejected: function Promise$$setRejected() { + /*jshint bitwise: false */ + this.$$bitField = (this.$$bitField | IS_REJECTED); + }, + + + $$setFulfilled: function Promise$$setFulfilled() { + /*jshint bitwise: false */ + this.$$bitField = (this.$$bitField | IS_FULFILLED); + }, + + + $$unsetFollowing: function Promise$$unsetFollowing() { + /*jshint bitwise: false */ + this.$$bitField = (this.$$bitField & (~IS_FOLLOWING)); + }, + + + $$setFollowing: function Promise$$setFollowing() { + /*jshint bitwise: false */ + this.$$bitField = (this.$$bitField | IS_FOLLOWING); + }, + + + $$setFinal: function Promise$$setFinal() { + /*jshint bitwise: false */ + this.$$bitField = (this.$$bitField | IS_FINAL); + }, + + + $$isFinal: function Promise$$isFinal() { + /*jshint bitwise: false */ + return (this.$$bitField & IS_FINAL) > 0; + }, + + + $$setRejectionIsUnhandled: function Promise$$setRejectionIsUnhandled() { + /*jshint bitwise: false */ + this.$$bitField = (this.$$bitField | IS_REJECTION_UNHANDLED); + }, + + + $$unsetRejectionIsUnhandled: function Promise$$unsetRejectionIsUnhandled() { + /*jshint bitwise: false */ + this.$$bitField = (this.$$bitField & ~IS_REJECTION_UNHANDLED); + }, + + + $$isRejectionUnhandled: function Promise$$isRejectionUnhandled() { + /*jshint bitwise: false */ + return (this.$$bitField & IS_REJECTION_UNHANDLED) > 0; + }, + + + $$setProxyHandlers: function Promise$$setProxyHandlers(receiver, promiseSlotValue) { + var index = this.$$length(); + + if (index >= MAX_LENGTH - CALLBACK_SIZE) { + index = 0; + this.$$setLength(0); + } + if (index === 0) { + this.$$promise0 = promiseSlotValue; + this.$$receiver0 = receiver; + } else { + var base = (index << 2) + index - CALLBACK_SIZE; + this[base + CALLBACK_PROMISE_OFFSET] = promiseSlotValue; + this[base + CALLBACK_RECEIVER_OFFSET] = receiver; + this[base + CALLBACK_FULFILL_OFFSET] = + this[base + CALLBACK_REJECT_OFFSET] = + this[base + CALLBACK_PROGRESS_OFFSET] = void 0; + } + this.$$setLength(index + 1); + }, + + + $$setProxied: function Promise$$setProxied() { + this.$$bitField = (this.$$bitField | IS_PROXIED); + }, + + + $$unsetProxied: function Promise$$unsetProxied() { + this.$$bitField = (this.$$bitField & (~IS_PROXIED)); + }, + + + $$tryFollow: function Promise$$tryFollow(value) { + if (this.$$isFollowingOrFulfilledOrRejected() || value === this) { + return false; + } + + var maybePromise = Promise$$cast(value, void 0, this.constructor); + if (!(maybePromise instanceof $Q)) { + return false; + } + + this.$$follow(maybePromise); + return true; + }, + + + $$invoke: function Promise$$invokeAsync(method, receiver, arg0) { + this.$$nextTick(function() { + method.call(receiver, arg0); + }); + }, + + + $$queueSettleAt: function Promise$$queueSettleAt(index) { + if (this.$$isRejectionUnhandled()) this.$$unsetRejectionIsUnhandled(); + this.$$invoke(this.$$settlePromiseAt, this, index); + }, + + + $$possiblyUnhandledRejection: function() {}, + + // TODO(@caitp): This is just a proxy for $exceptionHandler, but this should all be handled + // in $$possiblyUnhandledRejection instead... + $$exceptionHandler: function(e) {}, + + $$ensurePossibleRejectionHandled: function Promise$$ensurePossibleRejectionHandled() { + this.$$setRejectionIsUnhandled(); + // TODO(@caitp): improve error logging in $q + // if (this.$$possiblyUnhandledRejection !== void 0) { + // this.$$invoke(this.$$notifyUnhandledRejection, this, void 0); + // } + }, + + + $$notifyUnhandledRejectionIsHandled: function Promise$$notifyUnhandledRejectionIsHandled() { + // TODO(@caitp): improve error logging in $q + // if (isFunction(this.$$unhandledRejectionHandled)) { + // this.$$invoke(this.$$unhandledRejectionHandled, this, void 0); + // } + }, + + + $$notifyUnhandledRejection: function Promise$$notifyUnhandledRejection() { + // TODO(@caitp): improve error logging in $q + // if (this.$$isRejectionUnhandled()) { + // var reason = this.$$settledValue; + // var trace = this.$$getCarriedStackTrace(); + // + // this.$$setUnhandledRejectionIsNotified(); + // + // if (trace !== void 0) { + // this.$$unsetCarriedStackTrace(); + // reason = trace; + // } + // + // if (isFunction(this.$$possiblyUnhandledRejection)) { + // this.$$possiblyUnhandledRejection(reason, this); + // } + // } + }, + + + $$setUnhandledRejectionIsNotified: function Promise$$setUnhandledRejectionIsNotified() { + this.$$bitField = this.$$bitField | IS_UNHANDLED_REJECTION_NOTIFIED; + }, + + + $$unsetUnhandledRejectionIsNotified: function Promise$$unsetUnhandledRejectionIsNotified() { + this.$$bitField = this.$$bitField & (~IS_UNHANDLED_REJECTION_NOTIFIED); + }, + + + $$isUnhandledRejectionNotified: function Promise$$isUnhandledRejectionNotified() { + return (this.$$bitField & IS_UNHANDLED_REJECTION_NOTIFIED) > 0; + } +}); /** * @ngdoc service @@ -246,144 +1148,11 @@ function qFactory(nextTick, exceptionHandler) { * * @returns {Deferred} Returns a new instance of deferred. */ - var defer = function() { - var pending = [], - value, deferred; - - deferred = { - - resolve: function(val) { - if (pending) { - var callbacks = pending; - pending = undefined; - value = ref(val); - - if (callbacks.length) { - nextTick(function() { - var callback; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - callback = callbacks[i]; - value.then(callback[0], callback[1], callback[2]); - } - }); - } - } - }, - - - reject: function(reason) { - deferred.resolve(createInternalRejectedPromise(reason)); - }, - - - notify: function(progress) { - if (pending) { - var callbacks = pending; - - if (pending.length) { - nextTick(function() { - var callback; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - callback = callbacks[i]; - callback[2](progress); - } - }); - } - } - }, - - - promise: { - then: function(callback, errback, progressback) { - var result = defer(); - - var wrappedCallback = function(value) { - try { - result.resolve((isFunction(callback) ? callback : defaultCallback)(value)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }; - - var wrappedErrback = function(reason) { - try { - result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }; - - var wrappedProgressback = function(progress) { - try { - result.notify((isFunction(progressback) ? progressback : defaultCallback)(progress)); - } catch(e) { - exceptionHandler(e); - } - }; - - if (pending) { - pending.push([wrappedCallback, wrappedErrback, wrappedProgressback]); - } else { - value.then(wrappedCallback, wrappedErrback, wrappedProgressback); - } - - return result.promise; - }, - - "catch": function(callback) { - return this.then(null, callback); - }, - - "finally": function(callback) { - - function makePromise(value, resolved) { - var result = defer(); - if (resolved) { - result.resolve(value); - } else { - result.reject(value); - } - return result.promise; - } - - function handleCallback(value, isResolved) { - var callbackOutput = null; - try { - callbackOutput = (callback ||defaultCallback)(); - } catch(e) { - return makePromise(e, false); - } - if (isPromiseLike(callbackOutput)) { - return callbackOutput.then(function() { - return makePromise(value, isResolved); - }, function(error) { - return makePromise(error, false); - }); - } else { - return makePromise(value, isResolved); - } - } - - return this.then(function(value) { - return handleCallback(value, true); - }, function(error) { - return handleCallback(error, false); - }); - } - } - }; - - return deferred; - }; - - var ref = function(value) { if (isPromiseLike(value)) return value; return { then: function(callback) { - var result = defer(); + var result = Q.defer(); nextTick(function() { result.resolve(callback(value)); }); @@ -393,52 +1162,10 @@ function qFactory(nextTick, exceptionHandler) { }; - /** - * @ngdoc method - * @name $q#reject - * @kind function - * - * @description - * Creates a promise that is resolved as rejected with the specified `reason`. This api should be - * used to forward rejection in a chain of promises. If you are dealing with the last promise in - * a promise chain, you don't need to worry about it. - * - * When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of - * `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via - * a promise error callback and you want to forward the error to the promise derived from the - * current promise, you have to "rethrow" the error by returning a rejection constructed via - * `reject`. - * - * ```js - * promiseB = promiseA.then(function(result) { - * // success: do something and resolve promiseB - * // with the old or a new result - * return result; - * }, function(reason) { - * // error: handle the error if possible and - * // resolve promiseB with newPromiseOrValue, - * // otherwise forward the rejection to promiseB - * if (canHandle(reason)) { - * // handle the error and recover - * return newPromiseOrValue; - * } - * return $q.reject(reason); - * }); - * ``` - * - * @param {*} reason Constant, message, exception or an object representing the rejection reason. - * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. - */ - var reject = function(reason) { - var result = defer(); - result.reject(reason); - return result.promise; - }; - var createInternalRejectedPromise = function(reason) { return { then: function(callback, errback) { - var result = defer(); + var result = Q.defer(); nextTick(function() { try { result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); @@ -453,147 +1180,195 @@ function qFactory(nextTick, exceptionHandler) { }; - /** - * @ngdoc method - * @name $q#when - * @kind function - * - * @description - * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. - * This is useful when you are dealing with an object that might or might not be a promise, or if - * the promise comes from a source that can't be trusted. - * - * @param {*} value Value or a promise - * @returns {Promise} Returns a promise of the passed value or promise - */ - var when = function(value, callback, errback, progressback) { - var result = defer(), - done; - - var wrappedCallback = function(value) { - try { - return (isFunction(callback) ? callback : defaultCallback)(value); - } catch (e) { - exceptionHandler(e); - return reject(e); - } - }; - - var wrappedErrback = function(reason) { - try { - return (isFunction(errback) ? errback : defaultErrback)(reason); - } catch (e) { - exceptionHandler(e); - return reject(e); - } - }; - - var wrappedProgressback = function(progress) { - try { - return (isFunction(progressback) ? progressback : defaultCallback)(progress); - } catch (e) { - exceptionHandler(e); - } - }; - - nextTick(function() { - ref(value).then(function(value) { - if (done) return; - done = true; - result.resolve(ref(value).then(wrappedCallback, wrappedErrback, wrappedProgressback)); - }, function(reason) { - if (done) return; - done = true; - result.resolve(wrappedErrback(reason)); - }, function(progress) { - if (done) return; - result.notify(wrappedProgressback(progress)); - }); - }); - - return result.promise; - }; - - function defaultCallback(value) { return value; } function defaultErrback(reason) { - return reject(reason); + return Q.reject(reason); } - /** - * @ngdoc method - * @name $q#all - * @kind function - * - * @description - * Combines multiple promises into a single promise that is resolved when all of the input - * promises are resolved. - * - * @param {Array.|Object.} promises An array or hash of promises. - * @returns {Promise} Returns a single promise that will be resolved with an array/hash of values, - * each value corresponding to the promise at the same index/key in the `promises` array/hash. - * If any of the promises is resolved with a rejection, this resulting promise will be rejected - * with the same rejection value. - */ - function all(promises) { - var deferred = defer(), - counter = 0, - results = isArray(promises) ? [] : {}; - - forEach(promises, function(promise, key) { - counter++; - ref(promise).then(function(value) { - if (results.hasOwnProperty(key)) return; - results[key] = value; - if (!(--counter)) deferred.resolve(results); - }, function(reason) { - if (results.hasOwnProperty(key)) return; - deferred.reject(reason); - }); - }); - - if (counter === 0) { - deferred.resolve(results); + function Q(resolver) { + if (!(this instanceof Q)) { + return new Q(resolver); } - return deferred.promise; + $Q.call(this, resolver, nextTick); } - var $Q = function Q(resolver) { - if (!isFunction(resolver)) { - // TODO(@caitp): minErr this - throw new TypeError('Expected resolverFn'); - } + // Inherit from shared $Q + Q.prototype = createObject($Q.prototype); + + + defineProperties(Q.prototype, INVISIBLE|WRITABLE|CONFIGURABLE, { + constructor: Q + }); - if (!(this instanceof Q)) { - // More useful when $Q is the Promise itself. - return new Q(resolver); - } - var deferred = defer(); + defineProperties(Q.prototype, INVISIBLE|WRITABLE, { + $$possiblyUnhandledRejection: exceptionHandler || function(e) {}, + $$exceptionHandler: exceptionHandler || function(e) {} + }); - function resolveFn(value) { + + defineProperties(Q, WRITABLE, { + defer: function Q$defer() { + return new $Deferred(Q, nextTick); + }, + + + resolved: function Q$resolved(value) { + var deferred = Q.defer(); deferred.resolve(value); - } + return deferred.promise; + }, + + + /** + * @ngdoc method + * @name $q#all + * @kind function + * + * @description + * Combines multiple promises into a single promise that is resolved when all of the input + * promises are resolved. + * + * @param {Array.|Object.} promises An array or hash of promises. + * @returns {Promise} Returns a single promise that will be resolved with an array/hash of values, + * each value corresponding to the promise at the same index/key in the `promises` array/hash. + * If any of the promises is resolved with a rejection, this resulting promise will be rejected + * with the same rejection value. + */ + all: function Q$all(promises) { + var deferred = Q.defer(), + counter = 0, + results = isArray(promises) ? [] : {}; - function rejectFn(reason) { + forEach(promises, function(promise, key) { + counter++; + ref(promise).then(function(value) { + if (results.hasOwnProperty(key)) return; + results[key] = value; + if (!(--counter)) deferred.resolve(results); + }, function(reason) { + if (results.hasOwnProperty(key)) return; + deferred.reject(reason); + }); + }); + + if (counter === 0) { + deferred.resolve(results); + } + + return deferred.promise; + }, + + + /** + * @ngdoc method + * @name $q#reject + * @kind function + * + * @description + * Creates a promise that is resolved as rejected with the specified `reason`. This api should be + * used to forward rejection in a chain of promises. If you are dealing with the last promise in + * a promise chain, you don't need to worry about it. + * + * When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of + * `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via + * a promise error callback and you want to forward the error to the promise derived from the + * current promise, you have to "rethrow" the error by returning a rejection constructed via + * `reject`. + * + * ```js + * promiseB = promiseA.then(function(result) { + * // success: do something and resolve promiseB + * // with the old or a new result + * return result; + * }, function(reason) { + * // error: handle the error if possible and + * // resolve promiseB with newPromiseOrValue, + * // otherwise forward the rejection to promiseB + * if (canHandle(reason)) { + * // handle the error and recover + * return newPromiseOrValue; + * } + * return $q.reject(reason); + * }); + * ``` + * + * @param {*} reason Constant, message, exception or an object representing the rejection reason. + * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. + */ + reject: function(reason) { + var deferred = Q.defer(); deferred.reject(reason); - } + return deferred.promise; + }, - resolver(resolveFn, rejectFn); - return deferred.promise; - }; + /** + * @ngdoc method + * @name $q#when + * @kind function + * + * @description + * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. + * This is useful when you are dealing with an object that might or might not be a promise, or if + * the promise comes from a source that can't be trusted. + * + * @param {*} value Value or a promise + * @returns {Promise} Returns a promise of the passed value or promise + */ + when: function Q$when(value, callback, errback, progressback) { + var result = Q.defer(); + + function fulfillWhen(value) { + try { + if (isFunction(callback)) value = callback(value); + result.resolve(value); + return value; + } catch (e) { + return Q.reject(e); + } + } - $Q.defer = defer; - $Q.reject = reject; - $Q.when = when; - $Q.all = all; - return $Q; + function rejectWhen(reason) { + try { + var value = isFunction(errback) ? errback(reason) : Q.reject(reason); + return value; + } catch (e) { + return Q.reject(e); + } + } + + + function progressWhen(progress) { + try { + if (isFunction(progressback)) progress = progressback(progress); + result.notify(progress); + return progress; + } catch (e) { + // Exceptions thrown from progress callbacks are ignored + } + } + + + nextTick(function awaitValue() { + Promise$$castToPromise(value, void 0, Q).then(function(value) { + result.resolve(Promise$$castToPromise(value, result.promise, Q). + then(fulfillWhen, rejectWhen, progressWhen)); + }, function(reason) { + result.resolve(rejectWhen(reason)); + }, progressWhen); + }); + + return result.promise; + } + }); + + return Q; } diff --git a/src/ng/timeout.js b/src/ng/timeout.js index 4b4c28256501..20c60add6067 100644 --- a/src/ng/timeout.js +++ b/src/ng/timeout.js @@ -73,7 +73,7 @@ function $TimeoutProvider() { */ timeout.cancel = function(promise) { if (promise && promise.$$timeoutId in deferreds) { - deferreds[promise.$$timeoutId].reject('canceled'); + deferreds[promise.$$timeoutId].rejectGently('canceled'); delete deferreds[promise.$$timeoutId]; return $browser.defer.cancel(promise.$$timeoutId); } diff --git a/test/ng/qSpec.js b/test/ng/qSpec.js index f9baf4e2222f..d4ffb896efea 100644 --- a/test/ng/qSpec.js +++ b/test/ng/qSpec.js @@ -197,7 +197,7 @@ describe('q', function() { describe('$Q', function() { - var resolve, reject, resolve2, reject2; + var resolve = null, reject = null, resolve2 = null, reject2 = null; var createPromise = function() { return q(function(resolveFn, rejectFn) { if (resolve === null) { @@ -276,8 +276,9 @@ describe('q', function() { promise.then(success(), error()); resolve(createPromise()); - mockNextTick.flush(); - expect(logStr()).toBe(''); + + // Don't schedule a task to run, the new promise hasn't been fulfilled yet. + expect(mockNextTick.queue.length).toBe(0); resolve2('foo'); mockNextTick.flush(); @@ -853,8 +854,8 @@ describe('q', function() { promise.then(success(), error()); deferred.resolve(deferred2.promise); - mockNextTick.flush(); - expect(logStr()).toBe(''); + // Queue should be empty, new promise is not resolved yet. + expect(mockNextTick.queue.length).toBe(0); deferred2.resolve('foo'); mockNextTick.flush(); @@ -1565,6 +1566,7 @@ describe('q', function() { var rejectedPromise = q.reject('rejected'); expect(rejectedPromise['finally']).not.toBeUndefined(); expect(rejectedPromise['catch']).not.toBeUndefined(); + mockNextTick.flush(); }); }); @@ -1775,10 +1777,12 @@ describe('q', function() { mockNextTick.flush(); expect(logStr()).toBe(''); evilPromise.error('failed'); + mockNextTick.flush(); expect(logStr()).toBe('error(failed)->reject(failed)'); evilPromise.error('muhaha'); evilPromise.success('take this'); + expect(mockNextTick.queue.length).toBe(0); expect(logStr()).toBe('error(failed)->reject(failed)'); }); @@ -1797,6 +1801,7 @@ describe('q', function() { mockNextTick.flush(); expect(logStr()).toBe(''); evilPromise.progress('notification'); + mockNextTick.flush(); evilPromise.success('ok'); mockNextTick.flush(); expect(logStr()).toBe('progress(notification)->notification; success(ok)->ok'); @@ -1975,7 +1980,8 @@ describe('q', function() { promise.then(success1).then(success(2), error(2)); syncResolve(deferred, 'done'); expect(logStr()).toBe('success1(done)->throw(oops); error2(oops)->reject(oops)'); - expect(mockExceptionLogger.log).toEqual(['oops']); + // $exceptionHandler is not handling these anymore + // expect(mockExceptionLogger.log).toEqual(['oops']); }); @@ -1992,7 +1998,8 @@ describe('q', function() { promise.then(null, error1).then(success(2), error(2)); syncReject(deferred, 'nope'); expect(logStr()).toBe('error1(nope)->throw(oops); error2(oops)->reject(oops)'); - expect(mockExceptionLogger.log).toEqual(['oops']); + // $exceptionHandler is not handling these anymore + // expect(mockExceptionLogger.log).toEqual(['oops']); }); @@ -2009,7 +2016,8 @@ describe('q', function() { promise.then(success(), error(), progress(1, 'failed', true)).then(null, error(1), progress(2)); syncNotify(deferred, '10%'); expect(logStr()).toBe('progress1(10%)->throw(failed)'); - expect(mockExceptionLogger.log).toEqual(['failed']); + // $exceptionHandler is not handling these anymore + // expect(mockExceptionLogger.log).toEqual(['failed']); log = []; syncResolve(deferred, 'ok'); expect(logStr()).toBe('success(ok)->ok'); @@ -2025,7 +2033,8 @@ describe('q', function() { q.when('hi', success1, error()).then(success(), error(2)); mockNextTick.flush(); expect(logStr()).toBe('success1(hi)->throw(oops); error2(oops)->reject(oops)'); - expect(mockExceptionLogger.log).toEqual(['oops']); + // $exceptionHandler is not handling these anymore + // expect(mockExceptionLogger.log).toEqual(['oops']); }); @@ -2039,10 +2048,14 @@ describe('q', function() { it('should log exceptions thrown in a errback and reject the derived promise', function() { var error1 = error(1, 'oops', true); - q.when(q.reject('sorry'), success(), error1).then(success(), error(2)); + var wp = q.when(q.reject('sorry'), success(), error1); + wp.name = "q.when()"; + var wpt = wp.then(success(), error(2)); + wpt.name = "q.when().then(...)"; mockNextTick.flush(); expect(logStr()).toBe('error1(sorry)->throw(oops); error2(oops)->reject(oops)'); - expect(mockExceptionLogger.log).toEqual(['oops']); + // $exceptionHandler is not handling these anymore + // expect(mockExceptionLogger.log).toEqual(['oops']); }); @@ -2094,11 +2107,11 @@ describe('q', function() { it('should still reject the promise, when exception is thrown in error handler, even if exceptionHandler rethrows', function() { - deferred.promise.then(null, function() { throw 'reject again'; }).then(null, errorSpy); - deferred.reject('reject'); - mockNextTick.flush(); - expect(exceptionExceptionSpy).toHaveBeenCalled(); - expect(errorSpy).toHaveBeenCalled(); + deferred.promise.then(null, function() { throw 'reject again'; }).then(null, errorSpy); + deferred.reject('reject'); + mockNextTick.flush(); + expect(exceptionExceptionSpy).toHaveBeenCalled(); + expect(errorSpy).toHaveBeenCalled(); }); }); });