Recursion and asynchronous methods with a flag variable - javascript

I have the following method:
function getRelevantArticles(amount,
userSuggestions,
suggestionPage,
relevantArticles,
continueFlag,
success,
error)
{
if(continueFlag)
{
getSuggestedArticles(suggestionPage, userSuggestions, function (articles)
{
if(articles.length == 0)
getRelevantArticles(amount, userSuggestions, suggestionPage, relevantArticles, false, success, error); // continueFlag= false
getUnvisitedArticles(articles, function (unvisited)
{
for(var i = 0; i < unvisited.length; i++)
relevantArticles.push(unvisited[i]);
relevantArticles= filterRelevant(amount, userSuggestions, relevantArticles);
if(relevantArticles.length < amount)
getRelevantArticles(amount, userSuggestions, suggestionPage + 1, relevantArticles, true, success, error); // continueFlag= true
else
getRelevantArticles(amount, userSuggestions, suggestionPage, relevantArticles, false, success, error); // continueFlag= false
}, error);
}, error);
}
else if(success)
{
fillWithContent(relevantArticles, success, error); //Should be last method to execute
}
}
The context
I know it may be hard to understand and can be optimized a lot, I'll try my best to explain what it's doing (or trying to):
The method is first called with a flag of continueFlag= true, so it starts by calling getSuggestedArticles, which is an async method that makes an AJAX request. I pass in a callback function with the result of the request.
getSuggestedArticles gets me the article Ids related to the user suggestions. (The user suggestions is a list of topics that may be of interest to the user).
I pass in a suggestionPage because the suggestions can be a lot, and I should be able to get the relevant articles with just a few (the first page).
If no articles are retrieved, then it means we are out of suggestions (every suggestion has at least one article), i. e., we reached the last page, so we set the continueFlag flag to false, to call the finalizer method.
If there is at least one article, I call getUnvisitedArticles, which is another asynchronous method that makes an AJAX request. This method gives me the articles that were not visited or read by the user, which are the ones I care about.
I have a relevantArticles variable which keeps track of the articles that I found out to be relevant and will be presented to the user. Once I get the relevant ones from my current page of unvisited articles, and have appended them to the previous page's relevant ones, I check if I have the minimum amount of articles to show.
If I don't yet satisfy the minimum amount, then I go on with the next page (suggestionPage + 1);
If I reached the minimum threshold, then I go on with the finalizer method (continueFlag= false)
fillWithContent is the method which will be called when I finished identifying the relevant articles. It is an asynchronous method that will make an AJAX request, and will fill my article objects with additional information.
getSuggestedArticles(Number: suggestionPage, Array: userSuggestions, Function: success, Function: error)
Receives an array of user suggestions, and takes the nth page of this array (page size 100).
Let's suppose we call the method like this:
getSuggestedArticles(0, [ 745, 4567, 1500 ], function (data) {
var articles = data;
}, error);
The method makes request to Database Web API, and pass an array of suggested articles to the success function. In the preceding example, the articles variable would have an array like this (Note that all the returned suggested articles have at least one user suggestion among their topics):
[
{
id: 12345,
topics: [ 998, 1500, 323 ] //has user suggestion 1500
},
{
id: 45778,
topics: [ 009, 1500, 745] //Has user suggestion 745 and 1500
},
...
]
getUnvisitedArticles(Array: articles, Function: success, Function: error)
Receives an array of articles and returns all the ones who weren't visited by the user.
Let's suppose we call this method like this:
//We are using the same "articles" variable from the previous example
getUnvisitedArticles(articles, function (data) {
var unvisited = data;
}, error);
The function makes request to the Database Web Api and pass an array with the unvisited articles to the success function. In the preceding example, the variable unvisited would have an array like this:
[
{
id: 45778,
topics: [ 009, 1500, 745]
}
]
Notice that article with id 12345 is gone. This is because it has been visited by the user.
fillWithContent(Array: relevantArticles, Function: success, Function: error)
Receives an array of articles and fill these objects with additional information.
Let's suppose we call this method like this:
//We are using the same "unvisited" variable from the previous example
fillWithContent(unvisited, function (data) {
filledArticles = data;
}, error);
The function makes request to the Database Web Api and pass an array with the filled articles to the success function. In the preceding example, the variable filledArticles would have an array like this:
[
{
id: 45778,
topics: [009, 1500, 745],
title: 'Article title',
publicationDate: 'Some date',
author: 'Some author',
...
}
]
This is the array my caller is expeting, by caller I mean the one calling my getRelevantArticles function.
The problem
The problem with this method is that fillWithContent is called infinitely, therefore causing a great deal of requests to be made, the browser to crash, and a recursion overflow to arise.
I'm not calling this method from another place, so it has to be a problem with this function.
I writed a console.log(suggestionPage), and it seems it keeps on incrementing the variable infinitely too. It should have stopped at page 3, because articles.length == 0. But it's not stopping.
What is going on here?

I think you should split your workload into independent parts that are easy reason about and easy to combine for more complex results.
As I understand it, the work consists three basic parts, all done through Ajax:
[topic IDs] to [article stubs for those topics] (getSuggestedArticles)
[article stubs] to [relevant (unread) article stubs] (getUnvisitedArticles)
[article stubs] to [full articles] (fillWithContent)
All of those Ajax requests should be made in a paged manner, e.g. 1000 items are handled by 10 requests with 100 items each.
For that we define a utility function that takes a list of items, pages them, makes an Ajax request for each page (through a worker function we pass as an argument) and returns the combined result of all requests.
// utility: runs an ajax request and handles errors on the low level
function ajax(options) {
return $.ajax(options).fail(function(jqXHR, textStatus, errorThrown) {
console.log(textStatus, errorThrown, jqXHR);
});
}
// utility: makes paged Ajax requests through a worker function
function pagedAjax(ajaxFunc, items, pageSize) {
var temp = [].slice.call(items), page,
requests = [];
// start as many parallel Ajax requests as we have pages
while (temp.length) {
page = temp.splice(0, pageSize);
requests.push( ajaxFunc(page) );
}
// wait until all requests have finished, return combined result
return $.when.apply($, requests).then(function (results) {
var combined = [];
$.each(results, function (i, result) {
// result is an array [data, textStatus, jqXhr]
// push all contained items onto combined
// (the following assumes that data is an array of objects)
[].push.apply(combined, result[0]);
});
return combined;
});
}
Now we can set up our three worker functions. They take arbitrary amounts of input, because all paging is done by the utility function above:
// worker: retrieves a list of article IDs from topic IDs
function getSuggestedArticles(topics) {
return ajax({method: 'post', url: '/articlesByTopic', data: topics});
// or whatever API request returns a list of articles IDs from topic IDs
}
// worker: takes a list of article IDs, returns a list of _unread_ article IDs
function getUnvisitedArticles(articles) {
return ajax({method: 'post', url: '/unvisitedArticles', data: articles});
// or whatever API request returns a list of unvisited articles from IDs
}
// worker: takes a list of article IDs, returns a list of articles
function fillWithContent(articles) {
return ajax({method: 'post', url: '/articles', data: articles});
// or whatever API request fills articles with content
}
After that, combining the functions is not difficult anymore:
// takes a list of topic IDs, requests article IDs, filters them, returns actual articles
function getUnvisitedArticlesByTopic(topicIds) {
var pageSize = 100;
return pagedAjax(getSuggestedArticles, topicIds, pageSize)
.then(function (allArticles) {
return pagedAjax(getUnvisitedArticles, allArticles, pageSize);
})
.then(function (unvisitedArticles) {
return pagedAjax(fillWithContent, unvisitedArticles, pageSize);
});
}
And we can use it all through a very simple call:
// renders unvisited articles
function renderUnvisitedArticles() {
var topicIds = [9, 1500, 745];
getUnvisitedArticlesByTopic(topicIds).done(function (articles) {
$.each(articles, function (i, article) {
// show article on page
});
});
}
Benefits of this promise-based approach:
No callback hell.
Short functions that do exactly one thing.
No self-calling functions.
Good re-usability and testability of the individual parts.
Recommended reading is of course jQuery's documentation on Deferred objects.
Disclaimer: The code is indeed untested. If you find mistakes, tell me.

Related

Parse Platform - Retrieve relations in object

Okay, so I am really quite familiar with Parse and have used most features in daily development. However, now I need to work with some many-to-many relationships and can't seem to get it to work right. I've tried reading several posts on Stack... but no luck.
Any help would be appreciated:
1) 'Projects' class = (main grouping class and project details)
a) 'projectTasks' is setup as a relation column and points to the Tasks class.
2) 'Tasks' class = (tasks submitted by user and grouped in projects)
3) I want to be able to Query for certain projects, and get the relation data at same time.
test: function(){
console.warn("Running relations test....")
var deferred = $q.defer()
var Projects = Parse.Object.extend("Projects");
var query = new Parse.Query(Projects)
query.equalTo("objectId",Parse.User.current().id)
query.include("projectTasks")
query.find({
success: function(res){
console.log(res)
deferred.resolve(res)
},
error: function(e,r){
console.log(e,r)
deferred.reject(e)
}
})
return deferred.promise
},
The response I get never returns the actual data from the relation field... just another promise or whatever it is.
screen shot of response
Maybe I'm just going about this all wrong. Any help would be appreciated. I want to store a project, store tasks, and be able to associate tasks to a project and quickly retrieve all the info per project.
you need to do a second query to get the relation data, which is a second async call.
test: function(){
console.warn("Running relations test....")
var deferred = $q.defer()
var Projects = Parse.Object.extend("Projects");
var query = new Parse.Query(Projects)
query.equalTo("objectId",Parse.User.current().id)
query.include("projectTasks")
query.find({
success: function(res){ // my code just assumes you get an answer.....
console.log(res)
res[0].get('projectTasks').query().find().then(tasks => {
const response = {parent: res, tasks: tasks};
deferred.resolve(response);
}
},
error: function(e,r){
console.log(e,r)
deferred.reject(e)
}
})
return deferred.promise
},
if in the real life of this answer, you are getting an array of results, then the client code that is calling this probably wants to handle the second call to get the tasks as it iterates over the projects...

Querying a parse table and eagerly fetching Relations for matching

Currently, I have a table named Appointments- on appointments, I have a Relation of Clients.
In searching the parse documentation, I haven't found a ton of help on how to eagerly fetch all of the child collection of Clients when retrieving the Appointments. I have attempted a standard query, which looked like this:
var Appointment = Parse.Object.extend("Appointment");
var query = new Parse.Query(Appointment);
query.equalTo("User",Parse.User.current());
query.include('Rate'); // a pointer object
query.find().then(function(appointments){
let appointmentItems =[];
for(var i=0; i < appointments.length;i++){
var appt = appointments[i];
var clientRelation = appt.relation('Client');
clientRelation.query().find().then(function(clients){
appointmentItems.push(
{
objectId: appt.id,
startDate : appt.get("Start"),
endDate: appt.get("End"),
clients: clients, //should be a Parse object collection
rate : appt.get("Rate"),
type: appt.get("Type"),
notes : appt.get("Notes"),
scheduledDate: appt.get("ScheduledDate"),
confirmed:appt.get("Confirmed"),
parseAppointment:appt
}
);//add to appointmentitems
}); //query.find
}
});
This does not return a correct Clients collection-
I then switched over to attempt to do this in cloud code- as I was assuming the issue was on my side for whatever reason, I thought I'd create a function that did the same thing, only on their server to reduce the amount of network calls.
Here is what that function was defined as:
Parse.Cloud.define("GetAllAppointmentsWithClients",function(request,response){
var Appointment = Parse.Object.extend("Appointment");
var query = new Parse.Query(Appointment);
query.equalTo("User", request.user);
query.include('Rate');
query.find().then(function(appointments){
//for each appointment, get all client items
var apptItems = appointments.map(function(appointment){
var ClientRelation = appointment.get("Clients");
console.log(ClientRelation);
return {
objectId: appointment.id,
startDate : appointment.get("Start"),
endDate: appointment.get("End"),
clients: ClientRelation.query().find(),
rate : appointment.get("Rate"),
type: appointment.get("Type"),
notes : appointment.get("Notes"),
scheduledDate: appointment.get("ScheduledDate"),
confirmed:appointment.get("Confirmed"),
parseAppointment:appointment
};
});
console.log('apptItems Count is ' + apptItems.length);
response.success(apptItems);
})
});
and the resulting "Clients" returned look nothing like the actual object class:
clients: {_rejected: false, _rejectedCallbacks: [], _resolved: false, _resolvedCallbacks: []}
When I browse the data, I see the related objects just fine. The fact that Parse cannot eagerly fetch relational queries within the same call seems a bit odd coming from other data providers, but at this point I'd take the overhead of additional calls if the data was retrieved properly.
Any help would be beneficial, thank you.
Well, in your Cloud code example - ClientRelation.query().find() will return a Parse.Promise. So the output clients: {_rejected: false, _rejectedCallbacks: [], _resolved: false, _resolvedCallbacks: []} makes sense - that's what a promise looks like in console. The ClientRelation.query().find() will be an async call so your response.success(apptItems) is going to be happen before you're done anyway.
Your first example as far as I can see looks good though. What do you see as your clients response if you just output it like the following? Are you sure you're getting an array of Parse.Objects? Are you getting an empty []? (Meaning, do the objects with client relations you're querying actually have clients added?)
clientRelation.query().find().then(function(clients){
console.log(clients); // Check what you're actually getting here.
});
Also, one more helpful thing. Are you going to have more than 100 clients in any given appointment object? Parse.Relation is really meant for very large related collection of other objects. If you know that your appointments aren't going to have more than 100 (rule of thumb) related objects - a much easier way of doing this is to store your client objects in an Array within your Appointment objects.
With a Parse.Relation, you can't get around having to make that second query to get that related collection (client or cloud). But with a datatype Array you could do the following.
var query = new Parse.Query(Appointment);
query.equalTo("User", request.user);
query.include('Rate');
query.include('Clients'); // Assumes Client column is now an Array of Client Parse.Objects
query.find().then(function(appointments){
// You'll find Client Parse.Objects already nested and provided for you in the appointments.
console.log(appointments[0].get('Clients'));
});
I ended up solving this using "Promises in Series"
the final code looked something like this:
var Appointment = Parse.Object.extend("Appointment");
var query = new Parse.Query(Appointment);
query.equalTo("User",Parse.User.current());
query.include('Rate');
var appointmentItems = [];
query.find().then(function(appointments){
var promise = Parse.Promise.as();
_.each(appointments,function(appointment){
promise = promise.then(function(){
var clientRelation = appointment.relation('Clients');
return clientRelation.query().find().then(function(clients){
appointmentItems.push(
{
//...object details
}
);
})
});
});
return promise;
}).then(function(result){
// return/use appointmentItems with the sub-collection of clients that were fetched within the subquery.
});
You can apparently do this in parallel, but that was really not needed for me, as the query I'm using seems to return instantaniously. I got rid of the cloud code- as it didnt seem to provide any performance boost. I will say, the fact that you cannot debug cloud code seems truly limiting and I wasted a bit of time waiting for console.log statements to show themselves on the log of the cloud code panel- overall the Parse.Promise object was the key to getting this to work properly.

Make Twitter Bootstrap's typead search within multiple fields

By default, the typeahead plugin makes use of a single data source in order to fetch the results. What I would like is for it to search within multiple fields, so if say, I have:
var items = [
{'title': 'Acceptable', 'description': 'Good, but not great'}
]
It will search on both the title and description fields, ideally via AJAX.
Is this possible with this plugin?
Typeahead does not support using JSON objects without two tweaks. There are few pull-requests in Github for this, and I have submitted one myself, but, currently, you must manually override select and render. Additionally, you must also override highlighter, matcher, sorter, and updater, but those can done via the options passed into the typeahead.
var typeahead = control.typeahead({ /* ... */ }).data('typeahead');
// manually override select and render
// (change attr('data-value' ...) to data('value' ...))
// otherwise both functions are exact copies
typeahead.select = function() {
var val = this.$menu.find('.active').data('value')
this.$element.val(this.updater(val)).change()
return this.hide()
};
typeahead.render = function(items) {
var that = this
items = $(items).map(function (i, item) {
i = $(that.options.item).data('value', item)
i.find('a').html(that.highlighter(item))
return i[0]
});
items.first().addClass('active')
this.$menu.html(items)
return this
};
If you need help with the other ones, then let me know, but the gist of it is:
control.typehead({
matcher: function (item) {
var lcQuery = this.query.toLowerCase();
return ~item.title.toLowerCase().indexOf(lcQuery)
|| ~item.description.toLowerCase().indexOf(lcQuery);
}
};
I also have a JFiddle example related to the pull request that I made, but the sorting functions do not exist in 2.3.1, or even 3.x without the pull request being accepted, so you will have to override sorter in its entirety to effectively repeat what I did with matcher above (checking both while sorting).
As for the AJAX call, you can override the source method in the passed in options to get AJAX functionality. By not returning to the source call, it assumes that the second parameter, process, will be invoked with results.
control.typehead({
minLength: 3,
source: function(query, process) {
$.ajax({
url: url + encodeURIComponent(query),
type: 'GET',
success: process,
error: function() { /* ... */ }
});
}
});
Typeahead added support for multiple field searching in v10.3
https://github.com/twitter/typeahead.js/pull/811
Usage:
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('title', 'description'),

FB.api is there a way to limit individual fields

I'm using the Facebook Javascript SDK. Is there a way to limit the number of elements returned for a specific field when making a request for multiple fields.
Example: If I wanted to get the 'bio' and liked 'movies' from all my friends, but limit the number of movies returned to let's say 10 movies.
FB.api('/me/friends', { fields: 'bio, movies' }, function(response) {
// Do something with data
handleResponse(response);
});
If I add the limit parameter it only limits the number of friends returned, but I want ALL my friends returned and the number of movies limited.
FB.api('/me/friends', { fields: 'bio, movies', limit: 10 }, function(response) {
// Do something with data
handleResponse(response);
});
Is there any way to do this using the Javascript FB.api method? Any ideas/help is appreciated.
**Edit
OK, so using Tommy's example of 'field expansion' below, this is what my example FB.api call would look like if you wanted to limit the number of movies returned to 10:
FB.api('/me/friends', { fields: 'bio, movies.limit(10)' }, function(response) {
// Do something with data
handleResponse(response);
});
Yes, add another parameter called movies.limit.
Example call:
https://graph.facebook.com/me/friends?fields=movies&movies.limit=3&access_token=<access_token>

backbone.js - getting extra data along with the request

I have a collection which holds some of the users. Some information that is needed is how many total there are, how many pages, etc. How do I pass these back to the client? Or do they have to come from a separate view in which case I will need more than one ajax call? I'd like to have the collection fetch() and also receive some of this "meta data". What's a good way for handling this?
Generally, you need to handle this in the collection class' parse method. Its responsibility is to take the response and return back an array of model attributes. However, you could do more than that if you wished if you didn't mind the parse method having this additional responsibility.
UserList = Backbone.Collection.extend({
model: User,
url: '/users',
parse: function(data) {
if (!data) {
this.registered_users = 0;
return [];
}
this.registered_users = data.registered_users;
var users = _(data.users).map(
function(user_data) {
var user = {};
user['name'] = user_data.name;
return user;
}
);
return users;
    }
});
So in the trivial example above, presume the server returns a response which contains a count of registered users and and an array of user attributes. You would both parse through and return the user attributes and you would pick off the registered user count and just set it as a variable on the model.
The parse method would get called as part of a fetch. So no need to modify the fetch, just use the built-in hook method that you have.
Purists would say that you are giving the parse method a secondary responsibility which might surprise some people (e.g. returning something and modifying model state). However, I think this is okay.
One way to do this is to override the Collection::fetch() method so that it parses this metadata out of the response. You could have your backend return a response like this:
{
"collection": [
{ ... model 1 ... },
{ ... model 2 ... },
...
],
"total_rows": 98765,
"pages": 43
}
In your fetch method which overrides the original Backbone.Collection::fetch() method, you can handle each property of the object separately. Here's you could do the override with a slightly modified fetch method:
_.extend(Backbone.Collection.prototype, {
fetch : function(options) {
options || (options = {});
var collection = this;
var success = options.success;
options.success = function(resp) {
// Capture metadata
if (resp.total_rows) collection.total_rows = resp.total_rows;
if (resp.pages) collection.pages = resp.pages;
// Capture actual model data
collection[options.add ? 'add' : 'refresh'](
collection.parse(resp.collection), options);
// Call success callback if necessary
if (success) success(collection, resp);
};
options.error = wrapError(options.error, collection, options);
(this.sync || Backbone.sync).call(this, 'read', this, options);
return this;
});
Note that this approach using _.extend will affect all your classes which extend Backbone.Collection.
This way, you don't have to make 2 separate calls to the backend.
I would bootstrap the information at pagecreation. Write the information into the html document when the server creates the site. Like that you don't have to have an ajax call at all. I do that with the whole collection in ordner not to first load the page and then wait for the ajax call to return the information needed.
Code example with Python:
Line 64: https://github.com/ichbinadrian/maps/blob/master/python/main.py <- from here
Line 43: https://github.com/ichbinadrian/maps/blob/master/templates/index.html <- into here

Categories

Resources