events-ex

Browser-friendly enhanced event emitter ability and class. It's modified from event-emitter mainly. It can add/inject the event-able ability to your any class.
Features
- Modular Event-able Ability: Inject event capabilities into any class using
eventable(MyClass)without forced inheritance. - Core Event Enhancements:
- Bubbling & Interruption: Full support for event propagation and mid-stream cancellation.
- Listener Ordering: Precise control via the optional
indexparameter inon()andonce(). Supports special values'first'and'last'to ensure listeners stay at the boundaries. - Regex Subscription: Subscribe to multiple events using Regular Expressions.
- Hook-able System: Intercept and modify event behavior at the core level.
- Advanced Asynchronous Features (Specific to
emitAsync):- Configurable Concurrency: Choose between Serial (default) and Parallel execution for async listeners.
- Result Aggregation: Strategies to gather return values:
last(default),first(first success), andcollect(all results). - Fluent API Proxies: Use
.parallel()and.configure()for transient, side-effect-free execution context. - AbortSignal Support: Cancel async event emissions via
configure({ signal })oroncePromisewith anAbortSignal.
- Architecture: Rewritten core for improved performance and flexibility while maintaining broad compatibility.
- Event Utilities: Built-in support for
pipe,pipeAsync,oncePromise,unify,allOff, andhasListeners.
Differences
Difference with node events
broken change: The event supports bubbling and interruption- the
event objectas listener's "this" object:result: If set, the result is returned to theEvent Emitter.stopped: If set totrue, it prevents the remaining listeners from being executed.aborted: (Async only)trueif the emission was cancelled by anAbortSignal.target: TheEvent Emitterobject, which was originally thethisobject.type: triggered event type(name).resolved: (Async only) Indicates if a successful result has been found infirstmode.
broken change: Theemitreturn the result of listeners's callback function instead of the successful state.broken change: Thethisobject of listeners' callback function is theEventObject instead of the emitter object.- The emitter object is put into the
targetproperty of theEventObject.
- The emitter object is put into the
- the
- Enhanced
emitAsyncMethod (Unique to Async):- Sequential (Serial): Executes listeners one-by-one, respecting
this.stopped. - Concurrent (Parallel): Executes all listeners simultaneously.
- Result Strategies:
last: Returns the result of the final listener (or last to finish).first: Returns the first successful non-undefined result (skips errors).collect: Returns an array of all results in registration order.
- Sequential (Serial): Executes listeners one-by-one, respecting
- Fluent Configuration: Use
.parallel()or.configure({...})for one-time customized async emits. - Listener APIs:
on/once(event: string|RegExp, listener, index?: number|'first'|'last')- Index Parameter (Optional): Insertion position in the listener array.
'first'(-Infinity): Stays in the Head zone. The first listener added as'first'is placed at the very front.'last'(Infinity): Stays in the Tail zone. The first listener added as'last'will always remain at the absolute end.number: Relative index within the Body zone.
- Regex Event Matching: Listeners can bind to multiple events via regex patterns.
- Index Parameter (Optional): Insertion position in the listener array.
Difference with event-emitter
broken change: The event supports bubbling and interruption (see above).- Adds the
defaultMaxListenersclass property to keep compatibility with node events. - Adds the
setMaxListenersmethod to keep compatibility with node events. - Adds
error,newListenerandremoveListenerevents to keep compatibility with node events. - Adds
listeners()method to keep compatibility with node events. - Adds
listenerCount()class method to keep compatibility with node events.
- Adds async event emitting via
emitAsyncmethod.
Event Piping & Unification:
pipe(source, target): Sync event forwarding.pipeAsync(source, target, options): Async forwarding with configurable concurrency and aggregation.unify(emitter1, emitter2): Bi-directional synchronization.
Note: A listener's throw error does not break the notification flow—caught errors are collected and then re-emitted as an 'error' event after all listeners run. Exception: If the error is thrown inside an 'error' event listener itself, it propagates directly as a thrown exception (sync) or a rejected promise (async), preventing infinite recursion / stack overflow. This applies to both emit() and emitAsync().
Installation
npm install events-ex
Usage
Extends from EventEmitter class
import {EventEmitter} from 'events-ex';
class MyClass extends EventEmitter {}
Add/Inject the event-able ability to your class directly
import {eventable} from 'events-ex';
class MyClass extends MyRoot {}
// inject the eventable ability to MyClass
eventable(MyClass);
Core Feature: Listener Ordering (Index Parameter)
const ee = new EventEmitter();
ee.on('test', () => console.log('third'));
ee.on('test', () => console.log('first'), 'first'); // Always at the front
ee.on('test', () => console.log('last'), 'last'); // Always at the end
ee.on('test', () => console.log('second'), 1); // Body index 1 (relative to Head)
ee.emit('test');
// Output:
// first
// second
// third
// last
Core Feature: Regex Subscription
const ee = new EventEmitter();
ee.on(/^user\..*/, function(data) {
console.log(`Event ${this.type} triggered with`, data);
});
ee.emit('user.login', { id: 1 });
ee.emit('user.logout', { id: 1 });
Core Feature: Bubbling & Interruption
import {EventEmitter, states} from 'events-ex';
import {isObject} from 'util-ex';
class MyDb extends EventEmitter {
get(key) {
let result = this.emit('getting', key)
if(isObject(result)) {
if (result.state === states.ABORT) return
if (result.state === states.DONE) return result.result
}
return _get(key)
}
}
let db = new MyDb
db.on('getting', function(key){
let result = myGet(key);
if (result != null) {
this.result = { state: states.DONE, result: result }
this.stopped = true // Skip remaining listeners
} else {
this.result = { state: states.ABORT };
}
})
Async-Only Features: Concurrency & Aggregation
These features apply only to the emitAsync method.
const ee = new EventEmitter();
ee.on('task', async () => {
await sleep(100);
return 'result 1';
});
ee.on('task', async () => {
return 'result 2';
});
// 1. Default (Serial): Executes sequentially, returns 'result 2'
const res = await ee.emitAsync('task');
// 2. Parallel + Collect: Executes concurrently, returns ['result 1', 'result 2']
const allResults = await ee.parallel('collect').emitAsync('task');
// 3. Parallel + First: Executes concurrently, returns fastest success ('result 2')
const firstResult = await ee.parallel('first').emitAsync('task');
AbortSignal Support (Async Only)
Cancel async event emissions using the standard AbortSignal via configure({ signal }) or oncePromise options.
const emitter = new EventEmitter();
const controller = new AbortController();
emitter.on('task', async () => {
await sleep(500);
return 'done';
});
// emitAsync: pass signal via configure
setTimeout(() => controller.abort(), 200); // cancel after 200ms
try {
await emitter.configure({ signal: controller.signal }).emitAsync('task');
} catch (err) {
console.log(err.name); // 'AbortError'
}
// oncePromise: pass signal directly via options
const c2 = new AbortController();
setTimeout(() => c2.abort(), 100);
try {
await oncePromise(emitter, 'ready', { signal: c2.signal });
} catch (err) {
console.log(err.name); // 'AbortError'
}
Behavior:
- Serial mode: Checks
signal.abortedbefore each listener, throwsAbortErrorimmediately when triggered. WhenraiseErroris explicitly set (true/false/null), usesPromise.raceto interrupt an actively running listener as well. - Parallel mode: Races listener execution against the signal via
Promise.race. ThrowsAbortErrorwhen the signal wins. - pipeAsync: In serial mode, checks the source's signal before forwarding to each pipe target; skips remaining targets if aborted.
Eventobjects have anabortedfield (independent ofstopped) to track cancellation state.
Advanced Features
Async Concurrency Engine (For emitAsync Only)
| Option | Value | Description |
|---|---|---|
asyncMode |
'serial' |
(Default) Listeners run one by one. Supports this.stopped. |
'parallel' |
Listeners run concurrently. this.stopped is ignored. |
|
resultMode |
'last' |
(Default) Returns the result of the last listener (or last to finish). |
'first' |
Returns the first non-undefined and successful result. Skips errors. | |
'collect' |
Returns an array of all results in registration order. | |
signal |
AbortSignal |
An AbortSignal from an AbortController to cancel async event emission. Only passed via configure(), not stored on the instance. |
raiseError |
true |
Throws all listener errors immediately. Parallel mode aggregates multiple errors into an AggregateError. |
false |
Silently swallows listener errors. Used with signal to interrupt an executing listener in serial mode (added Promise.race). |
|
null |
(Default for sync emit) For 'error' events only: throws if there are no error listeners (Node.js default behavior). |
|
undefined |
(Default) Same as false for emitAsync. Keeps existing behavior unchanged. |
Proxy Isolation (Fluent API)
Calling .parallel() or .configure() returns a transient Proxy Object (Object.create(this)), allowing thread-safe, isolated configurations for specific emits.
Safe Injection (AoP Compatibility) & Name Collisions
When injecting event capabilities into an existing object or prototype via wrapEventEmitter(target) or eventable(MyClass), a minimal set of methods is injected to minimize the risk of name collisions:
on,once,offemit,emitAsyncsetEmitterOptions
Warning on Name Collisions: If your target object already has methods with these names, they will be overwritten.
Solution: Method Renaming
You can use the rename option in eventable to map the emitter methods to custom names on your target:
eventable(MyClass, {
rename: {
emitAsync: 'myEmitAsync',
on: 'addListener'
}
});
// Now use: inst.myEmitAsync('event')
Full EventEmitter vs. Minimal Injection
- Standalone: Calling
ee()ornew EventEmitter()without a target returns a full instance containing all advanced methods (including.parallel(),.configure(),.setMaxListeners(), etc.). - Injected: Passing a target to
ee(target)or usingeventableperforms a minimal injection to preserve the target's original footprint. UsesetEmitterOptionson the target to access advanced async configurations.
API
eventable(class[, options]) (events-ex/eventable)
Add the event-able ability to the class directly.
class: the class to be injected the ability.options(object): optional optionsinclude(string[]|string): only these emitter methods will be added to the classexclude(string[]|string): theses emitter methods would not be added to the classmethods(object): hooked methods to the classemitterOptions(object): default options for the emitter (e.g.,asyncMode,resultMode).rename(object): map the emitter methods to custom names on the class.- key: original method name (e.g., 'on', 'emitAsync').
- value: new method name.
hasListeners(obj[, name]) (events-ex/has-listeners)
import {hasListeners, wrapEventEmitter as ee} from 'events-ex';
var emitter = ee();
var listener = function () {};
hasListeners(emitter); // false
emitter.on('foo', listener);
hasListeners(emitter, 'foo'); // true
pipeAsync(source, target[, name, options]) (events-ex/pipe-async)
Creates an asynchronous pipeline.
options.asyncMode: Propagation mode ('serial' | 'parallel').options.resultMode: Aggregation strategy ('last'default |'first'|'collect').
oncePromise(emitter, type[, options]) (events-ex/once-promise)
Returns a Promise that resolves with the Event object when the specified event is emitted on the given emitter.
If an error event is emitted (and the waiting event is not error), the promise rejects.
If the provided AbortSignal is aborted, the promise rejects with an AbortError.
emitter(EventEmitter): The event emitter to listen on.type(string | RegExp): The event type to wait for. Supports regex for matching multiple events.options(Object): Optional configuration.signal(AbortSignal): An AbortSignal to cancel the wait.raiseError(boolean|null): Controls behavior when an'error'event is emitted on the emitter.true/undefined(default): The promise rejects with the error.false: The promise resolves with the error object instead of rejecting.
- Returns:
Promise<Event>— resolves with the Event object, which providestype,target, etc.
Note: The resolved Event object's
resultfield may not be the final value if other listeners have not yet run. For the definitive emit return value, useemit()oremitAsync()directly.
import {oncePromise, EventEmitter} from 'events-ex';
const ee = new EventEmitter();
// Wait for a data event
setTimeout(() => ee.emit('data', { id: 1 }), 100);
const evt = await oncePromise(ee, 'data');
console.log(evt.type); // 'data'
console.log(evt.target); // the emitter
// Wait for a regex-matched event – evt.type reveals the actual event
setTimeout(() => ee.emit('user.login', { name: 'Alice' }), 100);
const evt2 = await oncePromise(ee, /^user\./);
console.log(evt2.type); // 'user.login' (not the regex)
// Error handling: rejects on error (unless waiting for 'error')
try {
await oncePromise(ee, 'data');
} catch (err) {
console.error('Error occurred:', err);
}
// Waiting for 'error' event resolves normally
ee.emit('error', new Error('expected'));
await oncePromise(ee, 'error'); // resolves, not rejects
// Use AbortSignal for timeout cancellation
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
try {
const evt = await oncePromise(ee, 'response', { signal: controller.signal });
} catch (err) {
if (err.name === 'AbortError') {
console.log('timed out or cancelled');
}
}
setEmitterOptions(options)
Configures instance-wide defaults for asyncMode, resultMode, maxListeners, and raiseError.