| ... | ... | @@ -0,0 +1,85 @@ |
| 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 dispose: ts.Dispose | null = null; |
| 54 | return async.makeCancelable( |
| 55 | new Promise((resolve) => |
| 56 | dispose = this.on(channel, (...args) => { |
| 57 | UNWRAP(dispose)(); |
| 58 | resolve(args); |
| 59 | cb?.(...args); |
| 60 | }) |
| 61 | ), |
| 62 | () => dispose?.(), |
| 63 | ); |
| 64 | } |
| 65 | /** listen for any event */ |
| 66 | onAny(cb: (channel: keyof Map, args: unknown[]) => void): ts.Dispose { |
| 67 | this.#global.push(cb); |
| 68 | return ts.defer(() => |
| 69 | void (this.#global.splice(this.#global.indexOf(cb)), 1) |
| 70 | ); |
| 71 | } |
| 72 | /** emit an event on the specified channel */ |
| 73 | emit<C extends keyof Map>(channel: C, ...args: Map[C]): void { |
| 74 | this.#channels[channel]?.forEach((cb) => cb(...args)); |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | export namespace Events { |
| 79 | export type Map = { [key: string]: unknown[] }; |
| 80 | export type Handler<Args extends unknown[]> = (...args: Args) => void; |
| 81 | } |
| 82 | |
| 83 | import * as ts from "./ts.ts"; |
| 84 | import * as async from "./async.ts"; |
| 85 | import { UNWRAP } from "./assert.ts"; |