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(() => void (list.splice(list.indexOf(cb)), 1));
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>(
50 channel: C,
51 cb?: Events.Handler<Map[C]>,
52 ): async.Cancelable<Map[C]> {
53 let disposeEvent: ts.Dispose | null = null;
54 let disposeError: ts.Dispose | null = null;
55 function dispose() {
56 if (!disposeEvent) return;
57 disposeEvent?.();
58 UNWRAP(disposeError)();
59 disposeEvent = disposeError = null;
60 }
61 return async.makeCancelable(
62 new Promise((resolve, reject) => {
63 disposeEvent = this.on(channel, (...args) => {
64 dispose();
65 resolve(args);
66 cb?.(...args);
67 });
68 disposeError = this.on(channel, (...args) => {
69 dispose();
70 reject(args);
71 cb?.(...args);
72 });
73 }),
74 () => dispose(),
75 );
76 }
77 /** listen for any event */
78 onAny(cb: (channel: keyof Map, args: unknown[]) => void): ts.Dispose {
79 this.#global.push(cb);
80 return ts.defer(() => void (this.#global.splice(this.#global.indexOf(cb)), 1));
81 }
82 /** emit an event on the specified channel */
83 emit<C extends keyof Map>(channel: C, ...args: Map[C]): void {
84 const handlers = this.#channels[channel];
85 if (handlers) handlers.forEach((cb) => cb(...args));
86 else if (channel === "error") {
87 this.#global?.forEach((cb) => cb(channel, args));
88 throw args.length === 0
89 ? new Error("Error event emitted")
90 : args.length === 1
91 ? args[0]
92 : new AggregateError(args);
93 }
94 this.#global?.forEach((cb) => cb(channel, args));
95 }
96}
97
98export namespace Events {
99 export type Map = { [key: string]: unknown[] };
100 export type Handler<Args extends unknown[]> = (...args: Args) => void;
101}
102
103import { UNWRAP } from "./assert.ts";
104import * as async from "./async.ts";
105import * as ts from "./ts.ts";