3
votes

I was playing around with the message.react function and the awaitReactions function and I noticed something that bother me.

I was trying to determine if I should use a collector or the client.on('messageReactionAdd') (that is another question) for the following method:

  1. sending a message
  2. adding reactions to it
  3. doing something every time a reaction is added (for X seconds)

So I made a simple example to start, with a filter which return true every time, and I noticed that the collector was collecting the last emoji my bot was adding to the messages. Here is the code

const emojiNext = '➡';
const emojiPrevious = '⬅';
const emojiClap = '????';

function filter(reaction) {
  console.log('reacted to: ', reaction.emoji.name);
  return true;
}
function sleep(ms, a){
  return new Promise(resolve=>{
    setTimeout(resolve,ms);
  }).then(d => a);
}

function sendList(channel){
  channel.send('foo')
  .then(msg => {
    console.log('First');
    return msg.react(emojiPrevious);
  })
  .then(msgReaction => {
    console.log('Second', msgReaction.message.reactions.keys());
    return msgReaction.message.react(emojiNext);
  })
  .then(msgReaction => {
    console.log('Third', msgReaction.message.reactions.keys());
    return msgReaction.message.react(emojiClap);
  })
  // .then(msgReaction =>{
  //   return sleep(100, msgReaction);
  // })
  .then(msgReaction => {
    console.log('Fourth',  msgReaction.message.reactions.keys());
    msgReaction.message.awaitReactions(filter, {max: 2, time: 1000, errors: ['time']})
    .then(collected => {
      console.log('ending', collected);
    })
    .catch(collected => {
      console.log(`After 5 sec, only ${collected.size} out of 2 reacted: ${collected.map((v,k) => k)}`);
    });
  });
}

Debug

This example is a bit more developed than the first one I did because I tried some debug.

The example send a message, append with chained promise 3 emojis, and then start collecting emojis. However, as the following log show, the last emoji is collected (I never pressed any emoji myself, and I'm alone on my server):

First
Second [Map Iterator] { '⬅' }
Third [Map Iterator] { '⬅', '➡' }
Fourth [Map Iterator] { '⬅', '➡', '????' }
reacted to:  ????
After 5 sec, only 1 out of 2 reacted: ????

But sometimes (it depends) it works fine and the log are like this:

First
Second [Map Iterator] { '⬅' }
Third [Map Iterator] { '⬅', '➡' }
Fourth [Map Iterator] { '⬅', '➡', '????' }
After 5 sec, only 0 out of 2 reacted: 

I was lastly testing in the train, with an irregular network which might be the cause.

So I tried something else, I added the sleep function, which take an amount of ms and a value, and return a promise, which will return the value once resolved after the amount of ms has passed. (uncomment the 3 line to have this debug). This way the collector never collect the last emoji.

I also tried to return the promise before the then with the collector (let res = await channel.send...) and then execute the rest of the code. I still had the last emoji collected.

I know that I can use my filter to ignore bots or my emoji, and only focus on user's one (see code below) but I want to know what cause this behavior. Is there something I did bad? Is there something I didn't understand about promise?

function filter(reaction, user) {
  if(user.id === client.user.id) { return false; } // or user.bot to ignore all bot
  console.log('reacted to: ', reaction.emoji.name);
  return true;
}

In my opinion, and after seeing in the debug that the collector react after the log of everything, I think that the promise being resolved and Discord sending the information to the collector/the callback are different, but it's only a guess

note:
node.js version: v11.15.0
discord.js version: v11.5.1

1
Looks like some kind of race effect that's not under your control .... or just a ghost in the machine.Roamer-1888

1 Answers

2
votes

This is due to the separation between the REST API and the Gateway when interacting with Discord.

In the backend, message.react causes discord.js to fire off a request to Discord's REST API - Discord will receive this request, process it, and return a response, which causes the promise returned by message.react to either resolve or reject.

However, when you're listening for reactions, it's not listening to the REST API at all, it's listening to Gateway events - these are largely what causes events like client.on('messageReactionAdd') to be emitted on the client.

By the time you receive a response from discord saying that your reaction has posted successfully, discord hasn't necessarily finished sending out the messageReactionAdd event for your reaction to all clients (including your bot) yet! It might have, but it might not have. And yet you immediately, upon receiving the API response, start listening for reactions from the gateway. This is what's called a race condition - Discord's servers in charge of the REST API are in a race against its servers in charge of the Gateway for who will send that piece of info to your bot first.

This isn't something you can reasonably avoid, since you have absolutely no way of knowing when the Gateway will catch up with events and send your bot its own reaction - so what you should do here is account for the race condition in your filter, as you've acknowledged in the question. Another solution could involve adding a reaction event listener and only starting to listen for new reactions after the listener detects that your bot's reaction has come through the gateway, but this is much less clean for this specific case.


As an aside, I strongly suggest making use of async/await instead of those ugly and unreadable promise chains. Your code could be simplified to this:

async function sendList(channel){
  let msg = await channel.send('foo');
  console.log('First');
  let msgReaction = await msg.react(emojiPrevious);
  console.log('Second', msgReaction.message.reactions.keys());
  msgReaction = await msg.react(emojiNext);
  console.log('Third', msgReaction.message.reactions.keys());
  msgReaction = await msg.react(emojiClap);
  console.log('Fourth',  msgReaction.message.reactions.keys());
  try {
    let collected = await msgReaction.message.awaitReactions(filter, {max: 2, time: 1000, errors: ['time']})
    console.log('ending', collected);
  } catch(partialCollection) {
    console.log(`After 5 sec, only ${partialCollection.size} out of 2 reacted: ${partialCollection.map((v,k) => k)}`);
  }
}

Should be exactly identical to your current code but unbelievably more readable in my opinion.