3

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>
tobiasBora
  • 1,542
  • 14
  • 23
  • 2
    You are responsible for both parts of the code? I.e the one that emits the Event and the one that receives it? If so, it might be worth refactoring this without using Events, e.g call directly the *startmachine* handler from the *click* listener and handle the returned Promise nicely from there. – Kaiido Jan 19 '22 at 09:04
  • @Kalido Unfortunately no. I may try to ask for a pull request to the software so that it updates its code (in a backward compatible way), but ideally I'd prefer a solution that only changes the code on the 'startmachine' listener side (I'm not even sure the PR will be accepted). Otherwise, I guess I could use a "global variable array" and let the listeners add promises to this array that would be executed after the dispatchEvent. – tobiasBora Jan 19 '22 at 09:13
  • 1
    Then no luck, if you must wait for the Promise to resolve before knowing if the event should be prevented, the emitter side will always receive the event back before you can write to it. There might have been some hacks if you were from the other side, but here... – Kaiido Jan 19 '22 at 09:15
  • @Kaiido Ok thanks. I improved my question adding a dirty solution with one of these hacks, mentioning that I don't like it since I need to be on the other side. – tobiasBora Jan 19 '22 at 09:53

1 Answers1

1

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 :-/

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thanks for your answer. You recommend using promises due to how errors are handled? I also tried to update the code accordingly to use promise instead, but I can't find an elegant way to use the great `await` syntax to define my function: with promises the code quickly becomes unreadable. Unfortunately, pushing `async (resolve, reject) => ...` does not produce the same result as pushing `new Promise ...` – tobiasBora Jan 19 '22 at 10:27
  • Yes, calling a function can amount to a synchronous exception - if you just have an array of promises, pass it to `Promise.all` (or `Promise.allSettled`) and be done with it. I was thinking of declaring an `async function startMachine() { … }`, then `event.detail.waitFor.push(startMachine())` in the event handler. – Bergi Jan 19 '22 at 10:35
  • Oh, it's that simple to convert an async function to a promise ^^ Thanks a lot, I updated my answer! – tobiasBora Jan 19 '22 at 10:57
  • Also, the problem of my solution is that the function sending `dispatchEvent` needs to be async now... And it will likely break backward compatibility in the software I'm writting a plugin to... I guess there is no way to solve my problem without breaking compatibility? – tobiasBora Jan 19 '22 at 14:47
  • No, it's impossible to synchronously wait for asynchronous actions. – Bergi Jan 19 '22 at 16:50
  • It might be possible to make the function that dispatches the event backards-compatible by being "conditionally asynchronous", i.e. only returning a promise if the `detail.waitFor` array has entries and otherwise staying synchronous for the people not using this feature, but that's opening pandora's box. Don't do it, it's a can of worms - just make a major release. – Bergi Jan 19 '22 at 16:52