-1

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.

3
  • 1
    It is not possible to do this for arbitrary functions. Every asynchronous task has its own way of cancelling it (or none at all). Listening to a CancellationSignal is the golden way, but not every API supports it, you may have to do this yourself. Commented Dec 8, 2023 at 4:13
  • Please post your actual code with the webcodec decoder, as well as your attempt with the counter variable. Commented Dec 8, 2023 at 4:15
  • @Bergi thanks. So the code is a bit complicated but you can find it here github.com/leo-colisson/blenderpoint-web/blob/… (see how forceAddInCacheCounter is used in multiple places… and still I need to add it in wayyy more places) Commented Dec 9, 2023 at 3:23

1 Answer 1

2

There’s no way to cancel arbitrary tasks without modifying them to support cancellation. (And like the entire cooperative event loop concurrency model used in all mainstream JavaScript platforms as compared to preemptive multitasking, it’s for the best: having to worry that your code might stop at any point in its execution under normal conditions is a pain.)

The standard mechanism to use on the implementation side to support cancellation, though, is AbortSignal, and you can also forward these to a few DOM APIs that perform cancelable tasks.

Sign up to request clarification or add additional context in comments.

15 Comments

"you can forward these to any DOM APIs that perform cancelable tasks" - really?
@Bergi: Yes, e.g. if you want to stop trying to make a fetch happen. That might be the only implemented API that supports it so far…
Yes, the only API is not "any API"…
(I think there are a few more than just fetch, but mostly modern and rarely used ones, many common DOM APIs that are cancellable do not support abort signals)
@Bergi: I’m pretty sure there are more, but maybe I’m thinking of packages… but what’s an example of a cancellable task that doesn’t support AbortSignal? Everything else (including WebCodecs) is stream-likes.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.