emitterkit 0.1.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 +19 -0
- package/README.md +237 -0
- package/index.d.mts +43 -0
- package/index.mjs +93 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright 2026 EmitterKit Contributors
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
|
4
|
+
copy of this software and associated documentation files (the “Software”),
|
|
5
|
+
to deal in the Software without restriction, including without limitation
|
|
6
|
+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
7
|
+
and/or sell copies of the Software, and to permit persons to whom the
|
|
8
|
+
Software is furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
|
11
|
+
all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
18
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
19
|
+
DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# EmitterKit
|
|
2
|
+
|
|
3
|
+
Tiny, dependency-free, type-safe event emitters for TypeScript.
|
|
4
|
+
|
|
5
|
+
Create an event emitter, listen for named events, and emit them wherever they
|
|
6
|
+
need to be handled. TypeScript provides autocomplete for event names and checks
|
|
7
|
+
that each handler and emit call receives the right arguments.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install emitterkit
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createEventEmitter } from 'emitterkit';
|
|
19
|
+
|
|
20
|
+
interface AppEvents {
|
|
21
|
+
userSignedIn: [userId: string, rememberMe: boolean];
|
|
22
|
+
toast: [message: string];
|
|
23
|
+
ready: [];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const events = createEventEmitter<AppEvents>();
|
|
27
|
+
|
|
28
|
+
events.on('userSignedIn', (userId, rememberMe) => {
|
|
29
|
+
console.log(`${userId} signed in; remember: ${rememberMe}`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
events.emit('userSignedIn', 'ada', true);
|
|
33
|
+
events.emit('toast', 'Welcome back!');
|
|
34
|
+
events.emit('ready');
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The event key and its arguments are checked together:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
events.emit('toast'); // Type error: message is required.
|
|
41
|
+
events.emit('ready', 'unexpected'); // Type error: ready has no arguments.
|
|
42
|
+
events.on('missing', () => {}); // Type error: unknown event.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## API
|
|
46
|
+
|
|
47
|
+
### `createEventEmitter<Events>(options?)`
|
|
48
|
+
|
|
49
|
+
Creates a new isolated `EventEmitter`. `Events` maps each event key to a tuple of
|
|
50
|
+
arguments. Event keys may be strings, numbers, or symbols.
|
|
51
|
+
|
|
52
|
+
By default, the same handler can be registered multiple times and each call is
|
|
53
|
+
kept. Pass `{ dedupe: true }` to ignore repeated registrations of the same
|
|
54
|
+
handler for an event key.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const saved = Symbol('saved');
|
|
58
|
+
|
|
59
|
+
const emitter = createEventEmitter<{
|
|
60
|
+
[saved]: [path: string];
|
|
61
|
+
progress: [completed: number, total: number];
|
|
62
|
+
}>();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `emitter.on(eventKey, handler)`
|
|
66
|
+
|
|
67
|
+
Registers `handler` for `eventKey`. Registering the same function more than once
|
|
68
|
+
for an event key creates multiple registrations by default. Returns a function that
|
|
69
|
+
removes that specific registration.
|
|
70
|
+
|
|
71
|
+
### `emitter.once(eventKey, handler)`
|
|
72
|
+
|
|
73
|
+
Registers `handler` for one call only. It is removed immediately before its
|
|
74
|
+
first invocation. Returns a function that removes the handler before then.
|
|
75
|
+
|
|
76
|
+
### `emitter.watch(watcher)`
|
|
77
|
+
|
|
78
|
+
Registers `watcher` for every emitted event. The watcher receives the event key
|
|
79
|
+
followed by its arguments, and runs after handlers registered for that event key.
|
|
80
|
+
Returns a function that removes the watcher.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const stop = emitter.watch((eventKey, ...args) => {
|
|
84
|
+
console.log(eventKey, args);
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### `emitter.off(eventKey, handler)`
|
|
89
|
+
|
|
90
|
+
Unregisters the most recently registered matching handler and returns `true` when
|
|
91
|
+
it was registered, `false` when that event key and handler pair not found.
|
|
92
|
+
|
|
93
|
+
### `emitter.emit(eventKey, ...args)`
|
|
94
|
+
|
|
95
|
+
Calls every handler registered for `eventKey`, forwarding the supplied arguments.
|
|
96
|
+
Handlers added or removed while an event is being emitted take effect on the
|
|
97
|
+
next call to `emit`.
|
|
98
|
+
|
|
99
|
+
## Types
|
|
100
|
+
|
|
101
|
+
`EventEmitter`, `EventHandler`, `EventWatcher`, `EventMap`, and `EventEmitterOptions` are exported for
|
|
102
|
+
use in public APIs:
|
|
103
|
+
|
|
104
|
+
- `EventHandler<EventKey, Events>` is the callback type for one event key, with its arguments inferred from `Events`.
|
|
105
|
+
- `EventWatcher<Events>` is the callback type for observing every emitted event.
|
|
106
|
+
- `EventMap` is the base type for an object that associates event keys with their argument tuples.
|
|
107
|
+
- `EventEmitterOptions` configures an emitter; use `dedupe: true` to ignore repeated handler registrations.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import type { EventEmitter } from 'emitterkit';
|
|
111
|
+
|
|
112
|
+
type EditorEvents = {
|
|
113
|
+
changed: [contents: string];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
function connectEditor(events: EventEmitter<EditorEvents>) {
|
|
117
|
+
events.on('changed', (contents) => save(contents));
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Examples
|
|
122
|
+
|
|
123
|
+
### Destructuring methods
|
|
124
|
+
|
|
125
|
+
The emitter methods do not depend on `this`, so they can be destructured and
|
|
126
|
+
used directly:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const { on, emit } = createEventEmitter<{ saved: [path: string]; }>();
|
|
130
|
+
|
|
131
|
+
on('saved', (path) => console.log(`Saved ${path}`));
|
|
132
|
+
emit('saved', '/tmp/note.md');
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Removing a registration
|
|
136
|
+
|
|
137
|
+
`on`, `once`, and `watch` return a function that removes the exact registration
|
|
138
|
+
they created:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const emitter = createEventEmitter<{ changed: [] }>();
|
|
142
|
+
const stop = emitter.on('changed', () => console.log('Changed'));
|
|
143
|
+
stop();
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Watching all events
|
|
147
|
+
|
|
148
|
+
Use `watch` for logging, diagnostics, or other cross-cutting behavior:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
const emitter = createEventEmitter<{ saved: [path: string] }>();
|
|
152
|
+
const stop = emitter.watch((eventKey, ...args) => {
|
|
153
|
+
console.debug('Event emitted:', eventKey, args);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
emitter.emit('saved', '/tmp/note.md');
|
|
157
|
+
stop();
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Mixing with an object
|
|
161
|
+
|
|
162
|
+
Create a new object with event emitter methods mixed in:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const counter = {
|
|
166
|
+
value: 0,
|
|
167
|
+
...createEventEmitter<{ changed: [value: number] }>(),
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
counter.on('changed', (value) => console.log(value));
|
|
171
|
+
counter.value += 1;
|
|
172
|
+
counter.emit('changed', counter.value);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Adding events to a class
|
|
176
|
+
|
|
177
|
+
Create an emitter for each instance in the constructor. Interface merging adds
|
|
178
|
+
the emitter methods to the class type:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { createEventEmitter, type EventEmitter } from 'emitterkit';
|
|
182
|
+
|
|
183
|
+
type MyClazzEvents = {
|
|
184
|
+
changed: [value: number];
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
interface MyClazz extends EventEmitter<MyClazzEvents> {}
|
|
188
|
+
|
|
189
|
+
class MyClazz {
|
|
190
|
+
constructor() {
|
|
191
|
+
Object.assign(this, createEventEmitter<MyClazzEvents>());
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
setValue(value: number) {
|
|
195
|
+
this.emit('changed', value);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const instance = new MyClazz();
|
|
200
|
+
instance.on('changed', console.log);
|
|
201
|
+
instance.setValue(1);
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Event-specific methods / wrapping
|
|
205
|
+
|
|
206
|
+
Event-specific methods such as `onChanged` are useful when a class exposes a
|
|
207
|
+
small domain API and should not expose the generic emitter directly.
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import { createEventEmitter, type EventHandler } from 'emitterkit';
|
|
211
|
+
|
|
212
|
+
type CounterEvents = {
|
|
213
|
+
changed: [value: number];
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
class Counter {
|
|
217
|
+
private readonly events = createEventEmitter<CounterEvents>();
|
|
218
|
+
private value = 0;
|
|
219
|
+
|
|
220
|
+
onChanged(handler: EventHandler<'changed', CounterEvents>) {
|
|
221
|
+
return this.events.on('changed', handler);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
increment() {
|
|
225
|
+
this.value += 1;
|
|
226
|
+
this.events.emit('changed', this.value);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const counter = new Counter();
|
|
231
|
+
counter.onChanged(console.log);
|
|
232
|
+
counter.increment();
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## License
|
|
236
|
+
|
|
237
|
+
[MIT](LICENSE)
|
package/index.d.mts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** A mapping from event keys to the arguments emitted for that event. */
|
|
2
|
+
export type EventMap = object;
|
|
3
|
+
type EventArgs<EventKey extends keyof Events, Events extends EventMap> = Events[EventKey] extends readonly unknown[] ? Events[EventKey] : never;
|
|
4
|
+
/** A callback that receives the arguments for one event key. */
|
|
5
|
+
export type EventHandler<EventKey extends keyof Events, Events extends EventMap> = (...args: EventArgs<EventKey, Events>) => void;
|
|
6
|
+
/** A callback that receives every emitted event and its arguments. */
|
|
7
|
+
export type EventWatcher<Events extends EventMap> = <EventKey extends keyof Events>(eventKey: EventKey, ...args: EventArgs<EventKey, Events>) => void;
|
|
8
|
+
/** Options for {@link createEventEmitter}. */
|
|
9
|
+
export interface EventEmitterOptions {
|
|
10
|
+
/** Ignore registrations that use a handler already registered for the same event key. */
|
|
11
|
+
dedupe?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A lightweight, type-safe event emitter.
|
|
15
|
+
*
|
|
16
|
+
* Create one with {@link createEventEmitter}; handlers registered on an instance
|
|
17
|
+
* are notified only by `emit` calls on that same instance.
|
|
18
|
+
*/
|
|
19
|
+
export interface EventEmitter<Events extends EventMap = Record<string, unknown[]>> {
|
|
20
|
+
/** Register a handler for an event key. Duplicate registrations are ignored when `dedupe` is enabled. */
|
|
21
|
+
on<EventKey extends keyof Events>(this: void, eventKey: EventKey, handler: EventHandler<EventKey, Events>): () => void;
|
|
22
|
+
/** Register a handler for an event key that is removed after its first call. */
|
|
23
|
+
once<EventKey extends keyof Events>(this: void, eventKey: EventKey, handler: EventHandler<EventKey, Events>): () => void;
|
|
24
|
+
/** Register a callback that is notified for every emitted event. */
|
|
25
|
+
watch(this: void, watcher: EventWatcher<Events>): () => void;
|
|
26
|
+
/** Remove a handler. Returns whether that handler was registered. */
|
|
27
|
+
off<EventKey extends keyof Events>(this: void, eventKey: EventKey, handler: EventHandler<EventKey, Events>): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Notify every handler registered for an event key.
|
|
30
|
+
* Handlers added or removed while emitting take effect on the next emit.
|
|
31
|
+
*/
|
|
32
|
+
emit<EventKey extends keyof Events>(this: void, eventKey: EventKey, ...args: EventArgs<EventKey, Events>): void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Creates an isolated event emitter.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const emitter = createEventEmitter<{ saved: [id: string, at: Date] }>();
|
|
39
|
+
* emitter.on('saved', (id, at) => console.log(id, at.toISOString()));
|
|
40
|
+
* emitter.emit('saved', 'file-42', new Date());
|
|
41
|
+
*/
|
|
42
|
+
export declare function createEventEmitter<Events extends EventMap = Record<string, unknown[]>>({ dedupe }?: EventEmitterOptions): EventEmitter<Events>;
|
|
43
|
+
export {};
|
package/index.mjs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates an isolated event emitter.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* const emitter = createEventEmitter<{ saved: [id: string, at: Date] }>();
|
|
6
|
+
* emitter.on('saved', (id, at) => console.log(id, at.toISOString()));
|
|
7
|
+
* emitter.emit('saved', 'file-42', new Date());
|
|
8
|
+
*/
|
|
9
|
+
export function createEventEmitter({ dedupe = false } = {}) {
|
|
10
|
+
const WATCH_KEY = Symbol();
|
|
11
|
+
const registrations = new Map();
|
|
12
|
+
const noop = () => { };
|
|
13
|
+
const removeLatestRegistration = (eventKey, handler) => {
|
|
14
|
+
const eventRegistrations = registrations.get(eventKey);
|
|
15
|
+
if (!eventRegistrations) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
for (let index = eventRegistrations.length - 1; index >= 0; index -= 1) {
|
|
19
|
+
if (eventRegistrations[index].handler === handler) {
|
|
20
|
+
eventRegistrations.splice(index, 1);
|
|
21
|
+
if (eventRegistrations.length === 0) {
|
|
22
|
+
registrations.delete(eventKey);
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
};
|
|
29
|
+
const removeRegistration = (eventKey, registration) => {
|
|
30
|
+
const eventRegistrations = registrations.get(eventKey);
|
|
31
|
+
const index = eventRegistrations?.indexOf(registration) ?? -1;
|
|
32
|
+
if (index < 0 || !eventRegistrations) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
eventRegistrations.splice(index, 1);
|
|
36
|
+
if (eventRegistrations.length === 0) {
|
|
37
|
+
registrations.delete(eventKey);
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
};
|
|
41
|
+
const addRegistration = (eventKey, registration) => {
|
|
42
|
+
const eventRegistrations = registrations.get(eventKey);
|
|
43
|
+
if (dedupe && eventRegistrations?.some(({ handler }) => handler === registration.handler)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (eventRegistrations) {
|
|
47
|
+
eventRegistrations.push(registration);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
registrations.set(eventKey, [registration]);
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
on(eventKey, handler) {
|
|
56
|
+
const registration = { handler, invoke: handler };
|
|
57
|
+
if (!addRegistration(eventKey, registration)) {
|
|
58
|
+
return noop;
|
|
59
|
+
}
|
|
60
|
+
return () => removeRegistration(eventKey, registration);
|
|
61
|
+
},
|
|
62
|
+
once(eventKey, handler) {
|
|
63
|
+
let registration;
|
|
64
|
+
const invokeOnce = (...args) => {
|
|
65
|
+
removeRegistration(eventKey, registration);
|
|
66
|
+
handler(...args);
|
|
67
|
+
};
|
|
68
|
+
registration = { handler, invoke: invokeOnce };
|
|
69
|
+
if (!addRegistration(eventKey, registration)) {
|
|
70
|
+
return noop;
|
|
71
|
+
}
|
|
72
|
+
return () => removeRegistration(eventKey, registration);
|
|
73
|
+
},
|
|
74
|
+
watch(watcher) {
|
|
75
|
+
const registration = { handler: watcher, invoke: watcher };
|
|
76
|
+
if (!addRegistration(WATCH_KEY, registration)) {
|
|
77
|
+
return noop;
|
|
78
|
+
}
|
|
79
|
+
return () => removeRegistration(WATCH_KEY, registration);
|
|
80
|
+
},
|
|
81
|
+
off(eventKey, handler) {
|
|
82
|
+
return removeLatestRegistration(eventKey, handler);
|
|
83
|
+
},
|
|
84
|
+
emit(eventKey, ...args) {
|
|
85
|
+
for (const { invoke } of [...(registrations.get(eventKey) ?? [])]) {
|
|
86
|
+
invoke(...args);
|
|
87
|
+
}
|
|
88
|
+
for (const { invoke } of [...(registrations.get(WATCH_KEY) ?? [])]) {
|
|
89
|
+
invoke(eventKey, ...args);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "emitterkit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tiny, type-safe event emitters for TypeScript, similar to Node.js EventEmitter",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.mjs",
|
|
7
|
+
"types": "index.d.mts",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./index.d.mts",
|
|
13
|
+
"default": "./index.mjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"index.mjs",
|
|
20
|
+
"index.d.mts"
|
|
21
|
+
],
|
|
22
|
+
"homepage": "https://github.com/lionel87/emitterkit#readme",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/lionel87/emitterkit.git"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/lionel87/emitterkit/issues"
|
|
29
|
+
},
|
|
30
|
+
"author": "László BULIK",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"keywords": [
|
|
33
|
+
"event",
|
|
34
|
+
"event emitter",
|
|
35
|
+
"event bus",
|
|
36
|
+
"events",
|
|
37
|
+
"pubsub",
|
|
38
|
+
"typescript",
|
|
39
|
+
"type-safe"
|
|
40
|
+
]
|
|
41
|
+
}
|