Is it possible to create a promise loop until reject in angular - javascript

I wish to load ~10000 resources and doing this all at once during the resolve phase takes a bit too long due to certain calculations being done. So then I came to the idea to load the resources page by page sequentially, however since all these resources need to be visible (on a map) standard, user-input based pagination, doesn't really work.
I know that promises can be chained like:
promise.then(doThis).then(doThat).then(doWhat);
And I know that an array of promises can be resolved with $q.all like:
$q.all([doThis, doThat, doWhat]);
However what I want to is to call the same promise again and again in series until I hit a rejection.
Example:
function next() {
var deferred = $q.defer();
if(someCondition) {
deferred.reject();
} else {
//do something
//store data somewhere
deferred.resolve();
}
return deferred.promise;
}
Let's say that this function does some $http calls and stores the result somewhere in the service/controller. If it hits a certain condition (perhaps there aren't any pages anymore or an http error) it rejects a promise, otherwise it resolves it.
Now I'd like to do something like this pseudocode
$q.loop(next).untilError(handleError);
Where next will be called in a loop upon resolving the previous next call, until rejection.
Is something like this possible?

Check the console of this demo: JSFiddle.
It ensures the calling of the APIs are using userId from 1 to 5 sequentially. And stop at some condition (userId > 5).
angular.module('Joy', [])
.controller('JoyCtrl', ['$scope', '$http', function ($scope, $http) {
getUser(1, getUser);
function getUser(userId, next) {
if (userId > 5) {
console.log('Enough. Stop');
return;
}
$http.get('http://jsonplaceholder.typicode.com/posts?userId=' + userId).then(function (data) {
console.log(data.data);
next(userId + 1, next);
});
}
}]);

Related

Resolving a promise in Angular Service

We have a service, lets call it AccountService which exposes a method called getAccounts(customerId) among others.
In its implementation all it does is to fire up a $http GET request and return a promise to the calling controller which will put the returned array of accounts in the controller scope once resolved.
On a simplified view all looks like below:
// The service
.factory('AccountService', ['$http', function($http) {
var _getAccounts = function(customerId) {
var request = {
'method': 'GET',
'url': 'http://localhost:8081/accounts/' + customerId
};
return $(request);
};
return {
getAccounts: _getAccounts
};
}]);
// Inside the conntroller
AccountService.getAccounts($scope.customerId)
.then(function(response) {
$scope.accounts = response.data;
});
So once the promise will resolve the controller scope will get populated with the list of accounts.
Note that I kept the above code as simple as I could to get you the idea of what my problem is but in reality it will be code to deal with exceptions, watcher to refresh, etc. Everything works fine.
My problem is that this AccountService is used from lots of controllers and putting the promise resolve in all of these looks to me not only repeating all this boiler plate resolver code but also complicating the unit testing as I am obliged to r/test both successful and exception scenarios in every single controller test.
So my question is:
Is there a nice way to resolve the promise in the service and return the response to the controller, not the promise?
Please note I am a very beginner with Angular and JS so please be gentle if my question looks naive. I have heaps of java experience and my mind seems to go java like everywhere which may not be the case.
Thank you in advance for your inputs
To answer your original question:
Is there a nice way to resolve the promise in the service and return the response to the controller, not the promise?
In my opinion, no, there isn't. It boils down to the way asynchronous calls work - you either pass a callback (and the method returns nothing), or you don't pass a callback and the method returns an object which will be notified (a promise). There may be some workarounds, but I don't think it gets nicer than that.
One way to partially reduce the boilerplate is to use a catch in the service, and return the promise returned by it instead.
Consider the following extremely simplified example:
angular.module('myApp')
.factory('NetworkRequests', [
function() {
var _getData = function() {
var promise = new Promise((resolve, reject) => {
var a = true,
data = ['a', 'b', 'c'];
if (a) {
resolve(data);
} else {
reject('Rejection reason: ...');
}
});
return promise.catch((error) => {
// Notify some error handling service etc.
console.log(error);
return [];
});
};
return {
getData: _getData
};
}
]);
The promise variable would be the result from your http request. You should return some data in the catch function that makes sense in the controller context (e.g. empty array). Then you don't have to bother with error handling in the controller:
angular.module('myApp')
.controller('DataController', ['NetworkRequests',
function(NetworkRequests) {
NetworkRequests.getData().then((data) => {
this.data = data;
});
}
]);
Again, this doesn't solve the complete issue, but at least the error handling part can be encapsulated in the service.
You can design in such a way that once your $http is done with fetching the data, store it your factory variable (somewhat a cache), and for subsequent factory calls, you check if the cache has such data. If yes, return the cache data, else call the $http calls.
Here is the code:
.factory('AccountService', ['$http', '$q', function($http, $q) {
var cachedData = null;
var defered = $q.defer(); //create our own defered object
var _getAccounts = function(customerId) {
if (cachedData !== null) {
console.log('get from cachedData')
defered.resolve(cachedData); // resolve it so that the data is passed outside
return defered.promise; //return your own promise if cached data is found
} else {
var request = {
'method': 'GET',
'url': 'mockdata.json'
};
return $http(request).then((response) => { //return a normal $http promise if it is not.
console.log('get from $http');
cachedData = response.data;
return cachedData;
});
}
};
return {
getAccounts: _getAccounts
};
}]);
Here is the working plnkr. You can open up the console, and click the GetData button. You will see that first time it logs get from $http, where as subsequent calls it logs get from cachedData.
One way is to reuse an object and fill it with data. It is used by ngResource.
It is something like
var data = [];
function getAccounts(customerId) {
var promise = $http(...).then((response) => {
Object.assign(promise.data, response.data)
});
promise.data = [];
return promise;
};
Data is available for binding as $scope.accounts = AccountService.getAccounts(...).data. The obvious drawback is that there is a splash of unloaded content.
Another way is the one you've mentioned. It is being used most frequently. If there is a problem with WET code in controllers, it should be treated by eliminating WET code with class inheritance, not by changing the way it works.
Yet another way is the recommended one. Using a router and route/state resolvers eliminates the need for asynchronously loaded data. The data resolved in resolver is injected into route template as an array.

Angular $q promises and non-linear chaining

Hello everyone out there. I've hit a brick wall. I'm trying to research Angular async method calls, promise objects and implement a solution to a particular issue. After a few hours of trial and error and restructuring my code, I'm certain at this point the answer is right under my nose but I cannot see it.
I have a scenario where I must use $http to make a backend service call, which yields a promise. In the results of promise I will receive a data object with one string property and an array of 100 IDs. This backend service will only deliver payloads of 100 IDs at a time, so I must make x number of $http calls to reach the end of the list, as provided by this backend service. As I understand promises, I must evaluate the $http response in the promise's .then() method and if the string property is 'N' then I know I must call the backend service again for another batch of 100 IDs. After all IDs have been delivered the string will contain a 'Y' indicating that the end of this has been sent and don't call again. No comment required on this scheme, I know it's fairly lame, and unfortunately out of my control. ;-)
Everything I've studied regarding promises just seem to illustrate chaining of promises, in a linear fashion, if more async calls are needed. Whether the promise chain is nested or flat, it seems like I can only make a "known" or fixed number of sequential calls, e.g. Promise1 to Promise2, to Promise3, etc. Forgive my limited experience with promises here.
Here is a code example, you can see where I'm stuck here, nesting downward. I can't just "hope" after 3 or 5 or 10 calls I'll get all the IDs. I need to dynamically evaluate the result of each call and call again.
Here is the Angular service which invokes $http
(function() {
'use strict';
angular
.module('myapp')
.factory('IDservice', IDservice);
function IDservice($http) {
var service = {
model: {
error: {
state: false,
message: ''
},
ids: [],
end: '',
},
GetIDs: function() {
var request = 'some params...';
var url = 'the url...';
return $http.post(url, request).then(reqComplete).catch(reqFailed);
function reqComplete(response) {
service.model.ids = response.data.idList;
service.model.end = response.data.end;
return service.model;
}
function getIntradayMsrFailed(error) {
service.model.error.state = true;
service.model.error.message = error;
return service.model;
}
}
};
return service;
}
})();
Here is the Controller which invokes the Angular service, which ultimately drives some UI elements on the respective view:
(function() {
'use strict';
angular
.module('myapp')
.controller('AvailableIDsController', AvailableIDsController);
function AvailableIDsController($scope, $location, $timeout, IDservice) {
var vm = this;
vm.completeIDList = [];
activate();
function activate() {
IDservice.GetIDs().then(function(response){
vm.completeIDList = response.ids;
console.log('promise1 end');
if(response.end === 'N'){
IDservice.GetIDs().then(function(response){
angular.forEach(response.ids,function(nextID){
vm.comleteIDList.push(nextID);
});
console.log('promise2 end');
if(response.end === 'N'){
IDservice.GetIDs().then(function(response){
angular.forEach(response.ids,function(nextID){
vm.comleteIDList.push(nextID);
});
console.log('promise3 end');
});
}
});
}
});
console.log('mainthread end');
}
}
})();
You can see where that's going... and it's very, very ugly.
I need a way, inside the activate() method, to call a method which will take care of invoking the service call and return the result back up to activate(). Now, still in the activate() method, evaluate the result and determine whether to call again, etc. Where I'm stuck is once the main processing thread is done, you're left with program control in that first promise. From there, you can perform another promise, etc. and down the rabbit hole you go. Once I'm in this trap, all is lost. Clearly I'm not doing this right. I'm missing some other simple piece of the puzzle. Any suggestions would be so greatly appreciated!
You're looking for plain old recursion:
function AvailableIDsController($scope, $location, $timeout, IDservice) {
var vm = this;
vm.completeIDList = [];
return activate(0);
function activate(i) {
return IDservice.GetIDs().then(function(response) {
[].push.apply(vm.completeIDList, response.ids); // simpler than the loop
console.log('promise'+i+' end');
if (response.end === 'N'){
return activate(i+1);
}
});
}
}
Don't forget the returns so that the promises chain together and you can wait for the end of the requests.

Get variable value outside of Promise [duplicate]

This question already has answers here:
How can I access a variable outside a promise `.then` method?
(2 answers)
Closed 5 years ago.
I have a service in AngularJs which will return a value from Database.
userData.getUserData(function(response) {
$scope.uid = response
});
when I inject this service in my controller it will return a Promise,
But i need this Promise value Outside of my Function, how can i do that ?
Plunkr link of code
From your plunker code you have a service which looks like this:
angular.module('plunker');
.service('myService', function($firebaseRef, $firebaseObject){
this.getUserData = function(el) {
$firebaseObject($firebaseRef.users.child(this.localStorage().uid)).$loaded(function(data) {
el(data);
})
}
});
and a controller like this:
app.controller('MainCtrl', function($scope, myService) {
myService.getUserData(function(response) {
$scope.uid = response;
})
console.log($scope.uid);
$scope.postRequest = function(val) {
$firebaseArray($firebaseRef.requests.child($scope.uid)).$add(val);
console.log(val)
console.log($scope.request);
}
});
The problem is that the line console.log($scope.uid); prints undefined.
You are thinking in the terms of a standard blocking programming, but in this case the call to getUserData is non-blocking which means that you don't wait for the response, instead you only send the request to the server (Firebase) and continue to the next statement which is console.log.
The callback function(response) { $scope.uid = response; } will be invoked when the client reads success response (HTTP 2xx) returned by the server. This takes at least the time request travels to the server and response to travel back + the time it takes for the server to actually get the data. For example 150ms.
So, basically at the time the console.log statement is executed, the response callback was still not invoked, ie. the $scope.uid is not set which means that the console.log will print undefined.
To resolve this you need to execute your code, which depends on the response from the server, in the callback itself. For example something like this:
app.controller('MainCtrl', function($scope, myService) {
myService.getUserData(function(response) {
$scope.uid = response;
console.log($scope.uid);
// and any other code which depends on the $scope.uid
});
// ...
});
The cool factor would be to use AngularJS promises via $q service. For example, you could redefine your service like this:
angular.module('plunker');
.service('myService', function($q, $firebaseRef, $firebaseObject){
var deferred = $q.defer();
this.getUserData = function(el) {
$firebaseObject($firebaseRef.users.child(this.localStorage().uid)).$loaded(function(data) {
deferred.resolve(data);
});
};
return deferred.promise;
});
then in your controller you can use your service method like this:
app.controller('MainCtrl', function($scope, myService) {
myService.getUserData()
.then(function(data) {
$scope.uid = data;
console.log($scope.uid);
// and any other code
// you can also return promises here and then chain
// them, read about AngularJS promises
});
// ...
});
This is basically same as the example before, but with added benefit of better readability which is accomplished by avoiding callback hell.
I noticed that you have postRequest function which uses $scope.uid. I guess that you do not want to execute this function if you do not have $scope.uid. I also guess that this function is called by some event, like click on a button. My recommendation is that you disable the button or whatever else invokes this function until the $scope.uid is loaded.
For example like this:
<button type="button" ng-click="postRequest(something)" ng-disabled="uid === undefined">Post</button>
Hope this helps.
You issue that has been discussed has to do with the fact that you are trying to use $scope.uid before your promise has returned anything.
You can get around things like this by taking a few steps, mainly, you can init the scope var before you use. For instance if the response is an object you could just do something like this:
$scope.uid = {};
userData.getUserData(function(response) {
$scope.uid = response;
});
Then your var wont be undefined. But you should also consider when and how you are using this variable, that will effect if you want to init like this or not.
If you log like this, it will work
userData.getUserData(function(response) {
$scope.uid = response;
console.log($scope.uid);
});
and if you log like this it will not work because this log is not going to wait for you promise to return before logging;
userData.getUserData(function(response) {
$scope.uid = response;
});
console.log($scope.uid);
You'd need to provide more information to determine how best to deal with using this returned information and local variable. But the general idea of the problem is that you are attempting to log the variable before the promise is back.
TL:DR You have access to $scope.uid outside of the function, you need to wait for the reponse to give it the data before it will be inside, you can init it if you do not want it to start out as undefined
UPDATE : you need to use a callback to fire the second call After you have the first call back
userData.getUserData(function(response) {
$scope.postRequest(response);
});
$scope.postRequest = function(val) {
$firebaseArray($firebaseRef.requests.child($scope.uid)).$add(val);
console.log(val) console.log($scope.request);
}
Your plunk fixed : https://plnkr.co/edit/KbVoni3jfnHm54M80kYl?p=preview
You have to wait until the process of getting the response from userData.getUserData is done.
There are 3 ways as far as I know to solve this:
Using Callback
function getUserData(callback){
userData.getUserData(function(response) {
callback(response);
});
}
then you call that function
getUserData(function(response){
$scope.uid = response;
// then you can proceed and use the $scope.uid here
});
Wrap it in function
getUserData(function(response){
callAnotherFunction(response);
});
function callAnotherFunction(response){
console.log(response);
// You can use the value inside this function
}
Or use timeout
You can use $timeout to give time to the request and assign it to $scope.uid

Issue with race condition betwen 2 Controllers in Angular.js with Ajax and a localStorage value

The value for myIdentity is created only once and is persistant saved in the local storage after first usage of the following Angular.js controller.
The function $scope.createIdentity() is a complex function in a more global controller and saves the result to $localstorage.myIdentity.
This perfectly works!!! the createIdentity() function works with Q promise inside for itself.
But in the AppCtrl I have an issue with race conditions because $localstorage.myIdentity is not yet resolved before
my $http XHR Request is fired. So it doesn’t contain any value for myId.
This occurs only the first time the controller is used.
But I need to start the socket.io connection at the first call of the AppCtrl and this is here an bigger problem for me.
At a second usage of AppCtrl the correct value for myIdentity is yet available in $localStorage.
TimeacleControllers.controller('StatusCtrl', ['$scope', '$http', 'Socket', '$localStorage',
function ($scope, $http, Socket, $localStorage) {
if ($localStorage.myIdentity === undefined) {
$scope.createIdentity();
}
var myParams = {
myId: $localStorage.myIdentity
};
$http
.post('http://example.org', myParams)
.success(function (data) {
console.log('received data: ' + data);
Socket.connect();
Socket.on('connected', function () {
console.log("Connection!");
});
Socket.on('message', function (msg) {
console.log("Message: " + msg);
});
})
.error(function (err) {
// Handle login errors here
console.log("Error - " + err);
});
}]);
So what can you do here to make the Ajax Request wait until the $localstorage.myIdentity can be resolved? Please help.
You mentioned about Q promise but having problem with async sequence...?
If createIdentity is returning a promise, just put the http call inside .then
$scope.createIdentity().then(function() {
$http.... // or you can wrap it inside a function
})
Edit: If you have no access to the code that populate the variable (normally happens inside directive), you can setup a one time watch to monitor the value change.
var unwatch = $scope.$watch(function(){
return $localStorage.myIdentity;
}, function(newValue){
if (newValue === undefined) return;
$http...
unwatch();
});
You said $scope.createIdentity() uses a promise (and presumably is an async operation). Make it return a promise for your code to observe. When it resolves, make it resolve the identity.
Promises are chainable. When you return a promise from then, the next then listens for that promise instead of the original promise. In this case, we make the next then listen to the AJAX.
$scope.createIdentity().then(function(identity){
// When createIdentity resolves, use the data for the AJAX.
// Return the AJAX promise for the next chained then.
return $http.get(...);
}).then(function(data){
// When the AJAX completes, use the data for the socket call
Socket.connect();
...
});

Uploading multiple standalone attachments to couchdb using angularjs

I am trying to upload multiple files to a couchdb document using angularjs. I have attempted to do this using an angular.forEach loop, however this fails as the loop continues without waiting for the previous loop to finish its asynchronous $http calls.
This is how my code looks at the moment:
angular.forEach($scope.fileList, function(value, key){
couch.getRevisionId($scope.plan._id)
.then(function(response){
couch.uploadAttachment($scope.plan._id, response[1], value[0])
.then(function(response2){
console.log("Response2", response2);
},
function(reason2){
console.log("Reason2", reason);
});
},
function(reason){
console.log("GetrevisionId failed reason=", reason)
});
});
couch.getRevisionId is an $http HEAD call returns a promise of the current revision id of the document the attachment is being saved to. couch.uploadAttachment is an $http PUT call which saves the attachment to the document. This code works for the first file, unfortunately as the code runs asynchronously the next run of the loop runs before the first has PUT the previous attachment and so couch.getRevisionId returns the same revision id as the previous call, causing the code to fail with a 409 conflict as it attempts to save the attachment to an updated document.
Is there a way of delaying subsequent loops until after the getRevisionId and uploadAttachment promises have returned?
You could chain promises as described in the Q documentation about sequences, but using the Angular $q API
var asyncTask = function (value) {
var deferred = $q.defer();
...
return deferred.promise;
};
var list = [ ... ];
var deferred = $q.defer();
var promise = deferred.promise;
angular.forEach(list, function (value) {
promise = promise.then(function () {
return asyncTask(value);
});
});
deferred.resolve();
See a complete example here.
If your CouchDB service already relies on $q then you might not need to wrap your async task inside another higher level $q promise.

Categories

Resources