I have multiple async functions that the user can call at any time, but I want to make sure that all previously run functions (and the "threads" they might have spawned) are stopped when a new function is called as they would otherwise try to use the same resource (webcodec decoder) which is not supported.
How could I do that?
My attempts: For now, I use a global counter accessible to all functions that I increase and copy at the beginning of all functions, and everytime an async function is called, I send to it the copy of the counter, and I check at the beginning of the subroutine and right after it returned if the global counter has been changed, but it is really heavy to maintain when you have many nested calls to async functions (as you need to repeat and pass the value of the copied variable to all calls). Moreover, this will not work if we call inside async functions are not coded by myself. So I would prefer to have something like:
functionCurrentlyRun = null
async runFunction(f, args) {
if (functionCurrentlyRun) {
stopFunctionAndAllSubthreads(functionCurrentlyRun);
}
return await runAndSaveIn(f, args, functionCurrentlyRun)
}
async f1(args) {
return await someAsyncCalls();
}
f2(args) {
return await someAsyncCalls();
}
runFunction(f1, 42);
runFunction(f2, 43);
a bit like what is done with cancelAnimationFrame but for arbitrary functions.
EDIT Based on the answer, I tried to write this:
<!DOCTYPE html>
<body>
Hello <button id="buttonstart">Start me</button> <button id="buttonstop">Stop me</button>.
<script type="text/javascript">
const wait = (n) => new Promise((resolve) => setTimeout(resolve, n));
const controller = new AbortController();
const mainSignal = controller.signal;
document.getElementById("buttonstart").addEventListener("click", async () => {
console.log("Should be very first line");
setTimeout(() => console.log("First timeout"));
var x = await makeMeAbortIfNeeded(test3(), mainSignal);
console.log("Last line of the main loop. I received:", x);
})
document.getElementById("buttonstop").addEventListener("click", () => {
console.log("Click!");
controller.abort();
})
function makeMeAbortIfNeeded(promise, signal) {
return new Promise((resolve, reject) =>{
// If the signal is already aborted, immediately throw in order to reject the promise.
if (signal.aborted) {
reject(signal.reason);
}
const myListener = () => {
// Why isn't this working?? It s
console.log("Just received a signal to abort");
// Stop the main operation
// Reject the promise with the abort reason.
// WARNING: if the promise itself contains non blocking stuff, it will still continue to run.
reject(signal.reason);
};
promise.then(x => {
signal.removeEventListener("abort", myListener);
resolve(x);
});
signal.addEventListener("abort", myListener);
});
}
async function test3() {
console.log("[test3] A");
// See that you need to use it for any call to await functions, like:
await makeMeAbortIfNeeded(wait(3000), mainSignal);
// If instead you put:
// await wait(3000);
// Then the following message will still be printed even if test3() itself is wrapped.
console.log("[test3] B");
return 42
}
</script>
</body>
</html>
This works fine if you make sure to replace all await foo with await makeMeAbortIfNeeded(foo, mainSignal);, but the issue is that I don't know how to reset the controller to a non abort. (it is also a bit annoying to write this for all await, but seems like there are no other options.
CancellationSignalis the golden way, but not every API supports it, you may have to do this yourself.