| 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 | */ |
| 19 | export 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 | |
| 98 | export namespace Events { |
| 99 | export type Map = { [key: string]: unknown[] }; |
| 100 | export type Handler<Args extends unknown[]> = (...args: Args) => void; |
| 101 | } |
| 102 | |
| 103 | import { UNWRAP } from "./assert.ts"; |
| 104 | import * as async from "./async.ts"; |
| 105 | import * as ts from "./ts.ts"; |