javascriptwhile-looppromiseabortcontroller

Function doesn't run to completion when clearing NodeJS timeout


Ok - so I've written this and it works how I want it to, I just don't understand how it works / why it's working the way it does.

Can someone please explain what I'm missing here.

Here is the code:

const crypto = require('crypto');

class ProcessController extends AbortController {
  timeouts = new Map();

  sleep(ms) {
    const id = crypto.randomBytes(8).toString('hex');
    return new Promise((resolve) => {
      this.timeouts.set(id, setTimeout(() => {
        this.timeouts.delete(id);
        resolve();
      }, ms))
    })
  }

  abort() {
    super.abort();
    for (const timeout of this.timeouts.values()) {
      clearTimeout(timeout);
    }
    // not really necessary as not reusable but be tidy
    this.timeouts.clear();
  }
}

async function meLoop(controller) {
  const { signal } = controller;

  while (!signal.aborted) {
    console.log("START OF LOOP. Uptime:", Math.floor(process.uptime()));
    await controller.sleep(5 * 1000);
    console.log('END OF LOOP');
  }
  console.log("BEYOND LOOP. Uptime:", process.uptime());
}

async function main() {
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
  const controller = new ProcessController();
  meLoop(controller);
  await sleep(23 * 1000);
  controller.abort();

  console.log("END. Uptime:", process.uptime());
}

main();

And here is the outcome:

> node testBreakSleep.js
START OF LOOP. Uptime: 0
END OF LOOP
START OF LOOP. Uptime: 5
END OF LOOP
START OF LOOP. Uptime: 10
END OF LOOP
START OF LOOP. Uptime: 15
END OF LOOP
START OF LOOP. Uptime: 20
END. Uptime: 23.044878139

Basically, it's an abortable loop. My understanding was that during the time in the while loop, whatever is in there should run to completion before the while is evaluated again as Javascript is single threaded.

However, what I don't get, is that when abort() is called, and the setTimeouts are cleared, it kind of just exits instead of finishing the loop? I don't understand what happens to the promise? It doesn't resolve, and the end of the loop is never reached (this is good for me)

Can someone explain?

Edit - thought I should mention, if I comment out the clearTimeout in the abort() function, Node hangs around until the promise resolves.

> node testBreakSleep.js
START OF LOOP. Uptime: 0
END OF LOOP
START OF LOOP. Uptime: 5
END OF LOOP
START OF LOOP. Uptime: 10
END OF LOOP
START OF LOOP. Uptime: 15
END OF LOOP
START OF LOOP. Uptime: 20
END. Uptime: 23.037329278
END OF LOOP
BEYOND LOOP. Uptime: 25.041987186

Thanks


Solution

  • Calling await blocks until the promise is resolved or rejected, so that means that calling controller.abort() rigth after the loop has started won't be effective until the promise has resolved and the loop starts over. Adding an event listener (if you're working with nodejs you can use event emitters) to your cancellable async function might do the trick, maybe this example helps you to achieve what you want:

    function myCancellableFunction (signal) {
        return new Promise((res,rej) => {
            if (signal.aborted) return rej();
            const cancellableHandler = () => {
               // you may also want to perform a clean up here
               // like removing the timeout from your timeouts list
               signal.removeEventListener("abort",cancellableHandler);
               rej();
               
             }
            signal.addEventListener("abort",cancellableHandler);
            setTimeout(() => {
                signal.removeEventListener("abort",cancellableHandler);
                res();
            },2000);
        })
    }
    
    async function myLoopSignalFunction (signal) {
         while (true) {
             console.log("While loop beginning");
             // You can stop the while loop catching the error and breaking
             try {
                console.time("Cancellable elapsed time");
                await myCancellableFunction(signal);
                console.log("Cancellable function has been executed");
                console.timeEnd("Cancellable elapsed time");
             } catch(e)  {
                console.log("Function has been canceled");
                break;
             }
             
         }
    }
    
    async function main () {
        let controller = new AbortController();
        myLoopSignalFunction(controller.signal);
        setTimeout(() => {
          controller.abort();
          setTimeout(() => {
            controller = new AbortController()
            controller.abort()
            myLoopSignalFunction(controller.signal); // The controller is already aborted, nothing to do
          },1000);
        },10000);
       
    }
    main()