Async event handlers

Events.trigger() is implemented like all event handler as synchronous.

However there are many events

on(name: 'rename', callback: (file: TAbstractFile, oldPath: string) => any, ctx?: any): EventRef

Return type any allows besides all to use Promise<void> and have

app.vault.on('rename', renameHandler);

async function renameHandler(file: TAbstractFile, oldPath: string): Promise<void> {
}

The problem with this approach, is that when trigger() is executed, it returns immediately, not waiting for the async handler to complete its job. And multiple handlers for the same event might end up in the difficult to catch race conditions.

I think, trigger and tryTrigger has to be rewritten keeping the possibility of async handlers in mind.

class Events {
  private _: Record<string, EventRef[]> = {};

  trigger(name: string, ...data: any[]): void {
    this.triggerAsync(name, ...data);
  }

  tryTrigger(evt: EventRef, args: any[]): void {
    this.tryTriggerAsync(evt, args);
  }

  private triggerAsync(name: string, ...data: any[]): Promise<void> {
    let eventRefs = this._[name];
    if (eventRefs) {
      eventRefs = eventRefs.slice();
      for (const eventRef of eventRefs) {
        await this.tryTriggerAsync(eventRef, data);
      }
    }
  }

  private async tryTriggerAsync(evt: EventRef, args: any[]): Promise<void> {
    try {
      await evt.fn.apply(evt.ctx, args)
    } catch (error) {
      setTimeout(() => {
        throw error;
      }, 0);
    }
  }
}

You could also consider expose those ...Async methods. I understand that it is not possible to get rid of the sync versions anyways, but I think async versions could also be handy for plugin developers.

Additionally, I would like to bring attention

on(name: 'rename', callback: (file: TAbstractFile, oldPath: string) => any, ctx?: any): EventRef

callback return type is any, which is very misleading, because you don’t actually care about the return value

so I has to be

callback: (file: TAbstractFile, oldPath: string) => void

or if you are considering to add support for async event handlers, then

callback: (file: TAbstractFile, oldPath: string) => void | Promise<void>

Yes, with my suggestion, you cannot longer pass non-void callbacks.

app.vault.on('rename', myCallback); // compile error

function myCallback(file: TAbstractFile, oldPath: string): number {
  return 42;
}

But I think it makes code intent clearer. You can use the following workarounds:

app.vault.on('rename', (file, oldPath) => { myCallback(file, oldPath); });
app.vault.on('rename', omitReturnType(myCallback));

function omitReturnType<Args extends unknown[]>(fn: (...args: Args) => unknown): (...args: Args) => void {
  return (...args: Args) => {
    fn(...args);
  };
}