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?
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 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With