xref: /aosp_15_r20/external/perfetto/ui/src/base/events.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2024 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// We limit ourselves to listeners that have only one argument (or zero, if
16// using void). API-wise it's more robust to wrap arguments in an interface,
17// rather than passing them positionally.
18export type EvtListener<T> = (args: T) => unknown | Promise<unknown>;
19
20// For use in interfaces, when we want to expose only the listen() method and
21// not the emit().
22export interface Evt<T> {
23  addListener(listener: EvtListener<T>): Disposable;
24}
25
26/**
27 * Example usage:
28 *
29 * interface OnLoadArgs {loadTime: number};
30 *
31 * class MyClass {
32 *  readonly onLoad = new EvtSource<OnLoadArgs>();
33 *
34 *  private doLoad() {
35 *   this.onLoad.notify({loadTime: 42});
36 *  }
37 * }
38 *
39 * const myClass = new MyClass();
40 * const listener = (args) => console.log('Load time', args.loadTime);
41 * trash = new DisposableStack();
42 * trash.use(myClass.onLoad.listen(listener));
43 * ...
44 * trash.dispose();
45 */
46export class EvtSource<T> implements Evt<T> {
47  private listeners: EvtListener<T>[] = [];
48
49  /**
50   * Registers a new event listener.
51   * @param listener The listener to be called when the event is fired.
52   * @returns a Disposable object that will remove the listener on dispose.
53   */
54  addListener(listener: EvtListener<T>): Disposable {
55    const listeners = this.listeners;
56    listeners.push(listener);
57    return {
58      [Symbol.dispose]() {
59        // Erase the handler from the array. (splice(length, 1) is a no-op).
60        const pos = listeners.indexOf(listener);
61        listeners.splice(pos >= 0 ? pos : listeners.length, 1);
62      },
63    };
64  }
65
66  /**
67   * Fires the event, invoking all registered listeners with the provided data.
68   * @param args The data to be passed to the listeners.
69   * @returns a promise that resolves when all the listeners have fulfilled
70   * their promise - if they returned one - otherwise resolves immediately.
71   */
72  async notify(args: T): Promise<void> {
73    const promises: unknown[] = [];
74    for (const listener of this.listeners) {
75      promises.push(Promise.resolve(listener(args)));
76    }
77    await Promise.allSettled(promises);
78  }
79}
80