Why would $q take a while to resolve - javascript

I'm writing an AngularJS plugin for Umbraco and have created a simple view, controller and service. But for some reason my promise is taking a while to resolve.
I have used the inbuilt $q service to create and return my promise, I have logged out my variables and can see when the async service finishes but there is a noticeable time difference between that and the resolve function being called.
I have since discovered the promise looks like it is waiting for Umbracos GetRemainingTimeout service before it resolves.
Can someone explain why this might be happening?
viewController.js
angular.module('umbraco')
.controller('JaywingAnalyticsHelper.ViewController', function ($scope, googleService) {
googleService.checkAuth().then(function (signedIn){
$scope.isAuthorised = signedIn;
console.log(signedIn);
});
});
googleService.js
angular.module("umbraco")
.service('googleService', function ($q) {
var clientId = 'REMOVED_FOR_PRIVACY',
scopes = ['https://www.googleapis.com/auth/analytics.readonly'],
deferred = $q.defer();
this.checkAuth = function () {
gapi.load('auth2', function () {
gapi.auth2.init().then(function () {
var googleAuth = gapi.auth2.getAuthInstance();
var signedIn = googleAuth.isSignedIn.get();
console.log(signedIn);
deferred.resolve(signedIn);
}, function(){
deferred.reject(false);
});
});
return deferred.promise;
};
});
Umbraco version - 7.5.12
Angular version - 1.1.5

After finding some time to revisit this issue I have discovered the cause of why the promise was taking so long to respond.
Most endpoints can be reached within angular by using the $http service but gapi uses its own methods to make the requests and due to the angular life-cycle it is important to call $apply which prompts angular to update any bindings or watchers.
Two links here to the documentation and another great resource:
https://code.angularjs.org/1.1.5/docs/api/ng.$rootScope.Scope#$apply
http://jimhoskins.com/2012/12/17/angularjs-and-apply.html
This was annoyingly simple and can only be blamed on my lack of angular knowledge. In my case the promise was waiting for angular to get to a certain point in its life-cycle before resolving the promise rather than updating instantly. Wrapping it in the apply function fixes this problem.
$rootScope.$apply(function(){
deferred.resolve(signedIn);
});
For those interested there was a number of steps which led me to diagnosing this issue, including:
Moving the gapi call out of the service and back into the controller
This didn't have any effect, and the promise was still taking a while to resolve.
Swapping out the gapi call for a setTimeout
Again this didn't have any effect, and the promise was still taking a while to resolve but did show that the issue wasn't directly related to gapi.
Adding multiple setTimeouts of different lengths
This was the next step as it proved that the promises were resolving at the same time even though they should be seconds apart. Two important discoveries came out of this. Interacting with the view caused the promises to resolve (some kind of life-cycle trigger) and that there is an angular version of setTimeout called $timeout
Read into why $timeout exists
This led to learning more about the angular life-cycle and the $apply function why and when to use it. Problem solved.

Related

Replacing $http with Fetch API

I'm replacing $http with Fetch API and got replaced $q with Promise API. Because of that, Angular didn't run digest cycles anymore, thus UI didn't render. To solve this problem I tried Zone.js and that seems to solve our problems partially. Unfortunately its API completely changed in 0.6 so we're using legacy 0.5.15.
Now to the actual problem.
When refreshing the page Angular configs and bootstraps the application like expected. In this phase I'm initializing the Zone and decorating the $rootScope.apply with the Zone and $rootScope.$digest(). Now when I transition between states/routes (with ui-router) everything works as expected, but when full refreshing there's a race condition and the zone/digest doesn't run correctly. I'm not sure how to fix it.
I have the following code in a angular.run() block:
console.log('Zone setup begin');
const scopePrototype = $rootScope.constructor.prototype;
const originalApply = scopePrototype.$apply;
const zoneOptions = {
afterTask: function afterTask() {
try {
$rootScope.$digest();
} catch (e) {
$exceptionHandler(e);
throw e;
}
}
};
scopePrototype.$apply = function $applyFn() : void {
const scope = this;
const applyArgs = arguments;
window.zone.fork(zoneOptions).run(() => {
originalApply.apply(scope, applyArgs);
console.log('Zone + $digest run!');
});
};
console.log('Zone setup end');
Above you can see that I log to the console when the Zone initialization begins, when it ends and when it's run (+ Angular digest cycle). In my controller where I fetch the data via Fetch API I've added a console.log('Data fetched!'); so I know when the data has been fetched.
Now the output in console:
State transition with ui-router (works perfectly)
Notice that the digest is run in the end.
Zone setup begin
Zone setup end
Zone + $digest run!
Zone + $digest run!
Zone + $digest run!
Zone + $digest run!
Data fetched!
Zone + $digest run!
Full refresh on state/route (doesn't run in the end)
Zone setup begin
Zone setup end
Zone + $digest run!
Zone + $digest run!
Zone + $digest run!
Zone + $digest run!
Data fetched!
As you can see the Zone/digest doesn't run after the data is fetched, which is why the data and UI isn't rendered on the page.
Convert the ES6 promises created by the fetch API to AngularJS $q promises with $q.when.
Use $q.when to convert ES6 promises to AngularJS promises1
AngularJS modifies the normal JavaScript flow by providing its own event processing loop. This splits the JavaScript into classical and AngularJS execution context. Only operations which are applied in the AngularJS execution context will benefit from AngularJS data-binding, exception handling, property watching, etc...2 Since the promise comes from outside the AngularJS framework, the framework is unaware of changes to the model and does not update the DOM.
Use $q.when to convert the external promise to an Angular framework promise:
var myRequest = new Request('flowers.jpg');
$q.when(fetch(myRequest)).then(function(response) {
//code here
})
Use $q Service promises that are properly integrated with the AngularJS framework and its digest cycle.
$q.when
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.
-- AngularJS $q Service API Reference - $q.when
Rationale
Wrapping $q.when will work but in my team's experience it will be very finicky and prone to error. As one example, returning $q.when from inside the body of a Promise.then function will still chain as a regular Promise and you won't get a $digest on callbacks.
It also requires all authors to understand the difference between two very similar looking constructs (Promise/$q) and care about their concrete types for every level of an asynchronous call. If you are using modern conveniences like async/await (which abstracts the Promise types further), you're gonna be in even more trouble. Suddenly none of your code can be framework agnostic.
Our team decided it was worth committing a big monkey patch to ensure all the promises (and the async/await keywords) "just worked" without needing additional thinking.
Ugly? Yes. But we felt it was an okay tradeoff.
Patch Promise callbacks to always apply $rootScope
First we install the patch against Promise in a angular.run block:
angular.module(...).run(normalizePromiseSideEffects);
normalizePromiseSideEffects.$inject = ['$rootScope'];
function normalizePromiseSideEffects($rootScope) {
attachScopeApplicationToPromiseMethod('then');
attachScopeApplicationToPromiseMethod('catch');
attachScopeApplicationToPromiseMethod('finally');
function attachScopeApplicationToPromiseMethod(methodName) {
const NativePromiseAPI = window.Promise;
const nativeImplementation = NativePromiseAPI.prototype[methodName];
NativePromiseAPI.prototype[methodName] = function(...promiseArgs) {
const newPromiseArgs = promiseArgs.map(wrapFunctionInScopeApplication);
return nativeImplementation.bind(this)(...newPromiseArgs);
};
}
function wrapFunctionInScopeApplication(fn) {
if (!isFunction(fn) || fn.isScopeApplicationWrapped) {
return fn;
}
const wrappedFn = (...args) => {
const result = fn(...args);
// this API is used since it's $q was using in AngularJS src
$rootScope.$evalAsync();
return result;
};
wrappedFn.isScopeApplicationWrapped = true;
return wrappedFn;
}
}
Async/Await
If you want to support the use of async/await, you'll also need to configure Babel to always implement the syntax as Promises. We used babel-plugin-transform-async-to-promises.

JS mock promise on http call [duplicate]

It seems that promises do not resolve in Angular/Jasmine tests unless you force a $scope.$digest(). This is silly IMO but fine, I have that working where applicable (controllers).
The situation I'm in now is I have a service which could care less about any scopes in the application, all it does it return some data from the server but the promise doesn't seem to be resolving.
app.service('myService', function($q) {
return {
getSomething: function() {
var deferred = $q.defer();
deferred.resolve('test');
return deferred.promise;
}
}
});
describe('Method: getSomething', function() {
// In this case the expect()s are never executed
it('should get something', function(done) {
var promise = myService.getSomething();
promise.then(function(resp) {
expect(resp).toBe('test');
expect(1).toEqual(2);
});
done();
});
// This throws an error because done() is never called.
// Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
it('should get something', function(done) {
var promise = myService.getSomething();
promise.then(function(resp) {
expect(resp).toBe('test');
expect(1).toEqual(2);
done();
});
});
});
What is the correct way to test this functionality?
Edit: Solution for reference. Apparently you are forced to inject and digest the $rootScope even if the service is not using it.
it('should get something', function($rootScope, done) {
var promise = myService.getSomething();
promise.then(function(resp) {
expect(resp).toBe('test');
});
$rootScope.$digest();
done();
});
You need to inject $rootScope in your test and trigger $digest on it.
there is always the $rootScope, use it
inject(function($rootScope){
myRootScope=$rootScope;
})
....
myRootScope.$digest();
So I have be struggling with this all afternoon. After reading this post, I too felt that there was something off with the answer;it turns out there is. None of the above answers give a clear explanation as to where and why to use $rootScope.$digest. So, here is what I came up with.
First off why? You need to use $rootScope.$digest whenever you are responding from a non-angular event or callback. This would include pure DOM events, jQuery events, and other 3rd party Promise libraries other than $q which is part of angular.
Secondly where? In your code, NOT your test. There is no need to inject $rootScope into your test, it is only needed in your actual angular service. That is where all of the above fail to make clear what the answer is, they show $rootScope.$digest as being called from the test.
I hope this helps the next person that comes a long that has is same issue.
Update
I deleted this post yesterday when it got voted down. Today I continued to have this problem trying to use the answers, graciously provided above. So, I standby my answer at the cost of reputation points, and as such , I am undeleting it.
This is what you need in event handlers that are non-angular, and you are using $q and trying to test with Jasmine.
something.on('ready', function(err) {
$rootScope.$apply(function(){deferred.resolve()});
});
Note that it may need to be wrapped in a $timeout in some case.
something.on('ready', function(err) {
$timeout(function(){
$rootScope.$apply(function(){deferred.resolve()});
});
});
One more note. In the original problem examples you are calling done at the wrong time. You need to call done inside of the then method (or the catch or finally), of the promise, after is resolves. You are calling it before the promise resolves, which is causing the it clause to terminate.
From the angular documentation.
https://docs.angularjs.org/api/ng/service/$q
it('should simulate promise', inject(function($q, $rootScope) {
var deferred = $q.defer();
var promise = deferred.promise;
var resolvedValue;
promise.then(function(value) { resolvedValue = value; });
expect(resolvedValue).toBeUndefined();
// Simulate resolving of promise
deferred.resolve(123);
// Note that the 'then' function does not get called synchronously.
// This is because we want the promise API to always be async, whether or not
// it got called synchronously or asynchronously.
expect(resolvedValue).toBeUndefined();
// Propagate promise resolution to 'then' functions using $apply().
$rootScope.$apply();
expect(resolvedValue).toEqual(123);
}));

httpBackend Mock AJAX ES6 Promise in $q.when

I'm trying to mock a response to a JSONP GET request which is made with a function that returns an ES6 promise which I've wrapped in $q.when(). The code itself works just fine, however, in the unit tests the request is not being caught by $httpBackend and goes through right to the actual URL. Thus when flush() is called I get an error stating Error: No pending request to flush !. The JSONP request is made via jQuery's $.getJSON() inside the ES6 promise so I opted to try and catch all outgoing requests by providing a regex instead of a hard-coded URL.
I've been searching all over trying to figure this out for a while now and still have yet to understand what's causing the call to go through. I feel as if the HTTP request in the ES6 promise is being made "outside of Angular" so $httpBackend doesn't know about it / isn't able to catch it, although that may not be the case if the call was being made inside of a $q promise from the get-go. Can anyone possibly tell me why this call is going through and why a simple timeout will work just fine? I've tried all combinations of $scope.$apply, $scope.$digest, and $httpBackend.flush() here, but to no avail.
Maybe some code will explain it better...
Controller
function homeController() {
...
var self = this;
self.getData = function getData() {
$q.when(user.getUserInformation()).then(function() {
self.username = user.username;
});
};
}
Unit Test
...
beforeEach(module('home'));
describe('Controller', function() {
var $httpBackend, scope, ctrl;
beforeEach(inject(function(_$httpBackend_, $rootScope, $componentController) {
$httpBackend = _$httpBackend_;
scope = $rootScope.$new(); // used to try and call $digest or $apply
// have also tried whenGET, when('GET', ..), etc...
$httpBackend.whenJSONP(/.*/)
.respond([
{
"user_information": {
"username": "TestUser",
}
}
]);
ctrl = $componentController("home");
}));
it("should add the username to the controller", function() {
ctrl.getData(); // make HTTP request
$httpBackend.flush(); // Error: No pending request to flush !
expect(ctrl.username).toBe("TestUser");
});
});
...
For some reason this works, however:
it("should add the username to the controller", function() {
ctrl.getData(); // make HTTP request
setTimeout(() => {
// don't even need to call flush, $digest, or $apply...?
expect(ctrl.username).toBe("TestUser");
});
});
Thanks to Graham's comment, I was brought further down a different rabbit hole due to my lack of understanding several things which I will summarize here in case someone ends up in the same situation...
I didn't fully understand how JSONP works. It doesn't rely on XmlHttpRequest at all (see here). Rather than trying to fiddle with mocking responses to these requests through JSONP I simply switched the "debug" flag on the code I was using which disabled JSONP so the calls were then being made via XHR objects (this would fail the same origin policy if real responses were needed from this external API).
Instead of trying to use jasmine-ajax, I simply set a spy on jQuery's getJSON and returned a mock response. This finally sent the mocked response to the ES6 promise, but for some reason the then function of the $q promise object which resulted from wrapping the ES6 promise wasn't being called (nor any other error-handling functions, even finally). I also tried calling $scope.$apply() pretty much anywhere in the off chance it would help, but to no avail.
Basic implementation (in unit test):
...
spyOn($, 'getJSON').and.callFake(function (url, success) {
success({"username": "TestUser"}); // send mock data
});
ctrl.getData(); // make GET request
...
Problem (in controller's source):
// user.getUserInformation() returns an ES6 promise
$q.when(user.getUserInformation()).then(function() {
// this was never being called / reached! (in the unit tests)
});
Ultimately I used #2's implementation to send the data and just wrapped the assertions in the unit test inside of a timeout with no time duration specified. I realize that's not optimal and hopefully isn't how it should be done, but after trying for many hours I've about reached my limit and given up. If anyone has any idea as to how to improve upon this, or why then isn't being called, I would honestly love to hear it.
Unit Test:
...
ctrl.getData(); // make GET request
setTimeout(() => {
expect(ctrl.username).toBe("TestUser"); // works!
});

The promise of a promise again (Angular JS)

Updated with HTTP and initial code based on requests/Please look at the bottom of the post:
I've been posting several questions on my AngularJS learning curve of late and the SO community has been fantastic. I've been a traditional C programmer when I used to program and have recently started writing my own ionic/Angular JS app. I'm struggling with the promise version of traditional async calls when it comes to converting a custom function to a promise. I don't think I really understood and I find various examples very contrived. I'd appreciate some help. I have some code which is not working, and I have some conceptual questions:
Let's take this simple function:
angular.module('zmApp.controllers').service('ZMDataModel', function() { return { getMonitors: function () { return monitors; } }
getMonitors is a simple function that basically returns an array of monitors. But here is the rub: When the app first starts, I call an http factory that does an http get and goes about populating this monitor list. This http factory is different from this service but invokes a setMonitor method in this service to populate the array. When the array is populated, a variable called 'monitorsLoaded' is set to 1. When this variable is set to 1, I know for sure monitors is loaded.
Now, I have a view with a controller called "MontageCtrl". I want to wait for the monitors to load before I show the view. In a previous post, one person suggested I use route resolve, but I had to first convert my getMonitors to a promise. So here is what I did:
angular.module('zmApp.controllers').service('ZMDataModel', function($q) {
getMonitors: function () {
var _deferred = $q.defer();
if (monitorsLoaded!=0)
{
console.log ("**** RETURNING MONITORS *****");
_deferred.resolve(monitors);
}
console.log ("*** RETURNING PROMISE ***");
return _deferred.promise;
},
Next up, in app.js I connected the route as follows:
.state('app.montage', {
data: {requireLogin:false},
resolve: {
message: function(ZMDataModel)
{
console.log ("Inside app.montage resolve");
return ZMDataModel.getMonitors();
}
},
Finally I modified my controller to grab the promise as such:
angular.module('zmApp.controllers').controller('zmApp.MontageCtrl', function($scope,$rootScope, ZMHttpFactory, ZMDataModel,message) {
//var monsize =3;
console.log ("********* Inside Montage Ctrl");
It seems based on logs, I never go inside Montage Ctrl. Route resolve seems to be waiting for ever, whereas my logs are showing that after a while, monitorLoaded is being set to 1.
I have several conceptual questions:
a) In function getMonitors, which I crafted as per examples, why do people return a _deferred.promise but only assign a _deferred.resolve? (i.e. why not return it too?). Does it automatically return?
b) I noticed that if I moved var _deferred definition to my service and out of its sub function, it did work, but the next view that had the same route dependency did not. I'm very confused.
c) Finally I ready somewhere that there is a distinction between a service and a factory when it comes to route resolve as a service is only instantiated once. I am also very confused as in some route resolve examples people use when, and I am using .state.
At this stage, I'm deep into my own confusion. Can someone help clarify? All I really want is for various views to wait till monitorsLoaded is 1. And I want to do it via route resolves and promises, so I get the hang of promises once and for all.
Added: Here is the HTTP factory code as well as the app.run code that calls this when the app first starts. FYI, the http factory works well - the problems started when I crafted ZMDataModel - I wanted this to be a central data repository for all controllers to use -- so they did not have to call HTTP Factory each time to access data, and I could control when HTTP factory needs to be called
angular.module('zmApp.controllers').factory('ZMHttpFactory', ['$http', '$rootScope','$ionicLoading', '$ionicPopup','$timeout','ZMDataModel',
function($http, $rootScope, $ionicLoading, $ionicPopup, $timeout,ZMDataModel) {
return {
getMonitors: function() {
var monitors = [];
var apiurl = ZMDataModel.getLogin().apiurl;
var myurl = apiurl+"/monitors.json";
return $http({
url: myurl,
method: 'get'
}) //http
.then(function(response) {
var data = response.data;
//console.log("****YAY" + JSON.stringify(data));
// $rootScope.$broadcast ('handleZoneMinderMonitorsUpdate',monitors);
$ionicLoading.hide();
ZMDataModel.setMonitors(data.monitors);
ZMDataModel.setMonitorsLoaded(1);
//monitors = data.monitors;
return ZMDataModel.getMonitors();
},
function (result)
{
console.log ("**** Error in HTTP");
$ionicLoading.hide();
ZMDataModel.setMonitorsLoaded(1);
//$ionicPopup.alert ({title: "Error", template:"Error retrieving Monitors. \nPlease check if your Settings are correct. "});
return ZMDataModel.getMonitors();
}
); //then
}, //getMonitors
And here is the code in app.run that first calls this:
.run(function($ionicPlatform, $ionicPopup, $rootScope, $state,ZMDataModel, ZMHttpFactory)
{
ZMDataModel.init();
var loginData = ZMDataModel.getLogin();
if ( loginData.username && loginData.password && loginData.url && loginData.apiurl)
{
console.log ("VALID CREDENTIALS. Grabbing Monitors");
// this calls http factory getMonitors that eventually populated the ZMDataModel
// monitors array and sets monitorsLoaded to 1
ZMHttpFactory.getMonitors();
}
}
I finally solved all the problems. There were various issues with my initial attempts. My final resolved solution is here Am I returning this promise correctly?
The learnings:
a) Separating the HTTP get into a factory and the data model into another service was unnecessarily complicating life. But that separation was not the problem. Infact, the way the promise was coded above, on first run, if monitorsLoaded was 0, it would simply return the deferred promise and there was no ".success" or similar construct for me to get into the resolve code block again.
b) The biggest thing that was making me run around in loops was deferring or rejecting was simply setting a state. the return always has to be the promise - and it would return the state you set. I assumed return d.promise always means returning "in progress".

How long does an angular promise live?

I'm trying to understand the life cycle of angular services and components.
Say I have a controller that uses an http service:
function MyController($scope, $http) {
$http.get('/service').success(function(data) {
// handle the response
});
}
The thing is that this controller may be attached to a view. And I want to make sure that the response is discarded if the view has been removed, this is to prevent conflicts with other requests thay may be triggered in other parts of the application. Will the instance of the controller be destroyed and with it, pending calls from the $http service be canceled if the view is removed? For example, when the user navigates away (without reloading) from the page causing a Javascript render of a new section?
[Edit] I created a jsfiddle that shows that, at least for the $timeout service, the pending operations are still running after the $scope is destroyed by navigating away. Is there a simple way to attach async operations to the scope so that they will be destroyed automatically?
First, attach a reference to your promise, then pass that reference to the cancel function. This resolves the promise with a rejections. So you could also just use promise.reject() in place of cancel(promise)
function MyController($scope, $http) {
var promise = $http.get('/service');
promise.success(function(data){
});
$scope.$on(
"$destroy",
function() {
promise.reject("scope destroyed, promise no longer available");
}
);
}

Categories