@xyo-network/module-events 2.51.10

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.
Files changed (39) hide show
  1. package/LICENSE +165 -0
  2. package/README.md +13 -0
  3. package/dist/cjs/Events/Events.js +298 -0
  4. package/dist/cjs/Events/Events.js.map +1 -0
  5. package/dist/cjs/Events/index.js +5 -0
  6. package/dist/cjs/Events/index.js.map +1 -0
  7. package/dist/cjs/index.js +6 -0
  8. package/dist/cjs/index.js.map +1 -0
  9. package/dist/cjs/model/Event.js +3 -0
  10. package/dist/cjs/model/Event.js.map +1 -0
  11. package/dist/cjs/model/index.js +5 -0
  12. package/dist/cjs/model/index.js.map +1 -0
  13. package/dist/docs.json +3646 -0
  14. package/dist/esm/Events/Events.js +282 -0
  15. package/dist/esm/Events/Events.js.map +1 -0
  16. package/dist/esm/Events/index.js +2 -0
  17. package/dist/esm/Events/index.js.map +1 -0
  18. package/dist/esm/index.js +3 -0
  19. package/dist/esm/index.js.map +1 -0
  20. package/dist/esm/model/Event.js +2 -0
  21. package/dist/esm/model/Event.js.map +1 -0
  22. package/dist/esm/model/index.js +2 -0
  23. package/dist/esm/model/index.js.map +1 -0
  24. package/dist/types/Events/Events.d.ts +58 -0
  25. package/dist/types/Events/Events.d.ts.map +1 -0
  26. package/dist/types/Events/index.d.ts +2 -0
  27. package/dist/types/Events/index.d.ts.map +1 -0
  28. package/dist/types/index.d.ts +3 -0
  29. package/dist/types/index.d.ts.map +1 -0
  30. package/dist/types/model/Event.d.ts +28 -0
  31. package/dist/types/model/Event.d.ts.map +1 -0
  32. package/dist/types/model/index.d.ts +2 -0
  33. package/dist/types/model/index.d.ts.map +1 -0
  34. package/package.json +56 -0
  35. package/src/Events/Events.ts +388 -0
  36. package/src/Events/index.ts +1 -0
  37. package/src/index.ts +2 -0
  38. package/src/model/Event.ts +33 -0
  39. package/src/model/index.ts +1 -0
@@ -0,0 +1,388 @@
1
+ import { forget } from '@xylabs/forget'
2
+
3
+ import { EventAnyListener, EventArgs, EventData, EventFunctions, EventListener, EventName } from '../model'
4
+
5
+ /**
6
+ Emittery can collect and log debug information.
7
+
8
+ To enable this feature set the `DEBUG` environment variable to `emittery` or `*`. Additionally, you can set the static `isDebugEnabled` variable to true on the Emittery class, or `myEmitter.debug.enabled` on an instance of it for debugging a single instance.
9
+
10
+ See API for more information on how debugging works.
11
+ */
12
+ export type DebugLogger<TEventData extends EventData, TName extends keyof TEventData> = (
13
+ type: string,
14
+ debugName: string,
15
+ eventName?: TName,
16
+ eventData?: TEventData[TName],
17
+ ) => void
18
+
19
+ /**
20
+ Configure debug options of an instance.
21
+ */
22
+ export type DebugOptions<TEventData extends EventData> = {
23
+ enabled?: boolean
24
+ logger?: DebugLogger<TEventData, keyof TEventData>
25
+ readonly name: string
26
+ }
27
+
28
+ /**
29
+ Configuration options for Emittery.
30
+ */
31
+ export type Options<TEventData extends EventData> = {
32
+ readonly debug?: DebugOptions<TEventData>
33
+ }
34
+
35
+ const anyProducer = Symbol('anyProducer')
36
+ const resolvedPromise = Promise.resolve()
37
+
38
+ const listenerAdded = 'listenerAdded'
39
+ const listenerRemoved = 'listenerRemoved'
40
+
41
+ export type MetaEventData<TEventData extends EventData> = {
42
+ listenerAdded: { eventName?: keyof TEventData; listener: EventListener<TEventData> }
43
+ listenerRemoved: { eventName?: keyof TEventData; listener: EventListener<TEventData> }
44
+ }
45
+
46
+ let canEmitMetaEvents = false
47
+ let isGlobalDebugEnabled = false
48
+
49
+ function assertEventName(eventName: EventName) {
50
+ if (typeof eventName !== 'string' && typeof eventName !== 'symbol' && typeof eventName !== 'number') {
51
+ throw new TypeError('`eventName` must be a string, symbol, or number')
52
+ }
53
+ }
54
+
55
+ function assertListener(listener: object) {
56
+ if (typeof listener !== 'function') {
57
+ throw new TypeError('listener must be a function')
58
+ }
59
+ }
60
+
61
+ const isMetaEvent = (eventName: EventName) => eventName === listenerAdded || eventName === listenerRemoved
62
+
63
+ export class Events<TEventData extends EventData> implements EventFunctions<TEventData> {
64
+ static anyMap = new WeakMap()
65
+ static eventsMap = new WeakMap()
66
+ static producersMap = new WeakMap()
67
+
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ debug?: DebugOptions<any>
70
+
71
+ constructor(options: Options<TEventData> = {}) {
72
+ Events.anyMap.set(this, new Set())
73
+ Events.eventsMap.set(this, new Map())
74
+ Events.producersMap.set(this, new Map())
75
+
76
+ Events.producersMap.get(this).set(anyProducer, new Set())
77
+
78
+ this.debug = options.debug
79
+
80
+ if (this.debug) {
81
+ this.debug.enabled = !!this.debug.enabled
82
+
83
+ this.debug.logger =
84
+ this.debug.logger ??
85
+ ((type: string, debugName: string, eventName?: keyof TEventData, eventData?: TEventData[keyof TEventData]) => {
86
+ let eventDataString: string
87
+ let eventNameString: string | undefined
88
+ try {
89
+ // TODO: Use https://github.com/sindresorhus/safe-stringify when the package is more mature. Just copy-paste the code.
90
+ eventDataString = JSON.stringify(eventData)
91
+ } catch {
92
+ eventDataString = `Object with the following keys failed to stringify: ${Object.keys(eventData ?? {}).join(',')}`
93
+ }
94
+
95
+ if (typeof eventName === 'symbol' || typeof eventName === 'number') {
96
+ eventNameString = eventName.toString()
97
+ } else {
98
+ eventNameString = eventName
99
+ }
100
+
101
+ const currentTime = new Date()
102
+ const logTime = `${currentTime.getHours()}:${currentTime.getMinutes()}:${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`
103
+ console.log(`[${logTime}][events:${type}][${debugName}] Event Name: ${eventNameString}\n\tdata: ${eventDataString}`)
104
+ })
105
+ }
106
+ }
107
+
108
+ static get isDebugEnabled() {
109
+ // In a browser environment, `globalThis.process` can potentially reference a DOM Element with a `#process` ID,
110
+ // so instead of just type checking `globalThis.process`, we need to make sure that `globalThis.process.env` exists.
111
+
112
+ if (typeof globalThis.process?.env !== 'object') {
113
+ return isGlobalDebugEnabled
114
+ }
115
+
116
+ const { env } = globalThis.process ?? { env: {} }
117
+ return env.DEBUG === 'emittery' || env.DEBUG === '*' || isGlobalDebugEnabled
118
+ }
119
+
120
+ static set isDebugEnabled(newValue) {
121
+ isGlobalDebugEnabled = newValue
122
+ }
123
+
124
+ clearListeners(eventNames: keyof TEventData | keyof TEventData[]) {
125
+ const eventNamesArray = Array.isArray(eventNames) ? eventNames : [eventNames]
126
+
127
+ for (const eventName of eventNamesArray) {
128
+ this.logIfDebugEnabled('clear', eventName, undefined)
129
+
130
+ if (typeof eventName === 'string' || typeof eventName === 'symbol' || typeof eventName === 'number') {
131
+ const set = this.getListeners(eventName)
132
+ if (set) {
133
+ set.clear()
134
+ }
135
+
136
+ const producers = this.getEventProducers(eventName)
137
+ if (producers) {
138
+ for (const producer of producers) {
139
+ producer.finish()
140
+ }
141
+
142
+ producers.clear()
143
+ }
144
+ } else {
145
+ Events.anyMap.get(this).clear()
146
+
147
+ for (const [eventName, listeners] of Events.eventsMap.get(this).entries()) {
148
+ listeners.clear()
149
+ Events.eventsMap.get(this).delete(eventName)
150
+ }
151
+
152
+ for (const [eventName, producers] of Events.producersMap.get(this).entries()) {
153
+ for (const producer of producers) {
154
+ producer.finish()
155
+ }
156
+
157
+ producers.clear()
158
+ Events.producersMap.get(this).delete(eventName)
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ async emit(eventName: keyof TEventData, eventData?: TEventData[keyof TEventData]) {
165
+ await this.emitInternal(eventName, eventData)
166
+ }
167
+
168
+ async emitInternal<TEventName extends EventName, TEventArgs extends EventArgs>(eventName: TEventName, eventArgs?: TEventArgs) {
169
+ assertEventName(eventName)
170
+
171
+ if (isMetaEvent(eventName) && !canEmitMetaEvents) {
172
+ throw new TypeError('`eventName` cannot be meta event `listenerAdded` or `listenerRemoved`')
173
+ }
174
+
175
+ this.logIfDebugEnabled('emit', eventName, eventArgs)
176
+
177
+ this.enqueueProducers(eventName, eventArgs)
178
+
179
+ const listeners = this.getListeners(eventName) ?? new Set()
180
+ const anyListeners = Events.anyMap.get(this)
181
+ const staticListeners = [...listeners]
182
+ const staticAnyListeners = isMetaEvent(eventName) ? [] : [...anyListeners]
183
+
184
+ await resolvedPromise
185
+ await Promise.all([
186
+ ...staticListeners.map(async (listener) => {
187
+ if (listeners.has(listener)) {
188
+ return await listener(eventArgs)
189
+ }
190
+ }),
191
+ ...staticAnyListeners.map(async (listener) => {
192
+ if (anyListeners.has(listener)) {
193
+ return await listener(eventName, eventArgs)
194
+ }
195
+ }),
196
+ ])
197
+ }
198
+
199
+ async emitMetaEvent<TEventName extends EventName, TEventArgs extends EventArgs>(eventName: TEventName, eventArgs: TEventArgs) {
200
+ if (isMetaEvent(eventName)) {
201
+ try {
202
+ canEmitMetaEvents = true
203
+ await this.emitInternal(eventName, eventArgs)
204
+ } finally {
205
+ canEmitMetaEvents = false
206
+ }
207
+ }
208
+ }
209
+
210
+ async emitSerial(eventName: keyof TEventData, eventData: TEventData[keyof TEventData]) {
211
+ assertEventName(eventName)
212
+
213
+ if (isMetaEvent(eventName) && !canEmitMetaEvents) {
214
+ throw new TypeError('`eventName` cannot be meta event `listenerAdded` or `listenerRemoved`')
215
+ }
216
+
217
+ this.logIfDebugEnabled('emitSerial', eventName, eventData)
218
+
219
+ const listeners = this.getListeners(eventName) ?? new Set()
220
+ const anyListeners = Events.anyMap.get(this)
221
+ const staticListeners = [...listeners]
222
+ const staticAnyListeners = [...anyListeners]
223
+
224
+ await resolvedPromise
225
+
226
+ for (const listener of staticListeners) {
227
+ if (listeners.has(listener)) {
228
+ await listener(eventData)
229
+ }
230
+ }
231
+
232
+ for (const listener of staticAnyListeners) {
233
+ if (anyListeners.has(listener)) {
234
+ await listener(eventName, eventData)
235
+ }
236
+ }
237
+ }
238
+
239
+ getEventProducers(eventName?: keyof TEventData) {
240
+ const key = typeof eventName === 'string' || typeof eventName === 'symbol' || typeof eventName === 'number' ? eventName : anyProducer
241
+ const producers = Events.producersMap.get(this)
242
+ if (!producers.has(key)) {
243
+ return
244
+ }
245
+
246
+ return producers.get(key)
247
+ }
248
+
249
+ getListeners(eventName: keyof TEventData) {
250
+ const events = Events.eventsMap.get(this)
251
+ if (!events.has(eventName)) {
252
+ return
253
+ }
254
+
255
+ return events.get(eventName)
256
+ }
257
+
258
+ listenerCount(eventNames: keyof TEventData | keyof TEventData[]) {
259
+ const eventNamesArray = Array.isArray(eventNames) ? eventNames : [eventNames]
260
+ let count = 0
261
+
262
+ for (const eventName of eventNamesArray) {
263
+ if (typeof eventName === 'string') {
264
+ count +=
265
+ Events.anyMap.get(this).size +
266
+ (this.getListeners(eventName)?.size ?? 0) +
267
+ (this.getEventProducers(eventName)?.size ?? 0) +
268
+ (this.getEventProducers()?.size ?? 0)
269
+
270
+ continue
271
+ }
272
+
273
+ if (typeof eventName !== 'undefined') {
274
+ assertEventName(eventName)
275
+ }
276
+
277
+ count += Events.anyMap.get(this).size
278
+
279
+ for (const value of Events.eventsMap.get(this).values()) {
280
+ count += value.size
281
+ }
282
+
283
+ for (const value of Events.producersMap.get(this).values()) {
284
+ count += value.size
285
+ }
286
+ }
287
+
288
+ return count
289
+ }
290
+
291
+ logIfDebugEnabled<TEventName extends EventName, TEventArgs extends EventArgs>(type: string, eventName?: TEventName, eventArgs?: TEventArgs) {
292
+ if (Events.isDebugEnabled || this.debug?.enabled) {
293
+ this.debug?.logger?.(type, this.debug.name, eventName, eventArgs)
294
+ }
295
+ }
296
+
297
+ off(eventNames: keyof TEventData | keyof TEventData[], listener: object) {
298
+ assertListener(listener)
299
+
300
+ const eventNamesArray = Array.isArray(eventNames) ? eventNames : [eventNames]
301
+ for (const eventName of eventNamesArray) {
302
+ assertEventName(eventName)
303
+ const set = this.getListeners(eventName)
304
+ if (set) {
305
+ set.delete(listener)
306
+ if (set.size === 0) {
307
+ const events = Events.eventsMap.get(this)
308
+ events.delete(eventName)
309
+ }
310
+ }
311
+
312
+ this.logIfDebugEnabled('unsubscribe', eventName, undefined)
313
+
314
+ if (!isMetaEvent(eventName)) {
315
+ forget(this.emitMetaEvent(listenerRemoved, { eventName, listener }))
316
+ }
317
+ }
318
+ }
319
+
320
+ offAny(listener: object) {
321
+ assertListener(listener)
322
+
323
+ this.logIfDebugEnabled('unsubscribeAny', undefined, undefined)
324
+
325
+ Events.anyMap.get(this).delete(listener)
326
+ forget(this.emitMetaEvent(listenerRemoved, { listener }))
327
+ }
328
+
329
+ on(eventNames: keyof TEventData | keyof TEventData[], listener: object) {
330
+ assertListener(listener)
331
+
332
+ const eventNamesArray = Array.isArray(eventNames) ? eventNames : [eventNames]
333
+ for (const eventName of eventNamesArray) {
334
+ assertEventName(eventName)
335
+ let set = this.getListeners(eventName)
336
+ if (!set) {
337
+ set = new Set()
338
+ const events = Events.eventsMap.get(this)
339
+ events.set(eventName, set)
340
+ }
341
+
342
+ set.add(listener)
343
+
344
+ this.logIfDebugEnabled('subscribe', eventName, undefined)
345
+
346
+ if (!isMetaEvent(eventName)) {
347
+ forget(this.emitMetaEvent(listenerAdded, { eventName, listener }))
348
+ }
349
+ }
350
+
351
+ return this.off.bind(this, eventNames, listener)
352
+ }
353
+
354
+ onAny(listener: EventAnyListener<TEventData>) {
355
+ assertListener(listener)
356
+
357
+ this.logIfDebugEnabled('subscribeAny', undefined, undefined)
358
+
359
+ Events.anyMap.get(this).add(listener)
360
+ forget(this.emitMetaEvent(listenerAdded, { listener }))
361
+ return this.offAny.bind(this, listener)
362
+ }
363
+
364
+ once(eventNames: keyof TEventData | keyof TEventData[], listener: EventListener<TEventData>) {
365
+ const subListener = async (args: TEventData[keyof TEventData]) => {
366
+ this.off(eventNames, subListener)
367
+ await listener(args)
368
+ }
369
+ this.on(eventNames, subListener)
370
+ return this.off.bind(this, eventNames, subListener)
371
+ }
372
+
373
+ private enqueueProducers<TEventName extends EventName, TEventArgs extends EventArgs>(eventName: TEventName, eventData?: TEventArgs) {
374
+ const producers = Events.producersMap.get(this)
375
+ if (producers.has(eventName)) {
376
+ for (const producer of producers.get(eventName)) {
377
+ producer.enqueue(eventData)
378
+ }
379
+ }
380
+
381
+ if (producers.has(anyProducer)) {
382
+ const item = Promise.all([eventName, eventData])
383
+ for (const producer of producers.get(anyProducer)) {
384
+ producer.enqueue(item)
385
+ }
386
+ }
387
+ }
388
+ }
@@ -0,0 +1 @@
1
+ export * from './Events'
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './Events'
2
+ export * from './model'
@@ -0,0 +1,33 @@
1
+ import { Promisable } from '@xyo-network/promise'
2
+ import { BaseParams } from '@xyo-network/protocol'
3
+
4
+ export type EventName = PropertyKey
5
+ export type EventArgs = string | number | object
6
+ export type EventData = { [key: EventName]: EventArgs }
7
+ export type EventUnsubscribeFunction = () => void
8
+ export type EventAnyListener<TEventData extends EventData> = (
9
+ eventName: keyof TEventData,
10
+ eventData?: TEventData[keyof TEventData],
11
+ ) => Promisable<void>
12
+ export type EventListener<TEventData extends EventData> = (eventData?: TEventData[keyof TEventData]) => Promisable<void>
13
+
14
+ export type OncePromise<T> = {
15
+ off(): void
16
+ } & Promise<T>
17
+
18
+ export interface EventFunctions<TEventData extends EventData> {
19
+ clearListeners(eventNames: keyof TEventData | keyof TEventData[]): void
20
+ emit(eventName: keyof TEventData, eventData: TEventData[keyof TEventData]): Promise<void>
21
+ emitSerial(eventName: keyof TEventData, eventData: TEventData[keyof TEventData]): Promise<void>
22
+ listenerCount(eventNames: keyof TEventData | keyof TEventData[]): number
23
+ off(eventName: keyof TEventData | keyof TEventData[], listener: EventListener<TEventData>): void
24
+ offAny(listener: EventAnyListener<TEventData> | Promise<void>): void
25
+ on(eventName: keyof TEventData | keyof TEventData[], listener: EventListener<TEventData>): EventUnsubscribeFunction
26
+ onAny(listener: EventAnyListener<TEventData>): EventUnsubscribeFunction
27
+ once(eventName: keyof TEventData | keyof TEventData[], listener: EventListener<TEventData>): EventUnsubscribeFunction
28
+ }
29
+
30
+ export type EventDataParams<TEventData extends EventData | undefined = undefined, TParams extends BaseParams = BaseParams> = TParams & {
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ eventData?: TEventData extends EventData ? TEventData | any : any
33
+ }
@@ -0,0 +1 @@
1
+ export * from './Event'