I have adopted a pattern in one of my projects that I really like and I suspect it's likely to be something standard. I'd like to describe it here and see if you guys can tell me how other JavaScript programmers solve this type of problem.
A few rules I established for myself before I started the project. 1. Functional / Lazy evaluation wherever possible. Don't do a bunch of things at startup. 2. Use promises (I happen to be using Angular's $q, but the syntax is all pretty much similar)
For the sake of simplicity, let's say that I am wanting to update the screen and display say at the top the user's name, how many Likes they have, and their current Friend count. Some of this may seem contrived, but bare with me.
In my controller on the client side I would make simple calls to a client-side service that does something like:
function getCurrentUserName() {
getCurrentUser().then( function(user) {
return user.name;
}
}
function getCurrentUserLikeCount() {
getCurrentUser().then( function(user) {
return user.likeCount;
}
}
function getCurrentUserFriendCount() {
getCurrentUser().then( function(user) {
return user.friendCount;
}
}
Now I might have multiple UI elements that access the controller, and who knows what order the requests might come in at. Heck, it's possible there might be a 3rd party widget that access the controller, so there might be multiple calls to one of these routines coming in quickly.
Now, the first obvious problem is that getCurrentUser() is async and does some kind of an ajax/rest call to the server. So: 1) You don't need to make unnecessary calls to the server. If getCurrentUser() got called already, you don't need to call it again (given some rationale such as the user is still logged in, etc.)
So let's assume getCurrentUser() is smart enough to cache the results. On subsequent calls, it may return the cached value (still asynchronously using when()).
The second problem is, what happens when getCurrentUser() gets called while another call to getCurrentUser() is already processing. The cache hasn't been updated yet, so the second call doesn't see the cache so it makes another HTTP call to the server. You don't want that. So you need to know that there's a pending request to the server and throttle the call.
So the pattern I adopted is that an async function that makes a call to the server follows the following high-level idea: 1. If it's the first call, issue the call to the server, and immediately return a promise to the client. Also, hang on to this promise. When the server responds, resolve the promise but also keep the original value returned from the server (i.e. cache)
If it's not the first call, determine if the first call completed. If the first called has already completed, return the value that was returned from the server (i.e. the cache). Of course, you'd do this using when() since the client is expecting a promise.
If it's not the first call, but the first call has not yet completed, then return to the client the cached promise that was returned by the first call. When then promise is resolved, all clients are notified.
In fact, what also works with the angular $q (and probably others), is to reuse the original promise even after it was returned in the past. Then you can avoid caching the data as well since the promise contains the data returned from the server.
I call this whole thing throttling, for lack of a better term.
The following function wraps an existing async function and makes a new function that automatically throttles:
// This routine makes a custom throttle wrapper for the async-function that's passed in.
// A throttle function, when called, calls an async function (one that returns a promise)
// in such a way to ensure the async function is only called when it's not already in progress.
// in which case rather than calling it again, this routine returns the original promise to the caller.
// If it's already in progress, then the original promise is returned instead.
var makeThrottleFunction = function (asyncFunction) {
var asyncPromise = null;
return function (asyncInParam) {
if (!asyncPromise) {
var asyncDeferred = $q.defer();
asyncPromise = asyncDeferred.promise;
asyncFunction(asyncInParam).then(function (asyncOutParam) {
asyncDeferred.resolve(asyncOutParam);
}).catch(function (parameters) {
return $q.reject(parameters);
}).finally(function () {
asyncPromise = null;
});
}
return asyncPromise;
};
};
Example usage:
var getCurrentUser = makeThrottleFunction(function() {
// async call to server to get data
// return promise..
});
I'm curious what the experts have to say about this approach. What is an alternative, etc. I used it all over the place in my app and it works really well. I also don't think I could do without it, which begs the question: if something like this isn't already in the frameworks, then I may be missing something obvious, like some setting or library that already does this very exact thing. So I'd love to get some feedback.
Although I'm looking for opinions from others on how they solve this common problem, in order to satisfy this being a Q&A, I think a good answer would be one of the following: 1. Point to a library that does this type of thing. 2. Points out that there's a simpler way to do this.
asyncFunction = memoize(asyncFunction)
? This would return at the same promise (for the same parameters) every time, with the same resolved value. (It would also mean that a rejected promise would "throw" every time it was referenced).