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 +21 -0
- package/README.md +149 -0
- package/eventful.d.ts +106 -0
- package/eventful.js +370 -0
- package/package.json +31 -0
- package/tests/eventful.test.js +206 -0
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
|
+
});
|