Pick latest promise

Promise throttling and flatMapLatest equivalent

Problem: as the user types text, asynchronously search using the entered text. Display the results from the latest search.

This problem is classic power showcase of the functional reactive programming. Here is sample implementation using RxJs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var $input = $('#input');
/* Only get the value from each key up */
var keyups = Rx.Observable.fromEvent($input, 'keyup')
.map(function (e) {
return e.target.value;
})
.filter(function (text) {
return text.length > 2;
});
/* Now throttle/debounce the input for 500ms */
var throttled = keyups
.throttle(500 /* ms */); // 1
/* Now get only distinct values, so we eliminate the arrows and other control characters */
var distinct = throttled
.distinctUntilChanged();
/* search Wikipedia, returns a promise */
function searchWikipedia (term) {
return $.ajax({
url: 'http://en.wikipedia.org/w/api.php',
dataType: 'jsonp',
data: {
action: 'opensearch',
format: 'json',
search: term
}
}).promise();
}
var suggestions = distinct
.flatMapLatest(searchWikipedia); // 2
suggestions.subscribe(function (data) {
// display results
}, function onError(error) {
// display error message
});

Notice several interesting features reactive programming implements that are difficult to implement even using promises, and a major pain to implement using callbacks.

  • we throttle key events and only generate an event with entered text once the user stopped typing for 500ms, line // 1

  • Because AJAX requests could return out of order, we need to make sure we only display results from the last search request. We are ignoring any search results from non-last search requests, line // 2

Can we implement same features using promises?

Throttling

Ari Lerner in his ng-book gives good example how to throttle requests using AngularJs $timeout service.

1
2
3
4
5
6
7
8
9
10
11
12
app.controller('ServiceController', function($scope, $timeout, githubService) {
$scope.$watch('username', function(newUserName) { // 1
// If there is a timeout already in progress, cancel it
if (timeout) $timeout.cancel(timeout); // 2
timeout = $timeout(function search() {
githubService.search(newUserName)
.success(function (data) {
// render data
});
}, 350); }
});
});

Any time user changes the username value by typing into a text box, a callback function will execute (line // 1). Instead of searching immediately, we set a timeout of 350ms. If there is already a timeout, we cancel it (line // 2).

Using latest result

The above implementation still suffers from a huge issue. We still might execute several search requests in parallel.

at 0 ms user types 'john'
at 350ms we call the search service for 'john'
at 1000ms user gets tired of waiting and types 'mary'
at 1350ms we call the search service for `mary`
at 1400ms search returns results for `mary`
at 1600ms search returns results for `john`

Because the first search takes longer, user briefly sees second search results, then the screen flips to first results. How do we use only the results of the latest promise?

Promises are easy to either chain p1.then(p2).then ... or execute in parallel Q.all([p1, p2]).then .... It is much harder to control them when promises p1 and p2 can execute in parallel, but only the p2 should be used.

chaining

We could make sure the last promise is always used by chaining it to the current latest one

1
2
3
4
var latest;
// same timeout code as above
var search = githubService.search(userName); // 1
Q(latest).then(search);

This code always adds new search promise returned in // 1 to the current latest promise. The searches execute in parallel, but they will be displayed in order.

Unfortunately this code has a huge problem. Every promise in the chain has to resolve, before latest results can be displayed, even if the last search request has returned.

at 350ms latest promise is search for `john`
at 1350ms we add second link to search for `mary`
    search('john').then(search(`mary'))
at 1400ms the search for 'mary' gets resolved AND WAITS
at 1600ms the search for 'john' gets resolved,
    then the search for `mary` starts (already resolved)
    then `mary` results are displayed

We are correctly ignoring search results from non-latest promise, but we still wait for 200ms. This code also has another problem - if non-latest promise fails, the entire chain fails!

detecting latest promise

Instead of using chaining to get the latest results, we could just ignore results from non-latest promises. We can keep each promise separate, but add on a check

1
2
3
4
5
6
7
8
9
10
11
12
var latest;
function ensureLatest(p) {
latest = p;
return latest.then(function (myPromise, value) { // 1
if (myPromise !== latest) {
// this promise is no longer latest, ignore
throw new Error('not latest');
}
return value;
}.bind(null, latest));
}
ensureLatest(githubService.search(userName)).then(...)

We keep a reference to the latest promise, and after a promise has been resolved check if it is the same. If not, we throw an error we can detect in other parts of code. Our search example now executes correctly and efficiently

at 350ms latest promise is search for `john`
at 1350ms latest promise is search for `mary`
at 1400ms search for `mary` resolves
  it checks if it is still the latest promise (line // 1) - true
  prints results for `mary`
at 1600ms search for `john` resolves
  it checks if it is still the latest promise (line // 1) - false
  throws an error

Only the results from the latest executed search are displayed, just like the problem requires.

Update 1

I published the latest-promise as NPM package. It works should work with any promise and allows the rejected promise to check if it was cancelled because it became obsolete

1
2
3
4
5
6
7
8
9
10
11
var pickLatest = require('latest-promise');
var first = pickLatest(makeSlow());
var second = pickLatest(makeFast());
// first promise takes a long time to finish, while second is quick
// second promise has valid value, but the first one gets rejected
first.catch(function (err) {
if (pickLatest.wasObsolete(err)) {
// well, this promise finished AFTER second
// and should be ignored
}
});