asljs-eventful 0.2.3 → 0.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.
- package/LICENSE.md +1 -1
- package/README.md +21 -11
- package/dist/eventful-base.js +6 -0
- package/dist/eventful-base.test.js +22 -0
- package/dist/eventful-like.js +14 -0
- package/dist/eventful-like.test.js +29 -0
- package/dist/eventful.js +11 -14
- package/dist/eventful.test.js +206 -0
- package/dist/guards.js +1 -1
- package/dist/guards.test.js +24 -0
- package/dist/types.test.js +16 -0
- package/eventful.d.ts +17 -8
- package/eventful.js +10 -1
- package/package.json +10 -9
- package/dist/eventful.d.ts +0 -11
- package/dist/guards.d.ts +0 -5
- package/dist/types.d.ts +0 -97
package/LICENSE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 Alexandrite Software Ltd
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -36,7 +36,10 @@ obj.emit('greet', 'Hello');
|
|
|
36
36
|
```ts
|
|
37
37
|
import { eventful, type Eventful } from 'asljs-eventful';
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
type Events =
|
|
40
|
+
{ greet: [msg: string] };
|
|
41
|
+
|
|
42
|
+
const obj: { name: string } & Eventful<Events> =
|
|
40
43
|
eventful({ name: 'Alice' });
|
|
41
44
|
|
|
42
45
|
obj.on('greet',
|
|
@@ -72,6 +75,7 @@ class MyClass extends EventfulBase {
|
|
|
72
75
|
|
|
73
76
|
```ts
|
|
74
77
|
import { EventfulBase } from 'asljs-eventful';
|
|
78
|
+
|
|
75
79
|
class MyClass extends EventfulBase {
|
|
76
80
|
name: string;
|
|
77
81
|
|
|
@@ -158,12 +162,12 @@ const obj =
|
|
|
158
162
|
payload);
|
|
159
163
|
} });
|
|
160
164
|
|
|
161
|
-
// Tracing
|
|
162
|
-
// - 'new' on creation
|
|
163
|
-
// - 'on' when subscribing
|
|
164
|
-
// - 'off' when unsubscribing
|
|
165
|
-
// - 'emit' for sync emit
|
|
166
|
-
// - 'emitAsync' for async emit
|
|
165
|
+
// Tracing (event, payload):
|
|
166
|
+
// - 'new' on creation, { object }
|
|
167
|
+
// - 'on' when subscribing, { object, event, listener }
|
|
168
|
+
// - 'off' when unsubscribing, { object, event, listener }
|
|
169
|
+
// - 'emit' for sync emit, { object, event, args, listeners }
|
|
170
|
+
// - 'emitAsync' for async emit, { object, event, args, listeners }
|
|
167
171
|
```
|
|
168
172
|
|
|
169
173
|
Custom error handler for listener errors:
|
|
@@ -191,7 +195,9 @@ const obj =
|
|
|
191
195
|
|
|
192
196
|
### Global Events
|
|
193
197
|
|
|
194
|
-
`eventful` is also a global emitter. When you create an enhanced object via
|
|
198
|
+
`eventful` is also a global emitter. When you create an enhanced object via
|
|
199
|
+
`eventful(target, options)`, its lifecycle and actions are traced via the
|
|
200
|
+
per-instance `trace` hook and also emitted as global events on `eventful`.
|
|
195
201
|
|
|
196
202
|
```js
|
|
197
203
|
const offNew =
|
|
@@ -213,7 +219,9 @@ offNew();
|
|
|
213
219
|
offError();
|
|
214
220
|
```
|
|
215
221
|
|
|
216
|
-
Note: if a **global** `eventful.on('error', ...)` listener throws, `eventful`
|
|
222
|
+
Note: if a **global** `eventful.on('error', ...)` listener throws, `eventful`
|
|
223
|
+
throws a `ListenerError` (an `Error` subclass with fields
|
|
224
|
+
`{ error, object, event, listener }`) to avoid an infinite error loop.
|
|
217
225
|
|
|
218
226
|
## API
|
|
219
227
|
|
|
@@ -224,8 +232,10 @@ a new empty object is created.
|
|
|
224
232
|
|
|
225
233
|
- `target` (Object): The object to be enhanced with event capabilities.
|
|
226
234
|
- `options` (Object): Configuration options.
|
|
227
|
-
- `error` (Function | null): Optional error hook called with
|
|
228
|
-
|
|
235
|
+
- `error` (Function | null): Optional error hook called with
|
|
236
|
+
`{ error, object, event, listener }`.
|
|
237
|
+
- `trace` (Function | null): Optional trace hook called with
|
|
238
|
+
`(action, payload)`.
|
|
229
239
|
- `strict` (Boolean): If true, propagates listener errors; otherwise they
|
|
230
240
|
are isolated. Defaults to false.
|
|
231
241
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { EventfulBase, } from './eventful-base.js';
|
|
4
|
+
test('EventfulBase wires eventful methods in constructor', () => {
|
|
5
|
+
class Demo extends EventfulBase {
|
|
6
|
+
}
|
|
7
|
+
const demo = new Demo();
|
|
8
|
+
let seen = null;
|
|
9
|
+
demo.on('ping', value => seen = value);
|
|
10
|
+
demo.emit('ping', 7);
|
|
11
|
+
assert.equal(seen, 7);
|
|
12
|
+
});
|
|
13
|
+
test('EventfulBase passes options to eventful setup', () => {
|
|
14
|
+
class Demo extends EventfulBase {
|
|
15
|
+
}
|
|
16
|
+
const demo = new Demo({ strict: true,
|
|
17
|
+
error: () => { } });
|
|
18
|
+
demo.on('boom', () => {
|
|
19
|
+
throw new Error('boom');
|
|
20
|
+
});
|
|
21
|
+
assert.throws(() => demo.emit('boom'), Error);
|
|
22
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isFunction, isObject, } from './guards.js';
|
|
2
|
+
export function isEvenfulLike(value) {
|
|
3
|
+
if (!isObject(value)
|
|
4
|
+
&& !isFunction(value)) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
return typeof value.on === 'function';
|
|
8
|
+
}
|
|
9
|
+
export function asEventfulLike(value) {
|
|
10
|
+
if (isEvenfulLike(value)) {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { eventful, } from './eventful.js';
|
|
4
|
+
import { asEventfulLike, isEvenfulLike, } from './eventful-like.js';
|
|
5
|
+
test('isEvenfulLike returns true for eventful instances', () => {
|
|
6
|
+
const value = eventful();
|
|
7
|
+
assert.equal(isEvenfulLike(value), true);
|
|
8
|
+
});
|
|
9
|
+
test('isEvenfulLike returns true for plain object with on method', () => {
|
|
10
|
+
const value = { on: () => () => true };
|
|
11
|
+
assert.equal(isEvenfulLike(value), true);
|
|
12
|
+
});
|
|
13
|
+
test('isEvenfulLike returns false for non-like values', () => {
|
|
14
|
+
assert.equal(isEvenfulLike(undefined), false);
|
|
15
|
+
assert.equal(isEvenfulLike(null), false);
|
|
16
|
+
assert.equal(isEvenfulLike({}), false);
|
|
17
|
+
assert.equal(isEvenfulLike({ on: 1 }), false);
|
|
18
|
+
});
|
|
19
|
+
test('asEventfulLike returns value when compatible', () => {
|
|
20
|
+
const value = eventful();
|
|
21
|
+
const converted = asEventfulLike(value);
|
|
22
|
+
assert.equal(converted, value);
|
|
23
|
+
const typed = converted;
|
|
24
|
+
assert.ok(typed);
|
|
25
|
+
});
|
|
26
|
+
test('asEventfulLike returns undefined when incompatible', () => {
|
|
27
|
+
assert.equal(asEventfulLike({}), undefined);
|
|
28
|
+
assert.equal(asEventfulLike(42), undefined);
|
|
29
|
+
});
|
package/dist/eventful.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { ListenerError } from './types.js';
|
|
2
|
-
import {
|
|
1
|
+
import { ListenerError, } from './types.js';
|
|
2
|
+
import { asEventfulLike, isEvenfulLike, } from './eventful-like.js';
|
|
3
|
+
import { eventNameTypeGuard, functionTypeGuard, isFunction, isObject, } from './guards.js';
|
|
3
4
|
const eventfulImpl = (object = Object.create(null), options = {}) => {
|
|
4
5
|
if (!isObject(object)
|
|
5
6
|
&& !isFunction(object)) {
|
|
6
7
|
throw new TypeError('Expect an object or a function.');
|
|
7
8
|
}
|
|
8
9
|
for (const method of ['on', 'once', 'off', 'emit', 'emitAsync', 'has']) {
|
|
9
|
-
if (
|
|
10
|
+
if (method in object) {
|
|
10
11
|
throw new Error(`Method "${method}" already exists.`);
|
|
11
12
|
}
|
|
12
13
|
}
|
|
@@ -66,7 +67,7 @@ const eventfulImpl = (object = Object.create(null), options = {}) => {
|
|
|
66
67
|
eventful.emit('error', errorArgs);
|
|
67
68
|
}
|
|
68
69
|
function on(event, listener) {
|
|
69
|
-
|
|
70
|
+
eventNameTypeGuard(event);
|
|
70
71
|
functionTypeGuard(listener);
|
|
71
72
|
traceFn('on', { object,
|
|
72
73
|
event,
|
|
@@ -78,7 +79,7 @@ const eventfulImpl = (object = Object.create(null), options = {}) => {
|
|
|
78
79
|
: false;
|
|
79
80
|
}
|
|
80
81
|
function once(event, listener) {
|
|
81
|
-
|
|
82
|
+
eventNameTypeGuard(event);
|
|
82
83
|
functionTypeGuard(listener);
|
|
83
84
|
const off = on(event, (...args) => {
|
|
84
85
|
off();
|
|
@@ -87,7 +88,7 @@ const eventfulImpl = (object = Object.create(null), options = {}) => {
|
|
|
87
88
|
return off;
|
|
88
89
|
}
|
|
89
90
|
function off(event, listener) {
|
|
90
|
-
|
|
91
|
+
eventNameTypeGuard(event);
|
|
91
92
|
functionTypeGuard(listener);
|
|
92
93
|
traceFn('off', { object,
|
|
93
94
|
event,
|
|
@@ -95,11 +96,11 @@ const eventfulImpl = (object = Object.create(null), options = {}) => {
|
|
|
95
96
|
return remove(event, listener);
|
|
96
97
|
}
|
|
97
98
|
function has(event) {
|
|
98
|
-
|
|
99
|
+
eventNameTypeGuard(event);
|
|
99
100
|
return (map.get(event)?.size ?? 0) > 0;
|
|
100
101
|
}
|
|
101
102
|
function emit(event, ...args) {
|
|
102
|
-
|
|
103
|
+
eventNameTypeGuard(event);
|
|
103
104
|
const listeners = map.get(event)
|
|
104
105
|
|| emptySet;
|
|
105
106
|
traceFn('emit', { object,
|
|
@@ -120,7 +121,7 @@ const eventfulImpl = (object = Object.create(null), options = {}) => {
|
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
async function emitAsync(event, ...args) {
|
|
123
|
-
|
|
124
|
+
eventNameTypeGuard(event);
|
|
124
125
|
const listeners = map.get(event)
|
|
125
126
|
|| emptySet;
|
|
126
127
|
traceFn('emitAsync', { object,
|
|
@@ -145,9 +146,5 @@ const eventfulImpl = (object = Object.create(null), options = {}) => {
|
|
|
145
146
|
}
|
|
146
147
|
};
|
|
147
148
|
export const eventful = eventfulImpl;
|
|
149
|
+
export { isEvenfulLike, asEventfulLike, };
|
|
148
150
|
eventful(eventful);
|
|
149
|
-
export class EventfulBase {
|
|
150
|
-
constructor(options = {}) {
|
|
151
|
-
eventful(this, options);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { eventful, } from './eventful.js';
|
|
4
|
+
import { EventfulBase, } from './eventful-base.js';
|
|
5
|
+
test('eventful extends an object', () => {
|
|
6
|
+
const original = {};
|
|
7
|
+
const enhanced = eventful(original);
|
|
8
|
+
assert.equal(enhanced, original);
|
|
9
|
+
assertEventfulMethods(enhanced);
|
|
10
|
+
});
|
|
11
|
+
test('eventful extends a function', () => {
|
|
12
|
+
const original = () => { };
|
|
13
|
+
const enhanced = eventful(original);
|
|
14
|
+
assert.equal(enhanced, original);
|
|
15
|
+
assertEventfulMethods(enhanced);
|
|
16
|
+
});
|
|
17
|
+
test('eventful can be called without arguments', () => {
|
|
18
|
+
const enhanced = eventful();
|
|
19
|
+
assert.ok(enhanced !== null);
|
|
20
|
+
assertEventfulMethods(enhanced);
|
|
21
|
+
});
|
|
22
|
+
test('eventful can be extended by inheritance', () => {
|
|
23
|
+
class MyClass extends EventfulBase {
|
|
24
|
+
constructor(name) {
|
|
25
|
+
super();
|
|
26
|
+
this.name = name;
|
|
27
|
+
}
|
|
28
|
+
greet() {
|
|
29
|
+
this.emit('greet', `Hello, ${this.name}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const instance = new MyClass('Alice');
|
|
33
|
+
let greeting = null;
|
|
34
|
+
instance.on('greet', message => greeting = message);
|
|
35
|
+
instance.greet();
|
|
36
|
+
assert.equal(greeting, 'Hello, Alice');
|
|
37
|
+
});
|
|
38
|
+
test('eventful can be added during construction', () => {
|
|
39
|
+
class MyClass {
|
|
40
|
+
constructor(name) {
|
|
41
|
+
eventful(this);
|
|
42
|
+
this.name = name;
|
|
43
|
+
}
|
|
44
|
+
greet() {
|
|
45
|
+
this.emit('greet', `Hello, ${this.name}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const instance = new MyClass('Alice');
|
|
49
|
+
let greeting = null;
|
|
50
|
+
instance.on('greet', message => greeting = message);
|
|
51
|
+
instance.greet();
|
|
52
|
+
assert.equal(greeting, 'Hello, Alice');
|
|
53
|
+
});
|
|
54
|
+
test('trace is called on creation with action "new"', () => {
|
|
55
|
+
const recorder = createRecorder();
|
|
56
|
+
const object = eventful({}, { trace: recorder.write });
|
|
57
|
+
const creation = recorder.records().find(item => item.action === 'new');
|
|
58
|
+
assert.ok(creation);
|
|
59
|
+
assert.equal(creation.payload.object, object);
|
|
60
|
+
});
|
|
61
|
+
test('global trace is called on creation with action "new"', () => {
|
|
62
|
+
const recorder = createRecorder();
|
|
63
|
+
const off = eventful.on('new', (...args) => recorder.write('new', args[0]));
|
|
64
|
+
try {
|
|
65
|
+
const object = eventful({}, { trace: recorder.write });
|
|
66
|
+
const creation = recorder.records().find(item => item.action === 'new');
|
|
67
|
+
assert.ok(creation);
|
|
68
|
+
assert.equal(creation.payload.object, object);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
off();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
test('eventful throws when an object has one of the event emitter methods', () => {
|
|
75
|
+
for (const method of ['on', 'once', 'off', 'emit', 'emitAsync', 'has']) {
|
|
76
|
+
assert.throws(() => eventful({
|
|
77
|
+
[method]: () => { }
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
test('exceptions in listeners are suppressed in async emit by default', async () => {
|
|
82
|
+
const obj = eventful();
|
|
83
|
+
obj.on('test', () => { throw new Error('test error'); });
|
|
84
|
+
await assert.doesNotReject(() => obj.emitAsync('test'));
|
|
85
|
+
});
|
|
86
|
+
function assertEventfulMethods(object) {
|
|
87
|
+
const candidate = object;
|
|
88
|
+
assert.equal(typeof candidate.on, 'function');
|
|
89
|
+
assert.equal(typeof candidate.off, 'function');
|
|
90
|
+
assert.equal(typeof candidate.emit, 'function');
|
|
91
|
+
assert.equal(typeof candidate.emitAsync, 'function');
|
|
92
|
+
assert.equal(typeof candidate.has, 'function');
|
|
93
|
+
}
|
|
94
|
+
test('exceptions in listeners are suppressed in async emit by default', async () => {
|
|
95
|
+
const obj = eventful();
|
|
96
|
+
obj.on('test', () => { throw new Error('test error'); });
|
|
97
|
+
await assert.doesNotReject(() => obj.emitAsync('test'));
|
|
98
|
+
});
|
|
99
|
+
test('exceptions in listeners are suppressed in emit by default', () => {
|
|
100
|
+
const obj = eventful();
|
|
101
|
+
obj.on('test', () => { throw new Error('test error'); });
|
|
102
|
+
assert.doesNotThrow(() => obj.emit('test'));
|
|
103
|
+
});
|
|
104
|
+
test('strict mode propagates exceptions in listeners in async emit', async () => {
|
|
105
|
+
const obj = eventful({}, { error: () => { },
|
|
106
|
+
strict: true });
|
|
107
|
+
obj.on('test', () => { throw new Error('test error'); });
|
|
108
|
+
await assert.rejects(() => obj.emitAsync('test'));
|
|
109
|
+
});
|
|
110
|
+
test('strict mode propagates exceptions in listeners in emit', () => {
|
|
111
|
+
const obj = eventful({}, { error: () => { },
|
|
112
|
+
strict: true });
|
|
113
|
+
obj.on('test', () => { throw new Error('test error'); });
|
|
114
|
+
assert.throws(() => obj.emit('test'), Error);
|
|
115
|
+
});
|
|
116
|
+
test('non-strict runs other listeners even if one fails', async () => {
|
|
117
|
+
const obj = eventful({}, { error: () => { } });
|
|
118
|
+
let ran = 0;
|
|
119
|
+
obj.on('test', () => ran += 1);
|
|
120
|
+
obj.on('test', () => { throw new Error('boom'); });
|
|
121
|
+
obj.on('test', () => ran += 1);
|
|
122
|
+
await assert.doesNotReject(() => obj.emitAsync('test'));
|
|
123
|
+
assert.equal(ran, 2);
|
|
124
|
+
});
|
|
125
|
+
test('trace receives safe payload and action names', async () => {
|
|
126
|
+
const recorder = createRecorder();
|
|
127
|
+
const obj = eventful({}, { trace: recorder.write });
|
|
128
|
+
const off = obj.on('e', () => { });
|
|
129
|
+
obj.emit('e', 1, 2);
|
|
130
|
+
await obj.emitAsync('e', 3, 4);
|
|
131
|
+
off();
|
|
132
|
+
assert.ok(recorder.records().find(item => item.action === 'on'));
|
|
133
|
+
assert.ok(recorder.records().find(item => item.action === 'emit'));
|
|
134
|
+
assert.ok(recorder.records().find(item => item.action === 'emitAsync'));
|
|
135
|
+
const emitTrace = recorder.records().find(item => item.action === 'emit');
|
|
136
|
+
assert.ok(emitTrace);
|
|
137
|
+
assert.ok(Array.isArray(emitTrace.payload.listeners));
|
|
138
|
+
assert.equal(emitTrace.payload.event, 'e');
|
|
139
|
+
assert.deepEqual(emitTrace.payload.args, [1, 2]);
|
|
140
|
+
const emitAsyncTrace = recorder.records().find(item => item.action === 'emitAsync');
|
|
141
|
+
assert.ok(emitAsyncTrace);
|
|
142
|
+
assert.ok(Array.isArray(emitAsyncTrace.payload.listeners));
|
|
143
|
+
assert.equal(emitAsyncTrace.payload.event, 'e');
|
|
144
|
+
assert.deepEqual(emitAsyncTrace.payload.args, [3, 4]);
|
|
145
|
+
});
|
|
146
|
+
test('error hook runs for async rejection (non-strict)', async () => {
|
|
147
|
+
let errors = 0;
|
|
148
|
+
const obj = eventful({}, { error: () => errors += 1 });
|
|
149
|
+
obj.on('e', async () => { throw new Error('reject'); });
|
|
150
|
+
await assert.doesNotReject(() => obj.emitAsync('e'));
|
|
151
|
+
assert.equal(errors, 1);
|
|
152
|
+
});
|
|
153
|
+
test('has reflects subscribe and unsubscribe', () => {
|
|
154
|
+
const obj = eventful();
|
|
155
|
+
assert.equal(obj.has('x'), false);
|
|
156
|
+
const off = obj.on('x', () => { });
|
|
157
|
+
assert.equal(obj.has('x'), true);
|
|
158
|
+
off();
|
|
159
|
+
assert.equal(obj.has('x'), false);
|
|
160
|
+
});
|
|
161
|
+
test('strict mode propagates async rejections', async () => {
|
|
162
|
+
const obj = eventful({}, { error: () => { },
|
|
163
|
+
strict: true });
|
|
164
|
+
obj.on('e', async () => { throw new Error('nope'); });
|
|
165
|
+
await assert.rejects(() => obj.emitAsync('e'));
|
|
166
|
+
});
|
|
167
|
+
test('emit ignores errors when no error hook (non-strict)', () => {
|
|
168
|
+
const obj = eventful();
|
|
169
|
+
obj.on('x', () => { throw new Error('boom'); });
|
|
170
|
+
assert.doesNotThrow(() => obj.emit('x'));
|
|
171
|
+
});
|
|
172
|
+
test('throw in global error listener does not loop', () => {
|
|
173
|
+
let globalErrorCalls = 0;
|
|
174
|
+
const off = eventful.on('error', () => {
|
|
175
|
+
globalErrorCalls += 1;
|
|
176
|
+
if (globalErrorCalls > 1)
|
|
177
|
+
throw new Error('global error listener loop');
|
|
178
|
+
throw new Error('boom');
|
|
179
|
+
});
|
|
180
|
+
try {
|
|
181
|
+
const obj = eventful();
|
|
182
|
+
obj.on('e', () => { throw new Error('listener failed'); });
|
|
183
|
+
assert.throws(() => obj.emit('e'), Error);
|
|
184
|
+
assert.equal(globalErrorCalls, 1);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
off();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
test('event must be string or symbol', () => {
|
|
191
|
+
const obj = eventful();
|
|
192
|
+
assert.throws(() => obj.on(123, () => { }), TypeError);
|
|
193
|
+
assert.throws(() => obj.emit(123), TypeError);
|
|
194
|
+
const s = Symbol('e');
|
|
195
|
+
assert.doesNotThrow(() => obj.on(s, () => { }));
|
|
196
|
+
assert.doesNotThrow(() => obj.emit(s));
|
|
197
|
+
});
|
|
198
|
+
export function createRecorder() {
|
|
199
|
+
const records = [];
|
|
200
|
+
return {
|
|
201
|
+
write: (action, payload) => {
|
|
202
|
+
records.push({ action, payload });
|
|
203
|
+
},
|
|
204
|
+
records: () => records
|
|
205
|
+
};
|
|
206
|
+
}
|
package/dist/guards.js
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { eventNameTypeGuard, functionTypeGuard, isFunction, isObject, } from './guards.js';
|
|
4
|
+
test('eventNameTypeGuard accepts string and symbol', () => {
|
|
5
|
+
assert.doesNotThrow(() => eventNameTypeGuard('event'));
|
|
6
|
+
assert.doesNotThrow(() => eventNameTypeGuard(Symbol('event')));
|
|
7
|
+
});
|
|
8
|
+
test('eventNameTypeGuard throws for invalid values', () => {
|
|
9
|
+
assert.throws(() => eventNameTypeGuard(42), TypeError);
|
|
10
|
+
assert.throws(() => eventNameTypeGuard(null), TypeError);
|
|
11
|
+
});
|
|
12
|
+
test('isFunction returns expected result', () => {
|
|
13
|
+
assert.equal(isFunction(() => { }), true);
|
|
14
|
+
assert.equal(isFunction({}), false);
|
|
15
|
+
});
|
|
16
|
+
test('isObject returns expected result', () => {
|
|
17
|
+
assert.equal(isObject({ key: 'value' }), true);
|
|
18
|
+
assert.equal(isObject(null), false);
|
|
19
|
+
assert.equal(isObject(() => { }), false);
|
|
20
|
+
});
|
|
21
|
+
test('functionTypeGuard validates value', () => {
|
|
22
|
+
assert.doesNotThrow(() => functionTypeGuard(() => { }));
|
|
23
|
+
assert.throws(() => functionTypeGuard('nope'), TypeError);
|
|
24
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { ListenerError, } from './types.js';
|
|
4
|
+
test('ListenerError extends Error and stores context', () => {
|
|
5
|
+
const inner = new Error('inner');
|
|
6
|
+
const object = { id: 1 };
|
|
7
|
+
const listener = () => { };
|
|
8
|
+
const error = new ListenerError('failed', inner, object, 'test', listener);
|
|
9
|
+
assert.ok(error instanceof Error);
|
|
10
|
+
assert.equal(error.name, 'ListenerError');
|
|
11
|
+
assert.equal(error.message, 'failed');
|
|
12
|
+
assert.equal(error.error, inner);
|
|
13
|
+
assert.equal(error.object, object);
|
|
14
|
+
assert.equal(error.event, 'test');
|
|
15
|
+
assert.equal(error.listener, listener);
|
|
16
|
+
});
|
package/eventful.d.ts
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
export {
|
|
2
2
|
eventful,
|
|
3
|
-
|
|
3
|
+
isEvenfulLike,
|
|
4
|
+
asEventfulLike
|
|
4
5
|
} from './dist/eventful.js';
|
|
5
6
|
|
|
7
|
+
export {
|
|
8
|
+
EventfulBase
|
|
9
|
+
} from './dist/eventful-base.js';
|
|
10
|
+
|
|
6
11
|
export type {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
EventfulLike
|
|
13
|
+
} from './dist/eventful-like.js';
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
type EventName,
|
|
17
|
+
type EventMap,
|
|
18
|
+
type Eventful,
|
|
19
|
+
type EventfulFactory,
|
|
20
|
+
type EventfulOptions,
|
|
21
|
+
type Listener,
|
|
13
22
|
ListenerError,
|
|
14
|
-
TraceFn
|
|
23
|
+
type TraceFn
|
|
15
24
|
} from './dist/types.js';
|
package/eventful.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "asljs-eventful",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Lightweight event helper adding on/off/emit to any object.",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/**",
|
|
@@ -37,13 +37,14 @@
|
|
|
37
37
|
"doc": "docs"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
-
"
|
|
41
|
-
"build
|
|
42
|
-
"
|
|
43
|
-
"lint
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"test
|
|
47
|
-
"
|
|
40
|
+
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
41
|
+
"build": "npx tsc -p tsconfig.json",
|
|
42
|
+
"build:test": "npx tsc -p tsconfig.test.json",
|
|
43
|
+
"lint": "npx eslint .",
|
|
44
|
+
"lint:fix": "npx eslint . --fix",
|
|
45
|
+
"prepack": "npm run clean && npm run build",
|
|
46
|
+
"test": "npm run build:test && node --test dist/*.test.js",
|
|
47
|
+
"test:watch": "npm run build:test && node --watch --test dist/*.test.js",
|
|
48
|
+
"coverage": "npm run build:test && NODE_V8_COVERAGE=.coverage node --test dist/*.test.js && node -e \"console.log('Coverage in .coverage (use c8/istanbul if you want reports)')\""
|
|
48
49
|
}
|
|
49
50
|
}
|
package/dist/eventful.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { EventMap, Eventful, EventfulFn, EventfulOptions } from './types.js';
|
|
2
|
-
export declare const eventful: EventfulFn;
|
|
3
|
-
export declare class EventfulBase<E extends EventMap = EventMap> implements Eventful<E> {
|
|
4
|
-
on: Eventful<E>['on'];
|
|
5
|
-
once: Eventful<E>['once'];
|
|
6
|
-
off: Eventful<E>['off'];
|
|
7
|
-
emit: Eventful<E>['emit'];
|
|
8
|
-
emitAsync: Eventful<E>['emitAsync'];
|
|
9
|
-
has: Eventful<E>['has'];
|
|
10
|
-
constructor(options?: EventfulOptions);
|
|
11
|
-
}
|
package/dist/guards.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { EventName } from './types.js';
|
|
2
|
-
export declare function eventTypeGuard(value: any): asserts value is EventName;
|
|
3
|
-
export declare function isFunction(value: any): value is Function;
|
|
4
|
-
export declare function isObject(value: any): value is object;
|
|
5
|
-
export declare function functionTypeGuard(value: any): asserts value is Function;
|
package/dist/types.d.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
export type EventName = string | symbol;
|
|
2
|
-
export type EventMap = Record<EventName, any[]>;
|
|
3
|
-
export type Listener<Args extends any[] = any[]> = (...args: Args) => any;
|
|
4
|
-
export interface ListenerErrorArgs {
|
|
5
|
-
error: any;
|
|
6
|
-
object: object | Function;
|
|
7
|
-
event: EventName;
|
|
8
|
-
listener: Function;
|
|
9
|
-
}
|
|
10
|
-
export declare class ListenerError extends Error implements ListenerErrorArgs {
|
|
11
|
-
error: any;
|
|
12
|
-
object: object | Function;
|
|
13
|
-
event: EventName;
|
|
14
|
-
listener: Function;
|
|
15
|
-
constructor(message: string, error: any, object: object | Function, event: EventName, listener: Function);
|
|
16
|
-
}
|
|
17
|
-
type TraceAction = 'new' | 'on' | 'off' | 'emit' | 'emitAsync';
|
|
18
|
-
type TracePayloadByAction = {
|
|
19
|
-
new: {
|
|
20
|
-
object: object | Function;
|
|
21
|
-
};
|
|
22
|
-
on: {
|
|
23
|
-
object: object | Function;
|
|
24
|
-
event: EventName;
|
|
25
|
-
listener: Function;
|
|
26
|
-
};
|
|
27
|
-
off: {
|
|
28
|
-
object: object | Function;
|
|
29
|
-
event: EventName;
|
|
30
|
-
listener: Function;
|
|
31
|
-
};
|
|
32
|
-
emit: {
|
|
33
|
-
object: object | Function;
|
|
34
|
-
listeners: Function[];
|
|
35
|
-
event: EventName;
|
|
36
|
-
args: any[];
|
|
37
|
-
};
|
|
38
|
-
emitAsync: {
|
|
39
|
-
object: object | Function;
|
|
40
|
-
listeners: Function[];
|
|
41
|
-
event: EventName;
|
|
42
|
-
args: any[];
|
|
43
|
-
};
|
|
44
|
-
};
|
|
45
|
-
export type TraceFn = <A extends TraceAction>(action: A, args: TracePayloadByAction[A]) => void;
|
|
46
|
-
export type EventfulFn = EventfulFactory & Eventful;
|
|
47
|
-
export interface EventfulFactory {
|
|
48
|
-
<T extends object | Function | undefined, E extends EventMap = EventMap>(object?: T, options?: EventfulOptions): (T extends undefined ? {} : T) & Eventful<E>;
|
|
49
|
-
}
|
|
50
|
-
export interface EventfulOptions {
|
|
51
|
-
/**
|
|
52
|
-
* If true, exceptions from listeners are propagated (fail fast).
|
|
53
|
-
* When false, errors are isolated (ignored) after calling `error` hook.
|
|
54
|
-
*/
|
|
55
|
-
strict?: boolean;
|
|
56
|
-
/**
|
|
57
|
-
* Optional tracing hook. Receives action name and a safe payload.
|
|
58
|
-
* Actions include: 'new', 'on', 'off', 'emit', 'emitAsync'.
|
|
59
|
-
* Use to integrate with your logger without exposing internals.
|
|
60
|
-
*/
|
|
61
|
-
trace?: TraceFn | null;
|
|
62
|
-
/**
|
|
63
|
-
* Optional error hook. Receives structured context of listener failures
|
|
64
|
-
* (error, object, event, listener). Called for sync and async errors.
|
|
65
|
-
*/
|
|
66
|
-
error?: ((error: ListenerErrorArgs) => void) | null;
|
|
67
|
-
}
|
|
68
|
-
export interface Eventful<E extends EventMap = EventMap> {
|
|
69
|
-
/**
|
|
70
|
-
* Subscribe to an event. Returns an unsubscribe function.
|
|
71
|
-
*/
|
|
72
|
-
on(event: keyof E & EventName, listener: Listener<E[keyof E & EventName]>): () => boolean;
|
|
73
|
-
/**
|
|
74
|
-
* Subscribe once to an event. Returns an unsubscribe function
|
|
75
|
-
* (called automatically).
|
|
76
|
-
*/
|
|
77
|
-
once(event: keyof E & EventName, listener: Listener<E[keyof E & EventName]>): () => boolean;
|
|
78
|
-
/**
|
|
79
|
-
* Unsubscribe a previously registered listener. Returns true if removed.
|
|
80
|
-
*/
|
|
81
|
-
off(event: keyof E & EventName, listener: Listener<E[keyof E & EventName]>): boolean;
|
|
82
|
-
/**
|
|
83
|
-
* Emit an event synchronously. All listeners run in order.
|
|
84
|
-
* Errors are isolated (ignored) unless `strict` is true.
|
|
85
|
-
*/
|
|
86
|
-
emit(event: keyof E & EventName, ...args: E[keyof E & EventName]): void;
|
|
87
|
-
/**
|
|
88
|
-
* Emit an event and wait for all listeners (run in parallel).
|
|
89
|
-
* Errors are isolated (ignored) unless `strict` is true.
|
|
90
|
-
*/
|
|
91
|
-
emitAsync(event: keyof E & EventName, ...args: E[keyof E & EventName]): Promise<void>;
|
|
92
|
-
/**
|
|
93
|
-
* Returns true if there is at least one listener for the event.
|
|
94
|
-
*/
|
|
95
|
-
has(event: keyof E & EventName): boolean;
|
|
96
|
-
}
|
|
97
|
-
export {};
|