@xyo-network/module-events 2.53.2 → 2.53.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import { assertEx } from '@xylabs/assert'
2
2
  import { forget } from '@xylabs/forget'
3
+ import { Base, BaseParams } from '@xyo-network/core'
3
4
 
4
5
  import { EventAnyListener, EventArgs, EventData, EventFunctions, EventListener, EventName } from '../model'
5
6
 
@@ -10,29 +11,22 @@ To enable this feature set the `DEBUG` environment variable to `emittery` or `*`
10
11
 
11
12
  See API for more information on how debugging works.
12
13
  */
13
- export type DebugLogger<TEventData extends EventData, TName extends keyof TEventData> = (
14
- type: string,
15
- debugName: string,
16
- eventName?: TName,
17
- eventData?: TEventData[TName],
18
- ) => void
14
+ export type DebugLogger = (type: string, debugName: string, eventName?: EventName, eventData?: EventArgs) => void
15
+
16
+ type EventListenerInfo<TEventArgs extends EventArgs = EventArgs> = {
17
+ filter?: TEventArgs
18
+ listener: EventListener<TEventArgs>
19
+ }
19
20
 
20
21
  /**
21
22
  Configure debug options of an instance.
22
23
  */
23
- export type DebugOptions<TEventData extends EventData> = {
24
+ export type DebugOptions = {
24
25
  enabled?: boolean
25
- logger?: DebugLogger<TEventData, keyof TEventData>
26
+ logger?: DebugLogger
26
27
  readonly name: string
27
28
  }
28
29
 
29
- /**
30
- Configuration options for Emittery.
31
- */
32
- export type Options<TEventData extends EventData> = {
33
- readonly debug?: DebugOptions<TEventData>
34
- }
35
-
36
30
  const resolvedPromise = Promise.resolve()
37
31
 
38
32
  export type MetaEventData<TEventData extends EventData> = {
@@ -46,49 +40,29 @@ export type MetaEventData<TEventData extends EventData> = {
46
40
  }
47
41
  }
48
42
 
49
- let canEmitMetaEvents = false
50
- let isGlobalDebugEnabled = false
51
-
52
- function assertEventName(eventName: EventName) {
53
- if (typeof eventName !== 'string' && typeof eventName !== 'symbol' && typeof eventName !== 'number') {
54
- throw new TypeError('`eventName` must be a string, symbol, or number')
55
- }
56
- }
57
-
58
- function assertListener(listener: object) {
59
- if (typeof listener !== 'function') {
60
- throw new TypeError('listener must be a function')
61
- }
62
- }
63
-
64
43
  const isMetaEvent = (eventName: EventName) => eventName === 'listenerAdded' || eventName === 'listenerRemoved'
65
44
 
66
- export class Events<TEventData extends EventData = EventData> implements EventFunctions<TEventData> {
67
- static anyMap = new WeakMap<object, Set<EventAnyListener>>()
68
- static eventsMap = new WeakMap<object, Map<EventName, Set<EventListener>>>()
45
+ export type EventsParams = BaseParams<{ readonly debug?: DebugOptions }>
69
46
 
70
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
- debug?: DebugOptions<any>
47
+ export class Events<TEventData extends EventData = EventData> extends Base<EventsParams> implements EventFunctions<TEventData> {
48
+ protected static anyMap = new WeakMap<object, Set<EventAnyListener>>()
49
+ protected static eventsMap = new WeakMap<object, Map<EventName, Set<EventListenerInfo>>>()
50
+
51
+ private static canEmitMetaEvents = false
52
+ private static isGlobalDebugEnabled = false
72
53
 
73
54
  //this is here to be able to query the type, not use
74
55
  eventData = {} as TEventData
75
56
 
76
- constructor(options: Options<TEventData> = {}) {
77
- Events.anyMap.set(this, new Set<EventAnyListener>())
78
- Events.eventsMap.set(this, new Map<keyof TEventData, Set<EventListener>>())
79
-
80
- this.debug = options.debug
81
-
82
- if (this.debug) {
83
- this.debug.enabled = !!this.debug.enabled
84
-
85
- this.debug.logger =
86
- this.debug.logger ??
87
- ((type: string, debugName: string, eventName?: keyof TEventData, eventData?: TEventData[keyof TEventData]) => {
57
+ constructor(params: EventsParams = {}) {
58
+ const mutatedParams = { ...params }
59
+ if (mutatedParams.debug) {
60
+ mutatedParams.debug.logger =
61
+ mutatedParams.debug.logger ??
62
+ ((type: string, debugName: string, eventName?: EventName, eventData?: EventArgs) => {
88
63
  let eventDataString: string
89
64
  let eventNameString: string | undefined
90
65
  try {
91
- // TODO: Use https://github.com/sindresorhus/safe-stringify when the package is more mature. Just copy-paste the code.
92
66
  eventDataString = JSON.stringify(eventData)
93
67
  } catch {
94
68
  eventDataString = `Object with the following keys failed to stringify: ${Object.keys(eventData ?? {}).join(',')}`
@@ -102,9 +76,12 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
102
76
 
103
77
  const currentTime = new Date()
104
78
  const logTime = `${currentTime.getHours()}:${currentTime.getMinutes()}:${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`
105
- console.log(`[${logTime}][events:${type}][${debugName}] Event Name: ${eventNameString}\n\tdata: ${eventDataString}`)
79
+ this.logger.log(`[${logTime}][events:${type}][${debugName}] Event Name: ${eventNameString}\n\tdata: ${eventDataString}`)
106
80
  })
107
81
  }
82
+ super(mutatedParams)
83
+ Events.anyMap.set(this, new Set<EventAnyListener>())
84
+ Events.eventsMap.set(this, new Map<keyof TEventData, Set<EventListenerInfo>>())
108
85
  }
109
86
 
110
87
  static get isDebugEnabled() {
@@ -112,15 +89,19 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
112
89
  // so instead of just type checking `globalThis.process`, we need to make sure that `globalThis.process.env` exists.
113
90
 
114
91
  if (typeof globalThis.process?.env !== 'object') {
115
- return isGlobalDebugEnabled
92
+ return Events.isGlobalDebugEnabled
116
93
  }
117
94
 
118
95
  const { env } = globalThis.process ?? { env: {} }
119
- return env.DEBUG === 'emittery' || env.DEBUG === '*' || isGlobalDebugEnabled
96
+ return env.DEBUG === 'events' || env.DEBUG === '*' || Events.isGlobalDebugEnabled
120
97
  }
121
98
 
122
99
  static set isDebugEnabled(newValue) {
123
- isGlobalDebugEnabled = newValue
100
+ Events.isGlobalDebugEnabled = newValue
101
+ }
102
+
103
+ get debug() {
104
+ return this.params.debug
124
105
  }
125
106
 
126
107
  clearListeners(eventNames: keyof TEventData | (keyof TEventData)[]) {
@@ -152,53 +133,54 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
152
133
  async emitMetaEvent<TEventName extends keyof MetaEventData<TEventData>>(eventName: TEventName, eventArgs: MetaEventData<TEventData>[TEventName]) {
153
134
  if (isMetaEvent(eventName)) {
154
135
  try {
155
- canEmitMetaEvents = true
136
+ Events.canEmitMetaEvents = true
156
137
  await this.emitMetaEventInternal(eventName, eventArgs)
157
138
  } finally {
158
- canEmitMetaEvents = false
139
+ Events.canEmitMetaEvents = false
159
140
  }
160
141
  }
161
142
  }
162
143
 
163
144
  async emitSerial<TEventName extends keyof TEventData>(eventName: TEventName, eventArgs: TEventData[TEventName]) {
164
- assertEventName(eventName)
165
-
166
- if (isMetaEvent(eventName) && !canEmitMetaEvents) {
145
+ if (isMetaEvent(eventName) && !Events.canEmitMetaEvents) {
167
146
  throw new TypeError('`eventName` cannot be meta event `listenerAdded` or `listenerRemoved`')
168
147
  }
169
148
 
149
+ const filterMatch = (args: TEventData[TEventName], filter: TEventData[TEventName]) => {
150
+ if (filter) {
151
+ switch (typeof filter) {
152
+ case 'object':
153
+ return Object.entries(args).reduce((prev, [key, value]) => ((filter as Record<PropertyKey, unknown>)[key] === value ? true : prev), false)
154
+ default:
155
+ return args === filter
156
+ }
157
+ }
158
+ return true
159
+ }
160
+
170
161
  this.logIfDebugEnabled('emitSerial', eventName, eventArgs)
171
162
 
172
163
  const listeners = this.getListeners(eventName) ?? new Set()
164
+ const filteredListeners = [...listeners.values()]
165
+ .filter((value) => (value.filter ? filterMatch(eventArgs, value.filter as TEventData[TEventName]) : true))
166
+ .map((info) => info.listener)
173
167
  const anyListeners = assertEx(Events.anyMap.get(this))
174
- const staticListeners = [...listeners]
168
+ const staticListeners = [...filteredListeners]
175
169
  const staticAnyListeners = [...anyListeners]
176
170
 
177
171
  await resolvedPromise
178
172
 
179
173
  for (const listener of staticListeners) {
180
- if (listeners.has(listener)) {
181
- await listener(eventArgs)
182
- }
174
+ await this.safeCallListener(eventName, eventArgs, listener)
183
175
  }
184
176
 
185
177
  for (const listener of staticAnyListeners) {
186
- if (anyListeners.has(listener)) {
187
- await listener(eventName, eventArgs)
188
- }
189
- }
190
- }
191
-
192
- getListeners(eventName: keyof TEventData) {
193
- const events = assertEx(Events.eventsMap.get(this))
194
- if (!events.has(eventName)) {
195
- return
178
+ await this.safeCallAnyListener(eventName, eventArgs, listener)
196
179
  }
197
-
198
- return events.get(eventName)
199
180
  }
200
181
 
201
- listenerCount(eventNames: keyof TEventData | (keyof TEventData)[]) {
182
+ //TODO: Make test for this
183
+ listenerCount(eventNames?: keyof TEventData | (keyof TEventData)[]) {
202
184
  const eventNamesArray = Array.isArray(eventNames) ? eventNames : [eventNames]
203
185
  let count = 0
204
186
 
@@ -209,10 +191,6 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
209
191
  continue
210
192
  }
211
193
 
212
- if (typeof eventName !== 'undefined') {
213
- assertEventName(eventName)
214
- }
215
-
216
194
  count += assertEx(Events.anyMap.get(this)).size
217
195
 
218
196
  for (const value of assertEx(Events.eventsMap.get(this)).values()) {
@@ -223,19 +201,19 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
223
201
  return count
224
202
  }
225
203
 
226
- logIfDebugEnabled<TEventName extends EventName, TEventArgs extends EventArgs>(type: string, eventName?: TEventName, eventArgs?: TEventArgs) {
204
+ logIfDebugEnabled<TEventName extends EventName>(type: string, eventName?: TEventName, eventArgs?: EventArgs) {
227
205
  if (Events.isDebugEnabled || this.debug?.enabled) {
228
206
  this.debug?.logger?.(type, this.debug.name, eventName, eventArgs)
229
207
  }
230
208
  }
231
209
 
232
- off<TEventName extends keyof TEventData>(eventNames: TEventName | TEventName[], listener: EventListener<TEventData[TEventName]>) {
233
- assertListener(listener)
234
-
210
+ off<TEventName extends keyof TEventData, TEventListener = EventListener<TEventData[TEventName]>>(
211
+ eventNames: TEventName | TEventName[],
212
+ listener: TEventListener,
213
+ ) {
235
214
  const eventNamesArray = Array.isArray(eventNames) ? eventNames : [eventNames]
236
215
  for (const eventName of eventNamesArray) {
237
- assertEventName(eventName)
238
- const set = this.getListeners(eventName) as Set<EventListener<TEventData[TEventName]>>
216
+ const set = this.getListeners(eventName) as Set<TEventListener>
239
217
  if (set) {
240
218
  set.delete(listener)
241
219
  if (set.size === 0) {
@@ -253,8 +231,6 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
253
231
  }
254
232
 
255
233
  offAny(listener: EventAnyListener) {
256
- assertListener(listener)
257
-
258
234
  this.logIfDebugEnabled('unsubscribeAny', undefined, undefined)
259
235
 
260
236
  const typedMap = Events.anyMap.get(this) as Set<EventAnyListener<TEventData[keyof TEventData]>>
@@ -262,12 +238,13 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
262
238
  forget(this.emitMetaEvent('listenerRemoved', { listener: listener as EventAnyListener }))
263
239
  }
264
240
 
265
- on<TEventName extends keyof TEventData = keyof TEventData>(eventNames: TEventName | TEventName[], listener: EventListener<TEventData[TEventName]>) {
266
- assertListener(listener)
267
-
241
+ on<TEventName extends keyof TEventData = keyof TEventData>(
242
+ eventNames: TEventName | TEventName[],
243
+ listener: EventListener<TEventData[TEventName]>,
244
+ filter?: TEventData[TEventName],
245
+ ) {
268
246
  const eventNamesArray = Array.isArray(eventNames) ? eventNames : [eventNames]
269
247
  for (const eventName of eventNamesArray) {
270
- assertEventName(eventName)
271
248
  let set = this.getListeners(eventName)
272
249
  if (!set) {
273
250
  set = new Set()
@@ -275,7 +252,7 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
275
252
  events?.set(eventName, set)
276
253
  }
277
254
 
278
- set.add(listener as EventListener)
255
+ set.add({ filter, listener: listener as EventListener })
279
256
 
280
257
  this.logIfDebugEnabled('subscribe', eventName, undefined)
281
258
 
@@ -288,8 +265,6 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
288
265
  }
289
266
 
290
267
  onAny(listener: EventAnyListener) {
291
- assertListener(listener)
292
-
293
268
  this.logIfDebugEnabled('subscribeAny', undefined, undefined)
294
269
 
295
270
  Events.anyMap.get(this)?.add(listener as EventAnyListener)
@@ -300,7 +275,7 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
300
275
  once<TEventName extends keyof TEventData>(eventName: TEventName, listener: EventListener<TEventData[TEventName]>) {
301
276
  const subListener = async (args: TEventData[TEventName]) => {
302
277
  this.off(eventName, subListener)
303
- await listener(args)
278
+ await this.safeCallListener(eventName, args, listener)
304
279
  }
305
280
  this.on(eventName, subListener)
306
281
  return this.off.bind(this, eventName, subListener as EventListener)
@@ -309,30 +284,28 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
309
284
  private async emitInternal<TEventName extends keyof TEventData, TEventArgs extends TEventData[TEventName]>(
310
285
  eventName: TEventName,
311
286
  eventArgs: TEventArgs,
287
+ filter?: TEventArgs,
312
288
  ) {
313
- assertEventName(eventName)
314
-
315
- if (isMetaEvent(eventName) && !canEmitMetaEvents) {
289
+ if (isMetaEvent(eventName) && !Events.canEmitMetaEvents) {
316
290
  throw new TypeError('`eventName` cannot be meta event `listenerAdded` or `listenerRemoved`')
317
291
  }
318
292
 
319
293
  this.logIfDebugEnabled('emit', eventName, eventArgs)
320
294
 
321
295
  const listeners = this.getListeners(eventName) ?? new Set()
296
+ const filteredListeners = [...listeners.values()].filter((value) => (filter ? value.listener : true)).map((info) => info.listener)
322
297
  const anyListeners = assertEx(Events.anyMap.get(this))
323
- const staticListeners = [...listeners]
298
+ const staticListeners = [...filteredListeners]
324
299
  const staticAnyListeners = isMetaEvent(eventName) ? [] : [...anyListeners]
325
300
 
326
301
  await resolvedPromise
327
302
  await Promise.all([
328
303
  ...staticListeners.map(async (listener) => {
329
- if (listeners.has(listener)) {
330
- return await listener(eventArgs)
331
- }
304
+ await this.safeCallListener(eventName, eventArgs, listener)
332
305
  }),
333
306
  ...staticAnyListeners.map(async (listener) => {
334
307
  if (anyListeners.has(listener)) {
335
- return await listener(eventName, eventArgs)
308
+ await this.safeCallAnyListener(eventName, eventArgs, listener)
336
309
  }
337
310
  }),
338
311
  ])
@@ -342,31 +315,63 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
342
315
  eventName: TEventName,
343
316
  eventArgs: MetaEventData<TEventData>[TEventName],
344
317
  ) {
345
- assertEventName(eventName)
346
-
347
- if (isMetaEvent(eventName) && !canEmitMetaEvents) {
318
+ if (isMetaEvent(eventName) && !Events.canEmitMetaEvents) {
348
319
  throw new TypeError('`eventName` cannot be meta event `listenerAdded` or `listenerRemoved`')
349
320
  }
350
321
 
351
322
  this.logIfDebugEnabled('emit', eventName, eventArgs)
352
323
 
353
324
  const listeners = this.getListeners(eventName) ?? new Set()
325
+ const filteredListeners = [...listeners.values()].map((info) => info.listener)
354
326
  const anyListeners = assertEx(Events.anyMap.get(this))
355
- const staticListeners = [...listeners]
327
+ const staticListeners = [...filteredListeners]
356
328
  const staticAnyListeners = isMetaEvent(eventName) ? [] : [...anyListeners]
357
329
 
358
330
  await resolvedPromise
359
331
  await Promise.all([
360
332
  ...staticListeners.map(async (listener) => {
361
- if (listeners.has(listener)) {
362
- return await listener(eventArgs)
363
- }
333
+ await this.safeCallListener(eventName, eventArgs, listener)
364
334
  }),
365
335
  ...staticAnyListeners.map(async (listener) => {
366
336
  if (anyListeners.has(listener)) {
367
- return await listener(eventName, eventArgs)
337
+ await this.safeCallAnyListener(eventName, eventArgs, listener)
368
338
  }
369
339
  }),
370
340
  ])
371
341
  }
342
+
343
+ private getListeners<TEventName extends keyof TEventData>(eventName: TEventName) {
344
+ const events = assertEx(Events.eventsMap.get(this))
345
+ if (!events.has(eventName)) {
346
+ return
347
+ }
348
+
349
+ return events.get(eventName)
350
+ }
351
+
352
+ private async safeCallAnyListener<TEventData extends EventData, TEventName extends keyof EventData>(
353
+ eventName: TEventName,
354
+ eventArgs: TEventData[TEventName],
355
+ listener: EventAnyListener<TEventData[TEventName]>,
356
+ ) {
357
+ try {
358
+ return await listener(eventName, eventArgs)
359
+ } catch (ex) {
360
+ const error = ex as Error
361
+ this.logger?.error(`Listener[${String(eventName)}] Excepted: ${error.message}`)
362
+ }
363
+ }
364
+
365
+ private async safeCallListener<TEventData extends EventData, TEventName extends keyof EventData>(
366
+ eventName: TEventName,
367
+ eventArgs: TEventData[TEventName],
368
+ listener: EventListener<TEventData[TEventName]>,
369
+ ) {
370
+ try {
371
+ return await listener(eventArgs)
372
+ } catch (ex) {
373
+ const error = ex as Error
374
+ this.logger?.error(`Listener[${String(eventName)}] Excepted: ${error.message}`)
375
+ }
376
+ }
372
377
  }