admin管理员组

文章数量:1323317

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?

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?

Share edited Sep 19, 2019 at 12:38 trincot 352k36 gold badges272 silver badges325 bronze badges asked Sep 18, 2019 at 21:15 Eran AbirEran Abir 1,0975 gold badges20 silver badges35 bronze badges 17
  • If you have multiple promises chained together- the chain will stop when one of them is rejected. You don't have them chained so they will all execute every time. – chevybow Commented Sep 18, 2019 at 21:19
  • Aksi bit rekated ti question but you should move to async await its much simplier – Loki Commented Sep 18, 2019 at 21:20
  • this is not the scenario I want. I need to cancel only the last call of promise function if not yet resolved the promise function just visualize an async call like an API i want only the last promise to be resolve or any other promise that resolved before 1000 ms – Eran Abir Commented Sep 18, 2019 at 21:20
  • Your Promise construction should be new Promise((resolve, reject) => ...), also, what's the problem logging promise2 resolved? – Washington Guedes Commented Sep 18, 2019 at 21:21
  • 2 Your question is ambiguous. If really the last promise should be cancelled when a new request es in, your code example should produce "search1 rejected", not "search1 resolved", since the second call of search should identify the previous promise as not resolved. If however, you want to give precedence to which-ever promise resolves first, then the output should be "search1 resolved", and the other two rejected. Please clarify the logic in your question, and make the code consistent with that explanation. – trincot Commented Sep 19, 2019 at 12:42
 |  Show 12 more ments

3 Answers 3

Reset to default 3

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.

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 pletion, 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.

Similar to the answer of PatrickRoberts, I would suggest to use a Map to maintain a list of pending promises.

I would however not maintain a reference to the reject callback outside of the promise constructor. I would suggest to abandon the idea of rejecting an outdated promise. Instead, just ignore it. Wrap it in a promise that never resolves or rejects, but just remains a dead promise object that does not ever change state. In fact, that silent promise could be the same one for every case where you need it.

Here is how that could look:

const delay = (timeout, data) => new Promise(resolve => setTimeout(() => resolve(data), timeout));
const godot = new Promise(() => null);

const search = (function () { // closure...
    const requests = new Map; // ... so to have shared variables
    let id = 1;
    
    return async function search() {
        let duration = Math.floor(Math.random() * 2000);
        let request = delay(duration, "data" + id); // This would be an HTTP request
        requests.set(request, id++);
        let data = await request;
        if (!requests.has(request)) return godot; // Never resolve...
        for (let [pendingRequest, pendingId] of requests) {
            if (pendingRequest === request) break;
            requests.delete(pendingRequest);
            // Just for demo we output something. 
            // Not needed in a real scenario:
            console.log("ignoring search " + pendingId);
        }
        requests.delete(request);
        return data;
    }    
})();

const reportSuccess = data => console.log("search resolved with " + data);
const reportError = err => console.log('search rejected with ' + err);

// Generate a search at regular intervals.
// In a real scenario this could happen in response to key events.
// Each promise resolves with a random duration.
setInterval(() => search().then(reportSuccess).catch(reportError), 100);

本文标签: javascriptHow to cancel last Promise if not resolvedStack Overflow