asljs-eventful 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Alex Netkachov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # eventful
2
+
3
+ > Part of [Alexantrite Software Library][#1] - a set of high-quality and
4
+ performant JavaScript libraries for everyday use.
5
+
6
+ Lightweight event helper adding on/off/emit to any object.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install asljs-eventful
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ### Basic Example
17
+
18
+ ```js
19
+ import { eventful } from 'asljs-eventful';
20
+
21
+ const obj = eventful({ name: 'Alice' });
22
+ obj.on('greet', msg => console.log(`${msg}, ${obj.name}!`));
23
+ obj.emit('greet', 'Hello'); // Logs: "Hello, Alice!"
24
+ ```
25
+
26
+ ### Advanced Options
27
+
28
+ Trace event invocations to console:
29
+
30
+ ```js
31
+ const obj =
32
+ eventful(
33
+ { },
34
+ { trace: (object, action, payload) =>
35
+ console.log(
36
+ `Action: ${action}`,
37
+ payload) });
38
+
39
+ // Tracing actions include:
40
+ // - 'new' on creation: payload { object }
41
+ // - 'on' when subscribing: payload { event, listener }
42
+ // - 'off' when unsubscribing: payload { event, listener }
43
+ // - 'emit' for sync emit: payload { event, args, listeners }
44
+ // - 'emitAsync' for async emit: payload { event, args, listeners }
45
+ ```
46
+
47
+ Custom error handler for listener errors:
48
+
49
+ ```js
50
+ const obj =
51
+ eventful(
52
+ { },
53
+ { error: (err, { object, event, listener }) =>
54
+ console.error(
55
+ `Error in listener for event "${event}"`,
56
+ err) });
57
+ ```
58
+
59
+ Strict mode to propagate listener errors:
60
+
61
+ ```js
62
+ const obj =
63
+ eventful(
64
+ { },
65
+ { strict: true });
66
+ ```
67
+
68
+ Change the error handler or trace function globally:
69
+
70
+ ```js
71
+ eventful.options.trace =
72
+ (object, action, payload) =>
73
+ console.log(
74
+ `Action: ${action}`,
75
+ payload);
76
+
77
+ eventful.options.error =
78
+ (err, { object, event, listener }) =>
79
+ console.error(
80
+ `Error in listener for event "${event}"`,
81
+ err);
82
+ ```
83
+
84
+ ## API
85
+
86
+ ### eventful([target], [options])
87
+
88
+ Wraps the `target` object with event capabilities. If no target is provided, a new empty object is created.
89
+
90
+ - `target` (Object): The object to be enhanced with event capabilities.
91
+ - `options` (Object): Configuration options.
92
+ - `error` (Function): Custom error handler for listener errors `(err, { object, event, listener })`.
93
+ - `trace` (Function): Custom trace hook `(object, action, payload)` for `new`, `on`, `off`, `emit`, `emitAsync`.
94
+ - `strict` (Boolean): If true, propagates listener errors; otherwise they are isolated. Defaults to false.
95
+
96
+ ### on(event, listener)
97
+
98
+ Registers a listener for the specified event.
99
+
100
+ - `event` (String): The event name.
101
+ - `listener` (Function): The callback function to be invoked when the event is emitted.
102
+
103
+ Returns a function to remove the listener.
104
+
105
+ ### once(event, listener)
106
+
107
+ Registers a one-time listener for the specified event. The listener is removed after its first invocation.
108
+
109
+ - `event` (String): The event name.
110
+ - `listener` (Function): The callback function to be invoked when the event is emitted.
111
+
112
+ Returns a function to remove the listener.
113
+
114
+ ### off(event, listener)
115
+
116
+ Removes a listener for the specified event.
117
+
118
+ - `event` (String): The event name.
119
+ - `listener` (Function): The callback function to be removed.
120
+
121
+ ### emit(event, ...args)
122
+
123
+ Emits the specified event, invoking all registered listeners with the provided arguments.
124
+
125
+ - `event` (String): The event name.
126
+ - `...args` (Any): Arguments to pass to the listeners.
127
+
128
+ ### emitAsync(event, ...args)
129
+
130
+ Emits the specified event asynchronously, running listeners in parallel. In non-strict mode, all listeners run and rejections are isolated; in strict mode, the first rejection causes the returned promise to reject.
131
+
132
+ - `event` (String): The event name.
133
+ - `...args` (Any): Arguments to pass to the listeners.
134
+
135
+ Returns a Promise that resolves when all listeners have been invoked.
136
+
137
+ ### has(event)
138
+
139
+ Checks if there are any listeners registered for the specified event.
140
+
141
+ - `event` (String): The event name.
142
+
143
+ Returns `true` if there are listeners, otherwise `false`.
144
+
145
+ ## License
146
+
147
+ MIT License. See [LICENSE](LICENSE) for details.
148
+
149
+ [#1]: https://github.com/AlexandriteSoftware/asljs
package/eventful.d.ts ADDED
@@ -0,0 +1,106 @@
1
+
2
+ export type Listener<Args extends any[] = any[]> =
3
+ (...args: Args) => any;
4
+
5
+ export type EventMap =
6
+ Record<string | symbol, any[]>;
7
+
8
+ export interface Eventful<E extends EventMap =
9
+ Record<string | symbol, any[]>> {
10
+ /** Subscribe to an event. Returns an unsubscribe function. */
11
+ on<K extends keyof E>(
12
+ event: K,
13
+ listener: Listener<E[K]>
14
+ ): () => boolean;
15
+
16
+ /** Subscribe once to an event. Returns an unsubscribe function (called automatically). */
17
+ once<K extends keyof E>(
18
+ event: K,
19
+ listener: Listener<E[K]>
20
+ ): () => boolean;
21
+
22
+ /** Unsubscribe a previously registered listener. */
23
+ off<K extends keyof E>(
24
+ event: K,
25
+ listener: Listener<E[K]>
26
+ ): boolean;
27
+
28
+ /** Emit an event synchronously. */
29
+ emit<K extends keyof E>(
30
+ event: K,
31
+ ...args: E[K]
32
+ ): void;
33
+
34
+ /** Emit an event and wait for all listeners (run in parallel, errors isolated unless strict). */
35
+ emitAsync<K extends keyof E>(
36
+ event: K,
37
+ ...args: E[K]
38
+ ): Promise<void>;
39
+
40
+ /** Returns true if there is at least one listener for the event. */
41
+ has<K extends keyof E>(
42
+ event: K
43
+ ): boolean;
44
+ }
45
+
46
+
47
+ export interface ErrorContext {
48
+ object: object | Function;
49
+ event: string | symbol;
50
+ listener: Function;
51
+ }
52
+
53
+ type TracePayload =
54
+ | {
55
+ event: string | symbol;
56
+ args: any[];
57
+ listeners?: Function[];
58
+ }
59
+ | {
60
+ object: object | Function;
61
+ };
62
+
63
+ export interface EventfulOptions {
64
+ /** If true, exceptions from listeners are propagated. Otherwise they are swallowed after calling `error`. */
65
+ strict?: boolean;
66
+
67
+ /** Optional tracing hook; defaults to `eventful.trace`. */
68
+ trace?: ((
69
+ object: object | Function,
70
+ action: string,
71
+ payload: TracePayload
72
+ ) => void) | null;
73
+
74
+ /** Optional error hook; defaults to `eventful.error`. */
75
+ error?: ((
76
+ err: unknown,
77
+ context: ErrorContext
78
+ ) => void) | null;
79
+ }
80
+
81
+
82
+ export interface EventfulFactory {
83
+ <T extends object | Function | undefined,
84
+ E extends EventMap = Record<string | symbol, any[]>>(
85
+ object?: T,
86
+ options?: EventfulOptions
87
+ ): (T extends undefined ? {} : T) & Eventful<E>;
88
+
89
+ /** Global hooks you can override. */
90
+ options: {
91
+ /** Default tracer. Replace to integrate with your logger. */
92
+ trace: (
93
+ object: object | Function,
94
+ action: string,
95
+ payload: TracePayload
96
+ ) => void | null;
97
+
98
+ /** Default error handler. Replace to integrate with your logger. */
99
+ error: (
100
+ err: unknown,
101
+ context: ErrorContext
102
+ ) => void | null;
103
+ };
104
+ }
105
+
106
+ export const eventful: EventfulFactory;
package/eventful.js ADDED
@@ -0,0 +1,370 @@
1
+ /**
2
+ * @typedef {Object} ErrorContext
3
+ * @property {Object|Function} object The object emitting the event.
4
+ * @property {string|symbol} event The event name.
5
+ * @property {Function} listener The listener function that caused the error.
6
+ */
7
+
8
+ /**
9
+ * @typedef {Object} EventfulOptions
10
+ * @property {boolean} [strict] If true, exceptions from listeners are propagated.
11
+ * @property {(object: Object|Function, action: string, payload: any) => void} [trace] Optional tracing hook.
12
+ * @property {(err: any, context: ErrorContext) => void} [error] Optional error hook.
13
+ */
14
+
15
+ /**
16
+ * A mixin that adds event emitter capabilities to an object.
17
+ *
18
+ * The method returns the original object enhanced with the event
19
+ * subscribing and emitting methods.
20
+ *
21
+ * If no object is provided, a new empty object is created and enhanced.
22
+ *
23
+ * The options can configure the behavior of the event emitter.
24
+ *
25
+ * @param {Object|Function} object The object to enhance.
26
+ * @param {EventfulOptions} options The options to configure the event emitter.
27
+ * @returns {Object|Function} The enhanced object.
28
+ *
29
+ * @throws {TypeError} If the first argument is not an object or function.
30
+ */
31
+ const eventful =
32
+ (object = Object.create(null),
33
+ options = { }) =>
34
+ {
35
+ const emptySet =
36
+ new Set();
37
+
38
+ if (!isObject(object)
39
+ && !isFunction(object))
40
+ {
41
+ throw new TypeError(
42
+ 'Expect an object or a function.');
43
+ }
44
+
45
+ const { strict = false,
46
+ trace = null,
47
+ error = null } =
48
+ options;
49
+
50
+ const globalOptions =
51
+ eventful.options;
52
+
53
+ const traceFn =
54
+ trace || globalOptions.trace;
55
+
56
+ if (traceFn) {
57
+ traceFn(
58
+ object,
59
+ 'new',
60
+ { object });
61
+ }
62
+
63
+ for (const method of
64
+ [ 'on',
65
+ 'once',
66
+ 'off',
67
+ 'emit',
68
+ 'emitAsync',
69
+ 'has' ]) {
70
+ if (method in object) {
71
+ throw new Error(
72
+ `Method "${method}" already exists.`);
73
+ }
74
+ }
75
+
76
+ /** @type {Map<string|symbol, Set<Function>>} */
77
+ const map =
78
+ new Map();
79
+
80
+ const add =
81
+ (event, listener) => {
82
+ let listeners =
83
+ map.get(event);
84
+
85
+ if (!listeners) {
86
+ listeners = new Set();
87
+ map.set(event, listeners);
88
+ }
89
+
90
+ listeners.add(listener);
91
+ };
92
+
93
+ const remove =
94
+ (event, listener) => {
95
+ const listeners =
96
+ map.get(event);
97
+
98
+ if (!listeners)
99
+ return false;
100
+
101
+ const deleted =
102
+ listeners.delete(listener);
103
+
104
+ if (listeners.size === 0)
105
+ map.delete(event);
106
+
107
+ return deleted;
108
+ };
109
+
110
+ const getListeners =
111
+ event =>
112
+ map.get(event)
113
+ || emptySet;
114
+
115
+ /**
116
+ * Subscribe to an event.
117
+ *
118
+ * @type {(event: string|symbol, listener: Function) => () => boolean}
119
+ */
120
+ const on =
121
+ (event, listener) => {
122
+ functionTypeGuard(listener);
123
+
124
+ const traceFn =
125
+ trace
126
+ || globalOptions.trace;
127
+
128
+ if (traceFn) {
129
+ traceFn(
130
+ object,
131
+ 'on',
132
+ { event,
133
+ listener });
134
+ }
135
+
136
+ add(
137
+ event,
138
+ listener);
139
+
140
+ let active = true;
141
+
142
+ return () => {
143
+ if (!active)
144
+ return false;
145
+
146
+ active = false;
147
+
148
+ return remove(
149
+ event,
150
+ listener);
151
+ };
152
+ };
153
+
154
+ /**
155
+ * Subscribe to an event for a single occurrence.
156
+ *
157
+ * @type {(event: string|symbol, listener: Function) => () => boolean}
158
+ */
159
+ const once =
160
+ (event, listener) => {
161
+ functionTypeGuard(listener);
162
+
163
+ const off =
164
+ on(
165
+ event,
166
+ (...args) => {
167
+ off();
168
+ listener(...args);
169
+ });
170
+
171
+ return off;
172
+ };
173
+
174
+
175
+ /** @type {(event: string|symbol) => boolean} */
176
+ const has =
177
+ event =>
178
+ map.get(event)?.size > 0
179
+ || false;
180
+
181
+ /**
182
+ * Emit an event synchronously.
183
+ * All listeners run in order.
184
+ * Errors are isolated (ignored) unless `strict` is true.
185
+ *
186
+ * @param {string|symbol} event
187
+ * @param {...any} args
188
+ */
189
+ const emit =
190
+ (event, ...args) => {
191
+ const listeners =
192
+ getListeners(event);
193
+
194
+ const traceFn =
195
+ trace
196
+ || globalOptions.trace;
197
+
198
+ if (traceFn) {
199
+ traceFn(
200
+ object,
201
+ 'emit',
202
+ { listeners: [...listeners],
203
+ event,
204
+ args });
205
+ }
206
+
207
+ if (listeners.size === 0)
208
+ return;
209
+
210
+ for (const listener of listeners) {
211
+ try {
212
+ listener(...args);
213
+ } catch (err) {
214
+ const context =
215
+ { object,
216
+ event,
217
+ listener };
218
+
219
+ (error || globalOptions.error)(
220
+ err,
221
+ context);
222
+
223
+ if (strict)
224
+ throw err;
225
+ }
226
+ }
227
+ };
228
+
229
+ /**
230
+ * Emit an event and wait for all listeners (run in parallel).
231
+ * Errors are isolated (ignored) so all listeners run.
232
+ *
233
+ * @param {string|symbol} event
234
+ * @param {...any} args
235
+ * @returns {Promise<void>}
236
+ */
237
+ const emitAsync =
238
+ async (event, ...args) => {
239
+ const listeners =
240
+ getListeners(event);
241
+
242
+ const traceFn =
243
+ trace
244
+ || globalOptions.trace;
245
+
246
+ if (traceFn) {
247
+ traceFn(
248
+ object,
249
+ 'emitAsync',
250
+ { listeners: [...listeners],
251
+ event,
252
+ args });
253
+ }
254
+
255
+ if (listeners.size === 0)
256
+ return;
257
+
258
+ const calls =
259
+ Array.from(listeners)
260
+ .map(listener =>
261
+ new Promise(
262
+ (resolve, reject) => {
263
+ try {
264
+ Promise.resolve(listener(...args))
265
+ .then(resolve, err => {
266
+ const errorFn =
267
+ error
268
+ || globalOptions.error;
269
+
270
+ if (errorFn) {
271
+ const context =
272
+ { object,
273
+ event,
274
+ listener };
275
+
276
+ errorFn(err, context);
277
+ }
278
+
279
+ reject(err);
280
+ });
281
+ } catch (err) {
282
+ const errorFn =
283
+ error
284
+ || globalOptions.error;
285
+
286
+ if (errorFn) {
287
+ const context =
288
+ { object,
289
+ event,
290
+ listener };
291
+
292
+ errorFn(err, context);
293
+ }
294
+
295
+ reject(err);
296
+ }
297
+ }));
298
+
299
+ if (strict)
300
+ await Promise.all(calls);
301
+ else
302
+ await Promise.allSettled(calls);
303
+ };
304
+
305
+ /**
306
+ * Unsubscribe from an event.
307
+ *
308
+ * @param {string} event The event name.
309
+ * @param {Function} listener The handling function.
310
+ * @returns {boolean} True if unsubscribed, false if not found.
311
+ */
312
+ const off =
313
+ (event, listener) => {
314
+ functionTypeGuard(listener);
315
+
316
+ const traceFn =
317
+ trace
318
+ || globalOptions.trace;
319
+
320
+ if (traceFn) {
321
+ traceFn(
322
+ object,
323
+ 'off',
324
+ { event,
325
+ listener });
326
+ }
327
+
328
+ return remove(
329
+ event,
330
+ listener);
331
+ };
332
+
333
+ const attributes =
334
+ { enumerable: false,
335
+ configurable: true,
336
+ writable: true };
337
+
338
+ Object.defineProperties(
339
+ object,
340
+ { on: { value: on, ...attributes },
341
+ once: { value: once, ...attributes },
342
+ off: { value: off, ...attributes },
343
+ emit: { value: emit, ...attributes },
344
+ emitAsync: { value: emitAsync, ...attributes },
345
+ has: { value: has, ...attributes } });
346
+
347
+ return object;
348
+ };
349
+
350
+ eventful.options =
351
+ { trace: null,
352
+ error: null };
353
+
354
+ function isFunction(value) {
355
+ return typeof value === 'function';
356
+ }
357
+
358
+ function isObject(value) {
359
+ return value !== null
360
+ && typeof value === 'object';
361
+ }
362
+
363
+ function functionTypeGuard(value) {
364
+ if (!isFunction(value)) {
365
+ throw new TypeError(
366
+ 'Expect a function.');
367
+ }
368
+ }
369
+
370
+ export { eventful };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "asljs-eventful",
3
+ "version": "0.1.3",
4
+ "description": "Lightweight event helper adding on/off/emit to any object.",
5
+ "keywords": [
6
+ "events",
7
+ "javascript",
8
+ "js"
9
+ ],
10
+ "homepage": "https://github.com/alex-netkachov/asljs-eventful#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/alex-netkachov/asljs-eventful/issues"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/alex-netkachov/asljs-eventful.git"
17
+ },
18
+ "license": "MIT",
19
+ "author": "\"Alex Netkachov\" <alex.netkachov@gmail.com>",
20
+ "type": "module",
21
+ "main": "eventful.js",
22
+ "types": "eventful.d.ts",
23
+ "directories": {
24
+ "doc": "docs"
25
+ },
26
+ "scripts": {
27
+ "test": "node --test",
28
+ "test:watch": "node --watch --test",
29
+ "coverage": "NODE_V8_COVERAGE=.coverage node --test && node -e \"console.log('Coverage in .coverage (use c8/istanbul if you want reports)')\""
30
+ }
31
+ }
@@ -0,0 +1,206 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { eventful } from '../eventful.js';
4
+
5
+ test(
6
+ 'trace is called on creation with action "new"',
7
+ () => {
8
+ const traces = [];
9
+
10
+ const original = { };
11
+
12
+ const enhanced =
13
+ eventful(
14
+ original,
15
+ { trace:
16
+ (object, action, payload) =>
17
+ traces.push({ object, action, payload }) });
18
+
19
+ const creation =
20
+ traces.find(t => t.action === 'new');
21
+
22
+ assert.ok(creation);
23
+
24
+ assert.equal(
25
+ creation.payload.object,
26
+ original);
27
+
28
+ assert.equal(
29
+ creation.object,
30
+ enhanced);
31
+
32
+ assert.equal(
33
+ creation.payload.object,
34
+ enhanced);
35
+ });
36
+
37
+ test(
38
+ 'eventful creates an empty event emitter object',
39
+ () => {
40
+ const obj =
41
+ eventful();
42
+
43
+ assert.ok(obj);
44
+ assert.equal(typeof obj.on, 'function');
45
+ assert.equal(typeof obj.off, 'function');
46
+ assert.equal(typeof obj.emit, 'function');
47
+ assert.equal(typeof obj.emitAsync, 'function');
48
+ assert.equal(typeof obj.has, 'function');
49
+ });
50
+
51
+ test(
52
+ 'eventful throws when an object has one of the event emitter methods',
53
+ () => {
54
+ for (const method of ['on', 'once', 'off', 'emit', 'emitAsync', 'has']) {
55
+ assert.throws(
56
+ () => eventful({ [method]: () => { } }));
57
+ }
58
+ });
59
+
60
+ test(
61
+ 'exceptions in listeners are suppressed by default',
62
+ async () => {
63
+ const obj =
64
+ eventful(
65
+ { },
66
+ { error: () => { } });
67
+
68
+ obj.on(
69
+ 'test',
70
+ () => { throw new Error('test error'); });
71
+
72
+ await assert.doesNotReject(
73
+ () => obj.emitAsync('test'));
74
+ });
75
+
76
+ test(
77
+ 'strict mode propagates exceptions in listeners',
78
+ async () => {
79
+ const obj =
80
+ eventful(
81
+ { },
82
+ { error: () => { },
83
+ strict: true });
84
+
85
+ obj.on(
86
+ 'test',
87
+ () => { throw new Error('test error'); });
88
+
89
+ await assert.rejects(
90
+ () => obj.emitAsync('test'));
91
+ });
92
+
93
+ test(
94
+ 'async listener rejection is suppressed by default',
95
+ async () => {
96
+ const obj =
97
+ eventful(
98
+ { },
99
+ { error: () => {} });
100
+
101
+ obj.on(
102
+ 'test',
103
+ async () => { throw new Error('async fail'); });
104
+
105
+ await assert.doesNotReject(
106
+ () => obj.emitAsync('test'));
107
+ });
108
+
109
+ test(
110
+ 'non-strict runs other listeners even if one fails',
111
+ async () => {
112
+ const obj =
113
+ eventful(
114
+ { },
115
+ { error: () => {} });
116
+
117
+ let ran = 0;
118
+
119
+ obj.on(
120
+ 'test',
121
+ () => { ran += 1; });
122
+
123
+ obj.on(
124
+ 'test',
125
+ () => { throw new Error('boom'); });
126
+
127
+ obj.on(
128
+ 'test',
129
+ () => { ran += 1; });
130
+
131
+ await assert.doesNotReject(
132
+ () => obj.emitAsync('test'));
133
+
134
+ assert.equal(ran, 2);
135
+ });
136
+
137
+ test(
138
+ 'trace receives safe payload and action names',
139
+ async () => {
140
+ const traces = [];
141
+ const obj =
142
+ eventful(
143
+ { },
144
+ { trace:
145
+ (object, action, payload) =>
146
+ traces.push({ action, payload }) });
147
+
148
+ const off = obj.on('e', () => {});
149
+ obj.emit('e', 1, 2);
150
+ await obj.emitAsync('e', 3, 4);
151
+ off();
152
+
153
+ // Expect actions at least 'on', 'emit', 'emitAsync'
154
+ const actions = traces.map(t => t.action);
155
+ assert.ok(actions.includes('on'));
156
+ assert.ok(actions.includes('emit'));
157
+ assert.ok(actions.includes('emitAsync'));
158
+
159
+ const emitTrace = traces.find(t => t.action === 'emit');
160
+ assert.ok(Array.isArray(emitTrace.payload.listeners));
161
+ assert.equal(emitTrace.payload.event, 'e');
162
+ assert.deepEqual(emitTrace.payload.args, [1, 2]);
163
+
164
+ const emitAsyncTrace = traces.find(t => t.action === 'emitAsync');
165
+ assert.ok(Array.isArray(emitAsyncTrace.payload.listeners));
166
+ assert.equal(emitAsyncTrace.payload.event, 'e');
167
+ assert.deepEqual(emitAsyncTrace.payload.args, [3, 4]);
168
+ });
169
+
170
+ test(
171
+ 'error hook runs for async rejection (non-strict)',
172
+ async () => {
173
+ let errors = 0;
174
+ const obj =
175
+ eventful(
176
+ { },
177
+ { error: () => { errors += 1; } });
178
+
179
+ obj.on('e', async () => { throw new Error('reject'); });
180
+
181
+ await assert.doesNotReject(() => obj.emitAsync('e'));
182
+ assert.equal(errors, 1);
183
+ });
184
+
185
+ test(
186
+ 'has reflects subscribe and unsubscribe',
187
+ () => {
188
+ const obj = eventful();
189
+ assert.equal(obj.has('x'), false);
190
+ const off = obj.on('x', () => {});
191
+ assert.equal(obj.has('x'), true);
192
+ off();
193
+ assert.equal(obj.has('x'), false);
194
+ });
195
+
196
+ test(
197
+ 'strict mode propagates async rejections',
198
+ async () => {
199
+ const obj =
200
+ eventful(
201
+ { },
202
+ { error: () => { }, strict: true });
203
+
204
+ obj.on('e', async () => { throw new Error('nope'); });
205
+ await assert.rejects(() => obj.emitAsync('e'));
206
+ });