Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to cancel last Promise if not resolved?

Let's say I have a search function to make an HTTP call. Every call can take a different amount of time. So I need to cancel the last HTTP request and wait only for the last call

async function search(timeout){

   const data = await promise(timeout)
   return data;

}
// the promise function is only for visualizing an http call
function promise(timeout){
   return new Promise(resolve,reject){
       setTimeout(function(){      
           resolve()
       },timeout) 
   }
}
search(200)
.then(function(){console.log('search1 resolved')})
.catch(function() {console.log('search1 rejected')})
search(2000)
.then(function(){console.log('search2 resolved')})
.catch(function(){console.log('search2 rejected')})
search(1000)
.then(function(){console.log('search3 resolved')})
.catch(function(){console.log('search3 rejected')})

Need to see "search1 resolved" "search2 rejected" "search3 resolved"

How can I achieve this scenario?

like image 949
Eran Abir Avatar asked Sep 13 '25 07:09

Eran Abir


2 Answers

Promises aren't cancelable as such, but are cancelled in a limited sense by causing them to be rejected.

With that in mind, cancellation can be achieved with a small amount of elaboration around Promise.race() and the promise-returning function you wish to be cancelable.

function makeCancellable(fn) {
    var reject_; // cache for the latest `reject` executable
    return function(...params) {
        if(reject_) reject_(new Error('_cancelled_')); // If previous reject_ exists, cancel it.
                                                       // Note, this has an effect only if the previous race is still pending.
        let canceller = new Promise((resolve, reject) => { // create canceller promise
            reject_ = reject; // cache the canceller's `reject` executable
        });
        return Promise.race([canceller, fn.apply(null, params)]); // now race the promise of interest against the canceller
    }
}

Assuming your http call function is named httpRequest (promise is confusing):

const search = makeCancellable(httpRequest);

Now, each time search() is called, the cached reject executable is called to "cancel" the preceding search (if it exists and its race has not already fulfilled).

// Search 1: straightforward - nothing to cancel - httpRequest(200) is called
search(200)
.then(function() { console.log('search1 resolved') })
.catch(function(err) { console.log('search3 rejected', err) });

// Search 2: search 1 is cancelled and its catch callback fires - httpRequest(2000) is called
search(2000)
.then(function() { console.log('search2 resolved') })
.catch(function(err) { console.log('search3 rejected', err) });

// Search 3: search 2 is cancelled and its catch callback fires - httpRequest(1000) is called
search(1000)
.then(function() { console.log('search3 resolved') })
.catch(function(err) { console.log('search3 rejected', err) });

If necessary, the catch callbacks can test err.message === '_cancelled_' in order to distinguish between cancellation and other causes of rejection.

like image 123
Roamer-1888 Avatar answered Sep 14 '25 22:09

Roamer-1888


You can define a factory function to encapsulate your search() method with the requested cancellation behavior. Note that while Promise constructors are normally considered an anti-pattern, it is necessary in this case to keep a reference to each reject() function in the pending set in order to implement the early cancellation.

function cancellable(fn) {
  const pending = new Set();

  return function() {
    return new Promise(async (resolve, reject) => {
      let settle;
      let result;

      try {
        pending.add(reject);
        settle = resolve;
        result = await Promise.resolve(fn.apply(this, arguments));
      } catch (error) {
        settle = reject;
        result = error;
      }

      // if this promise has not been cancelled
      if (pending.has(reject)) {
        // cancel the pending promises from calls made before this
        for (const cancel of pending) {
          pending.delete(cancel);

          if (cancel !== reject) {
            cancel();
          } else {
            break;
          }
        }

        settle(result);
      }
    });
  };
}

// internal API function
function searchImpl(timeout) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, timeout);
  });
}

// pass your internal API function to cancellable()
// and use the return value as the external API function
const search = cancellable(searchImpl);

search(200).then(() => {
  console.log('search1 resolved');
}, () => {
  console.log('search1 rejected');
});

search(2000).then(() => {
  console.log('search2 resolved');
}, () => {
  console.log('search2 rejected');
});

search(1000).then(() => {
  console.log('search3 resolved');
}, () => {
  console.log('search3 rejected');
});

search(500).then(function() {
  console.log('search4 resolved');
}, () => {
  console.log('search4 rejected');
});

This factory function utilizes the insertion-order iteration of Set to cancel only the pending promises returned by calls made before the call returning the promise that has just settled.


Note that cancelling the promise using reject() does not terminate any underlying asynchronous process that the creation of the promise has initiated. Each HTTP request will continue to completion, as well as any of the other internal handlers that are called within search() before the promise is settled.

All cancellation() does is cause the internal state of the returned promise to transition from pending to rejected instead of fulfilled if a later promise settles first so that the appropriate handler(s) for promise resolution will be called by the consuming code.

like image 38
Patrick Roberts Avatar answered Sep 14 '25 20:09

Patrick Roberts