1/**
2 * type-safe, browser compatible, mini event emitter.
3 * this module is intended to be imported via the main class.
4 *
5 * ```ts
6 * import { Events } from '@clo/lib/Events';
7 * const emitter = new Events<{ change: [string] }>();
8 * ```
9 *
10 * @module
11 */
12
13/**
14 * minimal event emitter supporting typed events. Unlike `EventTarget` and
15 * `node:events`, this supports an `onAny`, which is used by `lib/progress.ts`
16 *
17 * beware that the type parameter cannot be an `interface`.
18 */
19export class Events<Map extends Events.Map> {
20 #channels: { [C in keyof Map]?: Array<(...args: Map[C]) => void> } = {};
21 #global: Array<(channel: keyof Map, args: unknown[]) => void> = [];
22
23 /**
24 * Listen for events on the specified channel
25 * ```ts
26 * using dispose = events.on("change", () => { ... });
27 * // disposable will remove the listener on scope exit.
28 * // alternatively, can use `const` and call `dispose()`.
29 * ```
30 */
31 on<C extends keyof Map>(channel: C, cb: Events.Handler<Map[C]>): ts.Dispose {
32 const list = (this.#channels[channel] ??= []);
33 list.push(cb);
34 return ts.defer(() => remove(list, cb));
35 }
36 /**
37 * listen for one event on the specified channel
38 * ```ts
39 * // can be awaited
40 * const [data] = await events.once("something");
41 * // can be given a callback
42 * events.once("something", (data) => { ... });
43 * // can be cancelled
44 * const cancelable = events.once("something");
45 * cancelable.then(() => assert(false));
46 * cancelable.cancel();
47 * ```
48 */
49 once<C extends keyof Map>(channel: C, cb?: Events.Handler<Map[C]>): async.Cancelable<Map[C]> {
50 let disposeEvent: ts.Dispose | null = null;
51 let disposeError: ts.Dispose | null = null;
52 function dispose() {
53 if (!disposeEvent) return;
54 disposeEvent?.();
55 UNWRAP(disposeError)();
56 disposeEvent = disposeError = null;
57 }
58 return async.makeCancelable(
59 new Promise((resolve, reject) => {
60 disposeEvent = this.on(channel, (...args) => {
61 dispose();
62 resolve(args);
63 cb?.(...args);
64 });
65 disposeError = this.on(channel, (...args) => {
66 dispose();
67 reject(args);
68 cb?.(...args);
69 });
70 }),
71 () => dispose(),
72 );
73 }
74 /** listen for any event */
75 onAny(cb: (channel: keyof Map, args: unknown[]) => void): ts.Dispose {
76 this.#global.push(cb);
77 return ts.defer(() => remove(this.#global, cb));
78 }
79 /** emit an event on the specified channel */
80 emit<C extends keyof Map>(channel: C, ...args: Map[C]): void {
81 const handlers = this.#channels[channel];
82 if (handlers) handlers.forEach((cb) => cb(...args));
83 else if (channel === "error") {
84 this.#global?.forEach((cb) => cb(channel, args));
85 throw args.length === 0
86 ? new Error("Error event emitted")
87 : args.length === 1
88 ? args[0]
89 : new AggregateError(args);
90 }
91 this.#global?.forEach((cb) => cb(channel, args));
92 }
93}
94
95function remove<T>(list: T[], item: T) {
96 const i = list.indexOf(item);
97 if (i !== -1) list.splice(i, 1);
98}
99
100export namespace Events {
101 export type Map = { [key: string]: unknown[] };
102 export type Handler<Args extends unknown[]> = (...args: Args) => void;
103}
104
105import { UNWRAP } from "./assert.ts";
106import * as async from "./async.ts";
107import * as ts from "./ts.ts";