event-emission 0.1.0

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.
package/src/factory.ts ADDED
@@ -0,0 +1,948 @@
1
+ import { BufferOverflowError } from './errors';
2
+ import {
3
+ createObservableProxy,
4
+ type ObservableEventMap,
5
+ type ObserveOptions,
6
+ } from './observe';
7
+ import { SymbolObservable } from './symbols';
8
+ import type {
9
+ AsyncIteratorOptions,
10
+ EventfulEvent,
11
+ EventTargetLike,
12
+ Listener,
13
+ Observer,
14
+ WildcardEvent,
15
+ WildcardListener,
16
+ } from './types';
17
+
18
+ /**
19
+ * Check if a pattern matches a wildcard pattern.
20
+ */
21
+ function matchesWildcard(eventType: string, pattern: string): boolean {
22
+ if (pattern === '*') return true;
23
+ if (pattern.endsWith(':*')) {
24
+ const namespace = pattern.slice(0, -2);
25
+ return eventType.startsWith(namespace + ':');
26
+ }
27
+ return false;
28
+ }
29
+
30
+ /**
31
+ * Options for createEventTarget.
32
+ *
33
+ * @property onListenerError - Custom error handler called when a listener throws.
34
+ * If not provided, errors are emitted as 'error' events or re-thrown.
35
+ */
36
+ export interface CreateEventTargetOptions {
37
+ /** Custom error handler for listener errors. Receives event type and error. */
38
+ onListenerError?: (type: string, error: unknown) => void;
39
+ }
40
+
41
+ /**
42
+ * Options for createEventTarget with observe mode.
43
+ * Extends CreateEventTargetOptions with proxy observation settings.
44
+ *
45
+ * @property observe - Must be true to enable observation mode.
46
+ * @property deep - If true, nested objects are also observed (default: false).
47
+ * @property cloneStrategy - Strategy for cloning previous state: 'shallow', 'deep', or 'path'.
48
+ */
49
+ export interface CreateEventTargetObserveOptions
50
+ extends CreateEventTargetOptions, ObserveOptions {
51
+ /** Must be true to enable observation mode. */
52
+ observe: true;
53
+ }
54
+
55
+ /**
56
+ * Creates a type-safe event target with DOM EventTarget and TC39 Observable compatibility.
57
+ *
58
+ * @template E - Event map type where keys are event names and values are event detail types.
59
+ * @param opts - Optional configuration options.
60
+ * @returns A type-safe event target implementing EventTargetLike.
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * // Define event types
65
+ * type Events = {
66
+ * 'user:login': { userId: string };
67
+ * 'user:logout': { reason: string };
68
+ * };
69
+ *
70
+ * // Create event target
71
+ * const events = createEventTarget<Events>();
72
+ *
73
+ * // Add typed listener
74
+ * events.addEventListener('user:login', (event) => {
75
+ * console.log(`User logged in: ${event.detail.userId}`);
76
+ * });
77
+ *
78
+ * // Dispatch typed event
79
+ * events.dispatchEvent({ type: 'user:login', detail: { userId: '123' } });
80
+ * ```
81
+ *
82
+ * @overload Creates a basic event target
83
+ */
84
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic constraint requires any
85
+ export function createEventTarget<E extends Record<string, any>>(
86
+ opts?: CreateEventTargetOptions,
87
+ ): EventTargetLike<E>;
88
+
89
+ /**
90
+ * Creates an observable proxy that dispatches events when properties change.
91
+ *
92
+ * @template T - The type of object to observe.
93
+ * @param target - The object to wrap with an observable proxy.
94
+ * @param opts - Configuration options with observe: true.
95
+ * @returns The proxied object with EventTargetLike methods mixed in.
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * // Create observable state
100
+ * const state = createEventTarget({ count: 0, user: { name: 'Alice' } }, {
101
+ * observe: true,
102
+ * deep: true,
103
+ * });
104
+ *
105
+ * // Listen for any update
106
+ * state.addEventListener('update', (event) => {
107
+ * console.log('State changed:', event.detail.current);
108
+ * });
109
+ *
110
+ * // Listen for specific property changes
111
+ * state.addEventListener('update:count', (event) => {
112
+ * console.log('Count changed to:', event.detail.value);
113
+ * });
114
+ *
115
+ * // Mutations trigger events automatically
116
+ * state.count = 1; // Triggers 'update' and 'update:count'
117
+ * state.user.name = 'Bob'; // Triggers 'update' and 'update:user.name'
118
+ * ```
119
+ *
120
+ * @overload Wraps an object with a Proxy that dispatches events on mutations
121
+ */
122
+ export function createEventTarget<T extends object>(
123
+ target: T,
124
+ opts: CreateEventTargetObserveOptions,
125
+ ): T & EventTargetLike<ObservableEventMap<T>>;
126
+
127
+ /**
128
+ * Creates a type-safe event target with DOM EventTarget and TC39 Observable compatibility.
129
+ *
130
+ * This is the main factory function for creating event emitters. It supports two modes:
131
+ *
132
+ * 1. **Basic Mode**: Creates a standalone event target for pub/sub messaging.
133
+ * 2. **Observe Mode**: Wraps an object with a Proxy that automatically dispatches
134
+ * events when properties are modified.
135
+ *
136
+ * Listener errors are handled via 'error' event: if a listener throws,
137
+ * an 'error' event is emitted. If no 'error' listener is registered,
138
+ * the error is re-thrown (Node.js behavior).
139
+ *
140
+ * @param targetOrOpts - Either the object to observe, or configuration options.
141
+ * @param opts - Configuration options when first argument is an object to observe.
142
+ * @returns Either an EventTargetLike or a proxied object with EventTargetLike methods.
143
+ */
144
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic constraint requires any
145
+ export function createEventTarget<T extends object, E extends Record<string, any>>(
146
+ targetOrOpts?: T | CreateEventTargetOptions,
147
+ opts?: CreateEventTargetObserveOptions,
148
+ ): EventTargetLike<E> | (T & EventTargetLike<ObservableEventMap<T>>) {
149
+ // Handle observe mode - opts.observe must be explicitly true
150
+ if (opts?.observe === true && targetOrOpts && typeof targetOrOpts === 'object') {
151
+ const target = targetOrOpts as T;
152
+ const eventTarget = createEventTargetInternal<ObservableEventMap<T>>({
153
+ onListenerError: opts.onListenerError,
154
+ });
155
+
156
+ const proxy = createObservableProxy(target, eventTarget, {
157
+ deep: opts.deep,
158
+ cloneStrategy: opts.cloneStrategy,
159
+ });
160
+
161
+ // Copy eventTarget methods onto the proxy
162
+ // Use defineProperty to avoid triggering proxy traps
163
+ const methodNames = [
164
+ 'addEventListener',
165
+ 'removeEventListener',
166
+ 'dispatchEvent',
167
+ 'clear',
168
+ 'once',
169
+ 'removeAllListeners',
170
+ 'pipe',
171
+ 'addWildcardListener',
172
+ 'removeWildcardListener',
173
+ 'complete',
174
+ 'subscribe',
175
+ 'toObservable',
176
+ 'events',
177
+ ] as const;
178
+
179
+ for (const name of methodNames) {
180
+ Object.defineProperty(proxy, name, {
181
+ value: eventTarget[name],
182
+ writable: false,
183
+ enumerable: false,
184
+ configurable: true,
185
+ });
186
+ }
187
+
188
+ // Add completed getter
189
+ Object.defineProperty(proxy, 'completed', {
190
+ get: () => eventTarget.completed,
191
+ enumerable: false,
192
+ configurable: true,
193
+ });
194
+
195
+ return proxy as T & EventTargetLike<ObservableEventMap<T>>;
196
+ }
197
+
198
+ // Original behavior
199
+ return createEventTargetInternal<E>(
200
+ targetOrOpts as CreateEventTargetOptions | undefined,
201
+ );
202
+ }
203
+
204
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic constraint requires any
205
+ function createEventTargetInternal<E extends Record<string, any>>(
206
+ opts?: CreateEventTargetOptions,
207
+ ): EventTargetLike<E> {
208
+ const listeners = new Map<string, Set<Listener<E[keyof E]>>>();
209
+ const wildcardListeners = new Set<WildcardListener<E>>();
210
+ let isCompleted = false;
211
+ const completionCallbacks = new Set<() => void>();
212
+
213
+ // Helper to handle listener errors: emit 'error' event or re-throw if no listener
214
+ const handleListenerError = (eventType: string, error: unknown) => {
215
+ // Prevent infinite recursion if 'error' listener itself throws
216
+ if (eventType === 'error') return;
217
+
218
+ // If custom error handler provided, use it
219
+ if (opts?.onListenerError) {
220
+ opts.onListenerError(eventType, error);
221
+ return;
222
+ }
223
+
224
+ const errorListeners = listeners.get('error');
225
+ if (errorListeners && errorListeners.size > 0) {
226
+ // Emit 'error' event with the error as detail
227
+ for (const rec of Array.from(errorListeners)) {
228
+ try {
229
+ void rec.fn({ type: 'error', detail: error } as EventfulEvent<E[keyof E]>);
230
+ } catch {
231
+ // Swallow errors from error handlers to prevent infinite loops
232
+ }
233
+ if (rec.once) errorListeners.delete(rec);
234
+ }
235
+ } else {
236
+ // No 'error' listener - re-throw (Node.js behavior)
237
+ throw error;
238
+ }
239
+ };
240
+
241
+ const notifyWildcardListeners = (eventType: string, detail: E[keyof E]) => {
242
+ if (wildcardListeners.size === 0) return;
243
+
244
+ for (const rec of Array.from(wildcardListeners)) {
245
+ if (!matchesWildcard(eventType, rec.pattern)) continue;
246
+
247
+ const wildcardEvent: WildcardEvent<E> = {
248
+ type: rec.pattern,
249
+ originalType: eventType as keyof E & string,
250
+ detail,
251
+ };
252
+
253
+ try {
254
+ const res = rec.fn(wildcardEvent);
255
+ if (res && typeof res.then === 'function') {
256
+ res.catch((error) => {
257
+ try {
258
+ handleListenerError(eventType, error);
259
+ } catch (rethrown) {
260
+ // Re-throw async errors via queueMicrotask to preserve stack trace
261
+ queueMicrotask(() => {
262
+ throw rethrown;
263
+ });
264
+ }
265
+ });
266
+ }
267
+ } catch (error) {
268
+ handleListenerError(eventType, error);
269
+ } finally {
270
+ if (rec.once) wildcardListeners.delete(rec);
271
+ }
272
+ }
273
+ };
274
+
275
+ const addEventListener: EventTargetLike<E>['addEventListener'] = (
276
+ type,
277
+ listener,
278
+ options,
279
+ ) => {
280
+ if (isCompleted) {
281
+ // Return no-op unsubscribe if already completed
282
+ return () => {};
283
+ }
284
+
285
+ const opts2 = options ?? {};
286
+ const record: Listener<E[keyof E]> = {
287
+ fn: listener as Listener<E[keyof E]>['fn'],
288
+ once: opts2.once,
289
+ signal: opts2.signal,
290
+ };
291
+ let set = listeners.get(type);
292
+ if (!set) {
293
+ set = new Set();
294
+ listeners.set(type, set);
295
+ }
296
+ set.add(record);
297
+ const unsubscribe = () => {
298
+ const setNow = listeners.get(type);
299
+ setNow?.delete(record);
300
+ if (record.signal && record.abortHandler) {
301
+ record.signal.removeEventListener('abort', record.abortHandler);
302
+ }
303
+ };
304
+ if (opts2.signal) {
305
+ const onAbort = () => unsubscribe();
306
+ record.abortHandler = onAbort;
307
+ opts2.signal.addEventListener('abort', onAbort, { once: true });
308
+ if (opts2.signal.aborted) onAbort();
309
+ }
310
+ return unsubscribe;
311
+ };
312
+
313
+ const addWildcardListener: EventTargetLike<E>['addWildcardListener'] = (
314
+ pattern,
315
+ listener,
316
+ options,
317
+ ) => {
318
+ if (isCompleted) return () => {};
319
+
320
+ const opts2 = options ?? {};
321
+ const record: WildcardListener<E> = {
322
+ fn: listener,
323
+ pattern,
324
+ once: opts2.once,
325
+ signal: opts2.signal,
326
+ };
327
+ wildcardListeners.add(record);
328
+
329
+ const unsubscribe = () => {
330
+ wildcardListeners.delete(record);
331
+ if (record.signal && record.abortHandler) {
332
+ record.signal.removeEventListener('abort', record.abortHandler);
333
+ }
334
+ };
335
+
336
+ if (opts2.signal) {
337
+ const onAbort = () => unsubscribe();
338
+ record.abortHandler = onAbort;
339
+ opts2.signal.addEventListener('abort', onAbort, { once: true });
340
+ if (opts2.signal.aborted) onAbort();
341
+ }
342
+
343
+ return unsubscribe;
344
+ };
345
+
346
+ const removeWildcardListener: EventTargetLike<E>['removeWildcardListener'] = (
347
+ pattern,
348
+ listener,
349
+ ) => {
350
+ for (const record of wildcardListeners) {
351
+ if (record.pattern === pattern && record.fn === listener) {
352
+ wildcardListeners.delete(record);
353
+ if (record.signal && record.abortHandler) {
354
+ record.signal.removeEventListener('abort', record.abortHandler);
355
+ }
356
+ break;
357
+ }
358
+ }
359
+ };
360
+
361
+ const dispatchEvent: EventTargetLike<E>['dispatchEvent'] = (event) => {
362
+ if (isCompleted) return false;
363
+
364
+ // Notify wildcard listeners first (no overhead if none registered)
365
+ notifyWildcardListeners(event.type, event.detail as E[keyof E]);
366
+
367
+ const set = listeners.get(event.type);
368
+ if (!set || set.size === 0) return true;
369
+ for (const rec of Array.from(set)) {
370
+ try {
371
+ const res = rec.fn(event as EventfulEvent<E[keyof E]>);
372
+ if (res && typeof res.then === 'function') {
373
+ res.catch((error) => {
374
+ try {
375
+ handleListenerError(event.type, error);
376
+ } catch (rethrown) {
377
+ // Re-throw async errors via queueMicrotask to preserve stack trace
378
+ queueMicrotask(() => {
379
+ throw rethrown;
380
+ });
381
+ }
382
+ });
383
+ }
384
+ } catch (error) {
385
+ handleListenerError(event.type, error);
386
+ } finally {
387
+ if (rec.once) set.delete(rec);
388
+ }
389
+ }
390
+ return true;
391
+ };
392
+
393
+ const removeEventListener: EventTargetLike<E>['removeEventListener'] = (
394
+ type,
395
+ listener,
396
+ ) => {
397
+ const set = listeners.get(type);
398
+ if (!set) return;
399
+
400
+ for (const record of set) {
401
+ if (record.fn === listener) {
402
+ set.delete(record);
403
+ if (record.signal && record.abortHandler) {
404
+ record.signal.removeEventListener('abort', record.abortHandler);
405
+ }
406
+ break;
407
+ }
408
+ }
409
+ };
410
+
411
+ const clear = () => {
412
+ // Clean up abort handlers before clearing
413
+ for (const set of listeners.values()) {
414
+ for (const record of set) {
415
+ if (record.signal && record.abortHandler) {
416
+ record.signal.removeEventListener('abort', record.abortHandler);
417
+ }
418
+ }
419
+ set.clear();
420
+ }
421
+ listeners.clear();
422
+
423
+ // Clear wildcard listeners too
424
+ for (const record of wildcardListeners) {
425
+ if (record.signal && record.abortHandler) {
426
+ record.signal.removeEventListener('abort', record.abortHandler);
427
+ }
428
+ }
429
+ wildcardListeners.clear();
430
+ // Note: clear() does NOT trigger completion callbacks or set isCompleted
431
+ // Use complete() for that
432
+ };
433
+
434
+ // New ergonomics
435
+
436
+ const once: EventTargetLike<E>['once'] = (type, listener, options) => {
437
+ return addEventListener(type, listener, { ...options, once: true });
438
+ };
439
+
440
+ const removeAllListeners: EventTargetLike<E>['removeAllListeners'] = (type) => {
441
+ if (type !== undefined) {
442
+ const set = listeners.get(type);
443
+ if (set) {
444
+ // Clean up abort handlers before clearing
445
+ for (const record of set) {
446
+ if (record.signal && record.abortHandler) {
447
+ record.signal.removeEventListener('abort', record.abortHandler);
448
+ }
449
+ }
450
+ set.clear();
451
+ listeners.delete(type);
452
+ }
453
+ } else {
454
+ // Clear all listeners for all types
455
+ for (const set of listeners.values()) {
456
+ for (const record of set) {
457
+ if (record.signal && record.abortHandler) {
458
+ record.signal.removeEventListener('abort', record.abortHandler);
459
+ }
460
+ }
461
+ set.clear();
462
+ }
463
+ listeners.clear();
464
+
465
+ // Clear wildcard listeners too
466
+ for (const record of wildcardListeners) {
467
+ if (record.signal && record.abortHandler) {
468
+ record.signal.removeEventListener('abort', record.abortHandler);
469
+ }
470
+ }
471
+ wildcardListeners.clear();
472
+ }
473
+ };
474
+
475
+ /**
476
+ * Pipe events from this emitter to another target.
477
+ *
478
+ * **Limitation**: Only forwards events for types that already have listeners
479
+ * when pipe() is called. Events for types registered afterward won't be piped.
480
+ *
481
+ * To ensure all events are piped, add at least one listener for each event type
482
+ * before calling pipe().
483
+ */
484
+ const pipe: EventTargetLike<E>['pipe'] = (target, mapFn) => {
485
+ if (isCompleted) {
486
+ return () => {};
487
+ }
488
+
489
+ const unsubscribes: Array<() => void> = [];
490
+
491
+ // Subscribe to all current and future events by listening to each event type
492
+ // We need to track event types we've subscribed to
493
+ const subscribedTypes = new Set<string>();
494
+
495
+ const subscribeToType = (type: string) => {
496
+ if (subscribedTypes.has(type)) return;
497
+ subscribedTypes.add(type);
498
+
499
+ const unsub = addEventListener(type as keyof E & string, (event) => {
500
+ if (mapFn) {
501
+ const mapped = mapFn(event);
502
+ if (mapped !== null) {
503
+ // Type assertion via unknown is needed because mapFn output type matches target's event map
504
+ target.dispatchEvent(
505
+ mapped as unknown as Parameters<typeof target.dispatchEvent>[0],
506
+ );
507
+ }
508
+ } else {
509
+ // Type assertion via unknown is needed because caller ensures E and T are compatible
510
+ target.dispatchEvent(
511
+ event as unknown as Parameters<typeof target.dispatchEvent>[0],
512
+ );
513
+ }
514
+ });
515
+ unsubscribes.push(unsub);
516
+ };
517
+
518
+ // Subscribe to all existing event types
519
+ for (const type of listeners.keys()) {
520
+ subscribeToType(type);
521
+ }
522
+
523
+ // Clean up on completion
524
+ const completionUnsub = () => {
525
+ for (const unsub of unsubscribes) {
526
+ unsub();
527
+ }
528
+ };
529
+ completionCallbacks.add(completionUnsub);
530
+
531
+ return () => {
532
+ completionCallbacks.delete(completionUnsub);
533
+ for (const unsub of unsubscribes) {
534
+ unsub();
535
+ }
536
+ };
537
+ };
538
+
539
+ const complete = () => {
540
+ if (isCompleted) return;
541
+ isCompleted = true;
542
+
543
+ // Trigger completion callbacks (pipes, subscriptions)
544
+ for (const cb of completionCallbacks) {
545
+ try {
546
+ cb();
547
+ } catch (err) {
548
+ // Completion callback errors use handleListenerError
549
+ // Use 'complete' as the event type for these errors
550
+ try {
551
+ handleListenerError('complete', err);
552
+ } catch {
553
+ // Swallow if no error listener
554
+ }
555
+ }
556
+ }
557
+ completionCallbacks.clear();
558
+
559
+ // Clear all listeners
560
+ for (const set of listeners.values()) {
561
+ for (const record of set) {
562
+ if (record.signal && record.abortHandler) {
563
+ record.signal.removeEventListener('abort', record.abortHandler);
564
+ }
565
+ }
566
+ set.clear();
567
+ }
568
+ listeners.clear();
569
+
570
+ // Clear wildcard listeners
571
+ for (const record of wildcardListeners) {
572
+ if (record.signal && record.abortHandler) {
573
+ record.signal.removeEventListener('abort', record.abortHandler);
574
+ }
575
+ }
576
+ wildcardListeners.clear();
577
+ };
578
+
579
+ // Observable interop
580
+ const subscribe: EventTargetLike<E>['subscribe'] = (
581
+ type,
582
+ observerOrNext,
583
+ error,
584
+ completeHandler,
585
+ ) => {
586
+ let observer: Observer<EventfulEvent<E[keyof E & string]>>;
587
+
588
+ if (typeof observerOrNext === 'function') {
589
+ observer = {
590
+ next: observerOrNext as (value: EventfulEvent<E[keyof E & string]>) => void,
591
+ error,
592
+ complete: completeHandler,
593
+ };
594
+ } else {
595
+ observer = (observerOrNext ?? {}) as Observer<EventfulEvent<E[keyof E & string]>>;
596
+ }
597
+
598
+ let closed = false;
599
+
600
+ if (isCompleted) {
601
+ // Already completed, call complete immediately
602
+ if (observer.complete) {
603
+ try {
604
+ observer.complete();
605
+ } catch {
606
+ // Swallow
607
+ }
608
+ }
609
+ return {
610
+ unsubscribe: () => {
611
+ closed = true;
612
+ },
613
+ get closed() {
614
+ return closed || isCompleted;
615
+ },
616
+ };
617
+ }
618
+
619
+ const unsub = addEventListener(type, (event) => {
620
+ if (closed) return;
621
+ if (observer.next) {
622
+ try {
623
+ observer.next(event as EventfulEvent<E[keyof E & string]>);
624
+ } catch (err) {
625
+ if (observer.error) {
626
+ try {
627
+ observer.error(err);
628
+ } catch {
629
+ // Swallow
630
+ }
631
+ }
632
+ }
633
+ }
634
+ });
635
+
636
+ // Track completion callback
637
+ const onComplete = () => {
638
+ if (closed) return;
639
+ closed = true;
640
+ if (observer.complete) {
641
+ try {
642
+ observer.complete();
643
+ } catch {
644
+ // Swallow
645
+ }
646
+ }
647
+ };
648
+ completionCallbacks.add(onComplete);
649
+
650
+ return {
651
+ unsubscribe: () => {
652
+ if (closed) return;
653
+ closed = true;
654
+ completionCallbacks.delete(onComplete);
655
+ unsub();
656
+ },
657
+ get closed() {
658
+ return closed || isCompleted;
659
+ },
660
+ };
661
+ };
662
+
663
+ const toObservable: EventTargetLike<E>['toObservable'] = () => {
664
+ const observable = {
665
+ subscribe: (
666
+ observerOrNext?:
667
+ | Observer<EventfulEvent<E[keyof E]>>
668
+ | ((value: EventfulEvent<E[keyof E]>) => void),
669
+ errorFn?: (error: unknown) => void,
670
+ completeFn?: () => void,
671
+ ) => {
672
+ // For the full observable, we listen to all events via wildcard
673
+ let next: ((value: EventfulEvent<E[keyof E]>) => void) | undefined;
674
+ let error: ((error: unknown) => void) | undefined;
675
+ let completeCallback: (() => void) | undefined;
676
+
677
+ if (typeof observerOrNext === 'function') {
678
+ next = observerOrNext;
679
+ error = errorFn;
680
+ completeCallback = completeFn;
681
+ } else if (observerOrNext) {
682
+ next = observerOrNext.next?.bind(observerOrNext);
683
+ error = observerOrNext.error?.bind(observerOrNext);
684
+ completeCallback = observerOrNext.complete?.bind(observerOrNext);
685
+ }
686
+
687
+ let closed = false;
688
+
689
+ if (isCompleted) {
690
+ if (completeCallback) {
691
+ try {
692
+ completeCallback();
693
+ } catch {
694
+ // Swallow
695
+ }
696
+ }
697
+ return {
698
+ unsubscribe: () => {
699
+ closed = true;
700
+ },
701
+ get closed() {
702
+ return true;
703
+ },
704
+ };
705
+ }
706
+
707
+ const wildcardListener = (event: WildcardEvent<E>) => {
708
+ if (closed) return;
709
+ if (next) {
710
+ try {
711
+ next({ type: event.originalType, detail: event.detail });
712
+ } catch (err) {
713
+ if (error) {
714
+ try {
715
+ error(err);
716
+ } catch {
717
+ // Swallow
718
+ }
719
+ }
720
+ }
721
+ }
722
+ };
723
+
724
+ const unsubscribe = addWildcardListener('*', wildcardListener);
725
+
726
+ const onComplete = () => {
727
+ if (closed) return;
728
+ closed = true;
729
+ if (completeCallback) {
730
+ try {
731
+ completeCallback();
732
+ } catch {
733
+ // Swallow
734
+ }
735
+ }
736
+ };
737
+ completionCallbacks.add(onComplete);
738
+
739
+ return {
740
+ unsubscribe: () => {
741
+ if (closed) return;
742
+ closed = true;
743
+ unsubscribe();
744
+ completionCallbacks.delete(onComplete);
745
+ },
746
+ get closed() {
747
+ return closed || isCompleted;
748
+ },
749
+ };
750
+ },
751
+ [SymbolObservable]() {
752
+ return observable;
753
+ },
754
+ };
755
+ return observable;
756
+ };
757
+
758
+ // Async iterator
759
+ function events<K extends keyof E & string>(
760
+ type: K,
761
+ options?: AsyncIteratorOptions,
762
+ ): AsyncIterableIterator<EventfulEvent<E[K]>> {
763
+ // If already completed, return an iterator that immediately yields done
764
+ if (isCompleted) {
765
+ return {
766
+ [Symbol.asyncIterator]() {
767
+ return this;
768
+ },
769
+ next(): Promise<IteratorResult<EventfulEvent<E[K]>>> {
770
+ return Promise.resolve({
771
+ value: undefined as unknown as EventfulEvent<E[K]>,
772
+ done: true,
773
+ });
774
+ },
775
+ return(): Promise<IteratorResult<EventfulEvent<E[K]>>> {
776
+ return Promise.resolve({
777
+ value: undefined as unknown as EventfulEvent<E[K]>,
778
+ done: true,
779
+ });
780
+ },
781
+ };
782
+ }
783
+
784
+ const signal = options?.signal;
785
+ const bufferSize = options?.bufferSize ?? Infinity;
786
+ const overflowStrategy = options?.overflowStrategy ?? 'drop-oldest';
787
+
788
+ const buffer: Array<EventfulEvent<E[K]>> = [];
789
+ let resolve: ((result: IteratorResult<EventfulEvent<E[K]>>) => void) | null = null;
790
+ let done = false;
791
+ let hasOverflow = false;
792
+
793
+ const unsub = addEventListener(type, (event) => {
794
+ if (done) return;
795
+
796
+ if (resolve) {
797
+ // Someone is waiting, resolve immediately
798
+ const r = resolve;
799
+ resolve = null;
800
+ r({ value: event, done: false });
801
+ } else {
802
+ // Buffer the event
803
+ if (buffer.length >= bufferSize && bufferSize !== Infinity) {
804
+ switch (overflowStrategy) {
805
+ case 'drop-oldest':
806
+ buffer.shift();
807
+ buffer.push(event);
808
+ break;
809
+ case 'drop-latest':
810
+ // Don't add the new event
811
+ break;
812
+ case 'throw':
813
+ unsub();
814
+ completionCallbacks.delete(onComplete);
815
+ done = true;
816
+ hasOverflow = true;
817
+ return;
818
+ }
819
+ } else {
820
+ buffer.push(event);
821
+ }
822
+ }
823
+ });
824
+
825
+ // Handle completion
826
+ const onComplete = () => {
827
+ done = true;
828
+ if (resolve) {
829
+ const r = resolve;
830
+ resolve = null;
831
+ r({ value: undefined as unknown as EventfulEvent<E[K]>, done: true });
832
+ }
833
+ };
834
+ completionCallbacks.add(onComplete);
835
+
836
+ // Handle abort signal
837
+ let onAbort: (() => void) | null = null;
838
+ if (signal) {
839
+ onAbort = () => {
840
+ done = true;
841
+ completionCallbacks.delete(onComplete);
842
+ unsub();
843
+ if (resolve) {
844
+ const r = resolve;
845
+ resolve = null;
846
+ r({ value: undefined as unknown as EventfulEvent<E[K]>, done: true });
847
+ }
848
+ };
849
+ signal.addEventListener('abort', onAbort, { once: true });
850
+ if (signal.aborted) onAbort();
851
+ }
852
+
853
+ const iterator: AsyncIterableIterator<EventfulEvent<E[K]>> = {
854
+ [Symbol.asyncIterator]() {
855
+ return this;
856
+ },
857
+ async next(): Promise<IteratorResult<EventfulEvent<E[K]>>> {
858
+ // Drain buffered events first, even if done
859
+ if (buffer.length > 0) {
860
+ return { value: buffer.shift()!, done: false };
861
+ }
862
+
863
+ // After buffer is drained, check for overflow error
864
+ if (hasOverflow) {
865
+ hasOverflow = false;
866
+ throw new BufferOverflowError(type, bufferSize);
867
+ }
868
+
869
+ if (done) {
870
+ return { value: undefined as unknown as EventfulEvent<E[K]>, done: true };
871
+ }
872
+
873
+ // Prevent concurrent next() calls - if there's already a pending promise, reject
874
+ if (resolve !== null) {
875
+ return Promise.reject(
876
+ new Error(
877
+ 'Concurrent calls to next() are not supported on this async iterator',
878
+ ),
879
+ );
880
+ }
881
+
882
+ // Wait for next event
883
+ return new Promise<IteratorResult<EventfulEvent<E[K]>>>((_resolve, _reject) => {
884
+ if (done) {
885
+ _resolve({ value: undefined as unknown as EventfulEvent<E[K]>, done: true });
886
+ return;
887
+ }
888
+ if (hasOverflow) {
889
+ hasOverflow = false;
890
+ _reject(new BufferOverflowError(type, bufferSize));
891
+ return;
892
+ }
893
+ resolve = _resolve;
894
+ });
895
+ },
896
+ return(): Promise<IteratorResult<EventfulEvent<E[K]>>> {
897
+ // Resolve any pending promise before cleanup
898
+ if (resolve) {
899
+ const r = resolve;
900
+ resolve = null;
901
+ r({ value: undefined as unknown as EventfulEvent<E[K]>, done: true });
902
+ }
903
+
904
+ done = true;
905
+ completionCallbacks.delete(onComplete);
906
+ unsub();
907
+
908
+ // Clean up abort signal listener
909
+ if (signal && onAbort) {
910
+ signal.removeEventListener('abort', onAbort);
911
+ }
912
+
913
+ return Promise.resolve({
914
+ value: undefined as unknown as EventfulEvent<E[K]>,
915
+ done: true,
916
+ });
917
+ },
918
+ };
919
+
920
+ return iterator;
921
+ }
922
+
923
+ const target: EventTargetLike<E> = {
924
+ addEventListener,
925
+ removeEventListener,
926
+ dispatchEvent,
927
+ clear,
928
+ once,
929
+ removeAllListeners,
930
+ pipe,
931
+ addWildcardListener,
932
+ removeWildcardListener,
933
+ complete,
934
+ get completed() {
935
+ return isCompleted;
936
+ },
937
+ subscribe,
938
+ toObservable,
939
+ events,
940
+ };
941
+
942
+ // Add Symbol.observable - return an observable that emits all events from all types
943
+ (target as EventTargetLike<E> & { [key: symbol]: unknown })[SymbolObservable] = () => {
944
+ return toObservable();
945
+ };
946
+
947
+ return target;
948
+ }