Javascript - Filter result from function that can return Promise boolean OR boolean

Viewed 82

I wonder if someone ever had this issue before.

Before I had an EventHandler that would look like this:

export interface EventHandler {
  name: string;
  canHandleEvent(event: EventEntity): boolean;
  handleEvent(event: EventEntity): Promise<void>;
}

And my filter function would work normally, also my tests were passing - where I was filtering the events using:

messages.forEach(message => {
      const event: EventEntity = JSON.parse(message.Body);
      this.handlers
        .filter(handler => handler.canHandleEvent(event)) // WORKED WELL
        .forEach(handler => {
           // LOGIC
        });

Currently, we had to change the canHandleEvent to either Boolean or Promise. Since we had some promises to be resolved and identify whether the event can be handle or not.

export interface EventHandler {
  // ...
  canHandleEvent(event: EventEntity): boolean | Promise<boolean>;
}

So, in order to solve it, I used Promise.resolve and Promise.all. No luck:

messages.forEach(async message => {
      const event: EventEntity = JSON.parse(message.Body);
      const handlersResolved = await Promise.all(this.handlers);

      handlersResolved
        .filter(handler => handler.canHandleEvent(event))
        .forEach(handler => {

Now, my tests pass for the Promise canHandleEvent, but they are failing for the events passed that is boolean. They look like this:

class HandlerB implements EventHandler {
  name = HandlerB.name;
  numRuns = 0;

  canHandleEvent(event: EventEntity): boolean {
    console.log('event', event)
    return event.eventType === EventType.ONE_EVENT || event.eventType === EventType.SECOND_EVENT;
  }
  async handleEvent(event: EventEntity): Promise<void> {
    return new Promise(resolve => {
      setTimeout(() => {
        this.numRuns += 1;
        resolve();
      }, 25);
    });
  }
}

And my test that are now failing and before was passing are:

    it('Should process handlers that match, including canHandleEvent that returns Promise<boolean> TRUE', async () => {
      setHandlers([handlerA, handlerB, handlerC]);

      const event = await createEvent(EventType.SECOND_EVENT);
      await sleep(1000);

      expect(handlerA.numRuns, 'handleA').to.eql(0);
      expect(handlerB.numRuns, 'handleB').to.eql(1);
      expect(handlerC.numRuns, 'handleC').to.eql(1); // handlerC is Promise<boolean>, it works fine
      expect(errorHandler.numRuns).to.eql(0);

      handlerC.numRuns = 0;
    });

    it('Should allow handlers to pass even if one has an error', async () => {
      setHandlers([handlerA, handlerB, errorHandler]);

      const event = await createEvent(EventType.USER_REGISTRATION_STATUS);
      await sleep(1000);

      expect(handlerA.numRuns, 'handlerA').to.eql(1);
      expect(handlerB.numRuns, 'handlerB').to.eql(1);
      expect(errorHandler.numRuns, 'errorHandler').to.eql(1);
    });

Any thoughts on how to solve this? I've tried to identify whether is promise or boolean before inside the .filter but still no luck:

      this.handlers
        .filter(async handler =>  {
          if(typeof handler.canHandleEvent(event).then == 'function') {
            const result = await Promise.resolve(handler.canHandleEvent(event))
            console.log('IS PROMISE!!', result);
            return result
          }
          console.log('IT IS NOT PROMISE', handler.canHandleEvent(event))
          return handler.canHandleEvent(event)
        })
2 Answers

Currently, we had to change the canHandleEvent to either Boolean or Promise...

Just to be clear, that's a massive semantic change that will ripple through every layer of the code that uses that method. You can't directly use filter with it anymore, for instance, and any synchronous function that uses it is now potentially asynchronous (and fundamentally, "potentially asynchronous" = "asynchronous"). But if it has to happen, it has to happen! :-)

Your original code using canHandleEvent like this:

messages.forEach(message => {
    const event: EventEntity = JSON.parse(message.Body);
    this.handlers
        .filter(handler => handler.canHandleEvent(event)) // WORKED WELL
        .forEach(handler => {
             // LOGIC
        });
});

has to become asynchronous, like this:

// Handles things in parallel, not series
await/*or return*/ Promise.all(messages.map(message => {
    const event: EventEntity = JSON.parse(message.Body);
    return Promise.all(this.handlers.map(handler => async {
        if (await handler.canHandleEvent(event)) {
            // LOGIC
        }
    }));
}));

Notice how each layer was affected. messages.forEach turned into building an array of promises via messages.map and waiting for them via await (or using .then, etc., or returning to a calling function). For each message, we do the same thing for handlers, since we can't know whether a handler can handle something synchronously. (No need for Promise.resolve, Promise.all will handle that for you.)

The code above assumes it's okay for all of this to overlap (both the messages and the handlers for a message), whereas before because it was all synchronous, they all happened in series (all of the relevant handlers for one message, in order, then all of the handlers for the next, etc.). If you need it to be in series like that, you can use for-of loops:

// Handles things in series, not parallel
// (In an `async` function)
for (const message of messages) {
    const event: EventEntity = JSON.parse(message.Body);
    for (const handler of this.handlers) {
        if (await handler.canHandleEvent(event)) {
            // LOGIC
        }
    }
}

In both cases, it's possible to handle the ones returning boolean differently (synchronously) from the ones returning promises, but it complicates the code.

To solve your issue, I think the most simple way is to first populate the array with the expected values, so you can properly filter.

const transformedHandlers = await Promise.all(this.handlers.map(async handler => {
   return {
      ...handler,
      eventCanBeHandled: await handler.canHandleEvent(event)
   }
}))

This will transform the array so you have a key that shows what handlers can be handled.

To finish it off you use your code like you would always do but instead of checking

canHandleEvent

you use the new field that has been introduced in the const transformedhandlers

below is the example:

transformedHandlers
.filter(handler => handler.eventCanBeHandled)
.forEach(handler => {
   // LOGIC
});

This should be enough to keep you code working like it used to.

sorry for my English. it's not my native language

Related