Dmitry Klymenko
Dmitry Klymenko

Reputation: 413

How to abort async function when 'stop' event has been emitted

I trying to make a puppeteer.js bot to be able to pause and resume its work. In general, i have a class with a dozen of async methods, event emitter and a property called 'state' with setter to change it. When I have event 'stop', I want some async functions to be aborted. How can I achieve this?

I thought i need to observe when this.state becomes 'stop', and run return; but hadn't found any solution. Then I decided to try to set a handler on an event which changes state to 'stop', but I cannot abort async functions from the handler on the stop event.

 constructor() {
    this.state = 'none';
    this.emiter = new events.EventEmitter();
    this.setHandler('stop', () => this.stop());
    this.setHandler('resume', () => this.resume());
    this.setHandler('onLoginPage', () => this.passAuth());
    // ...
    // And dozen of other states with its handlers
 }
 stop= () => this.setState('stoped', true);
 resume = () => this.setState(this.getPreviousState());
 getPreviousState = () => ...

 // Just an example of a state handler. It has async calls as well
 // I want to abort this function when event 'stop' is emitted
 @errorCatcher()
  async passAuth() {
    const { credentials } = Setup.Instance;
    await this.page.waitForSelector(LOGIN);
    await typeToInput(this.page, EMAIL_INPUT, credentials.login);
    await typeToInput(this.page, PWD_INPUT, credentials.pass);
    await Promise.all([
      await this.page.click(LOGIN),
      this.page.waitForNavigation({ timeout: 600000 }),
    ]);
    await this.page.waitFor(500);
    await DomMutations.setDomMutationObserver(this.page, this.socketEmitter);

 // ...
 // And dozen of handlers on corresponding state



 setState(nextState, resume) {
    // Avoiding to change state if we on pause.
    // But resume() can force setstate with argument resume = true;
    if (this.state === 'stoped' && !resume) return false;
    console.log(`\nEmmited FSM#${nextState}`);
    this.emiter.emit(`FSM#${nextState}`);
 }

 setHandler(state, handler) {
    this.emiter.on(`FSM#${state}`, async () => {
      this.state = state;
      console.log(`State has been changed: ${this.getPreviousState()} ==> ${this.state}. Runnig handler.\n`);
      // 
      // On the next line, we run a corresponding handler func,
      // like passAuth() for state 'onLoginPage'. It has has to be aborted
      // if emiter gets 'FSM#stoped' event.
      //
      await handler();
    });
  }

}```

I expect the async functions to be aborted when event emitter emits 'stop';

Upvotes: 0

Views: 650

Answers (1)

acrazing
acrazing

Reputation: 2284

It is impossible to do it natively.

Alternatively, there are two other way to do it.

  1. check your state after any call of await, for example:
class Stated {
  async run() {
    await foo()
    if(this.stopped) return
    await bar()
    if(this.stopped) return
    await done()
  }
}

const s = new Stated()
s.run()
  1. use generator with custom wrapper rather than async/await.
// the wrapper
function co(gen, isStopped = () => false) {
  return new Promise((resolve, reject) => {
    if (!gen || typeof gen.next !== 'function') return resolve(gen)
    onFulfilled()

    function onFulfilled(res) {
      let ret
      try {
        ret = gen.next(res)
      } catch (e) {
        return reject(e)
      }
      next(ret)
    }

    function onRejected(err) {
      let ret
      try {
        ret = gen.throw(err)
      } catch (e) {
        return reject(e)
      }
      next(ret)
    }

    function next(ret) {
      if (ret.done || isStopped()) return resolve(ret.value)
      Promise.resolve(ret.value).then(onFulfilled, onRejected)
    }
  });
}

// the following is your code:
class Stated {
  * run() {
    yield foo()
    yield bar()
    yield done()
  }
}

const s = new Stated()
co(s.run(), () => s.stopped)

Upvotes: 1

Related Questions