1

Basically I have two async functions. One of them is a simple 5 second timeout and the other is a complicated async function with multiple steps. Here is an example

const delay = ms => new Promise(res => setTimeout(res, ms));

class Runner {
  
  async start() {

      let printStuff = async () => {
        for(let i = 0 ; i < 50; i++){
          console.log(i);
          await delay(50);
        }
      }


      let printLetters = new Promise(async function(resolve, reject) {
        
        const length = Math.floor(Math.random() * 10)
        
        //dont know how long this will take
        for(let i = 0; i < length; i++){
          await printStuff();
        }
        
        resolve('letters');
      });

      let timeout = new Promise(async function(resolve, reject) {
        await delay(5000);
        resolve('timeout')
      });
      
      const finished = await Promise.all([timeout, printLetters]);
      if(finished === 'timeout'){
        this.stop();
      }
  }
  
  stop(){
    //stop printing stuff instantly
  }

}

const myRunner = new Runner();

myRunner.start();
//could call myRunner.stop(); if the user canceled it

The way I would implement this would add a global variable and include an if statement inside the for loop to check if the interrupt has been called but I am wondering if there is a better way to implement this. An issue with this solution would be it would print a few more numbers. I would have to add another check to the other for loop and this could get messy quickly.

Daniel
  • 2,223
  • 3
  • 9
  • 18
  • It really seems like you're thinking about this wrongly for an event-driven system like nodejs. If you could show us the REAL code that you want to run (not just makeup pseudo-code), then we could probably offer a better way to architect this. – jfriend00 Jan 20 '22 at 21:27
  • Also, this can't be code you're actually trying to run because it wouldn't even run. Neither of your first two `await` statements are allowed there because they aren't in `async` functions. Please show real code that is working to solve a real problem. When we can see the real problem (not just a theoretical problem), we can much better help you. – jfriend00 Jan 20 '22 at 21:32
  • I have edited the original post – Daniel Jan 20 '22 at 22:35
  • 1
    Maybe worth exploring a async library like https://rxjs.dev/ for this to sorta raise the level of abstraction. Otherwise I think what you're thinking sounds somewhat reasonable, – Zach Lysobey Jan 20 '22 at 22:41
  • 2
    [Never pass an `async function` as the executor to `new Promise`](https://stackoverflow.com/q/43036229/1048572)! – Bergi Jan 20 '22 at 22:46
  • how should I refactor this to avoid that – Daniel Jan 20 '22 at 22:56
  • Use `const printLetters = (async function() { …; return …; })();` or just `const timeout = delay(5000);` – Bergi Jan 20 '22 at 23:01
  • users can create a list of "events" they want to be completed. They then can set timeouts on said "events". Basically this script should run all the events and if one of them timeouts it should quit instantly. The printStuff function is an example of something similar to of these events and demonstrates how this approach makes it very difficult to quit instantly – Daniel Jan 20 '22 at 23:08
  • "*I would implement this with a global variable and include an if statement inside the for loop to check if the interrupt has been called*" - that sounds like a reasonable approach. (Use a property of the specific `Runner` instance though, not a global variable) – Bergi Jan 20 '22 at 23:10
  • what you're asking for is provided by [Promise.race()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race). Where one of the racers is a promise rejecting (with `setTimeout` in set time) and the other is whatever async code you're running in that particular case. If the real code settles before the setTimeout, that's the result. If not, it gets rejected (or fulfilled with some default data, if that makes more sense) by the promise wrapping the setTimeout. – tao Jan 20 '22 at 23:11
  • 1
    @tao - `Promise.race()` won't cause the non-timeout loop to stop running when the timeout fires. It will notify the caller when the first one finishes, but I think the OP wants to stop the loop too. – jfriend00 Jan 21 '22 at 02:17
  • One this that's off here is `const finished = await Promise.all([timeout, printLetters]);` followed by `if(finished === 'timeout'){...}`. `finished` will be an array here so that `if` statement is not correct. Perhaps you meant to use `Promise.race()` there? – jfriend00 Jan 21 '22 at 02:22

3 Answers3

1

Is this what you're after?

What changed:

  • Promise.all replaced with Promise.race
  • added isStopped prop which makes the "complicated async function with multiple steps" skip execution for the remaining steps. it doesn't kill it, though. Promises are not cancelable.

const delay = ms => new Promise(res => setTimeout(res, ms));

class Runner {
  isStopped = false;

  async start() {

    const printStuff = async () => {
      let i = 0;
      while (!this.isStopped) {
        console.log(i++);
        await delay(50);
      }
    }

    const printLetters = new Promise(
      resolve => printStuff()
        .then(() => resolve('letters'))
    );

    const timeout = new Promise(
      resolve => delay(5000)
        .then(() => resolve('timeout'))
    );

    const finished = await Promise.race([timeout, printLetters]);

    console.log({ finished });

    if (finished === 'timeout') {
      this.stop();
    }
  }

  stop() {
    this.isStopped = true;
  }
}

const myRunner = new Runner();

myRunner.start();
<button onclick="myRunner.stop()">stop</button>

Initial answer (left it in as the comments reference it, not what's above; and in case someone finds it useful in 2074):

Here's an example outlining what I was suggesting in the comment. run() below returns a race between a rejector happening after 1s and a fulfiller which resolves in random time between 0 and 2s.

const rejector = (timeout) => new Promise((resolve, reject) => {
  setTimeout(reject, timeout, 'rejected')
});

class Runner {
  run() {
    return Promise.race([
      rejector(1000), 
      new Promise((resolve, reject) => {
        setTimeout(resolve, Math.random() * 2000, 'fulfilled')
      })
    ])
  }
}

const t0 = performance.now();
[...Array(6).fill()].forEach((_, key) => {
  const runner = new Runner();
  runner.run()
    .then(r => console.log(`Proomise ${key} ${r} after ${performance.now() - t0}ms`))
    .catch(err => console.log(`Promise ${key} ${err} after ${performance.now() - t0}ms`));
})

Note: initially I placed the rejector inside the class but (at least for the above example) I don't see why it should not stay outside (in a real case scenario, imported from a helper file).

tao
  • 82,996
  • 16
  • 114
  • 150
  • Where's the `runner.stop()` method that the OP needs? – Bergi Jan 20 '22 at 23:42
  • 1
    @Bergi, good point, I'll add it in. – tao Jan 20 '22 at 23:43
  • this does not solve the problem though. If instead of the promise that resolves in 0-2 seconds if you replace it with printStuff() (from the original question) it will print something like "Promise 5 rejected after 1000.0...ms 5 5" The question is how do I force the other promise to quit – Daniel Jan 21 '22 at 00:00
1

Here is a simple demo that uses my own library.

import { CPromise } from "c-promise2";

const task = CPromise.promisify(function* () {
  let printStuff = CPromise.promisify(function* () {
    for (let i = 0; i < 10; i++) {
      console.log(i);
      yield CPromise.delay(100);
    }
  });

  const length = Math.floor(Math.random() * 10) + 3;

  //dont know how long this will take
  for (let i = 0; i < length; i++) {
    yield printStuff();
  }

  return "letters";
});

const promise = task()
  .timeout(5000)
  .then(
    (result) => console.log(`Done: ${result}`),
    (err) => console.warn(`Fail: ${err}`)
  );

setTimeout(() => {
  promise.cancel();
}, 2000);
Dmitriy Mozgovoy
  • 1,419
  • 2
  • 8
  • 7
  • Please [disclose your association](https://stackoverflow.com/help/promotion) with that library – Bergi Jan 26 '22 at 23:31
0

If you require an instantaneous stop capability, you would probably want to execute the print job as a external script. Then you use child processes like this.

const { spawn } = require('child_process');

class Runner {

......


    start() {
        this.job[somejobId] = spawn('command to execute script');
        //this can be anything, including a node script, e.g. `node start.js`
        .....
     }
    

    stop(jobId) {
       if (jobId) {
          //this will kill the script that you spawn above
          this.job[jobId].kill('SIGHUP');
       } 
    }

    stopAllJobs() {
         // looping through the job queue to kill all the jobs
         this.job.forEach(job => job.kill('SIGHUP'))
    }

}

You will have more info on how to start a child process from the node doc website https://nodejs.org/api/child_process.html#subprocesskillsignal

If your job (external script) is stalling, it's recommended that you only use the above codes if you have a minimum 2 CPU core, else it will affect your main process if your script is heavy.

Someone Special
  • 12,479
  • 7
  • 45
  • 76