Skip to content

Commit c010e90

Browse files
authored
Allow event names to be numbers (#96)
1 parent 3cf4a0a commit c010e90

File tree

5 files changed

+76
-55
lines changed

5 files changed

+76
-55
lines changed

index.d.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/* eslint-disable no-redeclare */
22

33
/**
4-
Emittery accepts strings and symbols as event names.
4+
Emittery accepts strings, symbols, and numbers as event names.
55
6-
Symbol event names can be used to avoid name collisions when your classes are extended, especially for internal events.
6+
Symbol event names are preferred given that they can be used to avoid name collisions when your classes are extended, especially for internal events.
77
*/
8-
type EventName = string | symbol;
8+
type EventName = PropertyKey;
99

1010
// Helper type for turning the passed `EventData` type map into a list of string keys that don't require data alongside the event name when emitting. Uses the same trick that `Omit` does internally to filter keys by building a map of keys to keys we want to keep, and then accessing all the keys to return just the list of keys we want to keep.
1111
type DatalessEventNames<EventData> = {
@@ -90,7 +90,7 @@ interface DebugOptions<EventData> {
9090
(type, debugName, eventName, eventData) => {
9191
eventData = JSON.stringify(eventData);
9292
93-
if (typeof eventName === 'symbol') {
93+
if (typeof eventName === 'symbol' || typeof eventName === 'number') {
9494
eventName = eventName.toString();
9595
}
9696
@@ -142,7 +142,7 @@ Emittery is a strictly typed, fully async EventEmitter implementation. Event lis
142142
import Emittery = require('emittery');
143143
144144
const emitter = new Emittery<
145-
// Pass `{[eventName: <string | symbol>]: undefined | <eventArg>}` as the first type argument for events that pass data to their listeners.
145+
// Pass `{[eventName: <string | symbol | number>]: undefined | <eventArg>}` as the first type argument for events that pass data to their listeners.
146146
// A value of `undefined` in this map means the event listeners should expect no data, and a type other than `undefined` means the listeners will receive one argument of that type.
147147
{
148148
open: string,
@@ -164,7 +164,7 @@ emitter.emit('other');
164164
```
165165
*/
166166
declare class Emittery<
167-
EventData = Record<string, any>, // When https://github.com/microsoft/TypeScript/issues/1863 ships, we can switch this to have an index signature including Symbols. If you want to use symbol keys right now, you need to pass an interface with those symbol keys explicitly listed.
167+
EventData = Record<EventName, any>,
168168
AllEventData = EventData & _OmnipresentEventData,
169169
DatalessEvents = DatalessEventNames<EventData>
170170
> {

index.js

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,22 @@ const producersMap = new WeakMap();
66
const anyProducer = Symbol('anyProducer');
77
const resolvedPromise = Promise.resolve();
88

9+
// Define symbols for "meta" events.
910
const listenerAdded = Symbol('listenerAdded');
1011
const listenerRemoved = Symbol('listenerRemoved');
1112

13+
// Define a symbol that allows internal code to emit meta events, but prevents userland from doing so.
14+
const metaEventsAllowed = Symbol('metaEventsAllowed');
15+
1216
let isGlobalDebugEnabled = false;
1317

14-
function assertEventName(eventName) {
15-
if (typeof eventName !== 'string' && typeof eventName !== 'symbol') {
16-
throw new TypeError('eventName must be a string or a symbol');
18+
function assertEventName(eventName, allowMetaEvents) {
19+
if (typeof eventName !== 'string' && typeof eventName !== 'symbol' && typeof eventName !== 'number') {
20+
throw new TypeError('`eventName` must be a string, symbol, or number');
21+
}
22+
23+
if (isMetaEvent(eventName) && allowMetaEvents !== metaEventsAllowed) {
24+
throw new TypeError('`eventName` cannot be meta event `listenerAdded` or `listenerRemoved`');
1725
}
1826
}
1927

@@ -33,7 +41,7 @@ function getListeners(instance, eventName) {
3341
}
3442

3543
function getEventProducers(instance, eventName) {
36-
const key = typeof eventName === 'string' || typeof eventName === 'symbol' ? eventName : anyProducer;
44+
const key = typeof eventName === 'string' || typeof eventName === 'symbol' || typeof eventName === 'number' ? eventName : anyProducer;
3745
const producers = producersMap.get(instance);
3846
if (!producers.has(key)) {
3947
producers.set(key, new Set());
@@ -147,7 +155,7 @@ function defaultMethodNamesOrAssert(methodNames) {
147155
return methodNames;
148156
}
149157

150-
const isListenerSymbol = symbol => symbol === listenerAdded || symbol === listenerRemoved;
158+
const isMetaEvent = eventName => eventName === listenerAdded || eventName === listenerRemoved;
151159

152160
class Emittery {
153161
static mixin(emitteryPropertyName, methodNames) {
@@ -223,7 +231,7 @@ class Emittery {
223231
eventData = `Object with the following keys failed to stringify: ${Object.keys(eventData).join(',')}`;
224232
}
225233

226-
if (typeof eventName === 'symbol') {
234+
if (typeof eventName === 'symbol' || typeof eventName === 'number') {
227235
eventName = eventName.toString();
228236
}
229237

@@ -245,13 +253,13 @@ class Emittery {
245253

246254
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames];
247255
for (const eventName of eventNames) {
248-
assertEventName(eventName);
256+
assertEventName(eventName, metaEventsAllowed);
249257
getListeners(this, eventName).add(listener);
250258

251259
this.logIfDebugEnabled('subscribe', eventName, undefined);
252260

253-
if (!isListenerSymbol(eventName)) {
254-
this.emit(listenerAdded, {eventName, listener});
261+
if (!isMetaEvent(eventName)) {
262+
this.emit(listenerAdded, {eventName, listener}, metaEventsAllowed);
255263
}
256264
}
257265

@@ -263,13 +271,13 @@ class Emittery {
263271

264272
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames];
265273
for (const eventName of eventNames) {
266-
assertEventName(eventName);
274+
assertEventName(eventName, metaEventsAllowed);
267275
getListeners(this, eventName).delete(listener);
268276

269277
this.logIfDebugEnabled('unsubscribe', eventName, undefined);
270278

271-
if (!isListenerSymbol(eventName)) {
272-
this.emit(listenerRemoved, {eventName, listener});
279+
if (!isMetaEvent(eventName)) {
280+
this.emit(listenerRemoved, {eventName, listener}, metaEventsAllowed);
273281
}
274282
}
275283
}
@@ -286,14 +294,14 @@ class Emittery {
286294
events(eventNames) {
287295
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames];
288296
for (const eventName of eventNames) {
289-
assertEventName(eventName);
297+
assertEventName(eventName, metaEventsAllowed);
290298
}
291299

292300
return iterator(this, eventNames);
293301
}
294302

295-
async emit(eventName, eventData) {
296-
assertEventName(eventName);
303+
async emit(eventName, eventData, allowMetaEvents) {
304+
assertEventName(eventName, allowMetaEvents);
297305

298306
this.logIfDebugEnabled('emit', eventName, eventData);
299307

@@ -302,7 +310,7 @@ class Emittery {
302310
const listeners = getListeners(this, eventName);
303311
const anyListeners = anyMap.get(this);
304312
const staticListeners = [...listeners];
305-
const staticAnyListeners = isListenerSymbol(eventName) ? [] : [...anyListeners];
313+
const staticAnyListeners = isMetaEvent(eventName) ? [] : [...anyListeners];
306314

307315
await resolvedPromise;
308316
await Promise.all([
@@ -319,8 +327,8 @@ class Emittery {
319327
]);
320328
}
321329

322-
async emitSerial(eventName, eventData) {
323-
assertEventName(eventName);
330+
async emitSerial(eventName, eventData, allowMetaEvents) {
331+
assertEventName(eventName, allowMetaEvents);
324332

325333
this.logIfDebugEnabled('emitSerial', eventName, eventData);
326334

@@ -351,7 +359,7 @@ class Emittery {
351359
this.logIfDebugEnabled('subscribeAny', undefined, undefined);
352360

353361
anyMap.get(this).add(listener);
354-
this.emit(listenerAdded, {listener});
362+
this.emit(listenerAdded, {listener}, metaEventsAllowed);
355363
return this.offAny.bind(this, listener);
356364
}
357365

@@ -364,7 +372,7 @@ class Emittery {
364372

365373
this.logIfDebugEnabled('unsubscribeAny', undefined, undefined);
366374

367-
this.emit(listenerRemoved, {listener});
375+
this.emit(listenerRemoved, {listener}, metaEventsAllowed);
368376
anyMap.get(this).delete(listener);
369377
}
370378

@@ -374,7 +382,7 @@ class Emittery {
374382
for (const eventName of eventNames) {
375383
this.logIfDebugEnabled('clear', eventName, undefined);
376384

377-
if (typeof eventName === 'string' || typeof eventName === 'symbol') {
385+
if (typeof eventName === 'string' || typeof eventName === 'symbol' || typeof eventName === 'number') {
378386
getListeners(this, eventName).clear();
379387

380388
const producers = getEventProducers(this, eventName);
@@ -414,7 +422,7 @@ class Emittery {
414422
}
415423

416424
if (typeof eventName !== 'undefined') {
417-
assertEventName(eventName);
425+
assertEventName(eventName, metaEventsAllowed);
418426
}
419427

420428
count += anyMap.get(this).size;

index.test-d.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ type AnyListener = (eventData?: unknown) => void | Promise<void>;
2121
ee.on('anEvent', async data => Promise.resolve());
2222
ee.on(['anEvent', 'anotherEvent'], async data => undefined);
2323
ee.on(Emittery.listenerAdded, ({eventName, listener}) => {
24-
expectType<string | symbol | undefined>(eventName);
24+
expectType<PropertyKey | undefined>(eventName);
2525
expectType<AnyListener>(listener);
2626
});
2727
ee.on(Emittery.listenerRemoved, ({eventName, listener}) => {
28-
expectType<string | symbol | undefined>(eventName);
28+
expectType<PropertyKey | undefined>(eventName);
2929
expectType<AnyListener>(listener);
3030
});
3131
}
@@ -47,11 +47,11 @@ type AnyListener = (eventData?: unknown) => void | Promise<void>;
4747
const test = async () => {
4848
await ee.once('anEvent');
4949
await ee.once(Emittery.listenerAdded).then(({eventName, listener}) => {
50-
expectType<string | symbol | undefined>(eventName);
50+
expectType<PropertyKey | undefined>(eventName);
5151
expectType<AnyListener>(listener);
5252
});
5353
await ee.once(Emittery.listenerRemoved).then(({eventName, listener}) => {
54-
expectType<string | symbol | undefined>(eventName);
54+
expectType<PropertyKey | undefined>(eventName);
5555
expectType<AnyListener>(listener);
5656
});
5757
};
@@ -102,13 +102,6 @@ type AnyListener = (eventData?: unknown) => void | Promise<void>;
102102
expectAssignable<typeof ee.debug.logger>(myLogger);
103103
}
104104

105-
// Userland can't emit the meta events
106-
{
107-
const ee = new Emittery();
108-
expectError(ee.emit(Emittery.listenerRemoved));
109-
expectError(ee.emit(Emittery.listenerAdded));
110-
}
111-
112105
// Strict typing for emission
113106
{
114107
const ee = new Emittery<{

readme.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ emitter.emit(myUnicorn, '🦋'); // Will trigger printing 'Unicorns love 🦋'
4040

4141
### eventName
4242

43-
Emittery accepts strings and symbols as event names.
43+
Emittery accepts strings, symbols, and numbers as event names.
4444

45-
Symbol event names can be used to avoid name collisions when your classes are extended, especially for internal events.
45+
Symbol event names are preferred given that they can be used to avoid name collisions when your classes are extended, especially for internal events.
4646

4747
### isDebugEnabled
4848

@@ -160,7 +160,7 @@ Default:
160160
eventData = JSON.stringify(eventData);
161161
}
162162

163-
if (typeof eventName === 'symbol') {
163+
if (typeof eventName === 'symbol' || typeof eventName === 'number') {
164164
eventName = eventName.toString();
165165
}
166166

@@ -222,7 +222,7 @@ emitter.emit('🐶', '🍖'); // log => '🍖'
222222

223223
##### Custom subscribable events
224224

225-
Emittery exports some symbols which represent custom events that can be passed to `Emitter.on` and similar methods.
225+
Emittery exports some symbols which represent "meta" events that can be passed to `Emitter.on` and similar methods.
226226

227227
- `Emittery.listenerAdded` - Fires when an event listener was added.
228228
- `Emittery.listenerRemoved` - Fires when an event listener was removed.

test/index.js

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,15 @@ test('on() - listenerAdded offAny', async t => {
133133
t.is(eventName, undefined);
134134
});
135135

136-
test('on() - eventName must be a string or a symbol', t => {
136+
test('on() - eventName must be a string, symbol, or number', t => {
137137
const emitter = new Emittery();
138138

139139
emitter.on('string', () => {});
140140
emitter.on(Symbol('symbol'), () => {});
141+
emitter.on(42, () => {});
141142

142143
t.throws(() => {
143-
emitter.on(42, () => {});
144+
emitter.on(true, () => {});
144145
}, TypeError);
145146
});
146147

@@ -327,14 +328,15 @@ test('off() - multiple event names', async t => {
327328
t.deepEqual(calls, [1, 1]);
328329
});
329330

330-
test('off() - eventName must be a string or a symbol', t => {
331+
test('off() - eventName must be a string, symbol, or number', t => {
331332
const emitter = new Emittery();
332333

333334
emitter.on('string', () => {});
334335
emitter.on(Symbol('symbol'), () => {});
336+
emitter.on(42, () => {});
335337

336338
t.throws(() => {
337-
emitter.off(42);
339+
emitter.off(true);
338340
}, TypeError);
339341
});
340342

@@ -362,13 +364,14 @@ test('once() - multiple event names', async t => {
362364
t.is(await promise, fixture);
363365
});
364366

365-
test('once() - eventName must be a string or a symbol', async t => {
367+
test('once() - eventName must be a string, symbol, or number', async t => {
366368
const emitter = new Emittery();
367369

368370
emitter.once('string');
369371
emitter.once(Symbol('symbol'));
372+
emitter.once(42);
370373

371-
await t.throwsAsync(emitter.once(42), TypeError);
374+
await t.throwsAsync(emitter.once(true), TypeError);
372375
});
373376

374377
test.cb('emit() - one event', t => {
@@ -407,13 +410,21 @@ test.cb('emit() - multiple events', t => {
407410
emitter.emit('🦄');
408411
});
409412

410-
test('emit() - eventName must be a string or a symbol', async t => {
413+
test('emit() - eventName must be a string, symbol, or number', async t => {
411414
const emitter = new Emittery();
412415

413416
emitter.emit('string');
414417
emitter.emit(Symbol('symbol'));
418+
emitter.emit(42);
415419

416-
await t.throwsAsync(emitter.emit(42), TypeError);
420+
await t.throwsAsync(emitter.emit(true), TypeError);
421+
});
422+
423+
test('emit() - userland cannot emit the meta events', async t => {
424+
const emitter = new Emittery();
425+
426+
await t.throwsAsync(emitter.emit(Emittery.listenerRemoved), TypeError);
427+
await t.throwsAsync(emitter.emit(Emittery.listenerAdded), TypeError);
417428
});
418429

419430
test.cb('emit() - is async', t => {
@@ -584,13 +595,21 @@ test.cb('emitSerial()', t => {
584595
emitter.emitSerial('🦄', 'e');
585596
});
586597

587-
test('emitSerial() - eventName must be a string or a symbol', async t => {
598+
test('emitSerial() - eventName must be a string, symbol, or number', async t => {
588599
const emitter = new Emittery();
589600

590601
emitter.emitSerial('string');
591602
emitter.emitSerial(Symbol('symbol'));
603+
emitter.emitSerial(42);
604+
605+
await t.throwsAsync(emitter.emitSerial(true), TypeError);
606+
});
607+
608+
test('emitSerial() - userland cannot emit the meta events', async t => {
609+
const emitter = new Emittery();
592610

593-
await t.throwsAsync(emitter.emitSerial(42), TypeError);
611+
await t.throwsAsync(emitter.emitSerial(Emittery.listenerRemoved), TypeError);
612+
await t.throwsAsync(emitter.emitSerial(Emittery.listenerAdded), TypeError);
594613
});
595614

596615
test.cb('emitSerial() - is async', t => {
@@ -1002,15 +1021,16 @@ test('listenerCount() - works with empty eventName strings', t => {
10021021
t.is(emitter.listenerCount(''), 1);
10031022
});
10041023

1005-
test('listenerCount() - eventName must be undefined if not a string nor a symbol', t => {
1024+
test('listenerCount() - eventName must be undefined if not a string, symbol, or number', t => {
10061025
const emitter = new Emittery();
10071026

10081027
emitter.listenerCount('string');
10091028
emitter.listenerCount(Symbol('symbol'));
1029+
emitter.listenerCount(42);
10101030
emitter.listenerCount();
10111031

10121032
t.throws(() => {
1013-
emitter.listenerCount(42);
1033+
emitter.listenerCount(true);
10141034
}, TypeError);
10151035
});
10161036

0 commit comments

Comments
 (0)