event-emission 0.2.1 → 0.3.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.
@@ -129,14 +129,29 @@ export abstract class EventEmission<E extends Record<string, any>> {
129
129
  // Convenience Methods
130
130
  // ==========================================================================
131
131
 
132
+ /**
133
+ * Adds a listener for the specified event type (Node.js-style).
134
+ * Returns an unsubscribe function.
135
+ */
136
+ on<K extends keyof E & string>(
137
+ type: K,
138
+ listener: EventListenerLike<EmissionEvent<E[K], K>>,
139
+ ): () => void;
140
+
132
141
  /**
133
142
  * Returns an observable for the specified event type.
134
143
  */
135
144
  on<K extends keyof E & string>(
136
145
  type: K,
137
146
  options?: OnOptions | boolean,
138
- ): ObservableLike<EmissionEvent<E[K], K>> {
139
- return this.#target.on(type, options);
147
+ ): ObservableLike<EmissionEvent<E[K], K>>;
148
+
149
+ on<K extends keyof E & string>(
150
+ type: K,
151
+ optionsOrListener?: OnOptions | boolean | EventListenerLike<EmissionEvent<E[K], K>>,
152
+ ): ObservableLike<EmissionEvent<E[K], K>> | (() => void) {
153
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
154
+ return this.#target.on(type, optionsOrListener as any);
140
155
  }
141
156
 
142
157
  /**
@@ -179,6 +194,104 @@ export abstract class EventEmission<E extends Record<string, any>> {
179
194
  return this.#target.pipe(target, mapFn);
180
195
  }
181
196
 
197
+ // ==========================================================================
198
+ // Node.js EventEmitter Compatibility
199
+ // ==========================================================================
200
+
201
+ /**
202
+ * Emits an event with the given type and detail.
203
+ * Returns true if the event type had registered listeners, false otherwise.
204
+ */
205
+ emit<K extends keyof E & string>(type: K, detail: E[K]): boolean {
206
+ return this.#target.emit(type, detail);
207
+ }
208
+
209
+ /**
210
+ * Removes a listener for the specified event type.
211
+ */
212
+ off<K extends keyof E & string>(
213
+ type: K,
214
+ listener: EventListenerLike<EmissionEvent<E[K], K>>,
215
+ ): void {
216
+ this.#target.off(type, listener);
217
+ }
218
+
219
+ /**
220
+ * Adds a listener for the specified event type.
221
+ * Returns an unsubscribe function.
222
+ */
223
+ addListener<K extends keyof E & string>(
224
+ type: K,
225
+ listener: EventListenerLike<EmissionEvent<E[K], K>>,
226
+ ): () => void {
227
+ return this.#target.addListener(type, listener);
228
+ }
229
+
230
+ /**
231
+ * Removes a listener for the specified event type.
232
+ */
233
+ removeListener<K extends keyof E & string>(
234
+ type: K,
235
+ listener: EventListenerLike<EmissionEvent<E[K], K>>,
236
+ ): void {
237
+ this.#target.removeListener(type, listener);
238
+ }
239
+
240
+ /**
241
+ * Adds a listener at the beginning of the listener list for the specified type.
242
+ * Returns an unsubscribe function.
243
+ */
244
+ prependListener<K extends keyof E & string>(
245
+ type: K,
246
+ listener: EventListenerLike<EmissionEvent<E[K], K>>,
247
+ ): () => void {
248
+ return this.#target.prependListener(type, listener);
249
+ }
250
+
251
+ /**
252
+ * Adds a one-time listener at the beginning of the listener list.
253
+ * Returns an unsubscribe function.
254
+ */
255
+ prependOnceListener<K extends keyof E & string>(
256
+ type: K,
257
+ listener: EventListenerLike<EmissionEvent<E[K], K>>,
258
+ ): () => void {
259
+ return this.#target.prependOnceListener(type, listener);
260
+ }
261
+
262
+ /**
263
+ * Returns an array of listener functions for the specified event type.
264
+ */
265
+ listeners<K extends keyof E & string>(
266
+ type: K,
267
+ ): Array<EventListenerLike<EmissionEvent<E[K], K>>> {
268
+ return this.#target.listeners(type);
269
+ }
270
+
271
+ /**
272
+ * Returns an array of listener functions for the specified event type.
273
+ * Once-listeners return a wrapper with a `.listener` property pointing to the original.
274
+ */
275
+ rawListeners<K extends keyof E & string>(
276
+ type: K,
277
+ ): Array<EventListenerLike<EmissionEvent<E[K], K>>> {
278
+ return this.#target.rawListeners(type);
279
+ }
280
+
281
+ /**
282
+ * Returns the number of listeners for the specified event type.
283
+ */
284
+ listenerCount<K extends keyof E & string>(type: K): number {
285
+ return this.#target.listenerCount(type);
286
+ }
287
+
288
+ /**
289
+ * Returns an array of event type names that have registered listeners.
290
+ */
291
+ eventNames(): Array<string> {
292
+ return this.#target.eventNames();
293
+ }
294
+
182
295
  // ==========================================================================
183
296
  // Wildcard Support
184
297
  // ==========================================================================
package/src/factory.ts CHANGED
@@ -10,6 +10,7 @@ import type {
10
10
  AddEventListenerOptionsLike,
11
11
  AsyncIteratorOptions,
12
12
  EmissionEvent,
13
+ EventListenerLike,
13
14
  EventTargetLike,
14
15
  Listener,
15
16
  ObservableLike,
@@ -207,6 +208,16 @@ export function createEventTarget<T extends object, E extends Record<string, any
207
208
  'subscribe',
208
209
  'toObservable',
209
210
  'events',
211
+ 'emit',
212
+ 'off',
213
+ 'addListener',
214
+ 'removeListener',
215
+ 'prependListener',
216
+ 'prependOnceListener',
217
+ 'listeners',
218
+ 'rawListeners',
219
+ 'listenerCount',
220
+ 'eventNames',
210
221
  ] as const;
211
222
 
212
223
  for (const name of methodNames) {
@@ -544,13 +555,14 @@ function createEventTargetInternal<E extends Record<string, any>>(
544
555
  }
545
556
  };
546
557
 
547
- const addEventListener: EventTargetLike<E>['addEventListener'] = (
548
- type,
549
- listener,
550
- options,
551
- ) => {
558
+ const addListenerInternal = (
559
+ type: string,
560
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Internal helper; callers provide type-safe wrappers
561
+ listener: EventListenerLike<any> | null,
562
+ options: AddEventListenerOptionsLike | boolean | undefined,
563
+ position: 'append' | 'prepend',
564
+ ): (() => void) => {
552
565
  if (isCompleted || !listener) {
553
- // Return no-op unsubscribe if already completed
554
566
  return () => {};
555
567
  }
556
568
 
@@ -589,7 +601,12 @@ function createEventTargetInternal<E extends Record<string, any>>(
589
601
  signal,
590
602
  removed: false,
591
603
  };
592
- list.push(record);
604
+
605
+ if (position === 'prepend') {
606
+ list.unshift(record);
607
+ } else {
608
+ list.push(record);
609
+ }
593
610
 
594
611
  const unsubscribe = () => removeListenerRecord(type, record);
595
612
 
@@ -603,6 +620,14 @@ function createEventTargetInternal<E extends Record<string, any>>(
603
620
  return unsubscribe;
604
621
  };
605
622
 
623
+ const addEventListener: EventTargetLike<E>['addEventListener'] = (
624
+ type,
625
+ listener,
626
+ options,
627
+ ) => {
628
+ return addListenerInternal(type, listener, options, 'append');
629
+ };
630
+
606
631
  const addWildcardListener: EventTargetLike<E>['addWildcardListener'] = (
607
632
  pattern,
608
633
  listener,
@@ -816,11 +841,30 @@ function createEventTargetInternal<E extends Record<string, any>>(
816
841
 
817
842
  // New ergonomics
818
843
 
819
- const on: EventTargetLike<E>['on'] = (type, options) => {
820
- /* eslint-disable @typescript-eslint/no-explicit-any */
844
+ const isListenerArg = (
845
+ arg: unknown,
846
+ ): arg is EventListenerLike<EmissionEvent<unknown>> => {
847
+ if (typeof arg === 'function') return true;
848
+ if (
849
+ arg &&
850
+ typeof arg === 'object' &&
851
+ typeof (arg as { handleEvent?: unknown }).handleEvent === 'function'
852
+ )
853
+ return true;
854
+ return false;
855
+ };
856
+
857
+ /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument */
858
+ const on: EventTargetLike<E>['on'] = ((type: any, optionsOrListener: any) => {
859
+ // Node-style overload: on(type, listener) → () => void
860
+ if (isListenerArg(optionsOrListener)) {
861
+ return addEventListener(type, optionsOrListener);
862
+ }
863
+
864
+ // Observable-style: on(type, options?) → ObservableLike
865
+ const options = optionsOrListener as OnOptions | boolean | undefined;
821
866
  return new Observable<EmissionEvent<any>>(
822
867
  (observer: SubscriptionObserver<EmissionEvent<any>>) => {
823
- /* eslint-enable @typescript-eslint/no-explicit-any */
824
868
  let opts: OnOptions;
825
869
  if (typeof options === 'boolean') {
826
870
  opts = { capture: options };
@@ -850,7 +894,6 @@ function createEventTargetInternal<E extends Record<string, any>>(
850
894
  observer.error(e.detail);
851
895
  };
852
896
 
853
- /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument */
854
897
  const unsubscribeEvent = addEventListener(
855
898
  type as keyof E & string,
856
899
  eventHandler as any,
@@ -865,7 +908,6 @@ function createEventTargetInternal<E extends Record<string, any>>(
865
908
  opts,
866
909
  );
867
910
  }
868
- /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument */
869
911
 
870
912
  return () => {
871
913
  unsubscribeEvent();
@@ -875,7 +917,8 @@ function createEventTargetInternal<E extends Record<string, any>>(
875
917
  };
876
918
  },
877
919
  );
878
- };
920
+ }) as EventTargetLike<E>['on'];
921
+ /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument */
879
922
 
880
923
  const onceMethod: EventTargetLike<E>['once'] = (type, listener, options) => {
881
924
  if (typeof options === 'boolean') {
@@ -1285,6 +1328,89 @@ function createEventTargetInternal<E extends Record<string, any>>(
1285
1328
  return iterator;
1286
1329
  }
1287
1330
 
1331
+ // Node.js EventEmitter compatibility methods
1332
+
1333
+ const emit: EventTargetLike<E>['emit'] = (type, detail) => {
1334
+ if (isCompleted) return false;
1335
+
1336
+ const list = listeners.get(type);
1337
+ const hasListeners = list !== undefined && list.length > 0;
1338
+ let hasWildcard = false;
1339
+ if (wildcardListeners.size > 0) {
1340
+ for (const rec of wildcardListeners) {
1341
+ if (matchesWildcard(type, rec.pattern)) {
1342
+ hasWildcard = true;
1343
+ break;
1344
+ }
1345
+ }
1346
+ }
1347
+
1348
+ dispatchEvent({ type, detail } as Parameters<EventTargetLike<E>['dispatchEvent']>[0]);
1349
+ return hasListeners || hasWildcard;
1350
+ };
1351
+
1352
+ const off: EventTargetLike<E>['off'] = (type, listener) => {
1353
+ removeEventListener(type, listener);
1354
+ };
1355
+
1356
+ const addListener: EventTargetLike<E>['addListener'] = (type, listener) => {
1357
+ return addEventListener(type, listener);
1358
+ };
1359
+
1360
+ const removeListener: EventTargetLike<E>['removeListener'] = (type, listener) => {
1361
+ removeEventListener(type, listener);
1362
+ };
1363
+
1364
+ const prependListener: EventTargetLike<E>['prependListener'] = (type, listener) => {
1365
+ return addListenerInternal(type, listener, undefined, 'prepend');
1366
+ };
1367
+
1368
+ const prependOnceListener: EventTargetLike<E>['prependOnceListener'] = (
1369
+ type,
1370
+ listener,
1371
+ ) => {
1372
+ return addListenerInternal(type, listener, { once: true }, 'prepend');
1373
+ };
1374
+
1375
+ const getListeners: EventTargetLike<E>['listeners'] = (type) => {
1376
+ const list = listeners.get(type);
1377
+ if (!list) return [];
1378
+ return list.filter((r) => !r.removed).map((r) => r.original);
1379
+ };
1380
+
1381
+ const rawListenersMethod: EventTargetLike<E>['rawListeners'] = (type) => {
1382
+ const list = listeners.get(type);
1383
+ if (!list) return [];
1384
+ /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
1385
+ return list
1386
+ .filter((r) => !r.removed)
1387
+ .map((r) => {
1388
+ if (r.once) {
1389
+ const wrapper = ((...args: any[]) => (r.callback as any)(...args)) as any;
1390
+ wrapper.listener = r.original;
1391
+ return wrapper;
1392
+ }
1393
+ return r.original;
1394
+ });
1395
+ /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
1396
+ };
1397
+
1398
+ const listenerCount: EventTargetLike<E>['listenerCount'] = (type) => {
1399
+ const list = listeners.get(type);
1400
+ if (!list) return 0;
1401
+ return list.filter((r) => !r.removed).length;
1402
+ };
1403
+
1404
+ const eventNamesMethod: EventTargetLike<E>['eventNames'] = () => {
1405
+ const names: string[] = [];
1406
+ for (const [type, list] of listeners) {
1407
+ if (list.some((r) => !r.removed)) {
1408
+ names.push(type);
1409
+ }
1410
+ }
1411
+ return names;
1412
+ };
1413
+
1288
1414
  const target: EventTargetLike<E> = {
1289
1415
  addEventListener,
1290
1416
  removeEventListener,
@@ -1303,6 +1429,16 @@ function createEventTargetInternal<E extends Record<string, any>>(
1303
1429
  subscribe,
1304
1430
  toObservable,
1305
1431
  events,
1432
+ emit,
1433
+ off,
1434
+ addListener,
1435
+ removeListener,
1436
+ prependListener,
1437
+ prependOnceListener,
1438
+ listeners: getListeners,
1439
+ rawListeners: rawListenersMethod,
1440
+ listenerCount,
1441
+ eventNames: eventNamesMethod,
1306
1442
  };
1307
1443
 
1308
1444
  // Add Symbol.observable - return an observable that emits all events from all types
@@ -202,6 +202,16 @@ export function fromEventTarget<E extends Record<string, unknown>>(
202
202
  return emitter.completed;
203
203
  },
204
204
  events: emitter.events,
205
+ emit: emitter.emit,
206
+ off: emitter.off,
207
+ addListener: emitter.addListener,
208
+ removeListener: emitter.removeListener,
209
+ prependListener: emitter.prependListener,
210
+ prependOnceListener: emitter.prependOnceListener,
211
+ listeners: emitter.listeners,
212
+ rawListeners: emitter.rawListeners,
213
+ listenerCount: emitter.listenerCount,
214
+ eventNames: emitter.eventNames,
205
215
  destroy: () => {
206
216
  // Clean up abort signal listener to prevent memory leak
207
217
  if (options?.signal && onAbort) {
package/src/types.ts CHANGED
@@ -253,16 +253,53 @@ export interface EventTargetLike<E extends Record<string, any>> {
253
253
  clear: () => void;
254
254
 
255
255
  // Ergonomics
256
- on: <K extends keyof E & string>(
257
- type: K,
258
- options?: OnOptions | boolean,
259
- ) => ObservableLike<EmissionEvent<E[K], K>>;
256
+ on: {
257
+ <K extends keyof E & string>(
258
+ type: K,
259
+ listener: EventListenerLike<EmissionEvent<E[K], K>>,
260
+ ): () => void;
261
+ <K extends keyof E & string>(
262
+ type: K,
263
+ options?: OnOptions | boolean,
264
+ ): ObservableLike<EmissionEvent<E[K], K>>;
265
+ };
260
266
  once: <K extends keyof E & string>(
261
267
  type: K,
262
268
  listener: EventListenerLike<EmissionEvent<E[K], K>> | null,
263
269
  options?: Omit<AddEventListenerOptionsLike, 'once'> | boolean,
264
270
  ) => () => void;
265
271
  removeAllListeners: <K extends keyof E & string>(type?: K) => void;
272
+
273
+ // Node.js EventEmitter compatibility
274
+ emit: <K extends keyof E & string>(type: K, detail: E[K]) => boolean;
275
+ off: <K extends keyof E & string>(
276
+ type: K,
277
+ listener: EventListenerLike<EmissionEvent<E[K], K>>,
278
+ ) => void;
279
+ addListener: <K extends keyof E & string>(
280
+ type: K,
281
+ listener: EventListenerLike<EmissionEvent<E[K], K>>,
282
+ ) => () => void;
283
+ removeListener: <K extends keyof E & string>(
284
+ type: K,
285
+ listener: EventListenerLike<EmissionEvent<E[K], K>>,
286
+ ) => void;
287
+ prependListener: <K extends keyof E & string>(
288
+ type: K,
289
+ listener: EventListenerLike<EmissionEvent<E[K], K>>,
290
+ ) => () => void;
291
+ prependOnceListener: <K extends keyof E & string>(
292
+ type: K,
293
+ listener: EventListenerLike<EmissionEvent<E[K], K>>,
294
+ ) => () => void;
295
+ listeners: <K extends keyof E & string>(
296
+ type: K,
297
+ ) => Array<EventListenerLike<EmissionEvent<E[K], K>>>;
298
+ rawListeners: <K extends keyof E & string>(
299
+ type: K,
300
+ ) => Array<EventListenerLike<EmissionEvent<E[K], K>>>;
301
+ listenerCount: <K extends keyof E & string>(type: K) => number;
302
+ eventNames: () => Array<string>;
266
303
  /**
267
304
  * Pipe events from this emitter to another target.
268
305
  * Forwards all events. If mapFn returns null, the event is skipped.