-1

Why does the Node.js event listener "pause" the execution of an async function to start second execution if to emit the same event second time? And the second question: how does it possible to finish first execution, then start the second one?

I.e., if to launch this code in Node.js:

import { EventEmitter } from "events";

let event = new EventEmitter();

event.on("myEvent", async function () {
  console.log("Start");
  await new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(console.log("Do job"));
    }, 1000);
  });
  console.log("Finish");
});

event.emit("myEvent"); // first emit
event.emit("myEvent"); // second emit

Then I'm getting such result:

Start
Start
Do job
Finish
Do job
Finish

Hovewer I'd like to see this:

Start
Do job
Finish
Start
Do job
Finish

UPDATE Below I put real code which contains the described problem

const web3 = new Web3(
  new Web3.providers.WebsocketProvider(
    "wss://eth-mainnet.g.alchemy.com/v2/<API-KEY>"
  )
);
let walletAddress = "0x123";

let options = {
  topics: [web3.utils.sha3("Transfer(address,address,uint256)")],
};

let subscription = web3.eth.subscribe("logs", options);

subscription.on("data", async (event) => {
  if (event.topics.length == 3) {
    let transaction = decodeTransaction(event); //just using web3.eth.abi.decodeLog(...)
    if (
      transaction.from === walletAddress ||
      transaction.to === walletAddress
    ) {
      const contract = new web3.eth.Contract(abi, event.address);
      let coinSymbol = await contract.methods.symbol().call(); //<-- The issue starts here
      await redisClient.hSet(
        walletAddress,
        coinSymbol,
        transaction.value
      );
    }
  }
});
TylerH
  • 20,799
  • 66
  • 75
  • 101
Volodymyr Nabok
  • 415
  • 1
  • 4
  • 11
  • So, what exactly is the problem with the code you added? And, if it has to do with `collectData()`, then please show that code also. – jfriend00 Oct 13 '22 at 08:38
  • collectData() is just a function that calls another async function. To be more clear, I replaced the collectData() by the async function that actually calls. As I understood, because of this function is asynchronous, it pauses execution while the promise is resolving. At this moment event listener receives new event. Because of previous event has not finished execution yet, new event executed in a wrong way (because read incorrect data from the DB). – Volodymyr Nabok Oct 13 '22 at 09:03
  • Maybe you want to use something like [`async-lock`](https://www.npmjs.com/package/async-lock) around your processing function. – CherryDT Oct 13 '22 at 09:03
  • But I'm not sure this is even your problem, because it looks as if you were trying to track balances, however what you _actually_ do is save for each wallet the last known transaction value for each coin (regradless of incoming or outgoing direction). - And even then, using the symbol as key is probably not the best idea, should be the contract address - the reason is that I could otherwise mess up your DB by deploying a contract with symbol `USDT` (which is not the real USDT contract of course) and send someone 1,000,000 "USDT" this way. – CherryDT Oct 13 '22 at 09:05

1 Answers1

0

The key here is that async functions don't block the interpreter and EventEmitter events don't wait for async event handlers to resolve their promise.

So, this is what happens:

  1. The first event.emit() gets called. This synchronously triggers the myEvent handler function to get called.
  2. That function executes. After outputting start, it hits an await. That causes it to suspend further execution of the function and immediately return a promise back to the caller. This causes the first event.emit(...) to be done as the eventEmitter object is not promise-aware - it pays no attention at all to the promise that your event handler function returns.
  3. The second event.emit() gets called. This synchronously triggers the myEvent handler function to get called.
  4. That function executes. After outputting start, it hits an await. That causes it to suspend further execution of the function and immediately return a promise back to the caller. This causes the second event.emit(...) to be done.

So, this is why your output starts with:

Start
Start

Then, sometime later (after a setTimeout() fires), console.log("Do job") outputs the promise gets resolved which causes the await to be satisfied and the function resumes execution after the await. This then outputs Finish.

So, at this point, the first timer has fired and you have:

Start
Start
Do job
Finish

Then the second setTimeout() fires and does the same and then you have this which is your full output:

Start
Start
Do job
Finish
Do job
Finish

The key here is that the EventEmitter class is not promise-aware for it's event handlers. It pays no attention to the promise that your async function returns and thus does not "wait" for it to resolve before allowing the rest of your code to continue executing.


If what you're trying to do in your subscription.on('data', ...) code is to serialize the processing of each event so you finish processing one before starting the processing of the next one, then you can queue your events and only process the next one when the prior one has finished. If a new event arrives while you're still processing a previous one, it just gets put in the queue and stays there until the prior one is done processing.

Here's how that code could look:

const eventQueue = [];
let eventInProgress = false;

async function processEvent(event) {
    try {
        eventInProgress = true;
        if (event.topics.length == 3) {
            let transaction = decodeTransaction(event); //just using web3.eth.abi.decodeLog(...)
            if (
                transaction.from === walletAddress ||
                transaction.to === walletAddress
            ) {
                const contract = new web3.eth.Contract(abi, event.address);
                let coinSymbol = await contract.methods.symbol().call(); //<-- The issue starts here
                await redisClient.hSet(
                    walletAddress,
                    coinSymbol,
                    transaction.value
                );
            }
        }
    } catch (e) {
        // have to decide what to do with rejections from either of the await statements
        console.log(e);
    } finally {
        eventInProgress = false;
        // if there are more events to process, then process the oldest one
        if (eventQueue.length) {
            processEvent(eventQueue.shift());
        }
    }
}

subscription.on("data", (event) => {
    // serialize the processing of events
    if (eventInProgress) {
        eventQueue.push(event);
    } else {
        processEvent(event);
    }
});
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Thanks @jfriend00! Any workaround applicable? – Volodymyr Nabok Oct 12 '22 at 17:40
  • @VolodymyrNabok - Well, you can't use `EventEmitter` to sequence event handlers containing asynchronous operations. `EventEmitter` just doesn't do that. If you showed the real code that illustrates the real operations here so we could see what you're trying to sequence, we could then make an actual suggestion for how to solve the real problem. This is why it's generally to YOUR benefit to show the real code and the real problem in your question, not a makeup example as we can help you better when you do that. – jfriend00 Oct 12 '22 at 20:08
  • Sure, you are right, I've updated my question by new snippet. – Volodymyr Nabok Oct 13 '22 at 07:23
  • @VolodymyrNabok - I've added to the end of my answer a method for serializing the processing of your `subscription.on("data", ...)` events so that you'll finish processing one before you start processing the next one using a simple queue. – jfriend00 Oct 14 '22 at 04:38