Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Reduced nextTick usage #87

Closed
wants to merge 12 commits into from

2 participants

@Twisol

Hullo! This pull request is the result of a discussion on #cujojs about reducing the number of nextTick calls incurred by chaining calls to then. It isn't perfect, but it passes (all but three of) the unit tests. In developing this patch, I also ended up refactoring some parts of the code to make it easier to understand; this culminated in entirely unifying the code paths taken by resolve/reject and progress events.

The highlights:

  1. defer now comes in two variants: an asap version that doesn't use nextTick, and a defer version that does. asap should only be used internally. This allows users to create new future computations that only incur a nextTick at the point of resolution, rather than between every promise in the chain.
  2. Progress events use the same machinery as resolve and reject events. This unifies the two event paths together, making the code a lot more understandable.
  3. A new function, shunt, is used to defer continuation of a computation until the next tick. The difference between defer and asap boils down to this guy.

Open issues:

  • Progress events are currently processed immediately, unlike the others. This seems in line with their intent, but should this be changed?
  • It would be nice to unify asap and defer in some way, since the code is literally almost identical between them. This was just the quickest way to make it work.
  • There's no way to wait on multiple promises without incurring a nextTick, as asap isn't exported. Rather than export asap directly, some and map should be updated to use asap. (Or better yet, fundamental synchronization primitives should be devised so that any multi-promise operation can be constructed. Maybe some and map fit the bill already; I just haven't looked into it.)
  • There's still some opportunity for reducing nextTick usage in defer, as it shunts every handler rather than shunting once and processing all handlers in the new turn. Same applies to thenning on a resolved promise.
@briancavalier

Hey @Twisol, thanks for getting the ball rolling here. I really like that your approach is completely different from the queueing approach that I'm exploring in parallel. We can compare different aspects of the two, borrow ideas, etc etc, and figure out which approach might be best.

I haven't fully digested everything you've done here, but some things that look especially interesting:

  1. Using promises for progress. This is very interesting, and has been discussed over at Promises/A+. We're still debating it, but it's good to experiment with it a bit to see it work. I'll look more closely at this next week.
  2. The idea of having an internal and external deferred is interesting. It's similar to the promises that the internal fulfilled() and rejected() functions create. Those should never be leaked to a caller. You can probably combine asap() and defer() by simply abstracting a parameterized _defer(config) function that asap() and defer() can use internally to create a pending object with the correct setup. However, see below about object shape optimization.

And a few initial concerns:

  1. My initial impression is that this may leak non-async promises to a caller. I'll add some unit tests for this. The idea is that there should be no way for a caller to ever be able to get a reference to a promise that will invoke onFulfilled/onRejected/onProgress immediately.
  2. Additionally, when() must guarantee that onFulfilled/onRejected/onProgress are never called immediately, even if you pass a synchronous/fake/busted promise to it. For example, you must be able to do when({ then: function(onFulfilled) { onFulfilled(123); } }, myOnFulfilled) and it must guarantee that myOnFulfilled is invoked in a future turn.
  3. defer() was carefully crafted to optimize the shape of the deferred object by specifying all of its fields in the object literal declaration. Believe it or not, this allows the JIT to create them much faster. For systems that create lots of deferreds, the difference can be significant. Hopefully, it's possible to retain this optimization even if you refactor asap() and defer() to share some common _defer(options) helper.
  4. It looks like there are a couple new levels of indirection in this code. At initial glance, it appears to call then() a few more times internally, and calls shunt() several times. I have no idea how this affects performance. It may be fine, or may be completely offset by your new next tick handling strategy. Just something I noticed.

I'll take a closer look on Monday. I'll also add some unit tests for 1 and 2, then you can rebase on async-resolutions again and that should help make sure you aren't leaking sync promises.

Thanks again! Def looking forward to exploring this more!

@Twisol

I glanced back at the original code, and it looks like progress events are processed asynchronously, so I'm trying to change my code to do the same. Unfortunately, throwing a shunt('progress') into defer's then causes a bunch more tests to fail, so I'm not sure how to go about doing this. I was able to put a shunt('progress') into when without incident.

@briancavalier

Unfortunately, using a named function expression that is named the same as the property causes a memory leak in older IE :( It's safe to ditch the function name--that'll prevent a leak, e.g. resolve: function(val) {, or to use a different function name, such as promiseResolve.

@briancavalier

Hmmm, it seems like something here is causing a node process not to exit when at least deferred has been created, regardless of whether it has been left pending, or has been fulfilled or rejected. I'm not sure what is causing it yet, but I know that node will not exit until the nextTick queue has been processed, so maybe something here is caught in a loop placing things on node's nextTick queue ... or maybe its something else entirely, not sure.

@briancavalier

Here's a gist that shows what looks like deadlock that would explain node not exiting. This is actually a good little test program, as well. As a calibration, the queueing implementation can finish this test in under 60 millis (if you remove the console.log in function f, with console.log usually around 170 millis). Give it a try, it may shed some light on the node-not-exiting problem.

@Twisol

It's not deadlock - you're actually bumping up against the call stack's maximum height. (I threw a console.log into the catch block in fulfilled, since the error wasn't able to propagate.) In a way, this makes sense - p.then(f).then(g) is basically equivalent to p.then(function(value) { return g(f(value)); }), and you get the same behavior if you try this without promises (but stopping around 14000 instead of 2371, and without keeping node from exiting).

It's probably possible to trampoline subsequent handlers from the same point in the stack, which is effectively manually optimizing tail-calls. That should prevent thenning from eating up too much of the stack. Of course, I still have no idea why node hung in the event loop in the first place.

@briancavalier

Ah yes, you're right. I should have known. When.js 1.x will also blow the stack since it is fully synchronous. I'm also still not sure why node is hanging even in cases where the stack is ok. I've seen this happen in at least one other promise lib, but I can't remember which one or what the cause was. I'll try to remember.

I like that the queuing approach can handle this case, especially since a pure nextTick approach will handle it, too. Other async promise impls currently handle it.

One thing I realized about the queue approach is that it can potentially starve the host environment's tick/timer queue. So I've been playing with adaptive queue processing strategies that introduce a nextTick into the queue processing periodically in order to yield a little time back to the host env. Maybe the same kind of thing would work here, i.e. reset the stack so that it doesn't blow up for really long promise chains. Sounds challenging to implemen, but may be necessary.

@Twisol

I've written a proof of concept for a solution in this gist, but I haven't integrated it into when.js yet. It scopes the queue/trampoline so that promises can't interfere with each other, and removes the nextTick logic from the deferred itself entirely (which would unify deferred and asap again).

@Twisol

Quick update: I'm running into some lovely issues with my current implementation, like - at the moment - a recursive infinite loop. I'm working on hammering these out, as I quite like this approach otherwise.

@briancavalier

Cool, thanks for continuing to investigate this.

I've merged a similar trampoline-style approach into the dev-200 branch. It tries to avoid starving the tick queue, but also increases the trampoline queue size (to a point) if you keep throwing large numbers of promise handlers at it.

One interesting thing I've learned about this approach is that for very short promise chains (10 promises or fewer) in systems where tasks must be isolated and forced to run sequentially (i.e. very limited concurrency), it degrades into being no better than using a "regular" nextTick impl. However, as the promise chains get longer, and as the system allows more concurrency (time-sliced concurrency, as in JS), the trampoline outperforms straight nextTick by larger and larger amounts.

My guess is that in real systems, the trampoline generally will perform better, possibly much better, even when using setTimeout, but there will be some cases where it could actually be slower when it has to rely on setTimeout. In those cases, though, using a setImmediate polyfill seems to level the playing field.

So, it seems like it'll be no worse than straight nextTick in the worst cases, and potentially much better in the best cases.

Keep me posted on how yours is coming along, and we can compare!

@briancavalier

Hey @Twisol, just wondering if you're still exploring ideas here. If you're happy with the approach in dev-200, let's close this. If not, I don't mind leaving it open if you plan to do more experiments.

Thanks!

@Twisol

Hey Brian,

Sorry for the silence on my part! The semester started up and I've had to re-prioritize my time. I did spend some time last night experimenting, and things are coming along nicely. I'll post my results in a couple days at the latest; otherwise I'm good with what you have.

@briancavalier

No worries, I understand. I'll leave this open for a while longer, and we can discuss your latest ideas. I'm pretty happy with what's in dev-200 right now, but definitely looking forward to seeing what you've been experimenting with.

@Twisol

Ta-da! https://gist.github.com/Twisol/869d40fa63244a5a6442

This isn't a complete implementation - it's missing some of the niceties when.js provides by default, as well as progress events, and some guarantees made by Promises/A+ - but the infrastructure is all there. I thought I should post what I have now before it gets much busier when I add the rest.

The idea here is that you have a Trigger (a resolver in your terminology) with an associated trampoline. When the trigger fires, it bounces any registered handlers back up to the trampoline, which resets the call stack. Furthermore, only the first Trigger in a promise chain - that being the one returned from the public API - delays execution, ensuring that consecutive callbacks don't have unnecessary nextTicks in-between.

As far as adaptive handling of the handler queue in your implementation, I feel like that's something the user should be responsible for, rather than the library itself. You run into the same problem whether you're using promises or not; handling it implicitly in the case of promises breaks a desirable symmetry.

@briancavalier

I haven't had time to dig into this in depth, but it looks similar in concept. I definitely like some things I saw when I skimmed it, like using method replacement to optimize some cases--I'm a big fan :) (as you probably noticed that when.js uses method replacement to optimize some things)

You might be right about not worrying about starving the tick queue ... I may revert that. Simplicity FTW :) If people run into problems, we could reinstate it.

I'll take a closer look soon.

@Twisol

Cool, glad to hear it. I have almost all tests passing now; the only issues are coming from poll.js, where everything is timing out. Not sure what's going on there. Everything else, including the Promises/A+ test suite, is green.

My code is totally different than the code in this pull request - I started completely fresh. Should I create a new pull request, or fiddle around with merging it?

@briancavalier

If it's that different, then let's do a new PR. I'll close this one and continue the discussion in the new one.

@Twisol

Until then, I've updated the gist with my changes.

EDIT: Okay, I'll do that.

@briancavalier

Cool, thanks.

@Twisol Twisol deleted the branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 11, 2013
  1. @Twisol

    Reorder some definitions to logically group functions together.

    Twisol authored
    Make some pairs of functions implemented symmetrically (fulfilled/rejected, resolve/reject)
  2. @Twisol
  3. @Twisol
Commits on Jan 12, 2013
  1. @Twisol
  2. @Twisol

    Normalize progress events, making them share the same machinery as re…

    Twisol authored
    …solve and reject.
    
    The two failing tests appear to be because they assume callbacks occur in the same turn as the call to resolve().
  3. @Twisol

    Add a few comments.

    Twisol authored
  4. @Twisol

    Implement deferred.resolve() symmetrically to deferred.reject() and d…

    Twisol authored
    …eferred.progress() to more clearly highlight intent.
  5. @Twisol
  6. @Twisol

    Make the shape of the deferred object clear up-front, as an attempt t…

    Twisol authored
    …o preserve object shape optimizations.
  7. @Twisol
  8. @Twisol
  9. @Twisol

    Remove a `nextTick` that was actually totally useless, because `shunt…

    Twisol authored
    …` already does this for us.
This page is out of date. Refresh to see the latest.
Showing with 277 additions and 192 deletions.
  1. +277 −192 when.js
View
469 when.js
@@ -44,97 +44,6 @@ define(function () {
: function(task) { setTimeout(task, 0); };
/**
- * Register an observer for a promise or immediate value.
- *
- * @param {*} promiseOrValue
- * @param {function?} [onFulfilled] callback to be called when promiseOrValue is
- * successfully fulfilled. If promiseOrValue is an immediate value, callback
- * will be invoked immediately.
- * @param {function?} [onRejected] callback to be called when promiseOrValue is
- * rejected.
- * @param {function?} [onProgress] callback to be called when progress updates
- * are issued for promiseOrValue.
- * @returns {Promise} a new {@link Promise} that will complete with the return
- * value of callback or errback or the completion value of promiseOrValue if
- * callback and/or errback is not supplied.
- */
- function when(promiseOrValue, onFulfilled, onRejected, onProgress) {
- // Get a trusted promise for the input promiseOrValue, and then
- // register promise handlers
- return resolve(promiseOrValue).then(onFulfilled, onRejected, onProgress);
- }
-
- /**
- * Returns promiseOrValue if promiseOrValue is a {@link Promise}, a new Promise if
- * promiseOrValue is a foreign promise, or a new, already-fulfilled {@link Promise}
- * whose value is promiseOrValue if promiseOrValue is an immediate value.
- *
- * @param {*} promiseOrValue
- * @returns Guaranteed to return a trusted Promise. If promiseOrValue is a when.js {@link Promise}
- * returns promiseOrValue, otherwise, returns a new, already-resolved, when.js {@link Promise}
- * whose resolution value is:
- * * the resolution value of promiseOrValue if it's a foreign promise, or
- * * promiseOrValue if it's a value
- */
- function promiseFor(promiseOrValue) {
- var promise, deferred;
-
- if(promiseOrValue instanceof Promise) {
- // It's a when.js promise, so we trust it
- promise = promiseOrValue;
-
- } else {
- // It's not a when.js promise. See if it's a foreign promise or a value.
- if(isPromise(promiseOrValue)) {
- // It's a thenable, but we don't know where it came from, so don't trust
- // its implementation entirely. Introduce a trusted middleman when.js promise
- deferred = defer();
-
- // IMPORTANT: This is the only place when.js should ever call .then() on an
- // untrusted promise. Don't expose the return value to the untrusted promise
- promiseOrValue.then(
- function(value) { deferred.resolve(value); },
- function(reason) { deferred.reject(reason); },
- function(update) { deferred.progress(update); }
- );
-
- promise = deferred.promise;
-
- } else {
- // It's a value, not a promise. Create a resolved promise for it.
- promise = fulfilled(promiseOrValue);
- }
- }
-
- return promise;
- }
-
- /**
- * Returns a fulfilled promise. If promiseOrValue is a value, it will be the fulfillment
- * value of the returned promise. If promiseOrValue is a promise, the returned promise will
- * parallel the state and value/reason of promiseOrValue.
- * @param {*} promiseOrValue the fulfillment value or a promise with whose state will be paralleled
- * @return {Promise} fulfilled promise or pending promise paralleling the state of promiseOrValue.
- */
- function resolve(promiseOrValue) {
- return defer().resolve(promiseOrValue);
- }
-
- /**
- * Returns a rejected promise for the supplied promiseOrValue. The returned
- * promise will be rejected with:
- * - promiseOrValue, if it is a value, or
- * - if promiseOrValue is a promise
- * - promiseOrValue's value after it is fulfilled
- * - promiseOrValue's reason after it is rejected
- * @param {*} promiseOrValue the rejected value of the returned {@link Promise}
- * @return {Promise} rejected {@link Promise}
- */
- function reject(promiseOrValue) {
- return when(promiseOrValue, rejected);
- }
-
- /**
* Trusted Promise constructor. A Promise created from this constructor is
* a trusted when.js promise. Any other duck-typed promise is considered
* untrusted.
@@ -208,7 +117,7 @@ define(function () {
function fulfilled(value) {
var p = new Promise(function(onFulfilled) {
try {
- return promiseFor(typeof onFulfilled == 'function' ? onFulfilled(value) : value);
+ return (typeof onFulfilled === 'function') ? promiseFor(onFulfilled(value)) : p;
} catch(e) {
return rejected(e);
}
@@ -228,7 +137,7 @@ define(function () {
function rejected(reason) {
var p = new Promise(function(_, onRejected) {
try {
- return promiseFor(typeof onRejected == 'function' ? onRejected(reason) : rejected(reason));
+ return (typeof onRejected === 'function') ? promiseFor(onRejected(reason)) : p;
} catch(e) {
return rejected(e);
}
@@ -237,23 +146,139 @@ define(function () {
return p;
}
+ function progressing(update) {
+ var p = new Promise(function(_, __, onProgress) {
+ try {
+ return (typeof onProgress === 'function') ? progressing(onProgress(update)) : p;
+ } catch(e) {
+ return progressing(e);
+ }
+ });
+
+ return p;
+ }
+
/**
- * Creates a new, Deferred with fully isolated resolver and promise parts,
- * either or both of which may be given out safely to consumers.
- * The Deferred itself has the full API: resolve, reject, progress, and
- * then. The resolver has resolve, reject, and progress. The promise
- * only has then.
+ * Determines if promiseOrValue is a promise or not. Uses the feature
+ * test from http://wiki.commonjs.org/wiki/Promises/A to determine if
+ * promiseOrValue is a promise.
*
- * @return {Deferred}
+ * @param {*} promiseOrValue anything
+ * @returns {boolean} true if promiseOrValue is a {@link Promise}
*/
- function defer() {
- var deferred, promise, handlers, progressHandlers,
- _bind, _progress, _resolve;
- /**
- * The promise for the new deferred
- * @type {Promise}
- */
- promise = new Promise(then);
+ function isPromise(promiseOrValue) {
+ return promiseOrValue && typeof promiseOrValue.then === 'function';
+ }
+
+ /**
+ * Returns promiseOrValue if promiseOrValue is a {@link Promise}, a new Promise if
+ * promiseOrValue is a foreign promise, or a new, already-fulfilled {@link Promise}
+ * whose value is promiseOrValue if promiseOrValue is an immediate value.
+ *
+ * @param {*} promiseOrValue
+ * @returns Guaranteed to return a trusted Promise. If promiseOrValue is a when.js {@link Promise}
+ * returns promiseOrValue, otherwise, returns a new, already-resolved, when.js {@link Promise}
+ * whose resolution value is:
+ * * the resolution value of promiseOrValue if it's a foreign promise, or
+ * * promiseOrValue if it's a value
+ */
+ function promiseFor(promiseOrValue) {
+ var deferred;
+
+ if (!isPromise(promiseOrValue)) {
+ // It's a value, not a promise. Create a resolved promise for it.
+ return fulfilled(promiseOrValue);
+ } else if (promiseOrValue instanceof Promise) {
+ // It's a when.js promise, so we trust it
+ return promiseOrValue;
+ } else {
+ // It's a thenable, but we don't know where it came from, so don't trust
+ // its implementation entirely. Introduce a trusted middleman when.js promise
+ deferred = asap();
+
+ // IMPORTANT: This is the only place when.js should ever call .then() on an
+ // untrusted promise. Don't expose the return value to the untrusted promise
+ promiseOrValue.then(
+ function(value) { deferred.resolve(value); },
+ function(reason) { deferred.reject(reason); },
+ function(update) { deferred.progress(update); }
+ );
+
+ return deferred.promise;
+ }
+ }
+
+
+ // Shunt a promise into the next turn of the event loop
+ // Could be made more efficient by pre-caching the results of
+ // shunt() for "resolve", "reject", and "progress".
+ function shunt(type) {
+ return function(value) {
+ var deferred = asap();
+ nextTick(function() {
+ deferred[type](value);
+ });
+ return deferred.promise;
+ };
+ }
+
+ /**
+ * Register an observer for a promise or immediate value.
+ *
+ * @param {*} promiseOrValue
+ * @param {function?} [onFulfilled] callback to be called when promiseOrValue is
+ * successfully fulfilled. If promiseOrValue is an immediate value, callback
+ * will be invoked immediately.
+ * @param {function?} [onRejected] callback to be called when promiseOrValue is
+ * rejected.
+ * @param {function?} [onProgress] callback to be called when progress updates
+ * are issued for promiseOrValue.
+ * @returns {Promise} a new {@link Promise} that will complete with the return
+ * value of callback or errback or the completion value of promiseOrValue if
+ * callback and/or errback is not supplied.
+ */
+ function when(promiseOrValue, onFulfilled, onRejected, onProgress) {
+ // Get a trusted promise for the input promiseOrValue, and then
+ // register promise handlers
+ var promise = promiseFor(promiseOrValue);
+
+ // If we're not continuing an existing future computation, start one now.
+ if (!(promiseOrValue instanceof Promise)) {
+ promise = promise.then(shunt('resolve'), shunt('reject'), shunt('progress'));
+ }
+
+ return promise.then(onFulfilled, onRejected, onProgress);
+ }
+
+ /**
+ * Returns a fulfilled promise. If promiseOrValue is a value, it will be the fulfillment
+ * value of the returned promise. If promiseOrValue is a promise, the returned promise will
+ * parallel the state and value/reason of promiseOrValue.
+ * @param {*} promiseOrValue the fulfillment value or a promise with whose state will be paralleled
+ * @return {Promise} fulfilled promise or pending promise paralleling the state of promiseOrValue.
+ */
+ function resolve(promiseOrValue) {
+ return when(promiseOrValue, fulfilled);
+ }
+
+ /**
+ * Returns a rejected promise for the supplied promiseOrValue. The returned
+ * promise will be rejected with:
+ * - promiseOrValue, if it is a value, or
+ * - if promiseOrValue is a promise
+ * - promiseOrValue's value after it is fulfilled
+ * - promiseOrValue's reason after it is rejected
+ * @param {*} promiseOrValue the rejected value of the returned {@link Promise}
+ * @return {Promise} rejected {@link Promise}
+ */
+ function reject(promiseOrValue) {
+ return when(promiseOrValue, rejected);
+ }
+
+
+ function asap() {
+ var deferred, promise, handlers,
+ _then, _progress, _resolve;
/**
* The full Deferred object, with {@link Promise} and {@link Resolver} parts
@@ -261,46 +286,46 @@ define(function () {
* @name Deferred
*/
deferred = {
- then: then, // DEPRECATED: use deferred.promise.then
- resolve: promiseResolve,
- reject: promiseReject,
- // TODO: Consider renaming progress() to notify()
- progress: promiseProgress,
+ // DEPRECATED: use deferred.promise.then
+ then: function(onFulfilled, onRejected, onProgress) {
+ return _then(onFulfilled, onRejected, onProgress);
+ },
- promise: promise,
+ resolve: function(val) {
+ return _resolve(promiseFor(val));
+ },
- resolver: {
- resolve: promiseResolve,
- reject: promiseReject,
- progress: promiseProgress
- }
+ reject: function(reason) {
+ return _resolve(rejected(reason));
+ },
+
+ // TODO: Consider renaming progress() to notify()
+ progress: function(update) {
+ return _progress(progressing(update));
+ },
+
+ promise: null,
+ resolver: null
};
+ /**
+ * The promise for the new deferred
+ * @type {Promise}
+ */
+ promise = new Promise(deferred.then);
+
handlers = [];
- progressHandlers = [];
-
- _bind = function(onFulfilled, onRejected, onProgress, next) {
- var progressHandler = typeof onProgress === 'function'
- ? function(update) {
- try {
- // Allow progress handler to transform progress event
- next.progress(onProgress(update));
- } catch(e) {
- // Use caught value as progress
- next.progress(e);
- }
- }
- : next.progress;
+
+ _then = function(onFulfilled, onRejected, onProgress) {
+ var next = asap();
handlers.push(function(promise) {
- promise.then(onFulfilled, onRejected).then(
- function(value) { next.resolve(value); },
- function(reason) { next.reject(reason); },
- progressHandler
- );
+ promise
+ .then(onFulfilled, onRejected, onProgress)
+ .then(next.resolve, next.reject, next.progress);
});
- progressHandlers.push(progressHandler);
+ return next.promise;
};
/**
@@ -309,7 +334,7 @@ define(function () {
* @param {*} update progress event payload to pass to all listeners
*/
_progress = function(update) {
- processQueue(progressHandlers, update);
+ processQueue(handlers, update);
return update;
};
@@ -320,83 +345,145 @@ define(function () {
* @param {*} value the value of this deferred
*/
_resolve = function(value) {
-
- value = promiseFor(value);
-
// Replace _resolve so that this Deferred can only be completed once
// Make _progress a noop, to disallow progress for the resolved promise.
+ // Make _then invoke callbacks "immediately"
_resolve = resolve;
_progress = noop;
-
- // Make _bind invoke callbacks "immediately"
- _bind = function(fulfilled, rejected, _, next) {
- nextTick(function() {
- value.then(fulfilled, rejected).then(
- function(value) { next.resolve(value); },
- function(reason) { next.reject(reason); },
- function(update) { next.progress(update); }
- );
- });
+ _then = function(onFulfilled, onRejected, onProgress) {
+ return value
+ .then(shunt('resolve'), shunt('reject'))
+ .then(onFulfilled, onRejected, onProgress);
};
// Notify handlers
processQueue(handlers, value);
- handlers = progressHandlers = undef;
+ handlers = undef;
return promise;
};
+ deferred.promise = promise;
+
+ deferred.resolver = {
+ resolve: deferred.resolve,
+ reject: deferred.reject,
+ progress: deferred.progress
+ };
+
return deferred;
+ }
+
+ /**
+ * Creates a new, Deferred with fully isolated resolver and promise parts,
+ * either or both of which may be given out safely to consumers.
+ * The Deferred itself has the full API: resolve, reject, progress, and
+ * then. The resolver has resolve, reject, and progress. The promise
+ * only has then.
+ *
+ * @return {Deferred}
+ */
+ function defer() {
+ var deferred, promise, handlers,
+ _then, _progress, _resolve;
/**
- * Wrapper to allow _then to be replaced safely
- * @param [onFulfilled] {Function} resolution handler
- * @param [onRejected] {Function} rejection handler
- * @param [onProgress] {Function} progress handler
- * @return {Promise} new Promise
+ * The full Deferred object, with {@link Promise} and {@link Resolver} parts
+ * @class Deferred
+ * @name Deferred
*/
- function then(onFulfilled, onRejected, onProgress) {
- var deferred = defer();
+ deferred = {
+ // DEPRECATED: use deferred.promise.then
+ then: function(onFulfilled, onRejected, onProgress) {
+ return _then(onFulfilled, onRejected, onProgress);
+ },
- _bind(onFulfilled, onRejected, onProgress, deferred);
+ resolve: function(val) {
+ return _resolve(promiseFor(val));
+ },
- return deferred.promise;
- }
+ reject: function(reason) {
+ return _resolve(rejected(reason));
+ },
+
+ // TODO: Consider renaming progress() to notify()
+ progress: function(update) {
+ return _progress(progressing(update));
+ },
+
+ promise: null,
+ resolver: null
+ };
/**
- * Wrapper to allow _resolve to be replaced
+ * The promise for the new deferred
+ * @type {Promise}
*/
- function promiseResolve(val) {
- return _resolve(val);
- }
+ promise = new Promise(deferred.then);
+
+ handlers = [];
+
+ _then = function(onFulfilled, onRejected, onProgress) {
+ var next = asap();
+
+ handlers.push(function(promise) {
+ promise
+ .then(shunt('resolve'), shunt('reject'))
+ .then(onFulfilled, onRejected, onProgress)
+ .then(next.resolve, next.reject, next.progress);
+ });
+
+ return next.promise;
+ };
/**
- * Wrapper to allow _reject to be replaced
+ * Issue a progress event, notifying all progress listeners
+ * @private
+ * @param {*} update progress event payload to pass to all listeners
*/
- function promiseReject(reason) {
- return _resolve(rejected(reason));
- }
+ _progress = function(update) {
+ processQueue(handlers, update);
+ return update;
+ };
/**
- * Wrapper to allow _progress to be replaced
+ * Transition from pre-resolution state to post-resolution state, notifying
+ * all listeners of the resolution or rejection
+ * @private
+ * @param {*} value the value of this deferred
*/
- function promiseProgress(update) {
- return _progress(update);
- }
- }
+ _resolve = function(value) {
+ // Replace _resolve so that this Deferred can only be completed once
+ // Make _progress a noop, to disallow progress for the resolved promise.
+ _resolve = resolve;
+ _progress = noop;
- /**
- * Determines if promiseOrValue is a promise or not. Uses the feature
- * test from http://wiki.commonjs.org/wiki/Promises/A to determine if
- * promiseOrValue is a promise.
- *
- * @param {*} promiseOrValue anything
- * @returns {boolean} true if promiseOrValue is a {@link Promise}
- */
- function isPromise(promiseOrValue) {
- return promiseOrValue && typeof promiseOrValue.then === 'function';
+ // Make _then invoke callbacks "immediately"
+ _then = function(onFulfilled, onRejected, onProgress) {
+ return value
+ .then(shunt('resolve'), shunt('reject'))
+ .then(onFulfilled, onRejected, onProgress);
+ };
+
+ // Notify handlers
+ processQueue(handlers, value);
+ handlers = undef;
+
+ return promise;
+ };
+
+ deferred.promise = promise;
+
+ deferred.resolver = {
+ resolve: deferred.resolve,
+ reject: deferred.reject,
+ progress: deferred.progress
+ };
+
+ return deferred;
}
+
/**
* Initiates a competitive race, returning a promise that will resolve when
* howMany of the supplied promisesOrValues have resolved, or will reject when
@@ -652,12 +739,10 @@ define(function () {
* @param {*} value argument passed to each function
*/
function processQueue(queue, value) {
- nextTick(function() {
- var handler, i = 0;
- while (handler = queue[i++]) {
- handler(value);
- }
- });
+ var handler, i = 0;
+ while (handler = queue[i++]) {
+ handler(value);
+ }
}
/**
Something went wrong with that request. Please try again.