Reputation: 283325
I've put together an example to demonstrate what I'm getting at:
function onInput(ev) {
let term = ev.target.value;
console.log(`searching for "${term}"`);
getSearchResults(term).then(results => {
console.log(`results for "${term}"`,results);
});
}
function getSearchResults(term) {
return new Promise((resolve,reject) => {
let timeout = getRandomIntInclusive(100,2000);
setTimeout(() => {
resolve([term.toLowerCase(), term.toUpperCase()]);
}, timeout);
});
}
function getRandomIntInclusive(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
<input onInput="onInput(event)">
Type in the "search" box and watch the console. The search results come back out of order!
How can we cancel any pending promises when there's new input and guarantee the results come back in order?
Upvotes: 3
Views: 216
Reputation: 42520
You can use Promise.race
to cancel the effect of a previous chain:
let cancel = () => {};
function onInput(ev) {
let term = ev.target.value;
console.log(`searching for "${term}"`);
cancel();
let p = new Promise(resolve => cancel = resolve);
Promise.race([p, getSearchResults(term)]).then(results => {
if (results) {
console.log(`results for "${term}"`,results);
}
});
}
function getSearchResults(term) {
return new Promise(resolve => {
let timeout = 100 + Math.floor(Math.random() * 1900);
setTimeout(() => resolve([term.toLowerCase(), term.toUpperCase()]), timeout);
});
}
<input onInput="onInput(event)">
Here we're doing it by injecting an undefined
result and testing for it.
Upvotes: 3
Reputation: 18908
Instead of using debounce, or timeouts, I set a small amount of state outside inside (suggestion by Jaromanda X) of this function that uses a referenced function. This way, you can just change the function reference to something like a noop
. The promise still resolves, but it won't take any action. However, the final one will not have changed its function reference:
var onInput = function() {
let logger = function(term, results) {
console.log(`results for "${term}"`, results);
};
let noop = Function.prototype;
let lastInstance = null;
function ActionManager(action) {
this.action = action;
}
return function onInput(ev) {
let term = ev.target.value;
console.log(`searching for "${term}"`);
if (lastInstance) {
lastInstance.action = noop;
}
let inst = new ActionManager(logger.bind(null, term));
lastInstance = inst;
getSearchResults(term).then(response => inst.action(response));
}
}();
/****************************************
* The rest of the JavaScript is included only for simulation purposes
****************************************/
function getSearchResults(term) {
return new Promise((resolve, reject) => {
let timeout = getRandomIntInclusive(100, 2000);
setTimeout(() => {
resolve([term.toLowerCase(), term.toUpperCase()]);
}, timeout);
});
}
function getRandomIntInclusive(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
<input onInput="onInput(event)">
Upvotes: 3
Reputation: 82
You can use async
package - a bunch of utilities to maintain asynchronous code. It was first developed for node.js but it can also be used in frontend.
You need series
function, it saves an order of promises. Here is a brief example in coffeescript:
async.series([
->
### do some stuff ###
Q 'one'
->
### do some more stuff ... ###
Q 'two'
]).then (results) ->
### results is now equal to ['one', 'two'] ###
doStuff()
.done()
### an example using an object instead of an array ###
async.series({
one: -> Q.delay(200).thenResolve(1)
two: -> Q.delay(100).thenResolve(2)
}).then (results) ->
### results is now equal to: {one: 1, two: 2} ###
doStuff()
.done()
See caolan.github.io/async/
Upvotes: -1
Reputation: 516
You shouldn't use setTimeout
's in promises the way you are doing it, because from the .then
you are returning the callback from the .setTimeout()
which would not work and mess up the order. To make the promises go in order you should make a function like shown below:
function wait(n){
return new Promise(function(resolve){
setTimeout(resolve, n)
});
}
and substitute the setTimeout()
's with that function like shown below:
wait(getRandomIntInclusive(100,2000)).then(function(){
// code
});
Upvotes: -1
Reputation: 23873
One workable solution is to include a latestTimestamp
and simply ignore any responses that come in with an early timestamp (and and therefore obsolete).
let latestTimestamp = 0;
function onInput(ev) {
let term = ev.target.value;
console.log(`searching for "${term}"`);
latestTimestamp = Date.now();
getSearchResults(term, latestTimestamp).then(results => {
if (results[2] !== latestTimestamp) {
console.log("Ignoring old answer");
} else {
console.log(`results for "${term}"`, results);
}
});
}
function getSearchResults(term, latestTimestamp) {
return new Promise((resolve, reject) => {
let timeout = getRandomIntInclusive(100, 2000);
setTimeout(() => {
resolve([term.toLowerCase(), term.toUpperCase(), latestTimestamp]);
}, timeout);
});
}
function getRandomIntInclusive(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
<input onInput="onInput(event)">
Upvotes: 1