event-emission 0.2.0 → 0.2.1

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 (43) hide show
  1. package/README.md +80 -24
  2. package/dist/event-emission.d.ts +13 -14
  3. package/dist/event-emission.d.ts.map +1 -1
  4. package/dist/factory.d.ts +1 -1
  5. package/dist/factory.d.ts.map +1 -1
  6. package/dist/index.cjs +474 -285
  7. package/dist/index.cjs.map +8 -9
  8. package/dist/index.d.ts +1 -7
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +449 -278
  11. package/dist/index.js.map +8 -9
  12. package/dist/interoperability.cjs +1605 -0
  13. package/dist/interoperability.cjs.map +15 -0
  14. package/dist/{interop.d.ts → interoperability.d.ts} +2 -1
  15. package/dist/interoperability.d.ts.map +1 -0
  16. package/dist/interoperability.js +1555 -0
  17. package/dist/interoperability.js.map +15 -0
  18. package/dist/observable.cjs +286 -0
  19. package/dist/observable.cjs.map +11 -0
  20. package/dist/observable.js +253 -0
  21. package/dist/observable.js.map +11 -0
  22. package/dist/observe.cjs +344 -0
  23. package/dist/observe.cjs.map +10 -0
  24. package/dist/observe.d.ts +6 -1
  25. package/dist/observe.d.ts.map +1 -1
  26. package/dist/observe.js +313 -0
  27. package/dist/observe.js.map +10 -0
  28. package/dist/symbols.d.ts +1 -1
  29. package/dist/types.cjs +35 -0
  30. package/dist/types.cjs.map +9 -0
  31. package/dist/types.d.ts +60 -25
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/types.js +3 -0
  34. package/dist/types.js.map +9 -0
  35. package/package.json +25 -1
  36. package/src/event-emission.ts +26 -20
  37. package/src/factory.ts +538 -218
  38. package/src/index.ts +4 -33
  39. package/src/{interop.ts → interoperability.ts} +24 -6
  40. package/src/observe.ts +54 -17
  41. package/src/symbols.ts +1 -1
  42. package/src/types.ts +75 -30
  43. package/dist/interop.d.ts.map +0 -1
package/src/factory.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  } from './observe';
8
8
  import { SymbolObservable } from './symbols';
9
9
  import type {
10
+ AddEventListenerOptionsLike,
10
11
  AsyncIteratorOptions,
11
12
  EmissionEvent,
12
13
  EventTargetLike,
@@ -23,13 +24,40 @@ import type {
23
24
  */
24
25
  function matchesWildcard(eventType: string, pattern: string): boolean {
25
26
  if (pattern === '*') return true;
26
- if (pattern.endsWith(':*')) {
27
- const namespace = pattern.slice(0, -2);
28
- return eventType.startsWith(namespace + ':');
29
- }
30
- return false;
27
+ return pattern.endsWith(':*') && eventType.startsWith(pattern.slice(0, -2) + ':');
31
28
  }
32
29
 
30
+ type EventPathStruct = {
31
+ invocationTarget: unknown;
32
+ invocationTargetInShadowTree: boolean;
33
+ shadowAdjustedTarget: unknown;
34
+ relatedTarget: unknown;
35
+ touchTargets: unknown[];
36
+ rootOfClosedTree: boolean;
37
+ slotInClosedTree: boolean;
38
+ };
39
+
40
+ type EventDispatchState = {
41
+ dispatchFlag: boolean;
42
+ initializedFlag: boolean;
43
+ stopPropagationFlag: boolean;
44
+ stopImmediatePropagationFlag: boolean;
45
+ canceledFlag: boolean;
46
+ inPassiveListenerFlag: boolean;
47
+ composedFlag: boolean;
48
+ eventPhase: number;
49
+ currentTarget: unknown;
50
+ target: unknown;
51
+ timeStamp: number;
52
+ path: EventPathStruct[];
53
+ type: string;
54
+ bubbles: boolean;
55
+ cancelable: boolean;
56
+ isTrusted: boolean;
57
+ };
58
+
59
+ const EVENT_STATE = Symbol('event-emission:event-state');
60
+
33
61
  /**
34
62
  * Options for createEventTarget.
35
63
  *
@@ -46,7 +74,7 @@ export interface CreateEventTargetOptions {
46
74
  * Extends CreateEventTargetOptions with proxy observation settings.
47
75
  *
48
76
  * @property observe - Must be true to enable observation mode.
49
- * @property deep - If true, nested objects are also observed (default: false).
77
+ * @property deep - If true, nested objects are also observed (default: true).
50
78
  * @property cloneStrategy - Strategy for cloning previous state: 'shallow', 'deep', or 'path'.
51
79
  */
52
80
  export interface CreateEventTargetObserveOptions
@@ -159,6 +187,7 @@ export function createEventTarget<T extends object, E extends Record<string, any
159
187
  const proxy = createObservableProxy(target, eventTarget, {
160
188
  deep: opts.deep,
161
189
  cloneStrategy: opts.cloneStrategy,
190
+ deepClone: opts.deepClone,
162
191
  });
163
192
 
164
193
  // Copy eventTarget methods onto the proxy
@@ -209,50 +238,238 @@ export function createEventTarget<T extends object, E extends Record<string, any
209
238
  function createEventTargetInternal<E extends Record<string, any>>(
210
239
  opts?: CreateEventTargetOptions,
211
240
  ): EventTargetLike<E> {
212
- const listeners = new Map<string, Set<Listener<E[keyof E]>>>();
241
+ const listeners = new Map<string, Array<Listener<E[keyof E]>>>();
213
242
  const wildcardListeners = new Set<WildcardListener<E>>();
214
243
  let isCompleted = false;
215
244
  const completionCallbacks = new Set<() => void>();
216
245
 
246
+ const now = () =>
247
+ typeof globalThis.performance?.now === 'function'
248
+ ? globalThis.performance.now()
249
+ : Date.now();
250
+
251
+ const initializeEventState = (
252
+ state: EventDispatchState,
253
+ type: string,
254
+ bubbles: boolean,
255
+ cancelable: boolean,
256
+ ) => {
257
+ state.initializedFlag = true;
258
+ state.stopPropagationFlag = false;
259
+ state.stopImmediatePropagationFlag = false;
260
+ state.canceledFlag = false;
261
+ state.isTrusted = false;
262
+ state.target = null;
263
+ state.currentTarget = null;
264
+ state.eventPhase = 0;
265
+ state.type = type;
266
+ state.bubbles = bubbles;
267
+ state.cancelable = cancelable;
268
+ };
269
+
270
+ const setCanceledFlag = (state: EventDispatchState) => {
271
+ if (state.cancelable && !state.inPassiveListenerFlag) {
272
+ state.canceledFlag = true;
273
+ }
274
+ };
275
+
217
276
  /**
218
- * Helper to create a DOM-compatible augmented event.
277
+ * Helper to create a DOM-compatible event.
219
278
  */
220
- const augmentEvent = <T>(type: string, detail: T): EmissionEvent<T> => {
221
- const baseEvent = {
222
- bubbles: false,
223
- cancelable: false,
224
- composed: false,
225
- defaultPrevented: false,
226
- eventPhase: 2, // AT_TARGET
279
+ const createEvent = <T>(
280
+ type: string,
281
+ detail: T,
282
+ init?: {
283
+ bubbles?: boolean;
284
+ cancelable?: boolean;
285
+ composed?: boolean;
286
+ timeStamp?: number;
287
+ target?: unknown;
288
+ currentTarget?: unknown;
289
+ eventPhase?: number;
290
+ },
291
+ ): EmissionEvent<T> => {
292
+ const state: EventDispatchState = {
293
+ dispatchFlag: false,
294
+ initializedFlag: true,
295
+ stopPropagationFlag: false,
296
+ stopImmediatePropagationFlag: false,
297
+ canceledFlag: false,
298
+ inPassiveListenerFlag: false,
299
+ composedFlag: Boolean(init?.composed),
300
+ eventPhase: init?.eventPhase ?? 0,
301
+ currentTarget: init?.currentTarget ?? init?.target ?? null,
302
+ target: init?.target ?? null,
303
+ timeStamp: init?.timeStamp ?? now(),
304
+ path: [],
305
+ type,
306
+ bubbles: Boolean(init?.bubbles),
307
+ cancelable: Boolean(init?.cancelable),
227
308
  isTrusted: false,
228
- timeStamp: Date.now(),
229
- NONE: 0,
230
- CAPTURING_PHASE: 1,
231
- AT_TARGET: 2,
232
- BUBBLING_PHASE: 3,
233
- composedPath: () => [target],
234
- stopImmediatePropagation: () => {},
235
- stopPropagation: () => {},
236
309
  };
237
310
 
238
- return Object.defineProperties(
239
- { ...baseEvent, type, detail },
240
- {
241
- target: { value: target, enumerable: true, configurable: true },
242
- currentTarget: { value: target, enumerable: true, configurable: true },
243
- preventDefault: {
244
- value: function (this: EmissionEvent<unknown>) {
245
- Object.defineProperty(this, 'defaultPrevented', {
246
- value: true,
247
- enumerable: true,
248
- configurable: true,
249
- });
250
- },
251
- enumerable: true,
252
- configurable: true,
311
+ const event = { detail } as EmissionEvent<T>;
312
+ Object.defineProperties(event, {
313
+ type: {
314
+ get: () => state.type,
315
+ enumerable: true,
316
+ configurable: true,
317
+ },
318
+ bubbles: {
319
+ get: () => state.bubbles,
320
+ enumerable: true,
321
+ configurable: true,
322
+ },
323
+ cancelable: {
324
+ get: () => state.cancelable,
325
+ enumerable: true,
326
+ configurable: true,
327
+ },
328
+ cancelBubble: {
329
+ get: () => state.stopPropagationFlag,
330
+ set: (value: boolean) => {
331
+ if (value) state.stopPropagationFlag = true;
332
+ },
333
+ enumerable: true,
334
+ configurable: true,
335
+ },
336
+ composed: {
337
+ get: () => state.composedFlag,
338
+ enumerable: true,
339
+ configurable: true,
340
+ },
341
+ currentTarget: {
342
+ get: () => state.currentTarget,
343
+ enumerable: true,
344
+ configurable: true,
345
+ },
346
+ defaultPrevented: {
347
+ get: () => state.canceledFlag,
348
+ enumerable: true,
349
+ configurable: true,
350
+ },
351
+ eventPhase: {
352
+ get: () => state.eventPhase,
353
+ enumerable: true,
354
+ configurable: true,
355
+ },
356
+ isTrusted: {
357
+ get: () => state.isTrusted,
358
+ enumerable: true,
359
+ configurable: true,
360
+ },
361
+ returnValue: {
362
+ get: () => !state.canceledFlag,
363
+ set: (value: boolean) => {
364
+ if (value === false) setCanceledFlag(state);
365
+ },
366
+ enumerable: true,
367
+ configurable: true,
368
+ },
369
+ srcElement: {
370
+ get: () => state.target,
371
+ enumerable: true,
372
+ configurable: true,
373
+ },
374
+ target: {
375
+ get: () => state.target,
376
+ enumerable: true,
377
+ configurable: true,
378
+ },
379
+ timeStamp: {
380
+ get: () => state.timeStamp,
381
+ enumerable: true,
382
+ configurable: true,
383
+ },
384
+ composedPath: {
385
+ value: () => state.path.map((entry) => entry.invocationTarget),
386
+ enumerable: true,
387
+ configurable: true,
388
+ },
389
+ initEvent: {
390
+ value: (newType: string, bubbles = false, cancelable = false) => {
391
+ if (state.dispatchFlag) return;
392
+ initializeEventState(state, newType, Boolean(bubbles), Boolean(cancelable));
393
+ },
394
+ enumerable: true,
395
+ configurable: true,
396
+ },
397
+ preventDefault: {
398
+ value: () => setCanceledFlag(state),
399
+ enumerable: true,
400
+ configurable: true,
401
+ },
402
+ stopImmediatePropagation: {
403
+ value: () => {
404
+ state.stopPropagationFlag = true;
405
+ state.stopImmediatePropagationFlag = true;
406
+ },
407
+ enumerable: true,
408
+ configurable: true,
409
+ },
410
+ stopPropagation: {
411
+ value: () => {
412
+ state.stopPropagationFlag = true;
253
413
  },
414
+ enumerable: true,
415
+ configurable: true,
416
+ },
417
+ NONE: { value: 0, enumerable: true, configurable: true },
418
+ CAPTURING_PHASE: { value: 1, enumerable: true, configurable: true },
419
+ AT_TARGET: { value: 2, enumerable: true, configurable: true },
420
+ BUBBLING_PHASE: { value: 3, enumerable: true, configurable: true },
421
+ [EVENT_STATE]: {
422
+ value: state,
423
+ enumerable: false,
424
+ configurable: false,
254
425
  },
255
- ) as EmissionEvent<T>;
426
+ });
427
+
428
+ return event;
429
+ };
430
+
431
+ const getEventState = (event: EmissionEvent<unknown>): EventDispatchState | undefined =>
432
+ (event as { [EVENT_STATE]?: EventDispatchState })[EVENT_STATE];
433
+
434
+ const normalizeAddListenerOptions = (
435
+ options?: AddEventListenerOptionsLike | boolean,
436
+ ) => {
437
+ if (typeof options === 'boolean') {
438
+ return {
439
+ capture: options,
440
+ passive: false,
441
+ once: false,
442
+ signal: null as AddEventListenerOptionsLike['signal'] | null,
443
+ };
444
+ }
445
+
446
+ return {
447
+ capture: Boolean(options?.capture),
448
+ passive: Boolean(options?.passive),
449
+ once: Boolean(options?.once),
450
+ signal: options?.signal ?? null,
451
+ };
452
+ };
453
+
454
+ const normalizeCaptureOption = (options?: { capture?: boolean } | boolean) => {
455
+ if (typeof options === 'boolean') return options;
456
+ return Boolean(options?.capture);
457
+ };
458
+
459
+ const removeListenerRecord = (type: string, record: Listener<E[keyof E]>) => {
460
+ if (record.removed) return;
461
+ record.removed = true;
462
+
463
+ const list = listeners.get(type);
464
+ if (list) {
465
+ const idx = list.indexOf(record);
466
+ if (idx >= 0) list.splice(idx, 1);
467
+ if (list.length === 0) listeners.delete(type);
468
+ }
469
+
470
+ if (record.signal && record.abortHandler) {
471
+ record.signal.removeEventListener('abort', record.abortHandler);
472
+ }
256
473
  };
257
474
 
258
475
  // Helper to handle listener errors: emit 'error' event or re-throw if no listener
@@ -267,33 +484,35 @@ function createEventTargetInternal<E extends Record<string, any>>(
267
484
  }
268
485
 
269
486
  const errorListeners = listeners.get('error');
270
- if (errorListeners && errorListeners.size > 0) {
271
- // Emit 'error' event with the error as detail
272
- const errorEvent = augmentEvent('error', error);
273
- for (const rec of Array.from(errorListeners)) {
274
- try {
275
- const fn = rec.fn as (event: EmissionEvent<unknown>) => void | Promise<void>;
276
- void fn(errorEvent as EmissionEvent<E[keyof E]>);
277
- } catch {
278
- // Swallow errors from error handlers to prevent infinite loops
279
- }
280
- if (rec.once) errorListeners.delete(rec);
281
- }
487
+ if (errorListeners && errorListeners.length > 0) {
488
+ dispatchEvent({ type: 'error', detail: error } as Parameters<
489
+ EventTargetLike<E>['dispatchEvent']
490
+ >[0]);
282
491
  } else {
283
492
  // No 'error' listener - re-throw (Node.js behavior)
284
493
  throw error;
285
494
  }
286
495
  };
287
496
 
288
- const notifyWildcardListeners = (eventType: string, detail: E[keyof E]) => {
497
+ const notifyWildcardListeners = (
498
+ eventType: string,
499
+ event: EmissionEvent<E[keyof E]>,
500
+ ) => {
289
501
  if (wildcardListeners.size === 0) return;
290
502
 
291
503
  for (const rec of Array.from(wildcardListeners)) {
292
504
  if (!matchesWildcard(eventType, rec.pattern)) continue;
293
505
 
294
- // Create wildcard event based on augmented event
295
- const baseAugmented = augmentEvent(rec.pattern, detail);
296
- const wildcardEvent: WildcardEvent<E> = Object.defineProperties(baseAugmented, {
506
+ // Create wildcard event based on a DOM-compatible event
507
+ const baseEvent = createEvent(rec.pattern, event.detail, {
508
+ target,
509
+ currentTarget: target,
510
+ eventPhase: 2,
511
+ bubbles: event.bubbles,
512
+ cancelable: event.cancelable,
513
+ composed: event.composed,
514
+ });
515
+ const wildcardEvent: WildcardEvent<E> = Object.defineProperties(baseEvent, {
297
516
  originalType: { value: eventType, enumerable: true, configurable: true },
298
517
  }) as WildcardEvent<E>;
299
518
 
@@ -317,6 +536,11 @@ function createEventTargetInternal<E extends Record<string, any>>(
317
536
  } finally {
318
537
  if (rec.once) wildcardListeners.delete(rec);
319
538
  }
539
+
540
+ const state = getEventState(wildcardEvent);
541
+ if (state?.stopImmediatePropagationFlag || state?.stopPropagationFlag) {
542
+ break;
543
+ }
320
544
  }
321
545
  };
322
546
 
@@ -325,36 +549,57 @@ function createEventTargetInternal<E extends Record<string, any>>(
325
549
  listener,
326
550
  options,
327
551
  ) => {
328
- if (isCompleted) {
552
+ if (isCompleted || !listener) {
329
553
  // Return no-op unsubscribe if already completed
330
554
  return () => {};
331
555
  }
332
556
 
333
- const opts2 = options ?? {};
334
- const record: Listener<E[keyof E]> = {
335
- fn: listener as Listener<E[keyof E]>['fn'],
336
- once: opts2.once,
337
- signal: opts2.signal,
338
- };
339
- let set = listeners.get(type);
340
- if (!set) {
341
- set = new Set();
342
- listeners.set(type, set);
557
+ const { capture, passive, once, signal } = normalizeAddListenerOptions(options);
558
+ let list = listeners.get(type);
559
+ if (!list) {
560
+ list = [];
561
+ listeners.set(type, list);
343
562
  }
344
- set.add(record);
345
- const unsubscribe = () => {
346
- const setNow = listeners.get(type);
347
- setNow?.delete(record);
348
- if (record.signal && record.abortHandler) {
349
- record.signal.removeEventListener('abort', record.abortHandler);
563
+
564
+ for (const existing of list) {
565
+ if (existing.original === listener && existing.capture === capture) {
566
+ return () =>
567
+ removeEventListener(type, listener, options as boolean | { capture?: boolean });
350
568
  }
569
+ }
570
+
571
+ const original = listener as Listener<E[keyof E]>['original'];
572
+ const callback =
573
+ typeof listener === 'function'
574
+ ? (listener as Listener<E[keyof E]>['callback'])
575
+ : (event: EmissionEvent<E[keyof E]>) =>
576
+ (
577
+ listener as {
578
+ handleEvent: Listener<E[keyof E]>['callback'];
579
+ }
580
+ ).handleEvent(event);
581
+
582
+ const record: Listener<E[keyof E]> = {
583
+ type,
584
+ original,
585
+ callback,
586
+ capture,
587
+ passive,
588
+ once,
589
+ signal,
590
+ removed: false,
351
591
  };
352
- if (opts2.signal) {
353
- const onAbort = () => unsubscribe();
592
+ list.push(record);
593
+
594
+ const unsubscribe = () => removeListenerRecord(type, record);
595
+
596
+ if (signal) {
597
+ const onAbort = () => removeListenerRecord(type, record);
354
598
  record.abortHandler = onAbort;
355
- opts2.signal.addEventListener('abort', onAbort, { once: true });
356
- if (opts2.signal.aborted) onAbort();
599
+ signal.addEventListener('abort', onAbort, { once: true });
600
+ if (signal.aborted) onAbort();
357
601
  }
602
+
358
603
  return unsubscribe;
359
604
  };
360
605
 
@@ -366,6 +611,11 @@ function createEventTargetInternal<E extends Record<string, any>>(
366
611
  if (isCompleted) return () => {};
367
612
 
368
613
  const opts2 = options ?? {};
614
+ for (const existing of wildcardListeners) {
615
+ if (existing.pattern === pattern && existing.fn === listener) {
616
+ return () => removeWildcardListener(pattern, listener);
617
+ }
618
+ }
369
619
  const record: WildcardListener<E> = {
370
620
  fn: listener,
371
621
  pattern,
@@ -395,37 +645,44 @@ function createEventTargetInternal<E extends Record<string, any>>(
395
645
  pattern,
396
646
  listener,
397
647
  ) => {
398
- for (const record of wildcardListeners) {
648
+ for (const record of Array.from(wildcardListeners)) {
399
649
  if (record.pattern === pattern && record.fn === listener) {
400
650
  wildcardListeners.delete(record);
401
651
  if (record.signal && record.abortHandler) {
402
652
  record.signal.removeEventListener('abort', record.abortHandler);
403
653
  }
404
- break;
405
654
  }
406
655
  }
407
656
  };
408
657
 
409
- const dispatchEvent: EventTargetLike<E>['dispatchEvent'] = (event) => {
410
- if (isCompleted) return false;
658
+ const invokeListeners = (
659
+ eventType: string,
660
+ event: EmissionEvent<E[keyof E]>,
661
+ phase: 'capturing' | 'bubbling',
662
+ listenersSnapshot: Array<Listener<E[keyof E]>>,
663
+ ) => {
664
+ const state = getEventState(event);
665
+ if (!state || state.stopPropagationFlag) return;
411
666
 
412
- // Augment event with DOM properties to ensure it's a superset of DOM Event
413
- const augmentedEvent = augmentEvent(event.type, event.detail);
667
+ state.currentTarget = target;
668
+ state.target = target;
669
+ state.eventPhase = event.AT_TARGET;
414
670
 
415
- // Notify wildcard listeners first (no overhead if none registered)
416
- notifyWildcardListeners(event.type, event.detail as E[keyof E]);
671
+ for (const rec of listenersSnapshot) {
672
+ if (rec.removed) continue;
673
+ if (phase === 'capturing' && !rec.capture) continue;
674
+ if (phase === 'bubbling' && rec.capture) continue;
675
+
676
+ if (rec.once) removeListenerRecord(rec.type, rec);
417
677
 
418
- const set = listeners.get(event.type);
419
- if (!set || set.size === 0) return true;
420
- for (const rec of Array.from(set)) {
678
+ if (rec.passive) state.inPassiveListenerFlag = true;
421
679
  try {
422
- const res = rec.fn(augmentedEvent as EmissionEvent<E[keyof E]>);
680
+ const res = rec.callback.call(state.currentTarget, event);
423
681
  if (res && typeof res.then === 'function') {
424
682
  res.catch((error) => {
425
683
  try {
426
- handleListenerError(event.type, error);
684
+ handleListenerError(eventType, error);
427
685
  } catch (rethrown) {
428
- // Re-throw async errors via queueMicrotask to preserve stack trace
429
686
  queueMicrotask(() => {
430
687
  throw rethrown;
431
688
  });
@@ -433,41 +690,116 @@ function createEventTargetInternal<E extends Record<string, any>>(
433
690
  });
434
691
  }
435
692
  } catch (error) {
436
- handleListenerError(event.type, error);
693
+ handleListenerError(eventType, error);
437
694
  } finally {
438
- if (rec.once) set.delete(rec);
695
+ if (rec.passive) state.inPassiveListenerFlag = false;
439
696
  }
697
+
698
+ if (state.stopImmediatePropagationFlag) break;
440
699
  }
441
- return true;
700
+ };
701
+
702
+ const dispatchEvent: EventTargetLike<E>['dispatchEvent'] = (eventInput) => {
703
+ if (isCompleted) return false;
704
+
705
+ let event: EmissionEvent<E[keyof E]>;
706
+ let state: EventDispatchState | undefined;
707
+
708
+ if (eventInput && typeof eventInput === 'object') {
709
+ state = getEventState(eventInput as EmissionEvent<unknown>);
710
+ if (state) {
711
+ event = eventInput as EmissionEvent<E[keyof E]>;
712
+ } else {
713
+ const input = eventInput as {
714
+ type: string;
715
+ detail?: E[keyof E];
716
+ bubbles?: boolean;
717
+ cancelable?: boolean;
718
+ composed?: boolean;
719
+ timeStamp?: number;
720
+ };
721
+ if (typeof input.type !== 'string') {
722
+ throw new TypeError('Event type must be a string');
723
+ }
724
+ event = createEvent(input.type, input.detail as E[keyof E], {
725
+ bubbles: input.bubbles,
726
+ cancelable: input.cancelable,
727
+ composed: input.composed,
728
+ timeStamp: input.timeStamp,
729
+ });
730
+ state = getEventState(event)!;
731
+ }
732
+ } else {
733
+ throw new TypeError('dispatchEvent expects an event object');
734
+ }
735
+
736
+ const dispatchState = state ?? getEventState(event)!;
737
+
738
+ if (dispatchState.dispatchFlag || !dispatchState.initializedFlag) {
739
+ const message =
740
+ 'Failed to execute dispatchEvent: event is already being dispatched';
741
+ if (typeof globalThis.DOMException === 'function') {
742
+ throw new globalThis.DOMException(message, 'InvalidStateError');
743
+ }
744
+ const err = new Error(message);
745
+ err.name = 'InvalidStateError';
746
+ throw err;
747
+ }
748
+
749
+ dispatchState.isTrusted = false;
750
+ dispatchState.dispatchFlag = true;
751
+ dispatchState.path = [
752
+ {
753
+ invocationTarget: target,
754
+ invocationTargetInShadowTree: false,
755
+ shadowAdjustedTarget: target,
756
+ relatedTarget: null,
757
+ touchTargets: [],
758
+ rootOfClosedTree: false,
759
+ slotInClosedTree: false,
760
+ },
761
+ ];
762
+
763
+ // Notify wildcard listeners first (no overhead if none registered)
764
+ notifyWildcardListeners(dispatchState.type, event);
765
+
766
+ const list = listeners.get(dispatchState.type);
767
+ const snapshot = list ? list.slice() : [];
768
+ invokeListeners(dispatchState.type, event, 'capturing', snapshot);
769
+ invokeListeners(dispatchState.type, event, 'bubbling', snapshot);
770
+
771
+ dispatchState.eventPhase = event.NONE;
772
+ dispatchState.currentTarget = null;
773
+ dispatchState.path = [];
774
+ dispatchState.dispatchFlag = false;
775
+ dispatchState.stopPropagationFlag = false;
776
+ dispatchState.stopImmediatePropagationFlag = false;
777
+
778
+ return !dispatchState.canceledFlag;
442
779
  };
443
780
 
444
781
  const removeEventListener: EventTargetLike<E>['removeEventListener'] = (
445
782
  type,
446
783
  listener,
784
+ options,
447
785
  ) => {
448
- const set = listeners.get(type);
449
- if (!set) return;
450
-
451
- for (const record of set) {
452
- if (record.fn === listener) {
453
- set.delete(record);
454
- if (record.signal && record.abortHandler) {
455
- record.signal.removeEventListener('abort', record.abortHandler);
456
- }
457
- break;
786
+ if (!listener) return;
787
+ const capture = normalizeCaptureOption(options);
788
+ const list = listeners.get(type);
789
+ if (!list) return;
790
+
791
+ for (const record of [...list]) {
792
+ if (record.original === listener && record.capture === capture) {
793
+ removeListenerRecord(type, record);
458
794
  }
459
795
  }
460
796
  };
461
797
 
462
798
  const clear = () => {
463
- // Clean up abort handlers before clearing
464
- for (const set of listeners.values()) {
465
- for (const record of set) {
466
- if (record.signal && record.abortHandler) {
467
- record.signal.removeEventListener('abort', record.abortHandler);
468
- }
799
+ for (const [type, list] of Array.from(listeners.entries())) {
800
+ for (const record of [...list]) {
801
+ removeListenerRecord(type, record);
469
802
  }
470
- set.clear();
471
803
  }
472
804
  listeners.clear();
473
805
 
@@ -491,7 +823,7 @@ function createEventTargetInternal<E extends Record<string, any>>(
491
823
  /* eslint-enable @typescript-eslint/no-explicit-any */
492
824
  let opts: OnOptions;
493
825
  if (typeof options === 'boolean') {
494
- opts = { signal: undefined }; // Map boolean capture to options if needed, but our internal target doesn't use capture
826
+ opts = { capture: options };
495
827
  } else {
496
828
  opts = options ?? {};
497
829
  }
@@ -546,31 +878,27 @@ function createEventTargetInternal<E extends Record<string, any>>(
546
878
  };
547
879
 
548
880
  const onceMethod: EventTargetLike<E>['once'] = (type, listener, options) => {
549
- return addEventListener(type, listener, { ...options, once: true });
881
+ if (typeof options === 'boolean') {
882
+ return addEventListener(type, listener, { capture: options, once: true });
883
+ }
884
+ return addEventListener(type, listener, { ...(options ?? {}), once: true });
550
885
  };
551
886
 
552
887
  const removeAllListeners: EventTargetLike<E>['removeAllListeners'] = (type) => {
553
888
  if (type !== undefined) {
554
- const set = listeners.get(type);
555
- if (set) {
556
- // Clean up abort handlers before clearing
557
- for (const record of set) {
558
- if (record.signal && record.abortHandler) {
559
- record.signal.removeEventListener('abort', record.abortHandler);
560
- }
889
+ const list = listeners.get(type);
890
+ if (list) {
891
+ for (const record of [...list]) {
892
+ removeListenerRecord(type, record);
561
893
  }
562
- set.clear();
563
894
  listeners.delete(type);
564
895
  }
565
896
  } else {
566
897
  // Clear all listeners for all types
567
- for (const set of listeners.values()) {
568
- for (const record of set) {
569
- if (record.signal && record.abortHandler) {
570
- record.signal.removeEventListener('abort', record.abortHandler);
571
- }
898
+ for (const [eventType, list] of Array.from(listeners.entries())) {
899
+ for (const record of [...list]) {
900
+ removeListenerRecord(eventType, record);
572
901
  }
573
- set.clear();
574
902
  }
575
903
  listeners.clear();
576
904
 
@@ -587,64 +915,49 @@ function createEventTargetInternal<E extends Record<string, any>>(
587
915
  /**
588
916
  * Pipe events from this emitter to another target.
589
917
  *
590
- * **Limitation**: Only forwards events for types that already have listeners
591
- * when pipe() is called. Events for types registered afterward won't be piped.
592
- *
593
- * To ensure all events are piped, add at least one listener for each event type
594
- * before calling pipe().
918
+ * Forwards all events. If mapFn returns null, the event is skipped.
595
919
  */
596
920
  const pipe: EventTargetLike<E>['pipe'] = (target, mapFn) => {
597
921
  if (isCompleted) {
598
922
  return () => {};
599
923
  }
600
924
 
601
- const unsubscribes: Array<() => void> = [];
602
-
603
- // Subscribe to all current and future events by listening to each event type
604
- // We need to track event types we've subscribed to
605
- const subscribedTypes = new Set<string>();
606
-
607
- const subscribeToType = (type: string) => {
608
- if (subscribedTypes.has(type)) return;
609
- subscribedTypes.add(type);
610
-
611
- const unsub = addEventListener(type as keyof E & string, (event) => {
612
- if (mapFn) {
613
- const mapped = mapFn(event);
614
- if (mapped !== null) {
615
- // Type assertion via unknown is needed because mapFn output type matches target's event map
616
- target.dispatchEvent(
617
- mapped as unknown as Parameters<typeof target.dispatchEvent>[0],
618
- );
619
- }
620
- } else {
621
- // Type assertion via unknown is needed because caller ensures E and T are compatible
925
+ const unsubscribe = addWildcardListener('*', (event) => {
926
+ if (mapFn) {
927
+ const mapped = mapFn(
928
+ createEvent(event.originalType, event.detail, {
929
+ target,
930
+ currentTarget: target,
931
+ eventPhase: 2,
932
+ bubbles: event.bubbles,
933
+ cancelable: event.cancelable,
934
+ composed: event.composed,
935
+ }) as EmissionEvent<E[keyof E & string], keyof E & string>,
936
+ );
937
+ if (mapped !== null) {
938
+ // Type assertion via unknown is needed because mapFn output type matches target's event map
622
939
  target.dispatchEvent(
623
- event as unknown as Parameters<typeof target.dispatchEvent>[0],
940
+ mapped as unknown as Parameters<typeof target.dispatchEvent>[0],
624
941
  );
625
942
  }
626
- });
627
- unsubscribes.push(unsub);
628
- };
629
-
630
- // Subscribe to all existing event types
631
- for (const type of listeners.keys()) {
632
- subscribeToType(type);
633
- }
943
+ } else {
944
+ // Type assertion via unknown is needed because caller ensures E and T are compatible
945
+ target.dispatchEvent({
946
+ type: event.originalType,
947
+ detail: event.detail,
948
+ } as unknown as Parameters<typeof target.dispatchEvent>[0]);
949
+ }
950
+ });
634
951
 
635
952
  // Clean up on completion
636
953
  const completionUnsub = () => {
637
- for (const unsub of unsubscribes) {
638
- unsub();
639
- }
954
+ unsubscribe();
640
955
  };
641
956
  completionCallbacks.add(completionUnsub);
642
957
 
643
958
  return () => {
644
959
  completionCallbacks.delete(completionUnsub);
645
- for (const unsub of unsubscribes) {
646
- unsub();
647
- }
960
+ unsubscribe();
648
961
  };
649
962
  };
650
963
 
@@ -669,13 +982,10 @@ function createEventTargetInternal<E extends Record<string, any>>(
669
982
  completionCallbacks.clear();
670
983
 
671
984
  // Clear all listeners
672
- for (const set of listeners.values()) {
673
- for (const record of set) {
674
- if (record.signal && record.abortHandler) {
675
- record.signal.removeEventListener('abort', record.abortHandler);
676
- }
985
+ for (const [eventType, list] of Array.from(listeners.entries())) {
986
+ for (const record of [...list]) {
987
+ removeListenerRecord(eventType, record);
677
988
  }
678
- set.clear();
679
989
  }
680
990
  listeners.clear();
681
991
 
@@ -688,7 +998,7 @@ function createEventTargetInternal<E extends Record<string, any>>(
688
998
  wildcardListeners.clear();
689
999
  };
690
1000
 
691
- // Observable interop
1001
+ // Observable interoperability
692
1002
  const subscribe: EventTargetLike<E>['subscribe'] = (
693
1003
  type,
694
1004
  observerOrNext,
@@ -782,7 +1092,16 @@ function createEventTargetInternal<E extends Record<string, any>>(
782
1092
 
783
1093
  const wildcardListener = (event: WildcardEvent<E>) => {
784
1094
  // Emit an augmented event with the actual type
785
- observer.next(augmentEvent(event.originalType, event.detail));
1095
+ observer.next(
1096
+ createEvent(event.originalType, event.detail, {
1097
+ target,
1098
+ currentTarget: target,
1099
+ eventPhase: 2,
1100
+ bubbles: event.bubbles,
1101
+ cancelable: event.cancelable,
1102
+ composed: event.composed,
1103
+ }),
1104
+ );
786
1105
  };
787
1106
 
788
1107
  const unsubscribe = addWildcardListener('*', wildcardListener);
@@ -804,22 +1123,22 @@ function createEventTargetInternal<E extends Record<string, any>>(
804
1123
  function events<K extends keyof E & string>(
805
1124
  type: K,
806
1125
  options?: AsyncIteratorOptions,
807
- ): AsyncIterableIterator<EmissionEvent<E[K]>> {
1126
+ ): AsyncIterableIterator<EmissionEvent<E[K], K>> {
808
1127
  // If already completed, return an iterator that immediately yields done
809
1128
  if (isCompleted) {
810
1129
  return {
811
1130
  [Symbol.asyncIterator]() {
812
1131
  return this;
813
1132
  },
814
- next(): Promise<IteratorResult<EmissionEvent<E[K]>>> {
1133
+ next(): Promise<IteratorResult<EmissionEvent<E[K], K>>> {
815
1134
  return Promise.resolve({
816
- value: undefined as unknown as EmissionEvent<E[K]>,
1135
+ value: undefined as unknown as EmissionEvent<E[K], K>,
817
1136
  done: true,
818
1137
  });
819
1138
  },
820
- return(): Promise<IteratorResult<EmissionEvent<E[K]>>> {
1139
+ return(): Promise<IteratorResult<EmissionEvent<E[K], K>>> {
821
1140
  return Promise.resolve({
822
- value: undefined as unknown as EmissionEvent<E[K]>,
1141
+ value: undefined as unknown as EmissionEvent<E[K], K>,
823
1142
  done: true,
824
1143
  });
825
1144
  },
@@ -830,26 +1149,33 @@ function createEventTargetInternal<E extends Record<string, any>>(
830
1149
  const bufferSize = options?.bufferSize ?? Infinity;
831
1150
  const overflowStrategy = options?.overflowStrategy ?? 'drop-oldest';
832
1151
 
833
- const buffer: Array<EmissionEvent<E[K]>> = [];
834
- let resolve: ((result: IteratorResult<EmissionEvent<E[K]>>) => void) | null = null;
1152
+ const buffer: Array<EmissionEvent<E[K], K>> = [];
1153
+ let resolve: ((result: IteratorResult<EmissionEvent<E[K], K>>) => void) | null = null;
835
1154
  let done = false;
836
1155
  let hasOverflow = false;
1156
+ let onAbort: (() => void) | null = null;
1157
+ const cleanupAbortListener = () => {
1158
+ if (signal && onAbort) {
1159
+ signal.removeEventListener('abort', onAbort);
1160
+ }
1161
+ };
837
1162
 
838
1163
  const unsub = addEventListener(type, (event) => {
839
1164
  if (done) return;
1165
+ const typedEvent = event;
840
1166
 
841
1167
  if (resolve) {
842
1168
  // Someone is waiting, resolve immediately
843
1169
  const r = resolve;
844
1170
  resolve = null;
845
- r({ value: event, done: false });
1171
+ r({ value: typedEvent, done: false });
846
1172
  } else {
847
1173
  // Buffer the event
848
1174
  if (buffer.length >= bufferSize && bufferSize !== Infinity) {
849
1175
  switch (overflowStrategy) {
850
1176
  case 'drop-oldest':
851
1177
  buffer.shift();
852
- buffer.push(event);
1178
+ buffer.push(typedEvent);
853
1179
  break;
854
1180
  case 'drop-latest':
855
1181
  // Don't add the new event
@@ -859,10 +1185,11 @@ function createEventTargetInternal<E extends Record<string, any>>(
859
1185
  completionCallbacks.delete(onComplete);
860
1186
  done = true;
861
1187
  hasOverflow = true;
1188
+ cleanupAbortListener();
862
1189
  return;
863
1190
  }
864
1191
  } else {
865
- buffer.push(event);
1192
+ buffer.push(typedEvent);
866
1193
  }
867
1194
  }
868
1195
  });
@@ -870,16 +1197,16 @@ function createEventTargetInternal<E extends Record<string, any>>(
870
1197
  // Handle completion
871
1198
  const onComplete = () => {
872
1199
  done = true;
1200
+ cleanupAbortListener();
873
1201
  if (resolve) {
874
1202
  const r = resolve;
875
1203
  resolve = null;
876
- r({ value: undefined as unknown as EmissionEvent<E[K]>, done: true });
1204
+ r({ value: undefined as unknown as EmissionEvent<E[K], K>, done: true });
877
1205
  }
878
1206
  };
879
1207
  completionCallbacks.add(onComplete);
880
1208
 
881
1209
  // Handle abort signal
882
- let onAbort: (() => void) | null = null;
883
1210
  if (signal) {
884
1211
  onAbort = () => {
885
1212
  done = true;
@@ -888,33 +1215,23 @@ function createEventTargetInternal<E extends Record<string, any>>(
888
1215
  if (resolve) {
889
1216
  const r = resolve;
890
1217
  resolve = null;
891
- r({ value: undefined as unknown as EmissionEvent<E[K]>, done: true });
1218
+ r({ value: undefined as unknown as EmissionEvent<E[K], K>, done: true });
892
1219
  }
893
1220
  };
894
1221
  signal.addEventListener('abort', onAbort, { once: true });
895
1222
  if (signal.aborted) onAbort();
896
1223
  }
897
1224
 
898
- const iterator: AsyncIterableIterator<EmissionEvent<E[K]>> = {
1225
+ const iterator: AsyncIterableIterator<EmissionEvent<E[K], K>> = {
899
1226
  [Symbol.asyncIterator]() {
900
1227
  return this;
901
1228
  },
902
- async next(): Promise<IteratorResult<EmissionEvent<E[K]>>> {
1229
+ async next(): Promise<IteratorResult<EmissionEvent<E[K], K>>> {
903
1230
  // Drain buffered events first, even if done
904
1231
  if (buffer.length > 0) {
905
1232
  return { value: buffer.shift()!, done: false };
906
1233
  }
907
1234
 
908
- // After buffer is drained, check for overflow error
909
- if (hasOverflow) {
910
- hasOverflow = false;
911
- throw new BufferOverflowError(type, bufferSize);
912
- }
913
-
914
- if (done) {
915
- return { value: undefined as unknown as EmissionEvent<E[K]>, done: true };
916
- }
917
-
918
1235
  // Prevent concurrent next() calls - if there's already a pending promise, reject
919
1236
  if (resolve !== null) {
920
1237
  return Promise.reject(
@@ -925,25 +1242,30 @@ function createEventTargetInternal<E extends Record<string, any>>(
925
1242
  }
926
1243
 
927
1244
  // Wait for next event
928
- return new Promise<IteratorResult<EmissionEvent<E[K]>>>((_resolve, _reject) => {
929
- if (done) {
930
- _resolve({ value: undefined as unknown as EmissionEvent<E[K]>, done: true });
931
- return;
932
- }
933
- if (hasOverflow) {
934
- hasOverflow = false;
935
- _reject(new BufferOverflowError(type, bufferSize));
936
- return;
937
- }
938
- resolve = _resolve;
939
- });
1245
+ return new Promise<IteratorResult<EmissionEvent<E[K], K>>>(
1246
+ (_resolve, _reject) => {
1247
+ if (hasOverflow) {
1248
+ hasOverflow = false;
1249
+ _reject(new BufferOverflowError(type, bufferSize));
1250
+ return;
1251
+ }
1252
+ if (done) {
1253
+ _resolve({
1254
+ value: undefined as unknown as EmissionEvent<E[K], K>,
1255
+ done: true,
1256
+ });
1257
+ return;
1258
+ }
1259
+ resolve = _resolve;
1260
+ },
1261
+ );
940
1262
  },
941
- return(): Promise<IteratorResult<EmissionEvent<E[K]>>> {
1263
+ return(): Promise<IteratorResult<EmissionEvent<E[K], K>>> {
942
1264
  // Resolve any pending promise before cleanup
943
1265
  if (resolve) {
944
1266
  const r = resolve;
945
1267
  resolve = null;
946
- r({ value: undefined as unknown as EmissionEvent<E[K]>, done: true });
1268
+ r({ value: undefined as unknown as EmissionEvent<E[K], K>, done: true });
947
1269
  }
948
1270
 
949
1271
  done = true;
@@ -951,12 +1273,10 @@ function createEventTargetInternal<E extends Record<string, any>>(
951
1273
  unsub();
952
1274
 
953
1275
  // Clean up abort signal listener
954
- if (signal && onAbort) {
955
- signal.removeEventListener('abort', onAbort);
956
- }
1276
+ cleanupAbortListener();
957
1277
 
958
1278
  return Promise.resolve({
959
- value: undefined as unknown as EmissionEvent<E[K]>,
1279
+ value: undefined as unknown as EmissionEvent<E[K], K>,
960
1280
  done: true,
961
1281
  });
962
1282
  },