Get state of Angular deferred?
Asked Answered
C

4

41

With jQuery deferreds I'm used to be able to check the current state like this:

var defer = $.Deferred();
defer.state();  //Returns the state of the deferred, eg 'resolved'

Is there a way to do the same for Angular deferreds? (or even better promises)

Cerebroside answered 6/6, 2014 at 22:24 Comment(0)
O
86

Update:

Due to refactoring of $q this is now possible although not documented:

promise.$$state.status === 0 // pending
promise.$$state.status === 1 // resolved
promise.$$state.status === 2 // rejected

Original:

Unlike most promise libraries (Bluebird,Q, when, RSVP etc), $q does not expose a synchronous inspection API.

There is no way to achieve this from the outside.

You have to call .then on the promise and code in that handler will run when the promise fulfills.

Oral answered 6/6, 2014 at 23:16 Comment(7)
On the bright side you can use a stronger Promise implementation in Angular seamlesslyOral
how-do-i-use-bluebird-with-angularRecalesce
@BenjaminGruenbaum what version of Angular is this in? 1.3.5? 1.2.28? thanksAssibilate
It looks like it was added to 1.3.0 - github.com/angular/angular.js/blob/v1.3.0/src/ng/q.js. I couldn't find any reference to $$state before that.Assibilate
status can also be -1, but I'm not sure what it signifies? github.com/angular/angular.js/blob/v1.4.3/src/ng/q.js#L365Palaeolithic
Properties start with "$$" should be considered as private API, and not used in your own code. However, there's not public API for exposing status of an Angular promise, I think.Hearts
@muttonUp: -1 appears to be a second resolved case, like 1. It is used to signify when the resolved value is a function, or if the value needs to be passed down a promise chain. I imagine -1 means something like "so far so good, but we have more code to run before the resolution is complete".Haemophilia
K
34

The answer to your question is: yes, there is a way. The other answers nicely cover the built-in limitations of $q. However, it's easy to add a state property to $q using the $provide service's decorator function.

  $provide.decorator('$q', function ($delegate) {
    var defer = $delegate.defer;
    $delegate.defer = function() {
      var deferred = defer();

      deferred.promise.state = deferred.state = 'pending';

      deferred.promise.then(function() {
        deferred.promise.state = deferred.state = 'fulfilled';
      }, function () {
        deferred.promise.state = deferred.state = 'rejected';
      }); 

      return deferred;
    };
    return $delegate;
  });

Put this decorator inside of a config block, and all $q-instantiated deferred and promise objects will have a state property with the value pending, fulfilled, or rejected.

Check out this plunk


Skeptical?

you are effectively modifying $q itself, wrapping every deferred with another deferred

Actually this is not the case. $q's original defer() constructor is called exactly one time. It is simply decorated with additional functionality by internally attaching an event handler via then. [Note that an additional defer object is instantiated as a result of the additional then callback which is automatically created with each deferred object... which is to be expected because this is how angular works internally.]

this wouldn't work because promises shouldn't be created with deferred but chained from promises that are returned from apis

Note that this code will decorate every deferred (and thus promise object) which is created by the $q service. This means that any API which utilizes $q will be automatically decorated with the state property. So regardless of how you use $q, whether with some API or on it's own, this solution decorates both the deferred object and the promise, and I have provided the plunk to prove it.


Production-worthy?

This approach is unit testable, it's guaranteed not to break any application already using $q, and it's flexible in the sense that you could later add additional decorators to $q without modifying the old one(s).

Kyungkyushu answered 7/6, 2014 at 8:0 Comment(9)
Yes this wouldn't work because promises shouldn't be created with deferred but chained from promises that are returned from apisRecalesce
I've responded to your comments in the answer.Kyungkyushu
@GilBirman I know what a provider is :) You are creating an extra deferred for every deferred (fourth line). That said, I give you points for creativity (although I hope you don't actually do this in Angular production code ;)).Oral
@BenjaminGruenbaum, I've updated the answer to explain why an additional defer object is being created, but not (as you thought) because of line 4. I don't use this code in production code, but it seems alright to me.Kyungkyushu
@BenjaminGruenbaum This may be a dumb question, but I'm curious as to why specifically you would be uncomfortable using this in production code.Hermes
@NateBarbettini well - for one thing $q went through several big changes over the lifespan of 1.2, a huge internal overhaul between 1.2 and 1.3, and almost underwent an even bigger overhaul. Today you'd do this completely differently (deferred and promises are prototype based) - you could override .then directly without the overhead of this solution (creating an overhead and allocating an extra closure for every single promise through Angular) Promises are slow in Angular as it is (getting better recently).Oral
I'm curious to hear the use-case of promises where instantiating a closure and some simple objects per promise would create any perceptible performance difference.Kyungkyushu
This definitely feels cleaner/safer than the solution in the accepted answer.Marianomaribel
This isn't working consistently in Angular 1.6.4. This only decorates promises made via $q.defer, and not every promise is generated from that method. In specific, the reject, when/resolve, and all methods specifically call new Promise() internally whereas race calls defer. The best way to handle this would be to decorate the constructor for Promise, but that entire API is internal.Quinlan
O
1

Updated:

Unfortunately this doesn't looks like its possible with $q. You'll have to put this code inside your then method.

myPromise()
.then(function() {
    // everything in here resolved
},
function() {
    // everything in here rejected
},
function() {
    // everything in here pending (with progress back)
});

Other:

This is for the Q library not angular's $q but similar.

Angular is inspired by the Q library, check out the source, its actually not that scary. https://github.com/kriskowal/q/blob/v1/q.js

You can use myPromise.inspect().state there are ['pending', 'rejected', 'fulfilled']

You also have:

myPromise.isFulfilled();
myPromise.isPending();
myPromise.isRejected();

Check out this JSfiddle and open the console for logged results. http://jsfiddle.net/S6LzP/

More granular, Looking at the defer function on line 488:

function defer() {
    // if "messages" is an "Array", that indicates that the promise has not yet
    // been resolved.  If it is "undefined", it has been resolved.  Each
    // element of the messages array is itself an array of complete arguments to
    // forward to the resolved promise.  We coerce the resolution value to a
    // promise using the `resolve` function because it handles both fully
    // non-thenable values and other thenables gracefully.
    var messages = [], progressListeners = [], resolvedPromise;

    var deferred = object_create(defer.prototype);
    var promise = object_create(Promise.prototype);

    promise.promiseDispatch = function (resolve, op, operands) {
        var args = array_slice(arguments);
        if (messages) {
            messages.push(args);
            if (op === "when" && operands[1]) { // progress operand
                progressListeners.push(operands[1]);
            }
        } else {
            nextTick(function () {
                resolvedPromise.promiseDispatch.apply(resolvedPromise, args);
            });
        }
    };

    // XXX deprecated
    promise.valueOf = function () {
        if (messages) {
            return promise;
        }
        var nearerValue = nearer(resolvedPromise);
        if (isPromise(nearerValue)) {
            resolvedPromise = nearerValue; // shorten chain
        }
        return nearerValue;
    };

    promise.inspect = function () {
        if (!resolvedPromise) {
            return { state: "pending" };
        }
        return resolvedPromise.inspect();
    };

    if (Q.longStackSupport && hasStacks) {
        try {
            throw new Error();
        } catch (e) {
            // NOTE: don't try to use `Error.captureStackTrace` or transfer the
            // accessor around; that causes memory leaks as per GH-111. Just
            // reify the stack trace as a string ASAP.
            //
            // At the same time, cut off the first line; it's always just
            // "[object Promise]\n", as per the `toString`.
            promise.stack = e.stack.substring(e.stack.indexOf("\n") + 1);
        }
    }

    // NOTE: we do the checks for `resolvedPromise` in each method, instead of
    // consolidating them into `become`, since otherwise we'd create new
    // promises with the lines `become(whatever(value))`. See e.g. GH-252.

    function become(newPromise) {
        resolvedPromise = newPromise;
        promise.source = newPromise;

        array_reduce(messages, function (undefined, message) {
            nextTick(function () {
                newPromise.promiseDispatch.apply(newPromise, message);
            });
        }, void 0);

        messages = void 0;
        progressListeners = void 0;
    }

    deferred.promise = promise;
    deferred.resolve = function (value) {
        if (resolvedPromise) {
            return;
        }

        become(Q(value));
    };

    deferred.fulfill = function (value) {
        if (resolvedPromise) {
            return;
        }

        become(fulfill(value));
    };
    deferred.reject = function (reason) {
        if (resolvedPromise) {
            return;
        }

        become(reject(reason));
    };
    deferred.notify = function (progress) {
        if (resolvedPromise) {
            return;
        }

        array_reduce(progressListeners, function (undefined, progressListener) {
            nextTick(function () {
                progressListener(progress);
            });
        }, void 0);
    };

    return deferred;
}

Mostly notably the method at the very bottom deferred.notify.

Example usage:

function requestOkText(url) {
    var request = new XMLHttpRequest();
    var deferred = Q.defer();

    request.open("GET", url, true);
    request.onload = onload;
    request.onerror = onerror;
    request.onprogress = onprogress;
    request.send();

    function onload() {
        if (request.status === 200) {
            deferred.resolve(request.responseText);
        } else {
            deferred.reject(new Error("Status code was " + request.status));
        }
    }

    function onerror() {
        deferred.reject(new Error("Can't XHR " + JSON.stringify(url)));
    }

    function onprogress(event) {
        deferred.notify(event.loaded / event.total);
    }

    return deferred.promise;
}

requestOkText("http://localhost:3000")
.then(function (responseText) {
    // If the HTTP response returns 200 OK, log the response text.
    console.log(responseText);
}, function (error) {
    // If there's an error or a non-200 status code, log the error.
    console.error(error);
}, function (progress) {
    // Log the progress as it comes in.
    console.log("Request progress: " + Math.round(progress * 100) + "%");
});
Ovotestis answered 6/6, 2014 at 23:35 Comment(2)
I think Angular's deferred's are inspired by the Q library but they're missing a lot of the functionality of Q: docs.angularjs.org/api/ng/service/$q... From what I've seen those methods aren't available on Angular promisesCerebroside
My mistake, I've quickly looked through the angular github github.com/angular/angular.js/blob/master/src/ng/q.js and it looks like this isn't possible but you should still be able to use notify. :/Ovotestis
Q
0

I whipped up a solution inspired by Gil and Travis's answers, which decorates the Promise constructor with methods closer to the Q implementation.

Note that this decoration relies on Promise.$$state. This was built for Angular 1.6.4, and theoretically should work all the way down to 1.3.x, but no guarantees on that or future releases:

(function() {
    'use strict';

    angular
        .module('your.module.name.goes.here')
        .config(configBlock);

    /** @ngInject */
    configBlock.$inject = ['$provide'];
    function configBlock($provide) {
        $provide.decorator('$q', ['$delegate', function ($delegate) {
            console.log($delegate);
            var Promise = $delegate.prototype.constructor;

            Promise.prototype.inspect = function () {
                var inspect = {};
                switch (this.$$state.status) {
                    case -1:
                    case 0:
                        inspect.state = 'pending';
                        break;
                    case 1:
                        inspect.state = 'fulfilled';
                        break;
                    case 2:
                        inspect.state = 'rejected';
                        break;
                    default:
                        inpsect.state = 'unknown';
                }
                return inspect;
            };

            Promise.prototype.isFulfilled = function () {
                return this.inspect().state === 'fulfilled';
            }
            Promise.isFulfilled = function (obj) {
                if (obj.constructor !== Promise) {
                    return true;
                }
                return obj.isFulfilled();
            }

            Promise.prototype.isRejected = function () {
                return this.inspect().state === 'rejected';
            }
            Promise.isRejected = function (obj) {
                if (obj.constructor !== Promise) {
                    return false;
                }
                return obj.isRejected();
            }

            Promise.prototype.isPending = function () {
                return this.inspect().state === 'pending';
            }
            Promise.isPending = function (obj) {
                if (obj.constructor !== Promise) {
                    return false;
                }
                return obj.isPending();
            }

            return $delegate;
        }]);
    }
})();
Quinlan answered 30/6, 2017 at 20:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.