mpen
mpen

Reputation: 283325

How to implement cancellable, ordered promises?

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

Answers (5)

jib
jib

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

KevBot
KevBot

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

mr_nameless
mr_nameless

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

Zaidhaan Hussain
Zaidhaan Hussain

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

Jeremy J Starcher
Jeremy J Starcher

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

Related Questions