I have a dilemma with multiple async functions that can be called by the user at any point in time. It is crucial for me to ensure that all previously executed functions (and any potential "threads" they may have initiated) are terminated when a new function is invoked. This is necessary because these functions might try to access the same resource, specifically the webcodec decoder, which is not supported if accessed concurrently.
So, how can I achieve this?
My current approach: I am utilizing a global counter that is shared among all functions. At the beginning of each function, I increment and store a copy of this counter. Whenever an async function is triggered, I pass a copy of this counter to it. I then check at the start and end of the subroutine whether the global counter has been modified. However, managing this becomes cumbersome, especially with nested calls to async functions where passing the copied value is required multiple times. Additionally, this method falls short when using async functions not developed by me. Ideally, I want something like:
functionCurrentlyRun = null
async function runFunction(f, args) {
if (functionCurrentlyRun) {
stopFunctionAndAllSubthreads(functionCurrentlyRun);
}
return await runAndSaveIn(f, args, functionCurrentlyRun)
}
async function f1(args) {
return await someAsyncCalls();
}
function f2(args) {
return await someAsyncCalls();
}
runFunction(f1, 42);
runFunction(f2, 43);
Similar to the behavior exhibited by cancelAnimationFrame
, but applicable to arbitrary functions.
EDIT: Following the response, I attempted to implement the following:
<!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 = () => {
console.log("Just received a signal to abort");
reject(signal.reason);
};
promise.then(x => {
signal.removeEventListener("abort", myListener);
resolve(x);
});
signal.addEventListener("abort", myListener);
});
}
async function test3() {
console.log("[test3] A");
await makeMeAbortIfNeeded(wait(3000), mainSignal);
console.log("[test3] B");
return 42
}
</script>
</body>
</html>
This solution works effectively by replacing all instances of await foo
with
await makeMeAbortIfNeeded(foo, mainSignal);
. The only drawback is the challenge of resetting the controller to a non-abort state. It also becomes tedious to rewrite every await
, yet no other alternatives seem viable.