From 26e4c93e753dc0b2b04216ae07f89fdb7f0937e8 Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Tue, 15 Jul 2014 13:05:06 -0400 Subject: [PATCH 1/8] WIP make Q more bloobirdy --- src/Angular.js | 28 ++++++ src/ng/q.js | 238 ++++++++++++++++++++++++++++++------------------- 2 files changed, 175 insertions(+), 91 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index 1caab1545dc0..ec0df6b2bb1d 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1572,3 +1572,31 @@ 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 = { + 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); + }); +} diff --git a/src/ng/q.js b/src/ng/q.js index e571636f4d64..b228e9f5c8f5 100644 --- a/src/ng/q.js +++ b/src/ng/q.js @@ -246,7 +246,7 @@ function qFactory(nextTick, exceptionHandler) { * * @returns {Deferred} Returns a new instance of deferred. */ - var defer = function() { + var _defer = function(promise) { var pending = [], value, deferred; @@ -293,92 +293,23 @@ function qFactory(nextTick, exceptionHandler) { }, - 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); - }); - } - } + promise: promise }; return deferred; }; + var defer = function() { + var resolveFn; + var rejectFn; + var promise = new $Q(function(resolve, reject) { + resolveFn = resolve; + rejectFn = reject; + }); + return _defer(new Promise) + }; + var ref = function(value) { if (isPromiseLike(value)) return value; return { @@ -564,31 +495,156 @@ function qFactory(nextTick, exceptionHandler) { return deferred.promise; } + var $Deferred = function Deferred() { + }; + + extend($Deferred.prototype, { + resolve: function(value) { + if (this._pending) { + var callbacks = this._pending; + } + }, + + reject: function(value) { + + }, + + notify: function(value) { + + } + }); + var $Q = function Q(resolver) { if (!isFunction(resolver)) { // TODO(@caitp): minErr this throw new TypeError('Expected resolverFn'); } - if (!(this instanceof Q)) { + if (!(this.constructor !== $Q)) { // More useful when $Q is the Promise itself. - return new Q(resolver); + return new $Q(resolver); } - var deferred = defer(); + defineProperties(this, INVISIBLE|WRITABLE, { + _bitField: NO_STATE, + + // From Bluebird: Typical promise has exactly one parallel handler, + // store the first ones directly on the Promise. + _fulfillmentHandler0: void 0, + _rejectionHandler0: void 0, + _promise0: void 0, + _receiver0: void 0, + _settledValue: void 0, + }); - function resolveFn(value) { - deferred.resolve(value); + if (resolver !== internalResolver) { + resolveFromResolver(promise, resolver); } + }; - function rejectFn(reason) { - deferred.reject(reason); + defineProperties($Q.prototype, WRITABLE, { + toString: function Promise$toString() { + return '[object Promise]'; } + }); - resolver(resolveFn, rejectFn); + function resolveFromResolver(promise, resolver) { + promise._setTrace(void 0); + promise._pushContext(); - return deferred.promise; - }; + function Promise$_resolver(val) { + if (promise._tryFollow(val)) { + return; + } + promise._fulfill(val); + } + function Promise$_rejecter(val) { + var trace = canAttach(val) ? val : new Error(val + ''); + promise._attachExtraTrace(trace); + markAsOriginatingFromRejection(val); + promise._reject(val, trace === val ? void 0 : trace); + } + + try { + resolver.call(null, Promise$_resolver, Promise$_rejecter); + this._popContext(); + } catch (e) { + this._popContext(); + promise._reject(e, canAttach(e) ? e : new Error(e + '')); + } + } + + function _reject(promise, reason, carriedStackTrace) { + if (promise._isFollowingOrFulfilledOrRejected()) return; + _rejectUnchecked(promise, reason, carriedStackTrace); + } + + function _rejectUnchecked(promise, reason, trace) { + if (!promise.isPending()) return; + if (reason === promise) { + + } + + promise._cleanValues(); + promise._setRejected(); + promise._settledValue = reason; + + if (_isFinal(promise)) { + nextTick(function() { + thrower(trace === void 0 ? reason : trace); + }); + return; + } + + var len = _length(promise); + + if (trace !== void 0) _setCarriedStackTrace(promise, trace); + + if (len > 0) { + nextTick(function() { + _rejectPromises(promise); + }); + } else { + _ensurePossibleRejectionHandled(promise); + } + } + + function _rejectPromises(promise) { + _settlePromises(promise); + _unsetCarriedStackTrace(promise); + } + + function _settlePromises(promise) { + var len = _length(promise); + for (var i=0; i Date: Tue, 22 Jul 2014 12:44:17 -0400 Subject: [PATCH 2/8] WIP subset of Bluebird functionality / implementation Things remaining: 1. Make this compatible with the original $q implementation 2. Sort out the error logging story (make use of Bluebird's machinery for this + $exceptionHandler by default) 3. Remove more bits that aren't needed for Angular. --- Gruntfile.js | 6 +- src/.jshintrc | 6 + src/Angular.js | 8 + src/ng/http.js | 2 +- src/ng/q.js | 1199 +++++++++++++++++++++++++++++++++------------- test/ng/qSpec.js | 4 +- 6 files changed, 877 insertions(+), 348 deletions(-) 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/src/.jshintrc b/src/.jshintrc index 2e2b32250df6..00d744ee00d8 100644 --- a/src/.jshintrc +++ b/src/.jshintrc @@ -87,6 +87,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 ec0df6b2bb1d..570709264262 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1581,6 +1581,7 @@ 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) @@ -1600,3 +1601,10 @@ function defineProperties(target, flags, propertyNameAndValues) { 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 b228e9f5c8f5..d25780991ade 100644 --- a/src/ng/q.js +++ b/src/ng/q.js @@ -1,4 +1,744 @@ '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) { + defineProperties(this, INVISIBLE|WRITABLE, { + promise: new Q(internalPromiseResolver) + }); +}; + +defineProperties($Deferred.prototype, WRITABLE, { + resolve: function Deferred$resolve(value) { + var promise = this.promise; + if (promise.$$tryFollow(value)) { + return; + } + + promise.$$invoke(promise.$$fulfill, promise, value); + }, + + + reject: function Deferred$reject(reason) { + var promise = this.promise; + var trace = isError(reason) ? reason : new Error(reason + ''); + promise.$$attachExtraTrace(trace); + promise.$$invoke(promise.$$reject, promise, reason); + }, + + + notify: function Deferred$notify(value) { + this.promise.$$invoke(this.promise.$$progress, this.promise, 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 + defineProperties(this, INVISIBLE|WRITABLE, { + $$bitField: NO_STATE, + + // From Bluebird: Typical promise has exactly one parallel handler, + // store the first ones directly on the Promise. + $$fulfillmentHandler0: void 0, + $$rejectionHandler0: void 0, + + // 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. + $$nextTick: nextTick, + + $$promise0: void 0, + $$receiver0: void 0, + $$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$$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 (typeof promise.$$progress === 'function') { + promise.$$progress(v); + } + } +} + + +defineProperties($Q.prototype, INVISIBLE|WRITABLE, { + then: function Promise$then(didFulfill, didReject, didProgress) { + var ret = new $Q(internalPromiseResolver, this.$$nextTick); + var callbackIndex = this.$$addCallbacks(didFulfill, didReject, didProgress, ret, void 0); + if (this.isResolved()) { + this.$$invoke(this.$$queueSettleAt, this, callbackIndex); + } + + return ret; + }, + + + 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); + } + }, + + + $$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.fullfilmentHandler0 = 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) { + if (this.isResolved()) throw new Error('Cannot proxy for resolved promise'); + if (this.$$isProxied()) throw new Error('Cannot proxy for proxied promise'); + if (arguments.length !== 1) throw new Error('$$proxyPromise: arguments.length should be 1'); + promise.$$setProxied(); + this.$$setProxyHandlers(promise, -1); + }, + + + $$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(); + }, + + + $$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 + '')); + } + }, + + + $$reject: function Promise$$reject(reason, carriedStackTrace) { + if (this.$$isFollowingOrFulfilledOrRejected()) return; + this.$$rejectUnchecked(reason, carriedStackTrace); + }, + + + $$rejectUnchecked: function Promise$$rejectUnchecked(reason, trace) { + var promise = this; + if (!promise.isPending()) return; + if (reason === this) { + var err = new Error('Self-resolution forbidden'); + this.$$attachExtraTrace(err); + return this.$$rejectUnchecked(err); + } + + this.$$setRejected(); + this.$$settledValue = reason; + + if (this.$$isFinal()) { + this.$$nextTick(function() { + thrower(trace === void 0 ? reason : trace); + }); + return; + } + + var len = this.$$length(); + + if (trace !== void 0) this.$$setCarriedStackTrace(trace); + + if (len > 0) { + this.$$invoke(this.$$rejectPromises, this, void 0); + } + + // TODO: improve error logging + // else { + // 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); + }, + + + $$notifyUnhandledRejectionIsHandled: function Promise$$notifyUnhandledRejectionIsHandled() { + if (typeof unhandledRejectionHandled === 'function') { + // TODO: improve error logging + // this.$$invoke(this.$$notifyUnhandledRejection, this, void 0); + // make jshint happy: + noop(); + } + }, + + + $$notifyUnhandledRejection: function Promise$$notifyUnhandledRejection() { + if (this.$$isRejectionUnhandled()) { + var reason = this.$$settledValue; + var trace = this.$$getCarriedStackTrace(); + + this.$$setUnhandledRejectionIsNotified(); + + if (trace !== void 0) { + this.$$unsetCarriedStackTrace(); + reason = trace; + } + } + }, +}); /** * @ngdoc service @@ -246,75 +986,11 @@ function qFactory(nextTick, exceptionHandler) { * * @returns {Deferred} Returns a new instance of deferred. */ - var _defer = function(promise) { - 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: promise - }; - - return deferred; - }; - - - var defer = function() { - var resolveFn; - var rejectFn; - var promise = new $Q(function(resolve, reject) { - resolveFn = resolve; - rejectFn = reject; - }); - return _defer(new Promise) - }; - 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)); }); @@ -324,52 +1000,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)); @@ -384,272 +1018,149 @@ 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 $Deferred = function Deferred() { - }; - - extend($Deferred.prototype, { - resolve: function(value) { - if (this._pending) { - var callbacks = this._pending; - } - }, - - reject: function(value) { - - }, - - notify: function(value) { - - } - }); - - var $Q = function Q(resolver) { - if (!isFunction(resolver)) { - // TODO(@caitp): minErr this - throw new TypeError('Expected resolverFn'); - } - - if (!(this.constructor !== $Q)) { - // More useful when $Q is the Promise itself. - return new $Q(resolver); - } - - defineProperties(this, INVISIBLE|WRITABLE, { - _bitField: NO_STATE, + // Inherit from shared $Q + Q.prototype = createObject($Q.prototype); - // From Bluebird: Typical promise has exactly one parallel handler, - // store the first ones directly on the Promise. - _fulfillmentHandler0: void 0, - _rejectionHandler0: void 0, - _promise0: void 0, - _receiver0: void 0, - _settledValue: void 0, - }); - - if (resolver !== internalResolver) { - resolveFromResolver(promise, resolver); - } - }; - defineProperties($Q.prototype, WRITABLE, { - toString: function Promise$toString() { - return '[object Promise]'; - } + defineProperties(Q.prototype, INVISIBLE|WRITABLE|CONFIGURABLE, { + constructor: Q }); - function resolveFromResolver(promise, resolver) { - promise._setTrace(void 0); - promise._pushContext(); - - function Promise$_resolver(val) { - if (promise._tryFollow(val)) { - return; - } - promise._fulfill(val); - } - function Promise$_rejecter(val) { - var trace = canAttach(val) ? val : new Error(val + ''); - promise._attachExtraTrace(trace); - markAsOriginatingFromRejection(val); - promise._reject(val, trace === val ? void 0 : trace); - } - try { - resolver.call(null, Promise$_resolver, Promise$_rejecter); - this._popContext(); - } catch (e) { - this._popContext(); - promise._reject(e, canAttach(e) ? e : new Error(e + '')); - } - } + defineProperties(Q, WRITABLE, { + defer: function Q$defer() { + return new $Deferred(Q, nextTick); + }, - function _reject(promise, reason, carriedStackTrace) { - if (promise._isFollowingOrFulfilledOrRejected()) return; - _rejectUnchecked(promise, reason, carriedStackTrace); - } - function _rejectUnchecked(promise, reason, trace) { - if (!promise.isPending()) return; - if (reason === promise) { - - } + resolved: function Q$resolved(value) { + var deferred = Q.defer(); + deferred.resolve(value); + return deferred.promise; + }, - promise._cleanValues(); - promise._setRejected(); - promise._settledValue = reason; - if (_isFinal(promise)) { - nextTick(function() { - thrower(trace === void 0 ? reason : trace); + /** + * @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) ? [] : {}; + + 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); + }); }); - return; - } - var len = _length(promise); - - if (trace !== void 0) _setCarriedStackTrace(promise, trace); + if (counter === 0) { + deferred.resolve(results); + } - if (len > 0) { - nextTick(function() { - _rejectPromises(promise); - }); - } else { - _ensurePossibleRejectionHandled(promise); - } - } + return deferred.promise; + }, - function _rejectPromises(promise) { - _settlePromises(promise); - _unsetCarriedStackTrace(promise); - } - function _settlePromises(promise) { - var len = _length(promise); - for (var i=0; i Date: Wed, 23 Jul 2014 17:02:14 -0400 Subject: [PATCH 3/8] WIP bluebird --- src/ng/q.js | 283 +++++++++++++++++++++++++++++++++++++++------- src/ng/timeout.js | 2 +- test/ng/qSpec.js | 113 ++++++++++-------- 3 files changed, 311 insertions(+), 87 deletions(-) diff --git a/src/ng/q.js b/src/ng/q.js index d25780991ade..a31c417d33a5 100644 --- a/src/ng/q.js +++ b/src/ng/q.js @@ -68,7 +68,7 @@ var $Deferred = function Deferred(Q) { defineProperties($Deferred.prototype, WRITABLE, { resolve: function Deferred$resolve(value) { var promise = this.promise; - if (promise.$$tryFollow(value)) { + if (promise.isResolved() || promise.$$tryFollow(value)) { return; } @@ -78,14 +78,24 @@ defineProperties($Deferred.prototype, WRITABLE, { 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.$$invoke(this.promise.$$progress, this.promise, value); + this.promise.$$progress(value); }, @@ -161,6 +171,19 @@ function Promise$$cast(obj, originalPromise, Q) { } +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; @@ -207,13 +230,43 @@ function Promise$$doThenable(x, then, originalPromise, Q) { function Promise$_progressFromThenable(v) { if (called) return; var promise = resolver.promise; - if (typeof promise.$$progress === 'function') { + 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 ret = new $Q(internalPromiseResolver, this.$$nextTick); @@ -226,6 +279,27 @@ defineProperties($Q.prototype, INVISIBLE|WRITABLE, { }, + catch: function Promise$catch(handler) { + var ret = new $Q(internalPromiseResolver, this.$$nextTick); + 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; @@ -304,6 +378,7 @@ defineProperties($Q.prototype, INVISIBLE|WRITABLE, { this.$$attachExtraTrace(err); return this.$$rejectUnchecked(err); } + this.$$setFulfilled(); this.$$settledValue = value; @@ -314,6 +389,11 @@ defineProperties($Q.prototype, INVISIBLE|WRITABLE, { }, + $$setTrace: function(trace) { + // TODO: improve error logging + }, + + $$attachExtraTrace: function Promise$$attachExtraTrace(trace) { // TODO: improve error logging }, @@ -336,7 +416,7 @@ defineProperties($Q.prototype, INVISIBLE|WRITABLE, { // ASSERT(this.isRejected()) /*jshint bitwise: false */ this.$$bitField = (this.$$bitField | IS_CARRYING_STACK_TRACE); - this.fullfilmentHandler0 = capturedTrace; + this.$$fulfillmentHandler0 = capturedTrace; }, @@ -347,12 +427,10 @@ defineProperties($Q.prototype, INVISIBLE|WRITABLE, { }, - $$proxyPromise: function Promise$$proxyPromise(promise) { - if (this.isResolved()) throw new Error('Cannot proxy for resolved promise'); - if (this.$$isProxied()) throw new Error('Cannot proxy for proxied promise'); - if (arguments.length !== 1) throw new Error('$$proxyPromise: arguments.length should be 1'); + $$proxyPromise: function Promise$$proxyPromise(promise, slot) { + if (arguments.length === 1) slot = -1; promise.$$setProxied(); - this.$$setProxyHandlers(promise, -1); + this.$$setProxyHandlers(promise, slot); }, @@ -371,6 +449,19 @@ defineProperties($Q.prototype, INVISIBLE|WRITABLE, { }, + $$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; @@ -388,7 +479,7 @@ defineProperties($Q.prototype, INVISIBLE|WRITABLE, { this.$$setTrace(void 0); function Promise$$resolver(val) { - if (promise._tryFollow(val)) { + if (promise.$$tryFollow(val)) { return; } promise.$$fulfill(val); @@ -408,13 +499,54 @@ defineProperties($Q.prototype, INVISIBLE|WRITABLE, { }, + $$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(); } - - // TODO: improve error logging - // else { - // this.$$ensurePossibleRejectionHandled(); - // } }, @@ -526,6 +653,7 @@ defineProperties($Q.prototype, INVISIBLE|WRITABLE, { trace = isError(error) ? error : new Error(error + ''); promise.$$attachExtraTrace(trace); promise.$$rejectUnchecked(error, trace); + this.$$exceptionHandler(error); } else { var castValue = Promise$$cast(x, promise, this.constructor); if (castValue instanceof $Q) { @@ -715,29 +843,62 @@ defineProperties($Q.prototype, INVISIBLE|WRITABLE, { }, + $$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() { - if (typeof unhandledRejectionHandled === 'function') { - // TODO: improve error logging - // this.$$invoke(this.$$notifyUnhandledRejection, this, void 0); - // make jshint happy: - noop(); - } + // TODO(@caitp): improve error logging in $q + // if (isFunction(this.$$unhandledRejectionHandled)) { + // this.$$invoke(this.$$unhandledRejectionHandled, this, void 0); + // } }, $$notifyUnhandledRejection: function Promise$$notifyUnhandledRejection() { - if (this.$$isRejectionUnhandled()) { - var reason = this.$$settledValue; - var trace = this.$$getCarriedStackTrace(); + // 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); + // } + // } + }, - this.$$setUnhandledRejectionIsNotified(); - if (trace !== void 0) { - this.$$unsetCarriedStackTrace(); - reason = trace; - } - } + $$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; + } }); /** @@ -1045,6 +1206,12 @@ function qFactory(nextTick, exceptionHandler) { }); + defineProperties(Q.prototype, INVISIBLE|WRITABLE, { + $$possiblyUnhandledRejection: exceptionHandler || function(e) {}, + $$exceptionHandler: exceptionHandler || function(e) {} + }); + + defineProperties(Q, WRITABLE, { defer: function Q$defer() { return new $Deferred(Q, nextTick); @@ -1155,10 +1322,50 @@ function qFactory(nextTick, exceptionHandler) { * @returns {Promise} Returns a promise of the passed value or promise */ when: function Q$when(value, callback, errback, progressback) { - var castValue = Promise$$cast(value, void 0, Q); - var promise = castValue instanceof Q ? castValue : Q.resolved(value); - promise.then(callback, errback, progressback); - return promise; + 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); + } + } + + + 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; } }); 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 5cabdd42a040..d6d8398fd67a 100644 --- a/test/ng/qSpec.js +++ b/test/ng/qSpec.js @@ -31,7 +31,7 @@ http://jsperf.com/throw-vs-return */ -ddescribe('q', function() { +describe('q', function() { var q, defer, deferred, promise, log; // The following private functions are used to help with logging for testing invocation of the @@ -197,7 +197,7 @@ ddescribe('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 @@ ddescribe('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(); @@ -298,7 +299,7 @@ ddescribe('q', function() { }); - iit('should not break if a callbacks registers another callback', function() { + it('should not break if a callbacks registers another callback', function() { var promise = createPromise(); promise.then(function() { log.push('outer'); @@ -853,8 +854,8 @@ ddescribe('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(); @@ -874,13 +875,14 @@ ddescribe('q', function() { }); - it('should support non-bound execution', function() { - var resolver = deferred.resolve; - promise.then(success(), error()); - resolver('detached'); - mockNextTick.flush(); - expect(logStr()).toBe('success(detached)->detached'); - }); + // By putting work in the prototype chain of an object, we can't support unbound execution. + // it('should support non-bound execution', function() { + // var resolver = deferred.resolve; + // promise.then(success(), error()); + // resolver('detached'); + // mockNextTick.flush(); + // expect(logStr()).toBe('success(detached)->detached'); + // }); it('should not break if a callbacks registers another callback', function() { @@ -993,13 +995,14 @@ ddescribe('q', function() { }); - it('should support non-bound execution', function() { - var rejector = deferred.reject; - promise.then(success(), error()); - rejector('detached'); - mockNextTick.flush(); - expect(logStr()).toBe('error(detached)->reject(detached)'); - }); + // By putting work in the prototype chain, we can't support unbound execution. + // it('should support non-bound execution', function() { + // var rejector = deferred.reject; + // promise.then(success(), error()); + // rejector('detached'); + // mockNextTick.flush(); + // expect(logStr()).toBe('error(detached)->reject(detached)'); + // }); }); @@ -1085,13 +1088,14 @@ ddescribe('q', function() { }); - it('should support non-bound execution', function() { - var notify = deferred.notify; - promise.then(success(), error(), progress()); - notify('detached'); - mockNextTick.flush(); - expect(logStr()).toBe('progress(detached)->detached'); - }); + // By putting work in the prototype chain of an object, we can't support unbound execution. + // it('should support non-bound execution', function() { + // var notify = deferred.notify; + // promise.then(success(), error(), progress()); + // notify('detached'); + // mockNextTick.flush(); + // expect(logStr()).toBe('progress(detached)->detached'); + // }); it("should not save and re-emit progress notifications between ticks", function () { @@ -1565,6 +1569,7 @@ ddescribe('q', function() { var rejectedPromise = q.reject('rejected'); expect(rejectedPromise['finally']).not.toBeUndefined(); expect(rejectedPromise['catch']).not.toBeUndefined(); + mockNextTick.flush(); }); }); @@ -1775,10 +1780,12 @@ ddescribe('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 +1804,7 @@ ddescribe('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 +1983,8 @@ ddescribe('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 +2001,8 @@ ddescribe('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 +2019,8 @@ ddescribe('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 +2036,8 @@ ddescribe('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 +2051,14 @@ ddescribe('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']); }); @@ -2084,21 +2100,22 @@ ddescribe('q', function() { }); - it('should still reject the promise, when exception is thrown in success handler, even if exceptionHandler rethrows', function() { - deferred.promise.then(function() { throw 'reject'; }).then(null, errorSpy); - deferred.resolve('resolve'); - mockNextTick.flush(); - expect(exceptionExceptionSpy).toHaveBeenCalled(); - expect(errorSpy).toHaveBeenCalled(); - }); + // TODO: fix these after improving error logging + // it('should still reject the promise, when exception is thrown in success handler, even if exceptionHandler rethrows', function() { + // deferred.promise.then(function() { throw 'reject'; }).then(null, errorSpy); + // deferred.resolve('resolve'); + // mockNextTick.flush(); + // expect(exceptionExceptionSpy).toHaveBeenCalled(); + // expect(errorSpy).toHaveBeenCalled(); + // }); - 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(); - }); + // 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(); + // }); }); }); From 4421bc7536b5c4bf0d4d1a792e639a095bbb1b6e Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Thu, 24 Jul 2014 11:28:50 -0400 Subject: [PATCH 4/8] WIP add promises-aplus-test-adapter-prefix.js --- .../promises-aplus-test-adapter-prefix.js | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 lib/promises-aplus/promises-aplus-test-adapter-prefix.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..ad992c84a244 --- /dev/null +++ b/lib/promises-aplus/promises-aplus-test-adapter-prefix.js @@ -0,0 +1,29 @@ +"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; From 9ceaca89da6cf6918d40c309e8a1d65f12287c25 Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Thu, 24 Jul 2014 14:55:20 -0400 Subject: [PATCH 5/8] MORE WIP --- src/ng/q.js | 4 +--- test/ng/qSpec.js | 33 ++++++++++++++++----------------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/ng/q.js b/src/ng/q.js index a31c417d33a5..87146700e303 100644 --- a/src/ng/q.js +++ b/src/ng/q.js @@ -523,9 +523,7 @@ defineProperties($Q.prototype, INVISIBLE|WRITABLE, { } } catch (e) { // TODO(@caitp): improve error logging - if (this.$$unhandledException) { - - } + this.$$exceptionHandler(e); } } else if (receiver instanceof $Q && receiver.$$isProxied()) { receiver.$$progressUnchecked(progressValue); diff --git a/test/ng/qSpec.js b/test/ng/qSpec.js index d6d8398fd67a..9964daef63ef 100644 --- a/test/ng/qSpec.js +++ b/test/ng/qSpec.js @@ -2100,22 +2100,21 @@ describe('q', function() { }); - // TODO: fix these after improving error logging - // it('should still reject the promise, when exception is thrown in success handler, even if exceptionHandler rethrows', function() { - // deferred.promise.then(function() { throw 'reject'; }).then(null, errorSpy); - // deferred.resolve('resolve'); - // mockNextTick.flush(); - // expect(exceptionExceptionSpy).toHaveBeenCalled(); - // expect(errorSpy).toHaveBeenCalled(); - // }); - - - // 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(); - // }); + it('should still reject the promise, when exception is thrown in success handler, even if exceptionHandler rethrows', function() { + deferred.promise.then(function() { throw 'reject'; }).then(null, errorSpy); + deferred.resolve('resolve'); + mockNextTick.flush(); + expect(exceptionExceptionSpy).toHaveBeenCalled(); + expect(errorSpy).toHaveBeenCalled(); + }); + + + 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(); + }); }); }); From eb09e52c692d37de2d82bb6b2cd0ace16c073e46 Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Thu, 24 Jul 2014 15:06:02 -0400 Subject: [PATCH 6/8] WIP fix $compile tests --- src/ng/q.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ng/q.js b/src/ng/q.js index 87146700e303..5bc4d089ae52 100644 --- a/src/ng/q.js +++ b/src/ng/q.js @@ -269,7 +269,8 @@ function Promise$handleFinalCallback(callback, value, isResolved, Q) { defineProperties($Q.prototype, INVISIBLE|WRITABLE, { then: function Promise$then(didFulfill, didReject, didProgress) { - var ret = new $Q(internalPromiseResolver, this.$$nextTick); + 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); @@ -280,7 +281,8 @@ defineProperties($Q.prototype, INVISIBLE|WRITABLE, { catch: function Promise$catch(handler) { - var ret = new $Q(internalPromiseResolver, this.$$nextTick); + 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); @@ -295,7 +297,7 @@ defineProperties($Q.prototype, INVISIBLE|WRITABLE, { return this.then(function(value) { return Promise$handleFinalCallback(handler, value, true, Q); }, function(error) { - return Promise$handleFinalCallback(handler, error, false, Q); + return Promise$handleFinalCallback(handler, error, false, Q); }); }, @@ -1357,7 +1359,7 @@ function qFactory(nextTick, exceptionHandler) { nextTick(function awaitValue() { Promise$$castToPromise(value, void 0, Q).then(function(value) { result.resolve(Promise$$castToPromise(value, result.promise, Q). - then(fulfillWhen, rejectWhen, progressWhen)); + then(fulfillWhen, rejectWhen, progressWhen)); }, function(reason) { result.resolve(rejectWhen(reason)); }, progressWhen); From 7084667553d0ac1d996f2ff01960efc3e4c22193 Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Thu, 24 Jul 2014 15:54:59 -0400 Subject: [PATCH 7/8] WIP make it a tiny bit faster, maybe... --- src/Angular.js | 4 ++-- src/ng/q.js | 28 ++++++++++++---------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index 570709264262..e2ed501eab9b 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1590,9 +1590,9 @@ function defineProperty(target, propertyName, flags, value) { desc.value = value; } Object.defineProperty(target, propertyName, desc); + } else { + target[propertyName] = value; } - } else { - target[propertyName] = value; } } diff --git a/src/ng/q.js b/src/ng/q.js index 5bc4d089ae52..9ac4c337d46e 100644 --- a/src/ng/q.js +++ b/src/ng/q.js @@ -60,9 +60,7 @@ function isError(obj) { } var $Deferred = function Deferred(Q) { - defineProperties(this, INVISIBLE|WRITABLE, { - promise: new Q(internalPromiseResolver) - }); + this.promise = new Q(internalPromiseResolver); }; defineProperties($Deferred.prototype, WRITABLE, { @@ -127,22 +125,20 @@ function $Q(resolver, nextTick) { } // Private properties - defineProperties(this, INVISIBLE|WRITABLE, { - $$bitField: NO_STATE, + this.$$bitField = NO_STATE; - // From Bluebird: Typical promise has exactly one parallel handler, - // store the first ones directly on the Promise. - $$fulfillmentHandler0: void 0, - $$rejectionHandler0: void 0, + // 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; - // 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. - $$nextTick: nextTick, + // From Bluebird: Typical promise has exactly one parallel handler, + // store the first ones directly on the Promise. + this.$$fulfillmentHandler0 = + this.$$rejectionHandler0 = - $$promise0: void 0, - $$receiver0: void 0, - $$settledValue: void 0 - }); + this.$$promise0 = + this.$$receiver0 = + this.$$settledValue = void 0; if (resolver !== internalPromiseResolver) this.$$resolveFromResolver(resolver); } From 7e51cbb407497eb2d4ea471e8b6535b166118559 Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Thu, 31 Jul 2014 09:22:36 -0400 Subject: [PATCH 8/8] WIP: re-add support for unbound execution of Deferred methods --- .../promises-aplus-test-adapter-prefix.js | 6 +++ src/.jshintrc | 1 + src/Angular.js | 13 ++++++ src/ng/q.js | 5 +++ test/ng/qSpec.js | 45 +++++++++---------- 5 files changed, 46 insertions(+), 24 deletions(-) diff --git a/lib/promises-aplus/promises-aplus-test-adapter-prefix.js b/lib/promises-aplus/promises-aplus-test-adapter-prefix.js index ad992c84a244..0c16d80f5a17 100644 --- a/lib/promises-aplus/promises-aplus-test-adapter-prefix.js +++ b/lib/promises-aplus/promises-aplus-test-adapter-prefix.js @@ -27,3 +27,9 @@ var defineProperties = function(target, flags, properties) { }; 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 00d744ee00d8..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, diff --git a/src/Angular.js b/src/Angular.js index e2ed501eab9b..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; diff --git a/src/ng/q.js b/src/ng/q.js index 9ac4c337d46e..b0cdf8d905fa 100644 --- a/src/ng/q.js +++ b/src/ng/q.js @@ -61,6 +61,11 @@ function isError(obj) { 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, { diff --git a/test/ng/qSpec.js b/test/ng/qSpec.js index 9964daef63ef..d4ffb896efea 100644 --- a/test/ng/qSpec.js +++ b/test/ng/qSpec.js @@ -875,14 +875,13 @@ describe('q', function() { }); - // By putting work in the prototype chain of an object, we can't support unbound execution. - // it('should support non-bound execution', function() { - // var resolver = deferred.resolve; - // promise.then(success(), error()); - // resolver('detached'); - // mockNextTick.flush(); - // expect(logStr()).toBe('success(detached)->detached'); - // }); + it('should support non-bound execution', function() { + var resolver = deferred.resolve; + promise.then(success(), error()); + resolver('detached'); + mockNextTick.flush(); + expect(logStr()).toBe('success(detached)->detached'); + }); it('should not break if a callbacks registers another callback', function() { @@ -995,14 +994,13 @@ describe('q', function() { }); - // By putting work in the prototype chain, we can't support unbound execution. - // it('should support non-bound execution', function() { - // var rejector = deferred.reject; - // promise.then(success(), error()); - // rejector('detached'); - // mockNextTick.flush(); - // expect(logStr()).toBe('error(detached)->reject(detached)'); - // }); + it('should support non-bound execution', function() { + var rejector = deferred.reject; + promise.then(success(), error()); + rejector('detached'); + mockNextTick.flush(); + expect(logStr()).toBe('error(detached)->reject(detached)'); + }); }); @@ -1088,14 +1086,13 @@ describe('q', function() { }); - // By putting work in the prototype chain of an object, we can't support unbound execution. - // it('should support non-bound execution', function() { - // var notify = deferred.notify; - // promise.then(success(), error(), progress()); - // notify('detached'); - // mockNextTick.flush(); - // expect(logStr()).toBe('progress(detached)->detached'); - // }); + it('should support non-bound execution', function() { + var notify = deferred.notify; + promise.then(success(), error(), progress()); + notify('detached'); + mockNextTick.flush(); + expect(logStr()).toBe('progress(detached)->detached'); + }); it("should not save and re-emit progress notifications between ticks", function () {