From 8c1edaf0a77a6d46a7527b2873e456d3ff62fab8 Mon Sep 17 00:00:00 2001 From: Petka Antonov Date: Sun, 28 Dec 2014 03:14:48 +0200 Subject: [PATCH] Fix memory leak Fixes the memory described at https://github.com/promises-aplus/promises-spec/issues/179 --- src/cancel.js | 2 +- src/constants.js | 5 +- src/finally.js | 2 + src/join.js | 9 +- src/map.js | 9 +- src/progress.js | 6 +- src/promise.js | 208 ++++++++++++----------- src/promise_array.js | 20 ++- src/reduce.js | 27 +-- src/synchronous_inspection.js | 79 +++++++-- src/thenables.js | 1 - src/timers.js | 7 +- src/using.js | 6 +- test/mocha/following.js | 306 +++++++++++++++++++++++----------- test/mocha/generator.js | 27 +-- 15 files changed, 445 insertions(+), 269 deletions(-) diff --git a/src/cancel.js b/src/cancel.js index 8b4493c3c..a8a4aeb5d 100644 --- a/src/cancel.js +++ b/src/cancel.js @@ -18,7 +18,7 @@ Promise.prototype._cancel = function (reason) { ASSERT(promiseToReject.isCancellable()); this._unsetCancellable(); promiseToReject._attachExtraTrace(reason); - promiseToReject._rejectUnchecked(reason); + promiseToReject._target()._rejectUnchecked(reason); }; Promise.prototype.cancel = function (reason) { diff --git a/src/constants.js b/src/constants.js index 4989d7584..e5ff31eb7 100644 --- a/src/constants.js +++ b/src/constants.js @@ -27,7 +27,7 @@ CONSTANT(CALLBACK_PROMISE_OFFSET, 3); CONSTANT(CALLBACK_RECEIVER_OFFSET, 4); CONSTANT(CALLBACK_SIZE, 5); //Layout for ._bitField -//QQWF NCTR BPHS UDLL LLLL LLLL LLLL LLLL +//QQWF NCTR B[R]HS UDLL LLLL LLLL LLLL LLLL //Q = isSettlePromisesQueued (Both bits are either on or off to represent // 1 bit due to 31-bit integers in 32-bit v8) //W = isFollowing (The promise that is being followed is not stored explicitly) @@ -36,8 +36,6 @@ CONSTANT(CALLBACK_SIZE, 5); //C = isCancellable //T = isFinal (used for .done() implementation) //B = isBound -//P = isProxied (optimization when .then listeners on a promise are -// just respective fate sealers on some other promise) //H = isRejectionUnhandled //S = isCarryingStackTrace //U = isUnhanldedRejectionNotified @@ -52,7 +50,6 @@ CONSTANT(IS_REJECTED, 0x8000000|0); CONSTANT(IS_CANCELLABLE, 0x4000000|0); CONSTANT(IS_FINAL, 0x2000000|0); CONSTANT(IS_BOUND, 0x800000|0); -CONSTANT(IS_PROXIED, 0x400000|0); CONSTANT(IS_REJECTION_UNHANDLED, 0x200000|0); CONSTANT(IS_CARRYING_STACK_TRACE, 0x100000|0); CONSTANT(IS_UNHANDLED_REJECTION_NOTIFIED, 0x80000|0); diff --git a/src/finally.js b/src/finally.js index de953193f..937f454b7 100644 --- a/src/finally.js +++ b/src/finally.js @@ -43,6 +43,7 @@ function finallyHandler(reasonOrValue) { if (ret !== undefined) { var maybePromise = tryConvertToPromise(ret, undefined); if (maybePromise instanceof Promise) { + maybePromise = maybePromise._target(); return promisedFinally(maybePromise, reasonOrValue, promise.isFulfilled()); } @@ -70,6 +71,7 @@ function tapHandler(value) { if (ret !== undefined) { var maybePromise = tryConvertToPromise(ret, undefined); if (maybePromise instanceof Promise) { + maybePromise = maybePromise._target(); return promisedFinally(maybePromise, value, true); } } diff --git a/src/join.js b/src/join.js index 12eac3ea1..6eee06a29 100644 --- a/src/join.js +++ b/src/join.js @@ -75,14 +75,15 @@ Promise.join = function () { for (var i = 0; i < last; ++i) { var maybePromise = tryConvertToPromise(arguments[i], undefined); if (maybePromise instanceof Promise) { - if (maybePromise.isPending()) { + maybePromise = maybePromise._target(); + if (maybePromise._isPending()) { maybePromise._then(callbacks[i], reject, undefined, ret, holder); - } else if (maybePromise.isFulfilled()) { + } else if (maybePromise._isFulfilled()) { callbacks[i].call(ret, - maybePromise._settledValue, holder); + maybePromise._value(), holder); } else { - ret._reject(maybePromise._settledValue); + ret._reject(maybePromise._reason()); maybePromise._unsetRejectionIsUnhandled(); } } else { diff --git a/src/map.js b/src/map.js index 72a006b96..e6fa69eaf 100644 --- a/src/map.js +++ b/src/map.js @@ -67,15 +67,16 @@ MappingPromiseArray.prototype._promiseFulfilled = function (value, index) { // anymore), the marker PENDING is put at that index var maybePromise = tryConvertToPromise(ret, this._promise); if (maybePromise instanceof Promise) { - if (maybePromise.isPending()) { + maybePromise = maybePromise._target(); + if (maybePromise._isPending()) { if (limit >= 1) this._inFlight++; values[index] = PENDING; return maybePromise._proxyPromiseArray(this, index); - } else if (maybePromise.isFulfilled()) { - ret = maybePromise._settledValue; + } else if (maybePromise._isFulfilled()) { + ret = maybePromise._value(); } else { maybePromise._unsetRejectionIsUnhandled(); - return this._reject(maybePromise._settledValue); + return this._reject(maybePromise._reason()); } } values[index] = ret; diff --git a/src/progress.js b/src/progress.js index 0b3e9774c..41359a524 100644 --- a/src/progress.js +++ b/src/progress.js @@ -13,7 +13,7 @@ Promise.prototype.progressed = function (handler) { Promise.prototype._progress = function (progressValue) { if (this._isFollowingOrFulfilledOrRejected()) return; - this._progressUnchecked(progressValue); + this._target()._progressUnchecked(progressValue); }; @@ -67,7 +67,7 @@ Promise.prototype._doProgressWith = function (progression) { Promise.prototype._progressUnchecked = function (progressValue) { - if (!this.isPending()) return; + ASSERT(!this._isFollowingOrFulfilledOrRejected()); var len = this._length(); var progress = this._progress; for (var i = 0; i < len; i++) { @@ -79,8 +79,6 @@ Promise.prototype._progressUnchecked = function (progressValue) { var receiver = this._receiverAt(i); if (typeof handler === "function") { handler.call(receiver, progressValue, promise); - } else if (receiver instanceof Promise && receiver._isProxied()) { - receiver._progressUnchecked(progressValue); } else if (receiver instanceof PromiseArray && !receiver._isResolved()) { receiver._promiseProgressed(progressValue, promise); diff --git a/src/promise.js b/src/promise.js index c32a1f949..a37e7a8eb 100644 --- a/src/promise.js +++ b/src/promise.js @@ -4,9 +4,8 @@ var makeSelfResolutionError = function () { return new TypeError(CIRCULAR_RESOLUTION_ERROR); }; var reflect = function() { - return new Promise.PromiseInspection(this); + return new Promise.PromiseInspection(this._target()); }; -var returnFirstElement = function(elements) { return elements[0]; }; var ASSERT = require("./assert.js"); var util = require("./util.js"); var async = require("./async.js"); @@ -42,7 +41,6 @@ var debugging = __DEBUG__ || !!( (process.env["BLUEBIRD_DEBUG"] || process.env["NODE_ENV"] === "development") ); - function Promise(resolver) { if (typeof resolver !== "function") { throw new TypeError(CONSTRUCT_ERROR_ARG); @@ -73,15 +71,18 @@ Promise.prototype.bind = function (thisArg) { var maybePromise = tryConvertToPromise(thisArg, this); var ret = new Promise(INTERNAL); ret._propagateFrom(this, PROPAGATE_TRACE | PROPAGATE_CANCEL); + var target = this._target(); if (maybePromise instanceof Promise) { - var binder = maybePromise.then(function(thisArg) { - ret._setBoundTo(thisArg); - }); - var p = Promise.all([this, binder]).then(returnFirstElement); - ret._follow(p); + target._then(INTERNAL, ret._reject, ret._progress, ret, null); + maybePromise._then(function(thisArg) { + if (ret._isPending()) { + ret._setBoundTo(thisArg); + ret._follow(target); + } + }, ret._reject, ret._progress, ret, null); } else { - ret._follow(this); ret._setBoundTo(thisArg); + ret._follow(target); } return ret; @@ -150,10 +151,10 @@ Promise.prototype.toJSON = function () { rejectionReason: undefined }; if (this.isFulfilled()) { - ret.fulfillmentValue = this._settledValue; + ret.fulfillmentValue = this.value(); ret.isFulfilled = true; } else if (this.isRejected()) { - ret.rejectionReason = this._settledValue; + ret.rejectionReason = this.reason(); ret.isRejected = true; } return ret; @@ -222,10 +223,10 @@ Promise.bind = function (thisArg) { ret._setTrace(undefined); if (maybePromise instanceof Promise) { - var p = maybePromise.then(function(thisArg) { + maybePromise._then(function(thisArg) { ret._setBoundTo(thisArg); - }); - ret._follow(p); + ret._fulfill(undefined); + }, ret._reject, ret._progress, ret, null); } else { ret._setBoundTo(thisArg); ret._setFulfilled(); @@ -305,11 +306,14 @@ Promise.prototype._then = function ( ret._propagateFrom(this, PROPAGATE_ALL); } + var target = this._target(); + if (target !== this && receiver === undefined) receiver = this._boundTo; var callbackIndex = - this._addCallbacks(didFulfill, didReject, didProgress, ret, receiver); + target._addCallbacks(didFulfill, didReject, didProgress, ret, receiver); - if (this.isResolved() && !this._isSettlePromisesQueued()) { - async.invoke(this._settlePromiseAtPostResolution, this, callbackIndex); + if (target._isResolved() && !target._isSettlePromisesQueued()) { + async.invoke( + target._settlePromiseAtPostResolution, target, callbackIndex); } return ret; @@ -318,7 +322,7 @@ Promise.prototype._then = function ( Promise.prototype._settlePromiseAtPostResolution = function (index) { ASSERT(typeof index === "number"); ASSERT(index >= 0); - ASSERT(this.isFulfilled() || this.isRejected()); + ASSERT(this._isFulfilled() || this._isRejected()); ASSERT(this._promiseAt(index) !== undefined); if (this._isRejectionUnhandled()) this._unsetRejectionIsUnhandled(); this._setLength(0); @@ -376,11 +380,13 @@ Promise.prototype._unsetCancellable = function () { }; Promise.prototype._setRejectionIsUnhandled = function () { - ASSERT(this.isRejected()); + ASSERT(!this._isFollowing()); + ASSERT(this._isRejected()); this._bitField = this._bitField | IS_REJECTION_UNHANDLED; }; Promise.prototype._unsetRejectionIsUnhandled = function () { + ASSERT(!this._isFollowing()); this._bitField = this._bitField & (~IS_REJECTION_UNHANDLED); if (this._isUnhandledRejectionNotified()) { this._unsetUnhandledRejectionIsNotified(); @@ -389,6 +395,7 @@ Promise.prototype._unsetRejectionIsUnhandled = function () { }; Promise.prototype._isRejectionUnhandled = function () { + ASSERT(!this._isFollowing()); return (this._bitField & IS_REJECTION_UNHANDLED) > 0; }; @@ -405,33 +412,38 @@ Promise.prototype._isUnhandledRejectionNotified = function () { }; Promise.prototype._setCarriedStackTrace = function (capturedTrace) { - ASSERT(this.isRejected()); + ASSERT(!this._isFollowing()); + ASSERT(this._isRejected()); this._bitField = this._bitField | IS_CARRYING_STACK_TRACE; //Since this field is not used in rejected promises, smuggle the trace there this._fulfillmentHandler0 = capturedTrace; }; Promise.prototype._unsetCarriedStackTrace = function () { - ASSERT(this.isRejected()); + ASSERT(!this._isFollowing()); + ASSERT(this._isRejected()); this._bitField = this._bitField & (~IS_CARRYING_STACK_TRACE); this._fulfillmentHandler0 = undefined; }; Promise.prototype._isCarryingStackTrace = function () { + ASSERT(!this._isFollowing()); return (this._bitField & IS_CARRYING_STACK_TRACE) > 0; }; Promise.prototype._getCarriedStackTrace = function () { - ASSERT(this.isRejected()); + ASSERT(this._isRejected()); return this._isCarryingStackTrace() ? this._fulfillmentHandler0 : undefined; }; Promise.prototype._receiverAt = function (index) { + ASSERT(!this._isFollowing()); var ret = index === 0 ? this._receiver0 - : this[(index << 2) + index - CALLBACK_SIZE + CALLBACK_RECEIVER_OFFSET]; + : this[ + index * CALLBACK_SIZE - CALLBACK_SIZE + CALLBACK_RECEIVER_OFFSET]; //Only use the bound value when not calling internal methods if (this._isBound() && ret === undefined) { return this._boundTo; @@ -440,22 +452,25 @@ Promise.prototype._receiverAt = function (index) { }; Promise.prototype._promiseAt = function (index) { + ASSERT(!this._isFollowing()); return index === 0 ? this._promise0 - : this[(index << 2) + index - CALLBACK_SIZE + CALLBACK_PROMISE_OFFSET]; + : this[index * CALLBACK_SIZE - CALLBACK_SIZE + CALLBACK_PROMISE_OFFSET]; }; Promise.prototype._fulfillmentHandlerAt = function (index) { + ASSERT(!this._isFollowing()); ASSERT(!this._isCarryingStackTrace()); return index === 0 ? this._fulfillmentHandler0 - : this[(index << 2) + index - CALLBACK_SIZE + CALLBACK_FULFILL_OFFSET]; + : this[index * CALLBACK_SIZE - CALLBACK_SIZE + CALLBACK_FULFILL_OFFSET]; }; Promise.prototype._rejectionHandlerAt = function (index) { + ASSERT(!this._isFollowing()); return index === 0 ? this._rejectionHandler0 - : this[(index << 2) + index - CALLBACK_SIZE + CALLBACK_REJECT_OFFSET]; + : this[index * CALLBACK_SIZE - CALLBACK_SIZE + CALLBACK_REJECT_OFFSET]; }; Promise.prototype._addCallbacks = function ( @@ -465,6 +480,7 @@ Promise.prototype._addCallbacks = function ( promise, receiver ) { + ASSERT(!this._isFollowing()); var index = this._length(); if (index >= MAX_LENGTH - CALLBACK_SIZE) { @@ -507,6 +523,7 @@ Promise.prototype._addCallbacks = function ( }; Promise.prototype._setProxyHandlers = function (receiver, promiseSlotValue) { + ASSERT(!this._isFollowing()); var index = this._length(); if (index >= MAX_LENGTH - CALLBACK_SIZE) { @@ -525,21 +542,14 @@ Promise.prototype._setProxyHandlers = function (receiver, promiseSlotValue) { }; Promise.prototype._proxyPromiseArray = function (promiseArray, index) { - ASSERT(!this.isResolved()); + ASSERT(!this._isFollowing()); + ASSERT(!this._isResolved()); ASSERT(arguments.length === 2); ASSERT(typeof index === "number"); ASSERT((index | 0) === index); this._setProxyHandlers(promiseArray, index); }; -Promise.prototype._proxyPromise = function (promise) { - ASSERT(!promise._isProxied()); - ASSERT(!this.isResolved()); - ASSERT(arguments.length === 1); - promise._setProxied(); - this._setProxyHandlers(promise, -15); -}; - Promise.prototype._setBoundTo = function (obj) { if (obj !== undefined) { this._bitField = this._bitField | IS_BOUND; @@ -585,7 +595,7 @@ Promise.prototype._callHandler = function ( //(for .spread() at the moment) var x; promise._pushContext(); - if (receiver === APPLY && !this.isRejected()) { + if (receiver === APPLY && !this._isRejected()) { ASSERT(isArray(value)); x = tryCatchApply(handler, value, this._boundTo); } else { @@ -598,10 +608,9 @@ Promise.prototype._callHandler = function ( Promise.prototype._settlePromiseFromHandler = function ( handler, receiver, value, promise ) { - if (promise.isRejected()) return; + if (promise._isRejected()) return; ASSERT(!promise._isFollowingOrFulfilledOrRejected()); var x = this._callHandler(handler, receiver, promise, value); - if (promise._isFollowing()) return; if (x === errorObj || x === promise || x === NEXT_FILTER) { var err = x === promise @@ -613,37 +622,65 @@ Promise.prototype._settlePromiseFromHandler = function ( } else { x = tryConvertToPromise(x, promise); if (x instanceof Promise) { - if (x.isRejected() && + x = x._target(); + if (x._isRejected() && !x._isCarryingStackTrace() && - !canAttachTrace(x._settledValue)) { - var trace = new Error(x._settledValue + ""); + !canAttachTrace(x._reason())) { + var trace = new Error(x._reason() + ""); promise._attachExtraTrace(trace); x._setCarriedStackTrace(trace); } promise._follow(x); - promise._propagateFrom(x, PROPAGATE_CANCEL); } else { promise._fulfillUnchecked(x); } } }; +Promise.prototype._target = function() { + return this._isFollowing() ? this._followee() : this; +}; + +Promise.prototype._followee = function() { + ASSERT(this._isFollowing()); + ASSERT(this._rejectionHandler0 instanceof Promise); + return this._rejectionHandler0; +}; + +Promise.prototype._setFollowee = function(promise) { + ASSERT(this._isFollowing()); + ASSERT(!(this._rejectionHandler0 instanceof Promise)); + this._rejectionHandler0 = promise; +}; + Promise.prototype._follow = function (promise) { ASSERT(arguments.length === 1); - ASSERT(this._isFollowingOrFulfilledOrRejected() === false); + ASSERT(!this._isFollowingOrFulfilledOrRejected()); + ASSERT(!promise._isFollowing()); ASSERT(promise !== this); - this._setFollowing(); - if (promise.isPending()) { + if (promise._isPending()) { + var len = this._length(); + for (var i = 0; i < len; ++i) { + promise._addCallbacks( + this._fulfillmentHandlerAt(i), + this._rejectionHandlerAt(i), + this._progressHandlerAt(i), + this._promiseAt(i), + this._receiverAt(i) + ); + } + this._setFollowing(); + this._setLength(0); + this._setFollowee(promise); this._propagateFrom(promise, PROPAGATE_CANCEL); - promise._proxyPromise(this); - } else if (promise.isFulfilled()) { - this._fulfillUnchecked(promise._settledValue); + } else if (promise._isFulfilled()) { + this._fulfillUnchecked(promise._value()); } else { - this._rejectUnchecked(promise._settledValue, + this._rejectUnchecked(promise._reason(), promise._getCarriedStackTrace()); } - + ASSERT(this._isFollowingOrFulfilledOrRejected()); if (promise._isRejectionUnhandled()) promise._unsetRejectionIsUnhandled(); if (debugging && @@ -663,7 +700,7 @@ Promise.prototype._tryFollow = function (value) { if (!(maybePromise instanceof Promise)) { return false; } - this._follow(maybePromise); + this._follow(maybePromise._target()); return true; }; @@ -694,6 +731,7 @@ Promise.prototype._attachExtraTrace = function (error) { }; Promise.prototype._cleanValues = function () { + ASSERT(!this._isFollowing()); if (this._cancellable()) { this._cancellationParent = undefined; } @@ -723,11 +761,12 @@ Promise.prototype._reject = function (reason, carriedStackTrace) { }; Promise.prototype._settlePromiseAt = function (index) { - var handler = this.isFulfilled() + ASSERT(!this._isFollowing()); + var handler = this._isFulfilled() ? this._fulfillmentHandlerAt(index) : this._rejectionHandlerAt(index); - ASSERT(this.isFulfilled() || this.isRejected()); + ASSERT(this._isFulfilled() || this._isRejected()); var carriedStackTrace = this._isCarryingStackTrace() ? this._getCarriedStackTrace() : undefined; @@ -744,45 +783,24 @@ Promise.prototype._settlePromiseAt = function (index) { } else { this._settlePromiseFromHandler(handler, receiver, value, promise); } - } else { - var done = false; - var isFulfilled = this.isFulfilled(); - //optimization when .then listeners on a promise are - //just respective fate sealers on some other promise - if (receiver !== undefined) { - if (receiver instanceof Promise && - receiver._isProxied()) { - if (receiver.isRejected()) return; - - ASSERT(!receiver.isFulfilled() && !receiver.isRejected()); - //Must be smuggled data if proxied - receiver._unsetProxied(); - - if (isFulfilled) receiver._fulfillUnchecked(value); - else receiver._rejectUnchecked(value, - this._getCarriedStackTrace()); - done = true; - } else if (receiver instanceof PromiseArray) { - if (!receiver._isResolved()) { - if (isFulfilled) { - receiver._promiseFulfilled(value, promise); - } - else { - receiver._promiseRejected(value, promise); - } - } - done = true; + } else if (receiver instanceof PromiseArray) { + if (!receiver._isResolved()) { + if (this._isFulfilled()) { + receiver._promiseFulfilled(value, promise); + } + else { + receiver._promiseRejected(value, promise); } } - - if (!done) { - if (isFulfilled) promise._fulfill(value); - else promise._reject(value, carriedStackTrace); - } + } else if (this._isFulfilled()) { + promise._fulfill(value); + } else { + promise._reject(value, carriedStackTrace); } }; Promise.prototype._clearCallbackDataAtIndex = function(index) { + ASSERT(!this._isFollowing()); if (index === 0) { if (!this._isCarryingStackTrace()) { this._fulfillmentHandler0 = undefined; @@ -801,18 +819,6 @@ Promise.prototype._clearCallbackDataAtIndex = function(index) { } }; -Promise.prototype._isProxied = function () { - return (this._bitField & IS_PROXIED) === IS_PROXIED; -}; - -Promise.prototype._setProxied = function () { - this._bitField = this._bitField | IS_PROXIED; -}; - -Promise.prototype._unsetProxied = function () { - this._bitField = this._bitField & (~IS_PROXIED); -}; - Promise.prototype._isSettlePromisesQueued = function () { return (this._bitField & IS_SETTLE_PROMISES_QUEUED) === IS_SETTLE_PROMISES_QUEUED; @@ -827,6 +833,7 @@ Promise.prototype._unsetSettlePromisesQueued = function () { }; Promise.prototype._queueSettlePromises = function() { + ASSERT(!this._isFollowing()); if (!this._isSettlePromisesQueued()) { async.invoke(this._settlePromises, this, undefined); this._setSettlePromisesQueued(); @@ -834,7 +841,7 @@ Promise.prototype._queueSettlePromises = function() { }; Promise.prototype._fulfillUnchecked = function (value) { - ASSERT(!this.isFulfilled() && !this.isRejected()); + ASSERT(!this._isFollowingOrFulfilledOrRejected()); if (value === this) { var err = makeSelfResolutionError(); this._attachExtraTrace(err); @@ -855,7 +862,7 @@ Promise.prototype._rejectUncheckedCheckError = function (reason) { }; Promise.prototype._rejectUnchecked = function (reason, trace) { - ASSERT(!this.isFulfilled() && !this.isRejected()); + ASSERT(!this._isFollowingOrFulfilledOrRejected()); if (reason === this) { var err = makeSelfResolutionError(); this._attachExtraTrace(err); @@ -946,6 +953,7 @@ Promise.prototype._popContext = function () { }; Promise.prototype._resolveFromSyncValue = function (value) { + ASSERT(!this._isFollowing()); if (value === errorObj) { this._setRejected(); var reason = value.e; diff --git a/src/promise_array.js b/src/promise_array.js index faebae53e..950307366 100644 --- a/src/promise_array.js +++ b/src/promise_array.js @@ -46,20 +46,21 @@ PromiseArray.prototype._init = function init(_, resolveValueIfEmpty) { var values = tryConvertToPromise(this._values, undefined); if (values instanceof Promise) { - this._values = values; values._setBoundTo(this._promise._boundTo); + values = values._target(); + this._values = values; //Expect the promise to be a promise //for an array - if (values.isFulfilled()) { + if (values._isFulfilled()) { //Fulfilled promise with hopefully //an array as a resolution value - values = values._settledValue; + values = values._value(); if (!isArray(values)) { var err = new Promise.TypeError(COLLECTION_ERROR); this.__hardReject__(err); return; } - } else if (values.isPending()) { + } else if (values._isPending()) { ASSERT(typeof resolveValueIfEmpty === "number"); ASSERT(resolveValueIfEmpty < 0); values._then( @@ -72,7 +73,7 @@ PromiseArray.prototype._init = function init(_, resolveValueIfEmpty) { return; } else { values._unsetRejectionIsUnhandled(); - this._reject(values._settledValue); + this._reject(values._reason()); return; } } else if (!isArray(values)) { @@ -98,14 +99,15 @@ PromiseArray.prototype._init = function init(_, resolveValueIfEmpty) { if (this._isResolved()) return; var maybePromise = tryConvertToPromise(values[i], promise); if (maybePromise instanceof Promise) { - if (maybePromise.isPending()) { + maybePromise = maybePromise._target(); + if (maybePromise._isPending()) { // Optimized for just passing the updates through maybePromise._proxyPromiseArray(this, i); - } else if (maybePromise.isFulfilled()) { - this._promiseFulfilled(maybePromise._settledValue, i); + } else if (maybePromise._isFulfilled()) { + this._promiseFulfilled(maybePromise._value(), i); } else { maybePromise._unsetRejectionIsUnhandled(); - this._promiseRejected(maybePromise._settledValue, i); + this._promiseRejected(maybePromise._reason(), i); } } else { this._promiseFulfilled(maybePromise, i); diff --git a/src/reduce.js b/src/reduce.js index 4531c51b4..63093c452 100644 --- a/src/reduce.js +++ b/src/reduce.js @@ -26,14 +26,15 @@ function ReductionPromiseArray(promises, fn, accum, _each) { var rejected = false; var isPromise = maybePromise instanceof Promise; if (isPromise) { - if (maybePromise.isPending()) { + maybePromise = maybePromise._target(); + if (maybePromise._isPending()) { maybePromise._proxyPromiseArray(this, -1); - } else if (maybePromise.isFulfilled()) { - accum = maybePromise._settledValue; + } else if (maybePromise._isFulfilled()) { + accum = maybePromise._value(); this._gotAccum = true; } else { maybePromise._unsetRejectionIsUnhandled(); - this._reject(maybePromise.reason()); + this._reject(maybePromise._reason()); rejected = true; } } @@ -119,14 +120,15 @@ ReductionPromiseArray.prototype._promiseFulfilled = function (value, index) { if (valuesPhaseIndex !== REDUCE_PHASE_INITIAL) return; value = values[i]; if (value instanceof Promise) { - if (value.isFulfilled()) { - value = value._settledValue; - } else if (value.isPending()) { + value = value._target(); + if (value._isFulfilled()) { + value = value._value(); + } else if (value._isPending()) { // Continue later when the promise at current index fulfills return; } else { value._unsetRejectionIsUnhandled(); - return this._reject(value.reason()); + return this._reject(value._reason()); } } @@ -142,16 +144,17 @@ ReductionPromiseArray.prototype._promiseFulfilled = function (value, index) { var maybePromise = tryConvertToPromise(ret, this._promise); if (maybePromise instanceof Promise) { + maybePromise = maybePromise._target(); // Callback returned a pending // promise so continue iteration when it fulfills - if (maybePromise.isPending()) { + if (maybePromise._isPending()) { valuesPhase[i] = REDUCE_PHASE_REDUCING; return maybePromise._proxyPromiseArray(this, i); - } else if (maybePromise.isFulfilled()) { - ret = maybePromise._settledValue; + } else if (maybePromise._isFulfilled()) { + ret = maybePromise._value(); } else { maybePromise._unsetRejectionIsUnhandled(); - return this._reject(maybePromise._settledValue); + return this._reject(maybePromise._reason()); } } diff --git a/src/synchronous_inspection.js b/src/synchronous_inspection.js index cfd7e67d4..d38d7dd71 100644 --- a/src/synchronous_inspection.js +++ b/src/synchronous_inspection.js @@ -1,9 +1,11 @@ "use strict"; module.exports = function(Promise) { +var ASSERT = require("./assert.js"); function PromiseInspection(promise) { if (promise !== undefined) { + promise = promise._target(); this._bitField = promise._bitField; - this._settledValue = promise.isResolved() + this._settledValue = promise._isResolved() ? promise._settledValue //Don't keep a reference to something that will never be //used @@ -15,42 +17,85 @@ function PromiseInspection(promise) { } } +PromiseInspection.prototype.value = function () { + if (!this.isFulfilled()) { + throw new TypeError(INSPECTION_VALUE_ERROR); + } + return this._settledValue; +}; + +PromiseInspection.prototype.error = +PromiseInspection.prototype.reason = function () { + if (!this.isRejected()) { + throw new TypeError(INSPECTION_REASON_ERROR); + } + return this._settledValue; +}; + PromiseInspection.prototype.isFulfilled = -Promise.prototype.isFulfilled = function () { +Promise.prototype._isFulfilled = function () { return (this._bitField & IS_FULFILLED) > 0; }; PromiseInspection.prototype.isRejected = -Promise.prototype.isRejected = function () { +Promise.prototype._isRejected = function () { return (this._bitField & IS_REJECTED) > 0; }; PromiseInspection.prototype.isPending = -Promise.prototype.isPending = function () { +Promise.prototype._isPending = function () { return (this._bitField & IS_REJECTED_OR_FULFILLED) === 0; }; -PromiseInspection.prototype.value = -Promise.prototype.value = function () { - if (!this.isFulfilled()) { +PromiseInspection.prototype.isResolved = +Promise.prototype._isResolved = function () { + return (this._bitField & IS_REJECTED_OR_FULFILLED) > 0; +}; + +Promise.prototype.isPending = function() { + return this._target()._isPending(); +}; + +Promise.prototype.isRejected = function() { + return this._target()._isRejected(); +}; + +Promise.prototype.isFulfilled = function() { + return this._target()._isFulfilled(); +}; + +Promise.prototype.isResolved = function() { + return this._target()._isResolved(); +}; + +Promise.prototype._value = function() { + ASSERT(!this._isFollowing()); + ASSERT(this._isFulfilled()); + return this._settledValue; +}; + +Promise.prototype._reason = function() { + ASSERT(!this._isFollowing()); + ASSERT(this._isRejected()); + return this._settledValue; +}; + +Promise.prototype.value = function() { + var target = this._target(); + if (!target.isFulfilled()) { throw new TypeError(INSPECTION_VALUE_ERROR); } - return this._settledValue; + return target._settledValue; }; -PromiseInspection.prototype.error = -PromiseInspection.prototype.reason = -Promise.prototype.reason = function () { - if (!this.isRejected()) { +Promise.prototype.reason = function() { + var target = this._target(); + if (!target.isRejected()) { throw new TypeError(INSPECTION_REASON_ERROR); } - return this._settledValue; + return target._settledValue; }; -PromiseInspection.prototype.isResolved = -Promise.prototype.isResolved = function () { - return (this._bitField & IS_REJECTED_OR_FULFILLED) > 0; -}; Promise.PromiseInspection = PromiseInspection; }; diff --git a/src/thenables.js b/src/thenables.js index 2255e96dd..44537ba6d 100644 --- a/src/thenables.js +++ b/src/thenables.js @@ -33,7 +33,6 @@ function tryConvertToPromise(obj, traceParent) { ret, null ); - ret._setFollowing(); return ret; } var then = getThen(obj); diff --git a/src/timers.js b/src/timers.js index b1331cb2e..debca7300 100644 --- a/src/timers.js +++ b/src/timers.js @@ -6,7 +6,7 @@ var errors = require("./errors.js"); var apiRejection = require("./errors_api_rejection")(Promise); var TimeoutError = Promise.TimeoutError; -var afterTimeout = function (promise, parent, message) { +var afterTimeout = function (promise, message) { //Don't waste time concatting strings or creating stack traces if (!promise.isPending()) return; if (typeof message !== "string") { @@ -67,12 +67,13 @@ function failureClear(reason) { } Promise.prototype.timeout = function (ms, message) { + var target = this._target(); ms = +ms; var ret = new Promise(INTERNAL).cancellable(); ret._propagateFrom(this, PROPAGATE_ALL); - ret._follow(this); + ret._follow(target); var handle = setTimeout(function timeoutTimeout() { - afterTimeout(ret, this, message); + afterTimeout(ret, message); }, ms); return ret._then(successClear, failureClear, undefined, handle, undefined); }; diff --git a/src/using.js b/src/using.js index 62f5955c7..047fe762c 100644 --- a/src/using.js +++ b/src/using.js @@ -86,8 +86,8 @@ module.exports = function (Promise, apiRejection, tryConvertToPromise) { }; Disposer.prototype.resource = function () { - if (this.promise().isFulfilled()) { - return this.promise()._settledValue; + if (this.promise()._isFulfilled()) { + return this.promise()._value(); } return null; }; @@ -162,7 +162,7 @@ module.exports = function (Promise, apiRejection, tryConvertToPromise) { Promise.prototype.disposer = function (fn) { if (typeof fn === "function") { - return new FunctionDisposer(fn, this); + return new FunctionDisposer(fn, this._target()); } throw new TypeError(); }; diff --git a/test/mocha/following.js b/test/mocha/following.js index 404b1bf28..315a3398a 100644 --- a/test/mocha/following.js +++ b/test/mocha/following.js @@ -8,113 +8,231 @@ var rejected = adapter.rejected; var pending = adapter.pending; var Promise = adapter; -describe("a promise A that is following a promise B", function() { - specify("Must not react to fulfill/reject/progress that don't come from promise B", function(done) { - var deferred = Promise.pending(); - var promiseA = deferred.promise; - var promiseB = Promise.pending().promise; - - promiseA.then( - assert.fail, - assert.fail, - assert.fail - ); - deferred.fulfill(promiseB); - - deferred.progress(1); - deferred.fulfill(1); - deferred.reject(1); - setTimeout(function(){ - assert.equal( promiseA.isPending(), true ); - assert.equal( promiseB.isPending(), true ); - done(); - }, 30); - }); +describe("Using deferreds", function() { + describe("a promise A that is following a promise B", function() { + specify("Must not react to fulfill/reject/progress that don't come from promise B", function(done) { + var deferred = Promise.pending(); + var promiseA = deferred.promise; + var promiseB = Promise.pending().promise; + var called = 0; + function incrementCalled() { + called++; + } + + promiseA.then( + incrementCalled, + incrementCalled, + incrementCalled + ); + deferred.fulfill(promiseB); + + deferred.progress(1); + deferred.fulfill(1); + deferred.reject(1); + setTimeout(function() { + assert.equal(0, called); + assert.equal( promiseA.isPending(), true ); + assert.equal( promiseB.isPending(), true ); + done(); + }, 30); + }); - specify("Must not start following another promise C", function(done) { - var deferred = Promise.pending(); - var promiseA = deferred.promise; - var promiseB = Promise.pending().promise; - var deferredC = Promise.pending(); - var promiseC = deferredC.promise; - - promiseA.then( - assert.fail, - assert.fail, - assert.fail - ); - deferred.fulfill(promiseB); - deferred.fulfill(promiseC); - - deferredC.progress(1); - deferredC.fulfill(1); - deferredC.reject(1); - - promiseC.then(function() { - assert.equal( promiseA.isPending(), true ); - assert.equal( promiseB.isPending(), true ); - assert.equal( promiseC.isPending(), false ); - done(); + specify("Must not start following another promise C", function(done) { + var deferred = Promise.pending(); + var promiseA = deferred.promise; + var promiseB = Promise.pending().promise; + var deferredC = Promise.pending(); + var promiseC = deferredC.promise; + var called = 0; + function incrementCalled() { + called++; + } + + + promiseA.then( + incrementCalled, + incrementCalled, + incrementCalled + ); + deferred.fulfill(promiseB); + deferred.fulfill(promiseC); + + deferredC.progress(1); + deferredC.fulfill(1); + deferredC.reject(1); + + promiseC.then(function() { + assert.equal(called, 0); + assert.equal( promiseA.isPending(), true ); + assert.equal( promiseB.isPending(), true ); + assert.equal( promiseC.isPending(), false ); + done(); + }); }); - }); - specify("Must react to fulfill/reject/progress that come from promise B", function(done) { - var deferred = Promise.pending(); - var promiseA = deferred.promise; - var deferredFollowee = Promise.pending(); - var promiseB = deferredFollowee.promise; + specify("Must react to fulfill/reject/progress that come from promise B", function(done) { + var deferred = Promise.pending(); + var promiseA = deferred.promise; + var deferredFollowee = Promise.pending(); + var promiseB = deferredFollowee.promise; + var called = 0; + function incrementCalled() { + called++; + } + var c = 0; + + promiseA.then(function(v){ + c++; + assert.equal(c, 2); + assert.equal(v, 1); + assert.equal(called, 0); + done(); + }, incrementCalled, function(v){ + c++; + assert.equal(v, 1); + }); + + deferred.fulfill(promiseB); + + + deferredFollowee.progress(1); + deferredFollowee.fulfill(1); + deferredFollowee.reject(1); - var c = 0; + }); + }); +}); - promiseA.then(function(v){ - c++; - assert.equal(c, 2); - assert.equal(v, 1); +describe("Using static immediate methods", function() { + describe("a promise A that is following a promise B", function() { + specify("Should be instantly fulfilled with Bs fulfillment value if B was fulfilled", function(done) { + var val = {}; + var B = Promise.fulfilled(val); + var A = Promise.fulfilled(B); + assert.equal( A.value(), val ); + assert.equal( A.value(), B.value() ); done(); - }, assert.fail, function(v){ - c++; - assert.equal(v, 1); }); - deferred.fulfill(promiseB); - - - deferredFollowee.progress(1); - deferredFollowee.fulfill(1); - deferredFollowee.reject(1); - - }); - - specify("Should be instantly fulfilled with Bs fulfillment value if B was fulfilled", function(done) { - var val = {}; - var B = Promise.fulfilled(val); - var A = Promise.fulfilled(B); - assert.equal( A.value(), val ); - assert.equal( A.value(), B.value() ); - done(); + specify("Should be instantly fulfilled with Bs parent fulfillment value if B was fulfilled with a parent", function(done) { + var val = {}; + var parent = Promise.fulfilled(val); + var B = Promise.fulfilled(parent); + var A = Promise.fulfilled(B); + assert.equal( A.value(), val ); + assert.equal( A.value(), B.value() ); + assert.equal( A.value(), parent.value() ); + done(); + }); }); - specify("Should be instantly fulfilled with Bs parent fulfillment value if B was fulfilled with a parent", function(done) { - var val = {}; - var parent = Promise.fulfilled(val); - var B = Promise.fulfilled(parent); - var A = Promise.fulfilled(B); - assert.equal( A.value(), val ); - assert.equal( A.value(), B.value() ); - assert.equal( A.value(), parent.value() ); - done(); + describe("Rejecting a promise A with promise B", function(){ + specify("Should reject promise A with B as reason ", function(done) { + var val = {}; + var B = Promise.fulfilled(val); + var A = Promise.rejected(B); + assert.equal( A.reason(), B ); + A.caught(function(){}); + done(); + }); }); +}); +describe("Using constructor", function() { + describe("a promise A that is following a promise B", function() { + specify("Must not react to fulfill/reject/progress that don't come from promise B", function(done) { + var resolveA; + var rejectA; + var promiseA = new Promise(function() { + resolveA = arguments[0]; + rejectA = arguments[1]; + }); + var promiseB = new Promise(function(){}); + var called = 0; + function incrementCalled() { + called++; + } + + promiseA.then( + incrementCalled, + incrementCalled, + incrementCalled + ); + + resolveA(promiseB); + resolveA(1); + rejectA(1); + setTimeout(function(){ + assert.equal(0, called); + assert.equal( promiseA.isPending(), true ); + assert.equal( promiseB.isPending(), true ); + done(); + }, 30); + }); -}); + specify("Must not start following another promise C", function(done) { + var resolveA; + var promiseA = new Promise(function(){ + resolveA = arguments[0]; + }); + var promiseB = new Promise(function(){}); + var resolveC, rejectC; + var promiseC = new Promise(function(){ + resolveC = arguments[0]; + rejectC = arguments[1]; + }); + var called = 0; + function incrementCalled() { + called++; + } + + promiseA.then( + incrementCalled, + incrementCalled, + incrementCalled + ); + resolveA(promiseB); + resolveA(promiseC); + resolveC(1); + rejectC(1); + promiseC.then(function() { + assert.equal(called, 0); + assert.equal( promiseA.isPending(), true ); + assert.equal( promiseB.isPending(), true ); + assert.equal( promiseC.isPending(), false ); + done(); + }); + }); -describe("Rejecting a promise A with promise B", function(){ - specify("Should reject promise A with B as reason ", function(done) { - var val = {}; - var B = Promise.fulfilled(val); - var A = Promise.rejected(B); - assert.equal( A.reason(), B ); - A.caught(function(){}); - done(); + specify("Must react to fulfill/reject/progress that come from promise B", function(done) { + var resolveA; + var rejectA; + var promiseA = new Promise(function() { + resolveA = arguments[0]; + rejectA = arguments[1]; + }); + var resolveB, rejectB; + var promiseB = new Promise(function(){ + resolveB = arguments[0]; + rejectB = arguments[1]; + }); + var called = 0; + function incrementCalled() { + called++; + } + var c = 0; + + promiseA.then(function(v){ + c++; + assert.equal(c, 1); + assert.equal(v, 1); + assert.equal(called, 0); + done(); + }, incrementCalled); + + resolveA(promiseB); + resolveB(1); + rejectB(1); + }); }); }); diff --git a/test/mocha/generator.js b/test/mocha/generator.js index 466557732..195dcee5a 100644 --- a/test/mocha/generator.js +++ b/test/mocha/generator.js @@ -342,21 +342,22 @@ describe("Promise.coroutine", function() { }); }); -describe("custom yield handlers", function(){ - Promise.coroutine.addYieldHandler(function(v) { - if (typeof v === "number") { - return Promise.delay(v); - } - }); +describe("custom yield handlers", function() { + specify("should work with timers", function(done) { + var n = 0; + Promise.coroutine.addYieldHandler(function(v) { + if (typeof v === "number") { + n = 1; + return Promise.resolve(n); + } + }); + - specify("should work with timers", function(done){ Promise.coroutine(function*() { - var now = Date.now(); - yield 50; - var then = Date.now() - now; - return then; - })().then(function(elapsed) { - assert(elapsed > 40); + return yield 50; + })().then(function(value) { + assert.equal(value, 1); + assert.equal(n, 1); done(); }); });