Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow symbol event names #43

Merged
merged 6 commits into from Feb 25, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 19 additions & 18 deletions index.d.ts
Expand Up @@ -23,7 +23,7 @@ declare class Emittery {

@returns An unsubscribe method.
*/
on(eventName: string, listener: (eventData?: unknown) => void): Emittery.UnsubscribeFn;
on(eventName: (string | symbol), listener: (eventData?: unknown) => void): Emittery.UnsubscribeFn;
stroncium marked this conversation as resolved.
Show resolved Hide resolved

/**
Get an async iterator which buffers data each time an event is emitted.
Expand Down Expand Up @@ -78,27 +78,27 @@ declare class Emittery {
}
```
*/
events(eventName:string): AsyncIterableIterator<unknown>
events(eventName:(string | symbol)): AsyncIterableIterator<unknown>

/**
Remove an event subscription.
*/
off(eventName: string, listener: (eventData?: unknown) => void): void;
off(eventName: (string | symbol), listener: (eventData?: unknown) => void): void;

/**
Subscribe to an event only once. It will be unsubscribed after the first
event.

@returns The event data when `eventName` is emitted.
*/
once(eventName: string): Promise<unknown>;
once(eventName: (string | symbol)): Promise<unknown>;

/**
Trigger an event asynchronously, optionally with some data. Listeners are called in the order they were added, but executed concurrently.

@returns A promise that resolves when all the event listeners are done. *Done* meaning executed if synchronous or resolved when an async/promise-returning function. You usually wouldn't want to wait for this, but you could for example catch possible errors. If any of the listeners throw/reject, the returned promise will be rejected with the error, but the other listeners will not be affected.
*/
emit(eventName: string, eventData?: unknown): Promise<void>;
emit(eventName: (string | symbol), eventData?: unknown): Promise<void>;

/**
Same as `emit()`, but it waits for each listener to resolve before triggering the next one. This can be useful if your events depend on each other. Although ideally they should not. Prefer `emit()` whenever possible.
Expand All @@ -107,14 +107,14 @@ declare class Emittery {

@returns A promise that resolves when all the event listeners are done.
*/
emitSerial(eventName: string, eventData?: unknown): Promise<void>;
emitSerial(eventName: (string | symbol), eventData?: unknown): Promise<void>;

/**
Subscribe to be notified about any event.

@returns A method to unsubscribe.
*/
onAny(listener: (eventName: string, eventData?: unknown) => unknown): Emittery.UnsubscribeFn;
onAny(listener: (eventName: (string | symbol), eventData?: unknown) => unknown): Emittery.UnsubscribeFn;

/**
Get an async iterator which buffers a tuple of an event name and data each time an event is emitted.
Expand Down Expand Up @@ -155,7 +155,7 @@ declare class Emittery {
/**
Remove an `onAny` subscription.
*/
offAny(listener: (eventName: string, eventData?: unknown) => void): void;
offAny(listener: (eventName: (string | symbol), eventData?: unknown) => void): void;

/**
Clear all event listeners on the instance.
Expand Down Expand Up @@ -196,7 +196,8 @@ declare namespace Emittery {
Maps event names to their emitted data type.
*/
interface Events {
[eventName: string]: any;
// Blocked by https://github.com/microsoft/TypeScript/issues/1863, should be
// `[eventName: (string | symbol)]: unknown;`
}

/**
Expand All @@ -216,27 +217,27 @@ declare namespace Emittery {
emitter.emit('end'); // TS compilation error
```
*/
class Typed<EventDataMap extends Events, EmptyEvents extends string = never> extends Emittery {
on<Name extends Extract<keyof EventDataMap, string>>(eventName: Name, listener: (eventData: EventDataMap[Name]) => void): Emittery.UnsubscribeFn;
class Typed<EventDataMap extends Events, EmptyEvents extends (string | symbol) = never> extends Emittery {
on<Name extends Extract<keyof EventDataMap, (string | symbol)>>(eventName: Name, listener: (eventData: EventDataMap[Name]) => void): Emittery.UnsubscribeFn;
on<Name extends EmptyEvents>(eventName: Name, listener: () => void): Emittery.UnsubscribeFn;

events<Name extends Extract<keyof EventDataMap, string>>(eventName: Name): AsyncIterableIterator<EventDataMap[Name]>;
events<Name extends Extract<keyof EventDataMap, (string | symbol)>>(eventName: Name): AsyncIterableIterator<EventDataMap[Name]>;
stroncium marked this conversation as resolved.
Show resolved Hide resolved

once<Name extends Extract<keyof EventDataMap, string>>(eventName: Name): Promise<EventDataMap[Name]>;
once<Name extends Extract<keyof EventDataMap, (string | symbol)>>(eventName: Name): Promise<EventDataMap[Name]>;
once<Name extends EmptyEvents>(eventName: Name): Promise<void>;

off<Name extends Extract<keyof EventDataMap, string>>(eventName: Name, listener: (eventData: EventDataMap[Name]) => void): void;
off<Name extends Extract<keyof EventDataMap, (string | symbol)>>(eventName: Name, listener: (eventData: EventDataMap[Name]) => void): void;
off<Name extends EmptyEvents>(eventName: Name, listener: () => void): void;

onAny(listener: (eventName: Extract<keyof EventDataMap, string> | EmptyEvents, eventData?: EventDataMap[Extract<keyof EventDataMap, string>]) => void): Emittery.UnsubscribeFn;
onAny(listener: (eventName: Extract<keyof EventDataMap, (string | symbol)> | EmptyEvents, eventData?: EventDataMap[Extract<keyof EventDataMap, string>]) => void): Emittery.UnsubscribeFn;
anyEvent(): AsyncIterableIterator<[Extract<keyof EventDataMap, string>, EventDataMap[Extract<keyof EventDataMap, string>]]>;

offAny(listener: (eventName: Extract<keyof EventDataMap, string> | EmptyEvents, eventData?: EventDataMap[Extract<keyof EventDataMap, string>]) => void): void;
offAny(listener: (eventName: Extract<keyof EventDataMap, (string | symbol)> | EmptyEvents, eventData?: EventDataMap[Extract<keyof EventDataMap, string>]) => void): void;

emit<Name extends Extract<keyof EventDataMap, string>>(eventName: Name, eventData: EventDataMap[Name]): Promise<void>;
emit<Name extends Extract<keyof EventDataMap, (string | symbol)>>(eventName: Name, eventData: EventDataMap[Name]): Promise<void>;
emit<Name extends EmptyEvents>(eventName: Name): Promise<void>;

emitSerial<Name extends Extract<keyof EventDataMap, string>>(eventName: Name, eventData: EventDataMap[Name]): Promise<void>;
emitSerial<Name extends Extract<keyof EventDataMap, (string | symbol)>>(eventName: Name, eventData: EventDataMap[Name]): Promise<void>;
emitSerial<Name extends EmptyEvents>(eventName: Name): Promise<void>;
}
}
Expand Down
4 changes: 2 additions & 2 deletions index.js
Expand Up @@ -7,8 +7,8 @@ const anyProducer = Symbol('anyProducer');
const resolvedPromise = Promise.resolve();

function assertEventName(eventName) {
if (typeof eventName !== 'string') {
throw new TypeError('eventName must be a string');
if (typeof eventName !== 'string' && typeof eventName !== 'symbol') {
throw new TypeError('eventName must be a string or a symbol');
}
}

Expand Down
16 changes: 14 additions & 2 deletions readme.md
Expand Up @@ -23,17 +23,29 @@ const Emittery = require('emittery');

const emitter = new Emittery();

const myEvent = Symbol('my symbol event');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use a better example. This doesn't really show how Symbols can be useful as event names.


emitter.on('馃', data => {
console.log(data);
// '馃寛'
console.log(data); // '馃寛'
});

emitter.on(myEvent, data => {
console.log(data); // '馃'
});

emitter.emit('馃', '馃寛');
emitter.emit(myEvent, '馃')

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add this example to the TS file too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have generic usage example in typescript, only examples to clarify some non-obvious things.

```


## API

### eventName

Emittery accepts strings and symbols as event names.
Symbol event names can be used to avoid name collisions when your classes are extended, especially for internal events.

### emitter = new Emittery()

#### on(eventName, listener)
Expand Down
46 changes: 40 additions & 6 deletions test/index.js
Expand Up @@ -15,8 +15,23 @@ test('on()', async t => {
t.deepEqual(calls, [1, 2]);
});

test('on() - eventName must be a string', t => {
test('on() - symbol eventName', async t => {
const emitter = new Emittery();
const eventName = Symbol('eventName');
const calls = [];
const listener1 = () => calls.push(1);
const listener2 = () => calls.push(2);
emitter.on(eventName, listener1);
emitter.on(eventName, listener2);
await emitter.emit(eventName);
t.deepEqual(calls, [1, 2]);
});

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

emitter.on('string', () => {});
emitter.on(Symbol('symbol'), () => {});

t.throws(() => {
emitter.on(42, () => {});
Expand Down Expand Up @@ -137,9 +152,12 @@ test('off()', async t => {
t.deepEqual(calls, [1]);
});

test('off() - eventName must be a string', t => {
test('off() - eventName must be a string or a symbol', t => {
const emitter = new Emittery();

emitter.on('string', () => {});
emitter.on(Symbol('symbol'), () => {});

t.throws(() => {
emitter.off(42);
}, TypeError);
Expand All @@ -161,8 +179,12 @@ test('once()', async t => {
t.is(await promise, fixture);
});

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

emitter.once('string');
emitter.once(Symbol('symbol'));

await t.throwsAsync(emitter.once(42), TypeError);
});

Expand Down Expand Up @@ -202,8 +224,12 @@ test.cb('emit() - multiple events', t => {
emitter.emit('馃');
});

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

emitter.emit('string');
emitter.emit(Symbol('symbol'));

await t.throwsAsync(emitter.emit(42), TypeError);
});

Expand Down Expand Up @@ -302,8 +328,12 @@ test.cb('emitSerial()', t => {
emitter.emitSerial('馃', 'e');
});

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

emitter.emitSerial('string');
emitter.emitSerial(Symbol('symbol'));

await t.throwsAsync(emitter.emitSerial(42), TypeError);
});

Expand Down Expand Up @@ -562,9 +592,13 @@ test('listenerCount() - works with empty eventName strings', t => {
t.is(emitter.listenerCount(''), 1);
});

test('listenerCount() - eventName must be undefined if not a string', t => {
test('listenerCount() - eventName must be undefined if not a string nor a symbol', t => {
const emitter = new Emittery();

emitter.listenerCount('string');
emitter.listenerCount(Symbol('symbol'));
emitter.listenerCount();

t.throws(() => {
emitter.listenerCount(42);
}, TypeError);
Expand Down