events-ex 2.3.0 → 2.4.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.
Files changed (122) hide show
  1. package/.babelrc +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/README.cn.md +114 -3
  4. package/README.md +106 -5
  5. package/coverage/all-off.js.html +133 -0
  6. package/coverage/base.css +224 -0
  7. package/coverage/block-navigation.js +87 -0
  8. package/coverage/clover.xml +572 -0
  9. package/coverage/consts.js.html +154 -0
  10. package/coverage/default-methods.js.html +2068 -0
  11. package/coverage/event-emitter.js.html +121 -0
  12. package/coverage/event.js.html +286 -0
  13. package/coverage/eventable.js.html +355 -0
  14. package/coverage/favicon.png +0 -0
  15. package/coverage/has-listeners.js.html +166 -0
  16. package/coverage/index.html +281 -0
  17. package/coverage/lcov-report/all-off.js.html +133 -0
  18. package/coverage/lcov-report/base.css +224 -0
  19. package/coverage/lcov-report/block-navigation.js +87 -0
  20. package/coverage/lcov-report/consts.js.html +154 -0
  21. package/coverage/lcov-report/default-methods.js.html +2068 -0
  22. package/coverage/lcov-report/event-emitter.js.html +121 -0
  23. package/coverage/lcov-report/event.js.html +286 -0
  24. package/coverage/lcov-report/eventable.js.html +355 -0
  25. package/coverage/lcov-report/favicon.png +0 -0
  26. package/coverage/lcov-report/has-listeners.js.html +166 -0
  27. package/coverage/lcov-report/index.html +281 -0
  28. package/coverage/lcov-report/once-promise.js.html +325 -0
  29. package/coverage/lcov-report/pipe-async.js.html +385 -0
  30. package/coverage/lcov-report/pipe.js.html +244 -0
  31. package/coverage/lcov-report/prettify.css +1 -0
  32. package/coverage/lcov-report/prettify.js +2 -0
  33. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  34. package/coverage/lcov-report/sorter.js +210 -0
  35. package/coverage/lcov-report/unify.js.html +259 -0
  36. package/coverage/lcov-report/wrap-event-emitter.js.html +223 -0
  37. package/coverage/lcov.info +1201 -0
  38. package/coverage/once-promise.js.html +325 -0
  39. package/coverage/pipe-async.js.html +385 -0
  40. package/coverage/pipe.js.html +244 -0
  41. package/coverage/prettify.css +1 -0
  42. package/coverage/prettify.js +2 -0
  43. package/coverage/sort-arrow-sprite.png +0 -0
  44. package/coverage/sorter.js +210 -0
  45. package/coverage/unify.js.html +259 -0
  46. package/coverage/wrap-event-emitter.js.html +223 -0
  47. package/docs/README.md +106 -5
  48. package/docs/all-off/functions/allOff.md +1 -1
  49. package/docs/consts/README.md +4 -0
  50. package/docs/consts/functions/createAbortError.md +18 -0
  51. package/docs/consts/variables/RegExpEventSymbol.md +1 -1
  52. package/docs/consts/variables/states.md +2 -2
  53. package/docs/default-methods/functions/getEventableMethods.md +34 -18
  54. package/docs/event/classes/Event.md +23 -13
  55. package/docs/event-emitter/classes/EventEmitter-1.md +2 -2
  56. package/docs/event-emitter/classes/EventEmitter.md +28 -28
  57. package/docs/eventable/functions/eventable.md +1 -1
  58. package/docs/has-listeners/functions/hasListeners.md +2 -2
  59. package/docs/index/README.md +12 -0
  60. package/docs/modules.md +1 -0
  61. package/docs/once-promise/README.md +17 -0
  62. package/docs/once-promise/functions/oncePromise.md +60 -0
  63. package/docs/pipe/functions/pipe.md +3 -3
  64. package/docs/pipe-async/functions/pipeAsync.md +3 -3
  65. package/docs/unify/functions/unify.md +1 -1
  66. package/docs/util/array-remove/functions/remove.md +1 -1
  67. package/docs/util/object-for-each/functions/forEach.md +1 -1
  68. package/docs/util/string-pad/functions/pad.md +1 -1
  69. package/docs/util/to-int/functions/toInt.md +1 -1
  70. package/docs/util/valid-callable/functions/validCallable.md +1 -1
  71. package/docs/util/valid-object/functions/validObject.md +1 -1
  72. package/docs/wrap-event-emitter/functions/wrapEventEmitter.md +3 -3
  73. package/docs/wrap-event-emitter/variables/methods.md +1 -1
  74. package/lib/all-off.mjs +16 -0
  75. package/lib/consts.d.ts +7 -1
  76. package/lib/consts.js +17 -2
  77. package/lib/consts.mjs +23 -0
  78. package/lib/default-methods.d.ts +12 -4
  79. package/lib/default-methods.js +174 -22
  80. package/lib/default-methods.mjs +661 -0
  81. package/lib/event-emitter.mjs +12 -0
  82. package/lib/event.d.ts +10 -4
  83. package/lib/event.js +6 -0
  84. package/lib/event.mjs +67 -0
  85. package/lib/eventable.d.ts +1 -1
  86. package/lib/eventable.mjs +90 -0
  87. package/lib/has-listeners.d.ts +1 -1
  88. package/lib/has-listeners.mjs +27 -0
  89. package/lib/index.d.ts +1 -0
  90. package/lib/index.js +12 -0
  91. package/lib/index.mjs +16 -0
  92. package/lib/once-promise.d.ts +23 -0
  93. package/lib/once-promise.js +82 -0
  94. package/lib/once-promise.mjs +80 -0
  95. package/lib/pipe-async.d.ts +4 -2
  96. package/lib/pipe-async.js +16 -3
  97. package/lib/pipe-async.mjs +100 -0
  98. package/lib/pipe.d.ts +1 -1
  99. package/lib/pipe.mjs +53 -0
  100. package/lib/unify.js +1 -1
  101. package/lib/unify.mjs +58 -0
  102. package/lib/util/array-remove.mjs +16 -0
  103. package/lib/util/object-for-each.mjs +20 -0
  104. package/lib/util/promise-any.mjs +45 -0
  105. package/lib/util/string-pad.mjs +19 -0
  106. package/lib/util/to-int.mjs +9 -0
  107. package/lib/util/valid-callable.mjs +6 -0
  108. package/lib/util/valid-object.mjs +8 -0
  109. package/lib/wrap-event-emitter.d.ts +1 -1
  110. package/lib/wrap-event-emitter.mjs +46 -0
  111. package/package.json +29 -20
  112. package/scripts/copy-to-mjs.mjs +79 -0
  113. package/src/consts.js +10 -0
  114. package/src/default-methods.js +169 -24
  115. package/src/event.js +6 -0
  116. package/src/index.js +1 -0
  117. package/src/once-promise.js +80 -0
  118. package/src/pipe-async.js +16 -3
  119. package/src/unify.js +1 -1
  120. package/tsconfig.json +2 -1
  121. package/vitest.config.js +61 -0
  122. package/.mocharc.yml +0 -3
@@ -1,6 +1,6 @@
1
1
  import {defineProperty, isArray, isFunction, isNumber, isObject, isRegExp as _isRegExp, isUndefined, isRegExpStr, toRegExp } from 'util-ex'
2
2
  import './util/promise-any'
3
- import {RegExpEventSymbol} from './consts'
3
+ import {RegExpEventSymbol, createAbortError} from './consts'
4
4
  import {Event} from './event';
5
5
 
6
6
  const create = Object.create
@@ -19,6 +19,12 @@ export function getEventableMethods(aClass) {
19
19
  * @param {Object} options - Configuration options for event emission.
20
20
  * @param {string} [options.asyncMode='serial'] - The mode of asynchronous emission ('serial' or 'parallel').
21
21
  * @param {string} [options.resultMode='last'] - The strategy for handling multiple return values ('last', 'first', 'collect').
22
+ * @param {AbortSignal} [options.signal] - An AbortSignal to cancel async event emission.
23
+ * @param {boolean|null} [options.raiseError] - Controls error handling behavior:
24
+ * - `true`: Always throw listener errors immediately.
25
+ * - `false`: Silently swallow listener errors.
26
+ * - `null`: Throw only for 'error' events with no error listeners (Node.js default).
27
+ * - `undefined` (default): Same as `false` for emitAsync.
22
28
  * @returns {import('./event-emitter').EventEmitter} A proxy object representing the configured EventEmitter.
23
29
  */
24
30
  configure(options) {
@@ -39,7 +45,7 @@ export function getEventableMethods(aClass) {
39
45
 
40
46
  /**
41
47
  * Sets the configuration options for the EventEmitter instance.
42
- * @param {Object} options - Configuration options for the emitter (e.g., asyncMode, resultMode, maxListeners).
48
+ * @param {Object} options - Configuration options for the emitter (e.g., asyncMode, resultMode, maxListeners, raiseError).
43
49
  * @returns {import('./event-emitter').EventEmitter} The EventEmitter instance for chaining.
44
50
  */
45
51
  setEmitterOptions(options) {
@@ -62,7 +68,7 @@ export function getEventableMethods(aClass) {
62
68
  * Adds a listener function to the specified event type.
63
69
  * @param {string|RegExp} type - The event type to listen for.
64
70
  * @param {Function} listener - The listener function to be called when the event is emitted.
65
- * @param {number|'first'|'last'} [index] - The index at which to insert the listener.
71
+ * @param {number|'first'|'last'} [index] - The index at which to insert the listener.
66
72
  * - 'first' or -Infinity: Adds to the "Head" zone. The first listener added as 'first' is placed at the very front.
67
73
  * - 'last' or Infinity: Adds to the "Tail" zone. The first listener added as 'last' will always be the very last one to execute.
68
74
  * - number: Inserts at the specified index within the "Body" (normal) zone.
@@ -88,8 +94,8 @@ export function getEventableMethods(aClass) {
88
94
  data = data[RegExpEventSymbol] || (data[RegExpEventSymbol] = create(null))
89
95
  }
90
96
 
91
- const isFirst = index === 'first' || index === -Infinity
92
- const isLast = index === 'last' || index === Infinity
97
+ const isFirst = index === -Infinity || index === 'first'
98
+ const isLast = index === Infinity || index === 'last'
93
99
 
94
100
  if (!data[type]) {
95
101
  if (isFirst || isLast || typeof index === 'number') {
@@ -158,11 +164,14 @@ export function getEventableMethods(aClass) {
158
164
  let fired = false
159
165
  const self = this
160
166
 
161
- async function _once() {
167
+ function _once() {
162
168
  self.off(type, _once)
163
169
  if (!fired) {
164
170
  fired = true
165
- await listener.apply(this, arguments)
171
+ const result = listener.apply(this, arguments)
172
+ if (result instanceof Promise) {
173
+ return result
174
+ }
166
175
  }
167
176
  }
168
177
  _once.listener = listener
@@ -183,6 +192,8 @@ export function getEventableMethods(aClass) {
183
192
  const listeners = r.listeners
184
193
  const evt = Event(this, r.type)
185
194
  const errs = []
195
+ const opts = _getOptions(this)
196
+ let _throwErr
186
197
  try {
187
198
  let i = 0
188
199
  let listener
@@ -191,6 +202,7 @@ export function getEventableMethods(aClass) {
191
202
  _notify(listener, evt, args)
192
203
  if (evt.stopped) {break}
193
204
  } catch(err) {
205
+ if (opts.raiseError === true) {throw err}
194
206
  errs.push({err: err, listener: listener})
195
207
  }
196
208
  ++i
@@ -198,13 +210,18 @@ export function getEventableMethods(aClass) {
198
210
  if (errs.length) {
199
211
  for (let i=0;i<errs.length;i++) {
200
212
  const it = errs[i]
213
+ if (r.type === 'error') {throw it.err}
201
214
  this.emit('error', it.err, 'notify', r.type, it.listener, args)
202
215
  }
203
216
  }
204
217
  } finally {
205
- // eslint-disable-next-line no-unsafe-finally
206
- return evt.end()
218
+ if (r.type === 'error' && opts.raiseError === null) {
219
+ const err = args[0]
220
+ _throwErr = err instanceof Error ? err : new Error(UnCAUGHT_ERR)
221
+ }
207
222
  }
223
+ if (_throwErr) throw _throwErr
224
+ return evt.end()
208
225
  },
209
226
 
210
227
  /**
@@ -213,18 +230,35 @@ export function getEventableMethods(aClass) {
213
230
  * @returns {Promise<*>} A promise that resolves with the result of the event.
214
231
  */
215
232
  async emitAsync(/* type, msg , ... */) {
233
+ const options = Object.assign({}, this._emitterOptions, this._eeRuntimeOptions)
234
+ if (options.signal && options.signal.aborted) {
235
+ throw createAbortError()
236
+ }
216
237
  const r = _emit.apply(this, arguments)
217
238
  if (!r) {return}
218
239
  const args = r.args
219
240
  const listeners = r.listeners
220
241
  const evt = Event(this, r.type)
221
- const options = Object.assign({}, this._emitterOptions, this._eeRuntimeOptions)
242
+ let _throwErr
222
243
  try {
223
244
  await _executeAsync.call(this, listeners, evt, args, options)
224
- } finally {
225
- // eslint-disable-next-line no-unsafe-finally
245
+ } catch (err) {
246
+ if (err && err.name === 'AbortError') throw err
247
+ if (options.raiseError === true) throw err
248
+ if (r.type === 'error') {
249
+ Object.defineProperty(err, '_errorInError', { value: true, enumerable: false })
250
+ throw err
251
+ }
252
+ if (err && err._errorInError) throw err
253
+ // Other unexpected errors: still return the event result
226
254
  return evt.end()
227
255
  }
256
+ if (r.type === 'error' && options.raiseError === null) {
257
+ const err = args[0]
258
+ _throwErr = err instanceof Error ? err : new Error(UnCAUGHT_ERR)
259
+ }
260
+ if (_throwErr) throw _throwErr
261
+ return evt.end()
228
262
  },
229
263
 
230
264
  setMaxListeners(n) {
@@ -386,6 +420,16 @@ export function getEventableMethods(aClass) {
386
420
 
387
421
  export default getEventableMethods
388
422
 
423
+ /**
424
+ * Merges instance-level emitter options with per-call runtime options.
425
+ * Runtime options (set via configure()) take precedence.
426
+ * @param {Object} self - The emitter instance.
427
+ * @returns {Object} Merged options.
428
+ */
429
+ function _getOptions(self) {
430
+ return Object.assign({}, self._emitterOptions, self._eeRuntimeOptions)
431
+ }
432
+
389
433
  function _emit(type, msg) {
390
434
  const data = this._events
391
435
  let listeners
@@ -399,8 +443,24 @@ function _emit(type, msg) {
399
443
  args.push(msg)
400
444
  }
401
445
  }
446
+ if (type === 'error') {
447
+ const opts = _getOptions(this)
448
+ // raiseError: true → always throw, bypass listener dispatch
449
+ if (opts.raiseError === true) {
450
+ /* v8 ignore next — msg is guaranteed truthy here; falsy case is caught by earlier !msg check */
451
+ if (!(msg instanceof Error)) {msg = new Error(msg ? UnCAUGHT_ERR + msg : UnCAUGHT_ERR)}
452
+ throw msg
453
+ }
454
+ }
402
455
  // If there is no 'error' event listener then throw.
403
456
  if (!listeners && type === 'error') {
457
+ const opts = _getOptions(this)
458
+ // raiseError: false → silently return, never throw
459
+ if (opts.raiseError === false) {
460
+ return
461
+ }
462
+ // raiseError: undefined / null → Node.js default behavior
463
+ /* v8 ignore next — msg is guaranteed truthy here; falsy case is caught by earlier !msg check */
404
464
  if (!(msg instanceof Error)) {msg = new Error(msg ? UnCAUGHT_ERR + msg : UnCAUGHT_ERR)}
405
465
  throw msg
406
466
  }
@@ -450,9 +510,54 @@ function _notify(listener, evt, args) {
450
510
  return result
451
511
  }
452
512
 
513
+ /**
514
+ * Creates a promise that rejects with AbortError when the given AbortSignal fires.
515
+ * Sets evt.aborted = true before rejecting.
516
+ * @param {AbortSignal} signal - The abort signal to listen on.
517
+ * @param {import('./event').Event} evt - The event object to mark as aborted.
518
+ * @returns {Promise<never>} A promise that rejects with AbortError on abort.
519
+ */
520
+ function _createAbortPromise(signal, evt) {
521
+ return new Promise((_, reject) => {
522
+ if (signal.aborted) {
523
+ evt.aborted = true
524
+ reject(createAbortError())
525
+ return
526
+ }
527
+ const onAbort = () => {
528
+ evt.aborted = true
529
+ reject(createAbortError())
530
+ }
531
+ signal.addEventListener('abort', onAbort, { once: true })
532
+ })
533
+ }
534
+
535
+ /**
536
+ * Executes all listener promises in parallel and assigns the result to evt.
537
+ * @param {Promise[]} promises - The listener promises to execute.
538
+ * @param {import('./event').Event} evt - The event object.
539
+ * @param {string} resultMode - 'last', 'first', or 'collect'.
540
+ */
541
+ async function _runParallelListeners(promises, evt, resultMode) {
542
+ if (resultMode === 'collect') {
543
+ const wrapped = promises.map(p => p.catch(() => undefined))
544
+ const results = await Promise.all(wrapped)
545
+ evt.result = results
546
+ } else if (resultMode === 'first') {
547
+ try {
548
+ await Promise.any(promises.map(p => p.then(res => res === undefined ? Promise.reject() : res)))
549
+ } catch (e) {
550
+ // If all rejected or returned undefined, ignore
551
+ }
552
+ } else {
553
+ await Promise.all(promises.map(p => p.catch(() => undefined)))
554
+ }
555
+ }
556
+
453
557
  async function _executeAsync(listeners, evt, args, options) {
454
558
  const asyncMode = options.asyncMode || 'serial'
455
559
  const resultMode = options.resultMode || 'last'
560
+ const signal = options.signal
456
561
  const errs = []
457
562
 
458
563
  if (resultMode === 'collect') {
@@ -472,6 +577,7 @@ async function _executeAsync(listeners, evt, args, options) {
472
577
  }
473
578
  return result
474
579
  } catch (err) {
580
+ if (options.raiseError === true) {throw err}
475
581
  errs.push({err, listener})
476
582
  throw err
477
583
  }
@@ -479,27 +585,54 @@ async function _executeAsync(listeners, evt, args, options) {
479
585
 
480
586
  if (asyncMode === 'parallel') {
481
587
  const promises = listeners.map(listener => notifyListener(listener))
482
- if (resultMode === 'collect') {
483
- evt.result = await Promise.all(promises.map(p => p.catch(() => undefined)))
484
- } else if (resultMode === 'first') {
485
- try {
486
- await Promise.any(promises.map(p => p.then(res => res === undefined ? Promise.reject() : res)))
487
- } catch (e) {
488
- // If all rejected or returned undefined, ignore
588
+
589
+ if (options.raiseError === true) {
590
+ // Collect all rejections into AggregateError (all parallel listeners start simultaneously)
591
+ const settled = signal
592
+ ? await Promise.race([
593
+ Promise.allSettled(promises),
594
+ _createAbortPromise(signal, evt)
595
+ ])
596
+ : await Promise.allSettled(promises)
597
+ const rejections = settled.filter(r => r.status === 'rejected').map(r => r.reason)
598
+ if (rejections.length > 0) {
599
+ throw rejections.length === 1 ? rejections[0] : new AggregateError(rejections)
489
600
  }
490
601
  } else {
491
- await Promise.all(promises.map(p => p.catch(() => undefined)))
602
+ const runParallel = () => _runParallelListeners(promises, evt, resultMode)
603
+
604
+ if (signal) {
605
+ await Promise.race([runParallel(), _createAbortPromise(signal, evt)])
606
+ } else {
607
+ await runParallel()
608
+ }
492
609
  }
493
610
  } else {
494
- // Serial mode (default)
611
+ // Serial mode
612
+ // Only race against abort signal when raiseError is explicitly set;
613
+ // default (undefined) keeps old behavior: check signal between listeners only.
614
+ const canRaceAbort = signal && 'raiseError' in options
495
615
  for (const listener of listeners) {
616
+ // Check AbortSignal before firing the next listener
617
+ if (signal && signal.aborted) {
618
+ evt.aborted = true
619
+ break
620
+ }
621
+
496
622
  try {
497
- const result = await notifyListener(listener)
623
+ const result = canRaceAbort
624
+ ? await Promise.race([notifyListener(listener), _createAbortPromise(signal, evt)])
625
+ : await notifyListener(listener)
498
626
  if (resultMode === 'collect') {
499
627
  evt.result.push(result)
500
628
  }
501
629
  if (evt.stopped || (resultMode === 'first' && evt.resolved)) break
502
630
  } catch (err) {
631
+ if (canRaceAbort && err && err.name === 'AbortError') {
632
+ evt.aborted = true
633
+ throw err
634
+ }
635
+ if (options.raiseError === true) throw err
503
636
  if (resultMode === 'collect') {
504
637
  evt.result.push(undefined)
505
638
  }
@@ -507,10 +640,22 @@ async function _executeAsync(listeners, evt, args, options) {
507
640
  }
508
641
  }
509
642
 
510
- if (errs.length) {
643
+ if (errs.length) {
511
644
  for (let i = 0; i < errs.length; i++) {
512
645
  const it = errs[i]
513
- this.emit('error', it.err, 'notify', evt.type, it.listener, args)
646
+ if (evt.type === 'error') {throw it.err}
647
+ // Temporarily clear signal so error re-emission is not blocked by an already-aborted signal
648
+ const savedSignal = this._eeRuntimeOptions && this._eeRuntimeOptions.signal
649
+ if (savedSignal) { delete this._eeRuntimeOptions.signal }
650
+ try {
651
+ await this.emitAsync('error', it.err, 'notify', evt.type, it.listener, args)
652
+ } finally {
653
+ if (savedSignal) { this._eeRuntimeOptions.signal = savedSignal }
654
+ }
514
655
  }
515
656
  }
657
+
658
+ if (evt.aborted) {
659
+ throw createAbortError()
660
+ }
516
661
  }
package/src/event.js CHANGED
@@ -30,6 +30,12 @@ Event.prototype.init = function(target, type) {
30
30
  * @public
31
31
  */
32
32
  this.stopped = false
33
+ /**
34
+ * Whether the event emission was aborted via AbortSignal.
35
+ * @type {boolean}
36
+ * @public
37
+ */
38
+ this.aborted = false
33
39
  /**
34
40
  * Whether a result has been resolved (for 'first' result mode)
35
41
  * @type {boolean}
package/src/index.js CHANGED
@@ -9,6 +9,7 @@ export * from './event-emitter'
9
9
  export * from './has-listeners'
10
10
  export * from './pipe'
11
11
  export * from './pipe-async'
12
+ export * from './once-promise'
12
13
  export * from './unify'
13
14
  export * from './wrap-event-emitter'
14
15
 
@@ -0,0 +1,80 @@
1
+ // @sourceType: module
2
+ import validObject from './util/valid-object'
3
+ import {createAbortError} from './consts'
4
+
5
+ /**
6
+ * Returns a Promise that resolves with the Event object when the specified event is emitted on the given emitter.
7
+ * If an 'error' event is emitted (and the waiting event is not 'error'), the promise rejects by default.
8
+ * If the provided AbortSignal is aborted, the promise rejects with an AbortError.
9
+ *
10
+ * Note: The resolved Event object's `result` field may not be the final value if other listeners have not yet run.
11
+ * For the definitive emit return value, use `emit()` or `emitAsync()` directly.
12
+ *
13
+ * @param {import('./event-emitter').EventEmitter} emitter - The event emitter to listen on.
14
+ * @param {string|RegExp} type - The event type to wait for.
15
+ * @param {Object} [options] - Optional configuration.
16
+ * @param {AbortSignal} [options.signal] - An AbortSignal to cancel the wait.
17
+ * @param {boolean|null} [options.raiseError] - Controls behavior when an 'error' event is emitted:
18
+ * - `true` / `undefined` (default): The promise rejects with the error.
19
+ * - `false`: The promise resolves with the error object instead of rejecting.
20
+ * @returns {Promise<import('./event').Event>} - A promise that resolves with the Event object.
21
+ * @throws {TypeError} - If emitter is not a valid event emitter object.
22
+ */
23
+ export function oncePromise(emitter, type, options) {
24
+ validObject(emitter)
25
+
26
+ const signal = options && options.signal
27
+ const raiseError = options && options.raiseError
28
+
29
+ return new Promise((resolve, reject) => {
30
+ let onAbort = null
31
+ let aborted = false
32
+
33
+ const cleanup = () => {
34
+ emitter.off(type, onEvent)
35
+ if (type !== 'error') {
36
+ emitter.off('error', onError)
37
+ }
38
+ if (signal && onAbort) {
39
+ signal.removeEventListener('abort', onAbort)
40
+ }
41
+ }
42
+
43
+ if (signal) {
44
+ if (signal.aborted) {
45
+ reject(createAbortError())
46
+ return
47
+ }
48
+ onAbort = () => {
49
+ /* v8 ignore next — defensive guard, { once: true } ensures callback fires at most once */
50
+ if (!aborted) {
51
+ aborted = true
52
+ cleanup()
53
+ reject(createAbortError())
54
+ }
55
+ }
56
+ signal.addEventListener('abort', onAbort, { once: true })
57
+ }
58
+
59
+ function onEvent() {
60
+ cleanup()
61
+ resolve(this) // resolve with the Event object
62
+ }
63
+
64
+ function onError(err) {
65
+ cleanup()
66
+ if (raiseError === false) {
67
+ resolve(err) // resolve with the error instead of rejecting
68
+ } else {
69
+ reject(err) // true | null | undefined → reject
70
+ }
71
+ }
72
+
73
+ emitter.on(type, onEvent)
74
+ if (type !== 'error') {
75
+ emitter.on('error', onError)
76
+ }
77
+ })
78
+ }
79
+
80
+ export default oncePromise
package/src/pipe-async.js CHANGED
@@ -17,7 +17,9 @@ const emit = methods.emitAsync
17
17
  * @param {string} [name='emitAsync'] - The name of the event to pipe (defaults to 'emitAsync').
18
18
  * @param {Object} [options] - Configuration for the pipeline.
19
19
  * @param {string} [options.asyncMode='serial'] - The mode of propagation ('serial' or 'parallel').
20
- * @param {string} [options.resultMode] - Strategy for aggregating results from the pipe chain ('collect', 'first').
20
+ * @param {string} [options.resultMode='last'] - Strategy for aggregating results from the pipe chain ('last', 'first', 'collect').
21
+ * @param {AbortSignal} [options.signal] - An AbortSignal to cancel forwarding to remaining pipe targets (serial mode only).
22
+ * Can also be set on the source emitter via `configure({ signal })`.
21
23
  * @returns {Object} - An object with a `close` method that removes the pipeline between the two event emitters.
22
24
  * @throws {TypeError} - If either of the arguments is not an event emitter object.
23
25
  */
@@ -59,6 +61,9 @@ export function pipeAsync(e1, e2/* , name, options */) {
59
61
  return fn.apply(target, args)
60
62
  }
61
63
 
64
+ // Read signal from source emitter's runtime options, if configured
65
+ const signal = this._eeRuntimeOptions && this._eeRuntimeOptions.signal
66
+
62
67
  if (asyncMode === 'parallel') {
63
68
  const promises = [emit.apply(this, arguments)]
64
69
  for (let i = 0; i < data.length; ++i) {
@@ -67,17 +72,25 @@ export function pipeAsync(e1, e2/* , name, options */) {
67
72
  const results = await Promise.all(promises)
68
73
  if (resultMode === 'collect') return results
69
74
  if (resultMode === 'first') return results.find(r => r !== undefined)
70
- return results[0] // Default: return main emitter's result
75
+ // Default: 'last' — return last non-undefined result in the pipe chain
76
+ for (let i = results.length - 1; i >= 0; i--) {
77
+ if (results[i] !== undefined) return results[i]
78
+ }
71
79
  } else {
72
80
  const mainResult = await emit.apply(this, arguments)
73
81
  const allResults = [mainResult]
74
82
  for (let i = 0; i < data.length; ++i) {
83
+ // Check signal before forwarding to each pipe target in serial mode
84
+ if (signal && signal.aborted) break
75
85
  const res = await forward(data[i], arguments)
76
86
  allResults.push(res)
77
87
  }
78
88
  if (resultMode === 'collect') return allResults
79
89
  if (resultMode === 'first') return allResults.find(r => r !== undefined)
80
- return mainResult // Default: return main emitter's result
90
+ // Default: 'last' — return last non-undefined result in the pipe chain
91
+ for (let i = allResults.length - 1; i >= 0; i--) {
92
+ if (allResults[i] !== undefined) return allResults[i]
93
+ }
81
94
  }
82
95
  }
83
96
  defineProperty(e1, name, desc.value, desc)
package/src/unify.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import validObject from './util/valid-object'
2
2
  import forEach from './util/object-for-each'
3
3
 
4
- const push = Array.prototype.apply
4
+ const push = Array.prototype.push
5
5
  const defineProperty = Object.defineProperty
6
6
  const create = Object.create
7
7
  const hasOwnProperty = Object.prototype.hasOwnProperty
package/tsconfig.json CHANGED
@@ -17,7 +17,8 @@
17
17
  // Types should go into this directory.
18
18
  // Removing this would place the .d.ts files
19
19
  // next to the .js files
20
- "outDir": "types"
20
+ "outDir": "lib",
21
+ "rootDir": "./src"
21
22
  },
22
23
  "typedocOptions": {
23
24
  "entryPointStrategy": "expand",
@@ -0,0 +1,61 @@
1
+ import { defineConfig } from 'vitest/config'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+
8
+ // util-ex has an exports field that only exposes "." and blocks deep
9
+ // imports like 'util-ex/lib/defineProperty'. Dependencies like
10
+ // custom-ability also import many util-ex/lib/ subpaths via bare
11
+ // specifiers (e.g., 'util-ex/lib/is/type/array').
12
+ //
13
+ // Vite's resolver strictly enforces the exports field, so we need
14
+ // to intercept resolution at a low level. We do this by having a
15
+ // Vite plugin that patches the util-ex package.json at config
16
+ // resolution time, adding wildcard exports for the lib directory.
17
+
18
+ export default defineConfig({
19
+ plugins: [
20
+ {
21
+ name: 'fix-util-ex-resolution',
22
+ configResolved() {
23
+ const pkgPath = path.resolve(__dirname, 'node_modules/util-ex/package.json')
24
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
25
+ if (!pkg._patched) {
26
+ pkg.exports = {
27
+ '.': pkg.exports['.'],
28
+ './lib/*': {
29
+ import: './lib/*.mjs',
30
+ require: './lib/*.js',
31
+ },
32
+ }
33
+ pkg._patched = true
34
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
35
+ }
36
+ },
37
+ },
38
+ ],
39
+ test: {
40
+ globals: true,
41
+ include: ['test/**/*-test.js'],
42
+ reporters: ['verbose'],
43
+ coverage: {
44
+ provider: 'v8',
45
+ reportsDirectory: './coverage',
46
+ reporter: ['text', 'html', 'lcov', 'clover'],
47
+ include: ['src/**'],
48
+ exclude: [
49
+ 'src/index.js',
50
+ 'src/event-emitter.d.ts',
51
+ 'src/util/**',
52
+ ],
53
+ thresholds: {
54
+ statements: 80,
55
+ branches: 70,
56
+ functions: 80,
57
+ lines: 80,
58
+ },
59
+ },
60
+ },
61
+ })
package/.mocharc.yml DELETED
@@ -1,3 +0,0 @@
1
- require: '@babel/register'
2
- spec:
3
- - 'test/**/*-test.js' # the positional arguments!