Search code examples
javascripteventsasync-awaitdispatchevent

Await dispatchEvent: How to listen synchronously to a dispatchEvent when listener is async


In order to develop a plugin for a software, I need to listen to an event spawned with dispatchEvent and do some actions when receiving this event. What is important is that my actions should be finished before the software continues to execute the code after dispatchEvent.

Because dispatchEvent is synchronous, it should be the case... Unfortunately, my actions are asynchronous (basically I need to wait for a video to be seeked), and in that case dispatchEvent continues before the end of my own code. Is it possible to also get a synchronous behavior for dispatchEvent when the called function is asynchronous?

Example:

I'd like the following code to produce the output:

Putting the clothes in the washing machine.
Machine started...
Machine ended...
Picking the washed clothes.

unfortunately the clothes are picked before the end of the washing machine:

Putting the clothes in the washing machine.
Machine started...
Picking the washed clothes.
Machine ended...

I used this code:

var button = document.getElementById('button');
button.addEventListener('click', event => {
  console.log("Putting the clothes in the washing machine.");
  const machineResult = button.dispatchEvent(new CustomEvent('startmachine', {
    'detail': {
      timeProgram: 3000
    }
  }));
  if (machineResult.defaultPrevented) {
    console.log("Picking the dirty clothes.");
  } else {
    console.log("Picking the washed clothes.");
  }
});
button.addEventListener('startmachine', async event => {
  console.log("Machine started...");
  await new Promise(r => setTimeout(r, event.detail.timeProgram));
  console.log("Machine ended...");
});
<button id="button">Wash my clothes</button>

EDIT: found a dirty solution I don't like

As apparently, it seems to be impossible, I used a different approach that unfortunately needs some changes after the code dispatchEvent (as I'm not responsible for this part of the code, I'd prefer an approach that does not change the code after dispatchEvent).

The idea is to add an array in the event where each listener can put an async function. Then, after dispatchEvent is done, the code executes all functions in this array, and if any returns false it considers that the event was aborted.

  var button = document.getElementById('button');
  button.addEventListener('click', async event => {
      console.log("Putting the clothes in the washing machine.");
      var listOfActions = [];
      const machineResult = button.dispatchEvent(new CustomEvent('startmachine', { 'detail': {timeProgram: 3000, listOfActions: listOfActions} }));
      results = await Promise.all(listOfActions.map(x => x()));
      if (machineResult.defaultPrevented || !results.every(x => x != false)) { // Do not use == true or boolean to ensure returning nothing does not abort.
          console.log("Picking the dirty clothes.");
      } else {
          console.log("Picking the washed clothes.");
      }
  });
  
  button.addEventListener('startmachine', event => {
      event.detail.listOfActions.push( async () => {
          console.log(">>> Machine started...");
          await new Promise(r => setTimeout(r, event.detail.timeProgram));
          console.log("<<< Machine ended...");
          // If you want to say that the clothes are dirty:
          // return false
      });
  });
<button id="button">Wash my clothes</button>

EDIT 2

Using promise is better to handle errors:

        var button = document.getElementById('button');
  button.addEventListener('click', async event => {
      console.log("Putting the clothes in the washing machine.");
      var listOfActions = [];
      let cancelled = !button.dispatchEvent(new CustomEvent('startmachine', { 'detail': {timeProgram: 3000, listOfActions: listOfActions} }));
      results = await Promise.all(listOfActions);
      // Do not use == true or boolean to ensure returning nothing does not abort.
      cancelled = cancelled || !results.every(x => x != false)
      if (cancelled) {
          console.log("Picking the dirty clothes.");
      } else {
          console.log("Picking the washed clothes.");
      }
  });
  
  button.addEventListener('startmachine', event => {
      event.detail.listOfActions.push((async () => {
          console.log(">>> Machine started...");
          await new Promise(r => setTimeout(r, event.detail.timeProgram));
          console.log("<<< Machine ended...");
          // If you want to say that the clothes are dirty:
          //return false
      })());
      // ^-- The () is useful to push a promise and not an async function.
      // I could also have written let f = async () => { ... };  event....push(f())
  });
<button id="button">Wash my clothes</button>


Solution

  • Is it possible to also get a synchronous behavior for dispatchEvent when the called function is asynchronous?

    No, it's not.

    The idea is to add an array in the event where each listener can put an async function. Then, after dispatchEvent is done, the code executes all functions in this array

    Yes, that's a good idea. I would however make it an array of promises, not an array of functions.

    The web platform APIs use this same pattern in the ExtendableEvent. Unfortunately, that constructor is only available inside of service workers :-/