main
ts 94 lines 3.42 KB
Raw
1 // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 type Listener = (...args: any[]) => unknown
4 type Listeners = Set<Listener>
5 const LISTENERS_SUFFIX = '\0listeners'
6
7 export interface OnOptions { warnAfter?: number, callNow?: boolean }
8
9 export class BetterEventEmitter {
10 protected listeners = new Map<string, Listeners>()
11 stop = Symbol()
12 on(event: string | string[], listener: Listener, { warnAfter=10, callNow=false }: OnOptions={}) {
13 if (typeof event === 'string')
14 event = [event]
15 for (const e of event) {
16 let cbs = this.listeners.get(e)
17 if (!cbs)
18 this.listeners.set(e, cbs = new Set())
19 cbs.add(listener)
20 if (cbs.size > warnAfter)
21 console.warn("Warning: many event listeners for ", e)
22 this.emit(e + LISTENERS_SUFFIX, cbs, listener)
23 }
24 if (callNow)
25 try { listener() }
26 catch {}
27 return () => {
28 for (const e of event) {
29 const cbs = this.listeners.get(e)
30 if (!cbs) continue
31 cbs.delete(listener)
32 this.emit(e + LISTENERS_SUFFIX, cbs)
33 }
34 }
35 }
36 // call me when listeners for the event have changed
37 onListeners(event: string, listener: Listener) {
38 return this.on(event + LISTENERS_SUFFIX, listener)
39 }
40 // returns the unsubscriber function, which is also a PromiseLike with the array of arguments received by the listener
41 once(event: string, listener?: Listener) {
42 let off: () => unknown
43 const pro = new Promise<any[]>(resolve => {
44 off = this.on(event, function(...args){
45 off()
46 resolve(args.slice(0, -1)) // remove the extra argument at the end of our emit()
47 return listener?.(...args)
48 })
49 })
50 return Object.assign(off!, { then: pro.then.bind(pro) } satisfies PromiseLike<any> as typeof pro)
51 }
52 multi(map: { [eventName: string]: Listener }, options?: OnOptions) {
53 const cbs = Object.entries(map).map(([name, cb]) => this.on(name.split(' '), cb, options))
54 return () => {
55 for (const cb of cbs) cb()
56 }
57 }
58 anyListener(event: string) {
59 return Boolean(this.listeners.get(event)?.size)
60 }
61 emit(event: string, ...args: any[]) {
62 let cbs = this.listeners.get(event)
63 if (!cbs?.size) return
64 const output: any[] = []
65 let prevented = false
66 const extra = {
67 event,
68 output,
69 preventDefault() { prevented = true }
70 }
71 for (const cb of cbs) {
72 const res = cb(...args, extra)
73 if (res === this.stop)
74 extra.preventDefault()
75 else if (res !== undefined)
76 output.push(res)
77 }
78 return Object.assign(output, {
79 isDefaultPrevented: () => prevented,
80 })
81 }
82 async emitAsync(event: string, ...args: any[]) {
83 const syncRet = this.emit(event, ...args)
84 if (!syncRet) return
85 const asyncRet: typeof syncRet = await Promise.all(syncRet)
86 return Object.assign(asyncRet, {
87 isDefaultPrevented: () => syncRet.isDefaultPrevented()
88 || asyncRet.some((r: any) => r === this.stop)
89 })
90 }
91 }
92
93 // app-wide events
94 export default new BetterEventEmitter