Background
I have a hierarchy of entities that looks like (Manufacturer -> Vehicle Type -> Vehicle Model -> Vehicle Submodel). Each manufacturer has multiple vehicle types, each vehicle type has multiple models, and each model has multiple submodels.
I need to retrieve a list of vehicle models given a manufacturer and type, and then for each of those models, retrieve all of its submodels (getSubmodels). Once they're all fetched I place them into a JSON object. I'm trying to use Angular's $q module to ensure that all submodels have been retrieved before I continue execution - aka I need all of the promises to be resolved before I can move on and render the page components.
Issue:
The 'happy' path works perfectly, but if one of the requests for submodels has an error, the rejected promise from the submodel function does not get caught by the $q.all().then block, and thus the overall getVehicleHierarchy promise does not get rejected.
angular.module('uiApp.services.hierarchy', ['restangular'])
.service('VehicleHierarchy', function VehicleHierarchy($http, $q, Restangular) {
this.getVehicleHierarchy = function (manufacturerId, vehicleTypeId, vehicleModelId) {
var that = this;
var deferred = $q.defer();
var promise = deferred.promise;
var promises = [];
Restangular.configuration.baseUrl = urlBuilder.buildHierarchyServiceUrl() + '/vehicle-hierarchy';
Restangular.one('manufacturer', manufacturerId).one('type', vehicleTypeId).one('class', vehicleClassId).customGET('models')
.then(function (models) {
var result = {};
_.forEach(models.result, function (model) {
result[parseInt(model.id)] = model;
});
_.forEach(result, function (model) {
promises.push(that.getSubmodels(manufacturerId, vehicleTypeId, vehicleClassId, model.id));
});
$q.all(promises).then(function (results) {
var i = 0;
_.forEach(result, function (model) {
result[parseInt(model.id)].subModels = results[i++];
}, function (errors) {
deferred.reject(errorResponse);
});
deferred.resolve(result);
});
}, function(error) {
deferred.reject('error!');
});
return promise;
};
this.getSubmodels = function (manufacturerId, vehicleTypeId, vehicleClassId, modelId) {
var submodels = {};
var deferred = $q.defer();
Restangular.configuration.baseUrl = urlBuilder.buildHierarchyServiceUrl() + '/vehicle-hierarchy';
Restangular.one('manufacturer', brandId).one('type', vehicleTypeId).one('class', vehicleClassId).one('model', modelId)
.customGET('submodels').then(function (submodelResponse) {
_.forEach(subclassResponse.result, function (subModel) {
subclasses[parseInt(subModel.id)] = subModel;
});
deferred.resolve(subclasses);
}, function (error) {
deferred.reject('error'!);
});
return deferred.promise;
};
});
});
You need to return the $q.all call for your error function in the then to catch it:
return $q.all(promises).then(function (results) {
Related
I want to write the unit test for the factory which have lot chain of promises. Below is my code snippet:
angular.module('myServices',[])
.factory( "myService",
['$q','someOtherService1', 'someOtherService2', 'someOtherService3', 'someOtherService4',
function($q, someOtherService1, someOtherService2, someOtherService3, someOtherService4) {
method1{
method2().then(
function(){ someOtherService3.method3();},
function(error){/*log error;*/}
);
return true;
};
var method2 = function(){
var defer = $q.defer();
var chainPromise = null;
angular.forEach(myObject,function(value, key){
if(chainPromise){
chainPromise = chainPromise.then(
function(){return method4(key, value.data);},
function(error){/*log error*/});
}else{
chainPromise = method4(key, value.data);
}
});
chainPromise.then(
function(){defer.resolve();},
function(error){defer.reject(error);}
);
return defer.promise;
};
function method4(arg1, arg2){
var defer = $q.defer();
someOtherService4.method5(
function(data) {defer.resolve();},
function(error) {defer.reject(error);},
[arg1,arg2]
);
return defer.promise;
};
var method6 = function(){
method1();
};
return{
method6:method6,
method4:method4
};
}]);
To test it, I have created spy object for all the services, but mentioning the problematic one
beforeEach( function() {
someOtherService4Spy = jasmine.createSpyObj('someOtherService4', ['method4']);
someOtherService4Spy.method4.andCallFake(
function(successCallback, errorCallback, data) {
// var deferred = $q.defer();
var error = function (errorCallback) { return error;}
var success = function (successCallback) {
deferred.resolve();
return success;
}
return { success: success, error: error};
}
);
module(function($provide) {
$provide.value('someOtherService4', someOtherService4);
});
inject( function(_myService_, $injector, _$rootScope_,_$q_){
myService = _myService_;
$q = _$q_;
$rootScope = _$rootScope_;
deferred = _$q_.defer();
});
});
it("test method6", function() {
myService.method6();
var expected = expected;
$rootScope.$digest();
expect(someOtherService3.method3.mostRecentCall.args[0]).toEqualXml(expected);
expect(someOtherService4Spy.method4).toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function), [arg,arg]);
expect(someOtherService4Spy.method4).toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function), [arg,arg]);
});
It is showing error on
expect(someOtherService3.method3.mostRecentCall.args[0]).toEqualXml(expected);
After debugging I found that it is not waiting for any promise to resolve, so method 1 return true, without even executing method3. I even tried with
someOtherService4Spy.method4.andReturn(function(){return deferred.promise;});
But result remain same.
My question is do I need to resolve multiple times ie for each promise. How can I wait till all the promises are executed.
method1 does not return the promise so how would you know the asynchrounous functions it calls are finished. Instead you should return:
return method2().then(
method6 calls asynchronous functions but again does not return a promise (it returns undefined) so how do you know it is finished? You should return:
return method1();
In a test you should mock $q and have it resolve or reject to a value but I can't think of a reason why you would have a asynchronous function that doesn't return anything since you won't know if it failed and when it's done.
Method 2 could be written in a more stable way because it would currently crash if the magically appearing myObject is empty (either {} or []
var method2 = function(){
var defer = $q.defer();
var keys = Object.keys(myObject);
return keys.reduce(
function(acc,item,index){
return acc.then(
function(){return method4(keys[index],myObject[key].data);},
function(err){console.log("error calling method4:",err,key,myObject[key]);}
)
}
,$q.defer().resolve()
)
};
And try not to have magically appearing variables in your function, this could be a global variable but your code does not show where it comes from and I doubt there is a need for this to be scoped outside your function(s) instead of passed to the function(s).
You can learn more about promises here you should understand why a function returns a promise (functions not block) and how the handlers are put on the queue. This would save you a lot of trouble in the future.
I did below modification to get it working. I was missing the handling of request to method5 due to which it was in hang state. Once I handled all the request to method 5 and provided successCallback (alongwith call to digest()), it started working.
someOtherService4Spy.responseArray = {};
someOtherService4Spy.requests = [];
someOtherService4Spy.Method4.andCallFake( function(successCallback, errorCallback, data){
var request = {data:data, successCallback: successCallback, errorCallback: errorCallback};
someOtherService4Spy.requests.push(request);
var error = function(errorCallback) {
request.errorCallback = errorCallback;
}
var success = function(successCallback) {
request.successCallback = successCallback;
return {error: error};
}
return { success: success, error: error};
});
someOtherService4Spy.flush = function() {
while(someOtherService4Spy.requests.length > 0) {
var cachedRequests = someOtherService4Spy.requests;
someOtherService4Spy.requests = [];
cachedRequests.forEach(function (request) {
if (someOtherService4Spy.responseArray[request.data[1]]) {
request.successCallback(someOtherService4Spy.responseArray[request.data[1]]);
} else {
request.errorCallback(undefined);
}
$rootScope.$digest();
});
}
}
Then I modified my test as :
it("test method6", function() {
myService.method6();
var expected = expected;
var dataDict = {data1:"data1", data2:"data2"};
for (var data in dataDict) {
if (dataDict.hasOwnProperty(data)) {
someOtherService4Spy.responseArray[dataDict[data]] = dataDict[data];
}
}
someOtherService4Spy.flush();
expect(someOtherService3.method3.mostRecentCall.args[0]).toEqualXml(expected);
expect(someOtherService4Spy.method4).toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function), [arg,arg]);
});
This worked as per my expectation. I was thinking that issue due to chain of promises but when I handled the method5 callback method, it got resolved. I got the idea of flushing of requests as similar thing I was doing for http calls.
I am using the when library with Node js. I create a deffered object, place the resolve inside an encapsulated Mongoose findOne() function, and return the promise outside. But it seems my promise is always returned before the data is retrieved.
User.prototype.getProfile = function(criteria) {
var deferred = when.defer();
var options = {
criteria: criteria,
select: 'name id email'
};
this.User.load(options, function(err, data) {
if (data) {
this.name = data.name;
this.email = data.email;
this.id = data.id;
} else {
return false;
}
console.log(data);
deferred.resolve();
});
console.log('returning promise');
return deferred.promise;
};
Caller
User.getProfile(req.query).then(
function success(data) {
res.send('Hello ' + User.name);// Hello ''
}
);
Outputs 'returning promise' before the data
Yes, promise will be returned to the caller instead of the data and that is how we can take advantage of the asynchronous functions. This is the common sequence of actions in handling async calls,
Make an async call.
Return a Promise to the caller.
At this point, caller doesn't have to wait for the result. It can simply define a then function, which knows what to do when the data is ready and move on to the next task.
Later point of time, resolve (or reject, if failed) the promise when you get the result from the async call.
Execute the then function on the Promise object, with the result from the async call.
So, your code will have to be modified a little bit, like this
User.prototype.getProfile = function(criteria) {
var deferred = when.defer();
var options = {
criteria: criteria,
select: 'name id email'
};
this.User.load(options, function(err, data) {
if (err) {
// Reject, if there is an error
deferred.reject(err);
} else {
// Resolve it with actual data
deferred.resolve(data);
}
});
return deferred.promise;
};
Then your caller will do something like this
userObject.getProfile()
.then(function(profileObject) {
console.log(profileObject);
// Do something with the retrieved `profileObject`
})
.catch(function(err) {
console.err("Failed to get Profile", err);
});
// Do something else here, as you don't have to wait for the data
Here, caller just calls getProfile and attaches a function which says what to do with the returned data and moves on.
Edit If you want the same object to be updated, then you can simply use similar code, but you need to preserve this in some other variable, because the binding of this happens at runtime.
User.prototype.getProfile = function(criteria) {
var deferred = when.defer();
var options = {
criteria: criteria,
select: 'name id email'
};
var self = this;
this.User.load(options, function(err, data) {
if (err) {
// Reject, if there is an error
deferred.reject(err);
} else {
self.name = data.name;
self.email = data.email;
self.id = data.id;
}
deferred.resolve(data);
});
return deferred.promise;
};
That's how promises work.
Since you have an async task that takes some time, and JavaScript is a single threaded language, you don't want to block your code and wait for that async operation to complete itself - otherwise nobody would use JavaScript!!
So what do you do? You create a promise and continue your code.
You add callbacks to that promise and when the promise is resolved your callbacks are invoked.
I didn't use the when library but what you want to do is something like this:
User.prototype.getProfile = function(criteria){
var deferred = when.defer();
var options = {
criteria : criteria,
select : 'name id email'
};
this.User.load(options, function(err, data) {
if (data) {
this.name = data.name;
this.email = data.email;
this.id = data.id;
console.log(data);
// the callback will invoke after the deferred object is resolved.
deferred.promise.then(function(o){ console.log('resolved!!!'); });
deferred.resolve(data);
}else{
deferred.reject('something bad occured');
return false;
}
});
return deferred.promise;
};
I am using a factory to share data between multiple controllers, here is my factory
var szGetData = "some url that works";
myApp.factory('Data', function ($http) {
var eventData = {};
eventData.getEvent = function (event) {
return $http.get(szGetData, event);
}
return eventData;
});
In my controllers I call the factory the same way for each one as follows:
Data.getEvent()
.success(function (event) {
$scope.eventData = event;
})
.error(function (error) {
$scope.status = 'Unable to load customer data: ' + error.message;
});
This all works and I get the data but it calls my web-service three times and each controller has its own copy of the data. I would like to have the controllers working of the same data and only call the web-service once. thanks for your suggestions.
You can keep track of the pending promise and return the same promise to all of the subsequent callers.
Optionally, if the request fails, you can "reset" the promise so that the next time getEvent is called (after it failed) it will try again.
var szGetData = "some url that works";
myApp.factory('Data', function ($http) {
var eventData = {};
var promise;
eventData.getEvent = function (event) {
if(!promise){
promise = $http.get(szGetData, event)
.error(function(){ // this is optional
promise = false;
});
}
return promise;
}
return eventData;
});
I have 2 functions, both returning promises:
var getToken = function() {
var tokenDeferred = $q.defer();
socket.on('token', function(token) {
tokenDeferred.resolve(token);
});
//return promise
return tokenDeferred.promise;
}
var getUserId = function() {
var userIdDeferred = $q.defer();
userIdDeferred.resolve('someid');
return userIdDeferred.promise;
}
Now I have a list of topics that I would like to update as soon as these two promises get resolved
var topics = {
firstTopic: 'myApp.firstTopic.',
secondTopic: 'myApp.secondTopic.',
thirdTopic: 'myApp.thirdTopic.',
fourthTopic: 'myApp.fourthTopic.',
};
Resolved topics should look like this myApp.firstTopic.someid.sometoken
var resolveTopics = function() {
$q.all([getToken(), getUserId()])
.then(function(){
//How can I resolve these topics in here?
});
}
$q.all creates a promise that is automatically resolved when all of the promises you pass it are resolved or rejected when any of the promises are rejected.
If you pass it an array like you do then the function to handle a successful resolution will receive an array with each item being the resolution for the promise of the same index, e.g.:
var resolveTopics = function() {
$q.all([getToken(), getUserId()])
.then(function(resolutions){
var token = resolutions[0];
var userId = resolutions[1];
});
}
I personally think it is more readable to pass all an object so that you get an object in your handler where the values are the resolutions for the corresponding promise, e.g.:
var resolveTopics = function() {
$q.all({token: getToken(), userId: getUserId()})
.then(function(resolutions){
var token = resolutions.token;
var userId = resolutions.userId;
});
}
I have this class:
(function(){
"use strict";
var FileRead = function() {
this.init();
};
p.read = function(file) {
var fileReader = new FileReader();
var deferred = $.Deferred();
fileReader.onload = function(event) {
deferred.resolve(event.target.result);
};
fileReader.onerror = function() {
deferred.reject(this);
};
fileReader.readAsDataURL(file);
return deferred.promise();
};
lx.FileRead = FileRead;
}(window));
The class is called in a loop:
var self = this;
$.each(files, function(index, file){
self.fileRead.read(file).done(function(fileB64){self.fileShow(file, fileB64, fileTemplate);});
});
My question is, is there a way to call a method once the loop has completed and self.fileRead has returned it's deferred for everything in the loop?
I want it to call the method even if one or more of the deferred fails.
$.when lets you wrap up multiple promises into one. Other promise libraries have something similar. Build up an array of promises returned by fileRead.read and then pass that array to $.when and hook up then/done/fail/always methods to the promise returned by .when
// use map instead of each and put that inside a $.when call
$.when.apply(null, $.map(files, function(index, file){
// return the resulting promise
return self.fileRead.read(file).done(function(fileB64){self.fileShow(file, fileB64, fileTemplate);});
}).done(function() {
//now everything is done
})
var self = this;
var processFiles = function (data) {
var promises = [];
$.each(files, function (index, file) {
var def = data.fileRead.read(file);
promises.push(def);
});
return $.when.apply(undefined, promises).promise();
}
self.processFiles(self).done(function(results){
//do stuff
});
$.when says "when all these promises are resolved... do something". It takes an infinite (variable) number of parameters. In this case, you have an array of promises;
I know this is closed but as the doc states for $.when: In the multiple-Deferreds case where one of the Deferreds is rejected, jQuery.when immediately fires the failCallbacks for its master Deferred. (emphasis on immediately is mine)
If you want to complete all Deferreds even when one fails, I believe you need to come up with your own plugin along those lines below. The $.whenComplete function expects an array of functions that return a JQueryPromise.
var whenComplete = function (promiseFns) {
var me = this;
return $.Deferred(function (dfd) {
if (promiseFns.length === 0) {
dfd.resolve([]);
} else {
var numPromises = promiseFns.length;
var failed = false;
var args;
var resolves = [];
promiseFns.forEach(function (promiseFn) {
try {
promiseFn().fail(function () {
failed = true;
args = arguments;
}).done(function () {
resolves.push(arguments);
}).always(function () {
if (--numPromises === 0) {
if (failed) {
//Reject with the last error
dfd.reject.apply(me, args);
} else {
dfd.resolve(resolves);
}
}
});
} catch (e) {
var msg = 'Unexpected error processing promise. ' + e.message;
console.error('APP> ' + msg, promiseFn);
dfd.reject.call(me, msg, promiseFn);
}
});
}
}).promise();
};
To address the requirement, "to call the method even if one or more of the deferred fails" you ideally want an .allSettled() method but jQuery doesn't have that particular grain of sugar, so you have to do a DIY job :
You could find/write a $.allSettled() utility or achieve the same effect with a combination of .when() and .then() as follows :
var self = this;
$.when.apply(null, $.map(files, function(index, file) {
return self.fileRead.read(file).then(function(fileB64) {
self.fileShow(file, fileB64, fileTemplate);
return fileB64;//or similar
}, function() {
return $.when();//or similar
});
})).done(myMethod);
If it existed, $.allSettled() would do something similar internally.
Next, "in myMethod, how to distinguish the good responses from the errors?", but that's another question :)