GithubHelp home page GithubHelp logo

sindresorhus / emittery Goto Github PK

View Code? Open in Web Editor NEW
1.7K 16.0 71.0 987 KB

Simple and modern async event emitter

License: MIT License

JavaScript 86.83% TypeScript 13.17%
event-emitter event-listener emitter async promise nodejs npm-package javascript

emittery's Introduction

Simple and modern async event emitter

It works in Node.js and the browser (using a bundler).

Emitting events asynchronously is important for production code where you want the least amount of synchronous operations. Since JavaScript is single-threaded, no other code can run while doing synchronous operations. For Node.js, that means it will block other requests, defeating the strength of the platform, which is scalability through async. In the browser, a synchronous operation could potentially cause lags and block user interaction.

Install

npm install emittery

Usage

import Emittery from 'emittery';

const emitter = new Emittery();

emitter.on('๐Ÿฆ„', data => {
	console.log(data);
});

const myUnicorn = Symbol('๐Ÿฆ„');

emitter.on(myUnicorn, data => {
	console.log(`Unicorns love ${data}`);
});

emitter.emit('๐Ÿฆ„', '๐ŸŒˆ'); // Will trigger printing '๐ŸŒˆ'
emitter.emit(myUnicorn, '๐Ÿฆ‹');  // Will trigger printing 'Unicorns love ๐Ÿฆ‹'

API

eventName

Emittery accepts strings, symbols, and numbers as event names.

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

isDebugEnabled

Toggle debug mode for all instances.

Default: true if the DEBUG environment variable is set to emittery or *, otherwise false.

Example:

import Emittery from 'emittery';

Emittery.isDebugEnabled = true;

const emitter1 = new Emittery({debug: {name: 'myEmitter1'}});
const emitter2 = new Emittery({debug: {name: 'myEmitter2'}});

emitter1.on('test', data => {
	// โ€ฆ
});

emitter2.on('otherTest', data => {
	// โ€ฆ
});

emitter1.emit('test');
//=> [16:43:20.417][emittery:subscribe][myEmitter1] Event Name: test
//	data: undefined

emitter2.emit('otherTest');
//=> [16:43:20.417][emittery:subscribe][myEmitter2] Event Name: otherTest
//	data: undefined

emitter = new Emittery(options?)

Create a new instance of Emittery.

options?

Type: object

Configure the new instance of Emittery.

debug?

Type: object

Configure the debugging options for this instance.

name

Type: string
Default: undefined

Define a name for the instance of Emittery to use when outputting debug data.

Example:

import Emittery from 'emittery';

Emittery.isDebugEnabled = true;

const emitter = new Emittery({debug: {name: 'myEmitter'}});

emitter.on('test', data => {
	// โ€ฆ
});

emitter.emit('test');
//=> [16:43:20.417][emittery:subscribe][myEmitter] Event Name: test
//	data: undefined
enabled?

Type: boolean
Default: false

Toggle debug logging just for this instance.

Example:

import Emittery from 'emittery';

const emitter1 = new Emittery({debug: {name: 'emitter1', enabled: true}});
const emitter2 = new Emittery({debug: {name: 'emitter2'}});

emitter1.on('test', data => {
	// โ€ฆ
});

emitter2.on('test', data => {
	// โ€ฆ
});

emitter1.emit('test');
//=> [16:43:20.417][emittery:subscribe][emitter1] Event Name: test
//	data: undefined

emitter2.emit('test');
logger?

Type: Function(string, string, EventName?, Record<string, any>?) => void

Default:

(type, debugName, eventName, eventData) => {
	if (typeof eventData === 'object') {
		eventData = JSON.stringify(eventData);
	}

	if (typeof eventName === 'symbol' || typeof eventName === 'number') {
		eventName = eventName.toString();
	}

	const currentTime = new Date();
	const logTime = `${currentTime.getHours()}:${currentTime.getMinutes()}:${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`;
	console.log(`[${logTime}][emittery:${type}][${debugName}] Event Name: ${eventName}\n\tdata: ${eventData}`);
}

Function that handles debug data.

Example:

import Emittery from 'emittery';

const myLogger = (type, debugName, eventName, eventData) => {
	console.log(`[${type}]: ${eventName}`);
};

const emitter = new Emittery({
	debug: {
		name: 'myEmitter',
		enabled: true,
		logger: myLogger
	}
});

emitter.on('test', data => {
	// โ€ฆ
});

emitter.emit('test');
//=> [subscribe]: test

on(eventName | eventName[], listener)

Subscribe to one or more events.

Returns an unsubscribe method.

Using the same listener multiple times for the same event will result in only one method call per emitted event.

import Emittery from 'emittery';

const emitter = new Emittery();

emitter.on('๐Ÿฆ„', data => {
	console.log(data);
});

emitter.on(['๐Ÿฆ„', '๐Ÿถ'], data => {
	console.log(data);
});

emitter.emit('๐Ÿฆ„', '๐ŸŒˆ'); // log => '๐ŸŒˆ' x2
emitter.emit('๐Ÿถ', '๐Ÿ–'); // log => '๐Ÿ–'
Custom subscribable events

Emittery exports some symbols which represent "meta" events that can be passed to Emitter.on and similar methods.

  • Emittery.listenerAdded - Fires when an event listener was added.
  • Emittery.listenerRemoved - Fires when an event listener was removed.
import Emittery from 'emittery';

const emitter = new Emittery();

emitter.on(Emittery.listenerAdded, ({listener, eventName}) => {
	console.log(listener);
	//=> data => {}

	console.log(eventName);
	//=> '๐Ÿฆ„'
});

emitter.on('๐Ÿฆ„', data => {
	// Handle data
});
Listener data
  • listener - The listener that was added.
  • eventName - The name of the event that was added or removed if .on() or .off() was used, or undefined if .onAny() or .offAny() was used.

Only events that are not of this type are able to trigger these events.

listener(data)

off(eventName | eventName[], listener)

Remove one or more event subscriptions.

import Emittery from 'emittery';

const emitter = new Emittery();

const listener = data => {
	console.log(data);
};

emitter.on(['๐Ÿฆ„', '๐Ÿถ', '๐ŸฆŠ'], listener);
await emitter.emit('๐Ÿฆ„', 'a');
await emitter.emit('๐Ÿถ', 'b');
await emitter.emit('๐ŸฆŠ', 'c');
emitter.off('๐Ÿฆ„', listener);
emitter.off(['๐Ÿถ', '๐ŸฆŠ'], listener);
await emitter.emit('๐Ÿฆ„', 'a'); // Nothing happens
await emitter.emit('๐Ÿถ', 'b'); // Nothing happens
await emitter.emit('๐ŸฆŠ', 'c'); // Nothing happens
listener(data)

once(eventName | eventName[])

Subscribe to one or more events only once. It will be unsubscribed after the first event.

Returns a promise for the event data when eventName is emitted. This promise is extended with an off method.

import Emittery from 'emittery';

const emitter = new Emittery();

emitter.once('๐Ÿฆ„').then(data => {
	console.log(data);
	//=> '๐ŸŒˆ'
});

emitter.once(['๐Ÿฆ„', '๐Ÿถ']).then(data => {
	console.log(data);
});

emitter.emit('๐Ÿฆ„', '๐ŸŒˆ'); // Log => '๐ŸŒˆ' x2
emitter.emit('๐Ÿถ', '๐Ÿ–'); // Nothing happens

events(eventName)

Get an async iterator which buffers data each time an event is emitted.

Call return() on the iterator to remove the subscription.

import Emittery from 'emittery';

const emitter = new Emittery();
const iterator = emitter.events('๐Ÿฆ„');

emitter.emit('๐Ÿฆ„', '๐ŸŒˆ1'); // Buffered
emitter.emit('๐Ÿฆ„', '๐ŸŒˆ2'); // Buffered

iterator
	.next()
	.then(({value, done}) => {
		// done === false
		// value === '๐ŸŒˆ1'
		return iterator.next();
	})
	.then(({value, done}) => {
		// done === false
		// value === '๐ŸŒˆ2'
		// Revoke subscription
		return iterator.return();
	})
	.then(({done}) => {
		// done === true
	});

In practice, you would usually consume the events using the for await statement. In that case, to revoke the subscription simply break the loop.

import Emittery from 'emittery';

const emitter = new Emittery();
const iterator = emitter.events('๐Ÿฆ„');

emitter.emit('๐Ÿฆ„', '๐ŸŒˆ1'); // Buffered
emitter.emit('๐Ÿฆ„', '๐ŸŒˆ2'); // Buffered

// In an async context.
for await (const data of iterator) {
	if (data === '๐ŸŒˆ2') {
		break; // Revoke the subscription when we see the value '๐ŸŒˆ2'.
	}
}

It accepts multiple event names.

import Emittery from 'emittery';

const emitter = new Emittery();
const iterator = emitter.events(['๐Ÿฆ„', '๐ŸฆŠ']);

emitter.emit('๐Ÿฆ„', '๐ŸŒˆ1'); // Buffered
emitter.emit('๐ŸฆŠ', '๐ŸŒˆ2'); // Buffered

iterator
	.next()
	.then(({value, done}) => {
		// done === false
		// value === '๐ŸŒˆ1'
		return iterator.next();
	})
	.then(({value, done}) => {
		// done === false
		// value === '๐ŸŒˆ2'
		// Revoke subscription
		return iterator.return();
	})
	.then(({done}) => {
		// done === true
	});

emit(eventName, data?)

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.

emitSerial(eventName, data?)

Same as above, 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.

If any of the listeners throw/reject, the returned promise will be rejected with the error and the remaining listeners will not be called.

onAny(listener)

Subscribe to be notified about any event.

Returns a method to unsubscribe.

listener(eventName, data)

offAny(listener)

Remove an onAny subscription.

anyEvent()

Get an async iterator which buffers a tuple of an event name and data each time an event is emitted.

Call return() on the iterator to remove the subscription.

import Emittery from 'emittery';

const emitter = new Emittery();
const iterator = emitter.anyEvent();

emitter.emit('๐Ÿฆ„', '๐ŸŒˆ1'); // Buffered
emitter.emit('๐ŸŒŸ', '๐ŸŒˆ2'); // Buffered

iterator.next()
	.then(({value, done}) => {
		// done === false
		// value is ['๐Ÿฆ„', '๐ŸŒˆ1']
		return iterator.next();
	})
	.then(({value, done}) => {
		// done === false
		// value is ['๐ŸŒŸ', '๐ŸŒˆ2']
		// Revoke subscription
		return iterator.return();
	})
	.then(({done}) => {
		// done === true
	});

In the same way as for events, you can subscribe by using the for await statement

clearListeners(eventNames?)

Clear all event listeners on the instance.

If eventNames is given, only the listeners for that events are cleared.

listenerCount(eventNames?)

The number of listeners for the eventNames or all events if not specified.

bindMethods(target, methodNames?)

Bind the given methodNames, or all Emittery methods if methodNames is not defined, into the target object.

import Emittery from 'emittery';

const object = {};

new Emittery().bindMethods(object);

object.emit('event');

TypeScript

The default Emittery class has generic types that can be provided by TypeScript users to strongly type the list of events and the data passed to their event listeners.

import Emittery from 'emittery';

const emitter = new Emittery<
	// Pass `{[eventName]: undefined | <eventArg>}` as the first type argument for events that pass data to their listeners.
	// 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.
	{
		open: string,
		close: undefined
	}
>();

// Typechecks just fine because the data type for the `open` event is `string`.
emitter.emit('open', 'foo\n');

// Typechecks just fine because `close` is present but points to undefined in the event data type map.
emitter.emit('close');

// TS compilation error because `1` isn't assignable to `string`.
emitter.emit('open', 1);

// TS compilation error because `other` isn't defined in the event data type map.
emitter.emit('other');

Emittery.mixin(emitteryPropertyName, methodNames?)

A decorator which mixins Emittery as property emitteryPropertyName and methodNames, or all Emittery methods if methodNames is not defined, into the target class.

import Emittery from 'emittery';

@Emittery.mixin('emittery')
class MyClass {}

const instance = new MyClass();

instance.emit('event');

Scheduling details

Listeners are not invoked for events emitted before the listener was added. Removing a listener will prevent that listener from being invoked, even if events are in the process of being (asynchronously!) emitted. This also applies to .clearListeners(), which removes all listeners. Listeners will be called in the order they were added. So-called any listeners are called after event-specific listeners.

Note that when using .emitSerial(), a slow listener will delay invocation of subsequent listeners. It's possible for newer events to overtake older ones.

Debugging

Emittery can collect and log debug information.

To enable this feature set the DEBUG environment variable to 'emittery' or '*'. Additionally you can set the static isDebugEnabled variable to true on the Emittery class, or myEmitter.debug.enabled on an instance of it for debugging a single instance.

See API for more details on how debugging works.

FAQ

How is this different than the built-in EventEmitter in Node.js?

There are many things to not like about EventEmitter: its huge API surface, synchronous event emitting, magic error event, flawed memory leak detection. Emittery has none of that.

Isn't EventEmitter synchronous for a reason?

Mostly backwards compatibility reasons. The Node.js team can't break the whole ecosystem.

It also allows silly code like this:

let unicorn = false;

emitter.on('๐Ÿฆ„', () => {
	unicorn = true;
});

emitter.emit('๐Ÿฆ„');

console.log(unicorn);
//=> true

But I would argue doing that shows a deeper lack of Node.js and async comprehension and is not something we should optimize for. The benefit of async emitting is much greater.

Can you support multiple arguments for emit()?

No, just use destructuring:

emitter.on('๐Ÿฆ„', ([foo, bar]) => {
	console.log(foo, bar);
});

emitter.emit('๐Ÿฆ„', [foo, bar]);

Related

  • p-event - Promisify an event by waiting for it to be emitted

emittery's People

Contributors

airhorns avatar alexiglesias93 avatar arantes555 avatar arthur-er avatar beckend avatar coreyfarrell avatar dangh avatar davidmurdoch avatar dinoboff avatar fregante avatar futpib avatar godliangcy avatar gurupras avatar iamnapo avatar jhecking avatar kikobeats avatar kohcojlb avatar linusu avatar lorenzofox3 avatar lukehorvat avatar novemberborn avatar revelt avatar richienb avatar sbencoding avatar sindresorhus avatar stroncium avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

emittery's Issues

TypeScript return type for listeners is void

The Emittery code supports awaiting a promise returned from the event listener function, yet the TypeScript return type of the listener is void:

    on<Name extends EventNameFromDataMap<EventDataMap>>(eventName: Name, listener: (eventData: EventDataMap[Name]) => void): Emittery.UnsubscribeFn;
    on<Name extends EmptyEvents>(eventName: Name, listener: () => void): Emittery.UnsubscribeFn;

Should this not at least support returning a promise, or have I misunderstood something?

Why is Emittery.Typed a separate class?

Curious why Typed needs to be a separate class? Couldn't the type signature of the main class just be

class Emittery<EventDataMap extends Events = Events, EmptyEvents extends EventName = never> {
}

to get basically the same result?

`export = Emittery` might not be correct?

export = Emittery;

Microsoft's "API Extractor" (a bit like webpack, but for TypeScript types) doesn't understand this syntax and errors because of it. See: microsoft/rushstack#2220 This is likely an API Extractor bug, and this issue here could be closed, but I thought I'd open it in case a "fix" was warranted anyway.

I think this "fixes" it:

export default Emittery

But I don't know enough about .d.ts files to understand the full ramifications of making this change.

Typescript error: Index signature is missing in type ...

Hello, thanks for the great library. I'm using it with typescript defined event types (to have autocomplete and type checks on event names and parameters)

Example of such definition:

interface EventTypes {
  orderCreated: Order;
  orderPaymentReceived: Order;
  orderPaymentFailed: Order;
  orderCompleted: Order;
  orderCanceled: Order;
}

export class EventService extends Emittery.Typed<EventTypes> {
}

After latest changes in type definition of Events

	interface Events {
		[eventName: string]: any;
	}

it started failing with:

error TS2344: Type 'EventTypes' does not satisfy the constraint 'Events'.
Index signature is missing in type 'EventTypes'.

Support Symbol as `eventName`

Issuehunt badges

See #12 (comment) for use-case.

I think #13 should be implemented first though, so we can make sure it's not hard to define in a TS type definition.


Note: This issue has a bounty, so it's expected that you are an experienced programmer and that you give it your best effort if you intend to tackle this. Don't forget, if applicable, to add tests, docs (double-check for typos), and update TypeScript definitions. And don't be sloppy. Review your own diff multiple times and try to find ways to improve and simplify your code. Instead of asking too many questions, present solutions. The point of an issue bounty is to reduce my workload, not give me more. Include a ๐Ÿฆ„ in your PR description to indicate that you've read this. Thanks for helping out ๐Ÿ™Œ - @sindresorhus


IssueHunt Summary

stroncium stroncium has been rewarded.

Backers (Total: $60.00)

Submitted pull Requests


Tips


IssueHunt has been backed by the following sponsors. Become a sponsor

Fix type compatibility with `p-event`

emittery/index.test-d.ts

Lines 261 to 280 in c4c11e4

// TODO: Fix type compatibility with `p-event`.
// Compatibility with p-event, without explicit types
// {
// const ee = new Emittery();
// pEvent.iterator(ee, 'data', {
// resolutionEvents: ['finish']
// });
// }
// Compatibility with p-event, with explicit types
// {
// const ee = new Emittery<{
// data: unknown;
// error: unknown;
// finish: undefined;
// }>();
// pEvent.iterator(ee, 'data', {
// resolutionEvents: ['finish']
// });
// }

Add TypeScript definition

It'd be great to have a TypeScript definition. Note that there should be a build step to duplicate the definition for the emittery/legacy module.

Enabling debug can cause circular reference error

Repro:

import Emittery from 'emittery'

Emittery.isDebugEnabled = true

const emitter = new Emittery<{foo: any}>()

emitter.on('foo', ev => console.log({ev}))

const x = {} as any
x.x = x

emitter.emit('foo', x)

Error:

TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    --- property 'x' closes the circle

Playground of the above: https://codesandbox.io/s/typescript-playground-export-forked-i5qzj6?file=/index.ts

Surfaced as a bug in umzug: sequelize/umzug#541 (comment)

await emitter.emit() for async and sync listeners

This will be similar to

As I understand that emit is done asynchronously anyway, it would make sense to await for all listeners to finish, async or not, if need be. For example, https://github.com/patarapolw/liteorm/blob/978e0b1eb0b4713fb68dd14f65e9775030c11c2d/src/collection.ts#L170

BTW, about Type Definition, I propose it would be like this https://github.com/patarapolw/types-async-eventemitter/blob/master/src/index.ts

`error` event for error handling

Currently, when you call emit() you can attache .catch() to the returned Promise for error handling. It has a limitation coming from Promise side as Promise can be only once resolved or rejected. But you can have multiple listeners for the event and if multiple listneres errors out then you will be notified only about the first one.

I would like to propose to have defined event name (error seems like good name and follows also similar event in the Node's EventEmitter) which would be called upon listener's error throw or rejection with the object passed with structure:

{
   event: string // name of the event of the listener that throw the error
   error: Error object
}

Emit seems synchronous

Oh this looks really nice @sindresorhus! Having written some of these things before I had a look through the code and something stood out.

Looking at these two lines in the emit() function: https://github.com/sindresorhus/emittery/blob/82cda78b460fd5cd972addec73bd04eac6039a06/index.js#L41:L42

Listeners are executed synchronously, in the same tick as the call to emit(). If a listener throws (rather than returning a rejected promise) no subsequent listeners will be called. This seems to go against the API description:

emit(eventName, [data])

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

Returns a promise for 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.

If I'm reading this right then the following should fix the behavior:

// Top-level
const resolved = Promise.resolve()

async emit(eventName, eventData) {
		await resolved;
		const listeners = [...this._getListeners(eventName)].map(async listener => listener(eventData));
		// and so on
}

Also, rather than [...listeners].map(fn) you could use Array.from(listeners, fn) which I imagine is marginally more efficient.

TS: Constrained generics

I guess is a more generic TS question, but I am trying to extend Emittery.Typed with a generic class that has already some predefined event's names using the generic's contrain like this:

import Emittery from 'emittery'

interface BasicEvents {
  hello: string
}

class GenericTest<ValueEvents extends BasicEvents, NonValueEvents extends string | symbol> extends Emittery.Typed<ValueEvents, NonValueEvents> {
  someMethod (): void {
    this.emit('hello', 'world') // This should be alright
  }
}

But I am getting TS error:

error TS2345: Argument of type '"hello"' is not assignable to parameter of type 'Extract<keyof ValueEvents, string | symbol>'.

330       this.emit('hello', e)
                    ~~~~~~~

Any ideas what I am missing?

TS Playground

`.emit()` should not receive the returned value from `.on()`

I noticed that you can return a value from an event handler, e.g.,

event.on("myEvent", () => 123);
const value = await event.emit("myEvent");
value[0] === 123; // true

But the Emittery.Typed types don't support the return type, so typescript thinks value is always void.

Is this a feature that you'd like to be supported in the TypeScript types? If so, I can probably find some time to put a PR together. Are there any gotchas I should be aware of?

I hacked together something to make it "work" for my code base (it's not quite right), though this would result in breaking changes:

class Typed<EventDataMap extends Events, EmptyEvents extends EventName = never> extends Emittery.Typed {
	on<Name extends EventNameFromDataMap<EventDataMap>>(eventName: Name, listener: (eventData: Parameters<EventDataMap[Name]>[0]) => void): Emittery.UnsubscribeFn;
	on<Name extends EmptyEvents>(eventName: Name, listener: () => void): Emittery.UnsubscribeFn;

	events<Name extends EventNameFromDataMap<EventDataMap>>(eventName: Name): AsyncIterableIterator<Parameters<EventDataMap[Name]>[0]>;

	once<Name extends EventNameFromDataMap<EventDataMap>>(eventName: Name): Promise<ReturnType<EventDataMap[Name]>>;
	once<Name extends EmptyEvents>(eventName: Name): Promise<ReturnType<EventDataMap[Name]>>;

	off<Name extends EventNameFromDataMap<EventDataMap>>(eventName: Name, listener: (eventData: Parameters<EventDataMap[Name]>[0]) => void): void;
	off<Name extends EmptyEvents>(eventName: Name, listener: () => void): void;

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

	offAny(listener: (eventName: EventNameFromDataMap<EventDataMap> | EmptyEvents, eventData?: EventDataMap[EventNameFromDataMap<EventDataMap>]) => void): void;

	emit<Name extends EventNameFromDataMap<EventDataMap>>(eventName: Name, eventData: Parameters<EventDataMap[Name]>[0]): Promise<ReturnType<EventDataMap[Name]>>;
	emit<Name extends EmptyEvents>(eventName: Name): Promise<ReturnType<EventDataMap[Name]>>;

	emitSerial<Name extends EventNameFromDataMap<EventDataMap>>(eventName: Name, eventData: Parameters<EventDataMap[Name]>[0]): Promise<ReturnType<EventDataMap[Name]>>;
	emitSerial<Name extends EmptyEvents>(eventName: Name): Promise<ReturnType<EventDataMap[Name]>>;
}

Suggestion: support synchronous events/emit

I am impressed by the api design of this library, but a big case does need support of synchrounous events: to proxy react input onChange event.

If this event is emitted asynchronously, the composition input like Chinese, Japanese will all fail down.

TypeScript: 0.8.1 breaks re-exported `on`

import Emittery = require('emittery');

type Events = {
  foobar: string;
}

export class MyThing {
  private readonly eventEmitter = new Emittery<Events>();

  on = this.eventEmitter.on.bind(this.eventEmitter);
}

Using 0.8.0 this works fine, but upgrading to 0.8.1 gives

src/index.ts:10:3 - error TS2527: The inferred type of 'on' references an inaccessible 'unique symbol' type. A type annotation is necessary.

10   on = this.eventEmitter.on.bind(this.eventEmitter);
     ~~

src/index.ts:10:3 - error TS4029: Public property 'on' of exported class has or is using name 'listenerAdded' from external module "/Users/simen/repos/emittery-stuff/node_modules/emittery/index" but cannot be named.

10   on = this.eventEmitter.on.bind(this.eventEmitter);
     ~~

src/index.ts:10:3 - error TS4029: Public property 'on' of exported class has or is using name 'listenerRemoved' from external module "/Users/simen/repos/emittery-stuff/node_modules/emittery/index" but cannot be named.

10   on = this.eventEmitter.on.bind(this.eventEmitter);
     ~~


Found 3 errors.

Comes from #73.

I've put together a small repo showing this: https://github.com/SimenB/emittery-type-error.

Note that this only happens if you produce declaration files from source

Wildcard emission

Hi, thanks for your great work.
Does Emittery support wildcard emit?

Simple clarification

Hi! Just for a simple clarification, is this project for NodeJS only? If so, just a tip, add it to your README ๐Ÿ˜‰

Keep up the good work! My best regards! ๐Ÿ‘

catching errors requires await even when calling synchronous functions

const Emittery = require('emittery')
const eventEmitter = new Emittery()

async function asyncFunction() {
    throw new Error('Async function error')
}

eventEmitter.on('callAsyncFunction', asyncFunction)

const runAsyncFunction = async() => {
    try {
        await eventEmitter.emit('callAsyncFunction')
    } catch(e) {
        console.log(e)
    }
}

runAsyncFunction() // Works as expected and prints 'Async function error'.


function syncFunction() {
    throw new Error('Sync function error')
}

eventEmitter.on('callSyncFunction', syncFunction) 

try {
    eventEmitter.emit('callSyncFunction') // Needs await if I want to catch the error even though it is calling a sync function to work as expected.
} catch(e) {
    console.log(e)
}
// Unhandled Promise Rejection. Requires await before eventEmitter.emit('callSyncFunction') even though calling sync function to allow it to catch the error.

Right now I am using the in-built node eventEmitter for synchronous functions and this one for async functions. If this is fixed, I can simply use this emitter and not use the in-built one at all.

Obfuscate internal _events and _anyEvents fields?

_events and _anyEvents could potentially conflict when Emittery is subclassed. We could hide them in a WeakMap or otherwise namespace the fields so collisions are less likely.

If we don't go the WeakMap route we should add them as private fields in the TypeScript definition.

@sindresorhus what's your preference?

TS: `listenerAdded` and `listenerRemoved` not valid values for Emittery.Typed

When trying to subscribe to listener's added / removed on the Emittery.Typed class, TypeScript (v. 3.9.5) complains that:

src/utils.ts:68:13 - error TS2769: No overload matches this call.
  Overload 1 of 2, '(eventName: Extract<keyof T, string | symbol>, listener: (eventData: T[Extract<keyof T, string | symbol>]) => void): UnsubscribeFn', gave the following error.
    Argument of type 'unique symbol' is not assignable to parameter of type 'Extract<keyof T, string | symbol>'.
  Overload 2 of 2, '(eventName: never, listener: () => void): UnsubscribeFn', gave the following error.
    Argument of type 'unique symbol' is not assignable to parameter of type 'never'.

68     this.on(Emittery.listenerAdded, ({ eventName }: Emittery.ListenerChangedData) => {
               ~~~~~~~~~~~~~~~~~~~~~~


src/utils.ts:75:13 - error TS2769: No overload matches this call.
  Overload 1 of 2, '(eventName: Extract<keyof T, string | symbol>, listener: (eventData: T[Extract<keyof T, string | symbol>]) => void): UnsubscribeFn', gave the following error.
    Argument of type 'unique symbol' is not assignable to parameter of type 'Extract<keyof T, string | symbol>'.
  Overload 2 of 2, '(eventName: never, listener: () => void): UnsubscribeFn', gave the following error.
    Argument of type 'unique symbol' is not assignable to parameter of type 'never'.

75     this.on(Emittery.listenerRemoved, () => {
               ~~~~~~~~~~~~~~~~~~~~~~~~

No way to prepend listeners or run listeners in reversed order in emit

Usage case:

  1. On new requiest an event request fired. This allows to capture request state and deserialize access tokens, session from cookies, etc.
  2. On just before the body would be sent, event before-response fired with request and response refs, to serialize security claims into cookies if necessary.
    It is important to schedule for request event handlers like
[interceptSession, rebuildClaims]

while on the before-response the event handlers must be run in opposite order:

[serializeClaims, packSession]

But with current API I can not directly express this with Emittery.
So, I would like to see either

prepend<TName, TData>(eventName:TName, data:TData);

in an Emittery instance, or some flag like:

emitSerial<TName, TData>(name:TName, data:TData, reversed?:boolean):Promise<void>;

in my case I can wrap event handlers registration and postpone actual on calls, until the building of web application will be completed.
But async match of EventEmitter.prependListener and EventEmitter.prependOnceListener may be helpful in some other cases when delayed listeners construction is not possible.

Typescript error while trying to listen to multiple events

type EventMap = {
  ACCOUNT_CREATED: undefined;
  APP_UNLOCKED: undefined;
};

const emitter = new Emittery<EventMap>();
const events: (keyof EventMap)[] = ['ACCOUNT_CREATED', 'APP_UNLOCKED'];
emitter.on(['ACCOUNT_CREATED', 'APP_UNLOCKED'], console.log);

throws

Argument of type '(keyof EventMap)[]' is not assignable to parameter of type 'keyof EventMap | keyof OmnipresentEventData'.

Support multiple events in `on()`

Issuehunt badges

I considered adding support for multiple events in on(), but wanted to wait to see if this is something people actually need.

Relevant Node.js issue: nodejs/node#15697

// @silverwind


IssueHunt Summary

lukks lukks has been rewarded.

Backers (Total: $40.00)

Submitted pull Requests


Tips

v0.9.0 throws a reference error on browser bundles

I'm using emittery on a frontend codebase and esbuild for compiling and bundling, and after updating to v0.9.0 the following error started popping up in the console:

Uncaught ReferenceError: process is not defined
at Function.get isDebugEnabled [as isDebugEnabled]

The error is originated here, as the process variable is not available on the browser.

Suggestion: support numeric event names

It would be cool if numeric "event names" were supported. They can be especially useful in typescript:

import * as emittery from 'emittery';

enum ProtocolMessage {
    Action,
    OtherAction,
}

type ProtocolMessages = {
    [ProtocolMessage.Action]: { aaa: string }
    [ProtocolMessage.OtherAction]: { bbb: 123 }
}

const ee = new emittery.Typed<ProtocolMessages>();

// Argument of type 'ProtocolMessage.Action' is not assignable to parameter of type 'never'.(2345)
ee.emit(ProtocolMessage.Action, { aaa: "messaggeee" });

Allow event's listener to choose if events should be serial or non-serial

Currently, there is the emit()/emitSerial() that allows the emitter (emitting side) to specify if the event's processing should be awaited or not (eq. serial processing or not). I would like to argue that most of the time the emitter does not know and understand in what context are the events processed and if serial/parallel processing should be used, instead the listener is aware of this.

I would like to propose similar functionality on the listener side with functions on() and new onSerial().

I see it that when listener is registered with onSerial(), there would be a specific queue created for this listener which upon emit() would the emitted data be pushed to this queue.

A thing I am not so sure about is how it should behave for all the combinations of emit()/emitSerial() and on()/onSerial(), but that can be though out if this proposition would get traction.

Please let me know your thoughts, happy to write PR for this.

Release notes for 0.8.0 are slightly wrong

Minor thing - the release notes for https://github.com/sindresorhus/emittery/releases/v0.8.0 are not quite right. Right now it's:

-const emitter = new Emittery.Typed<{value: string}, 'open' | 'close'>();
+const emitter = new Emittery<
+	{
+		open: string,
+		close: undefined
+	}
+>();

When it should be:

-const emitter = new Emittery.Typed<{value: string}, 'open' | 'close'>();
+const emitter = new Emittery<
+	{
+		value: string,
+		open: undefined,
+		close: undefined
+	}
+>();
(just for fun, here's an extremely unhelpful diff-diff which I don't think GitHub knows how to render):
-const emitter = new Emittery.Typed<{value: string}, open | close>();
+const emitter = new Emittery<
+{
[-+open:-]{++value:+} string,
{++open: undefined,+}
+close: undefined
+}
+>();

Enhance `emit` return logic

First, thx for this great package!

At first I don't know if the promise of emit resolves to the results of event handlers after a trying with codesandbox. I thought this nice feature could be mentioned in the README.

According to the doc, promise of emit would wait for all events to be done or any to be rejected. Could you support more options such as return on the first resolve?

Add convenience method to bind Emittery methods to own instance

Issuehunt badges

EventEmitter in Node.js is usually subclassed, but I don't like this pattern, and I don't think we should recommend it. It's better to have an internal Emittery instance and just explicitly expose some of the methods.

We could make this even easier, by providing a convenience method to bind the Emittery method to the instance:

class Foo {
	constructor() [
		this.emitter = new Emittery();
		this.emitter.bindMethods(this, ['on', 'off']);
	]
}

The above would quickly expose the on and off methods, but nothing else.


IssueHunt Summary

stroncium stroncium has been rewarded.

Sponsors (Total: $60.00)

Tips

emittery/legacy is not created when installing from GitHub

Apologies, I still haven't figured out how to do this. We should build legacy.js when installing a clone from GitHub, during CI, before publishing, and when using npm to install from GitHub. The latter isn't working now.

$ npm i https://github.com/sindresorhus/emittery
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN [email protected] No description
npm WARN [email protected] No repository field.

+ [email protected]
added 1 package in 4.043s
$ ls node_modules/emittery
index.js  license  package.json  readme.md

Built-in debug info

Issuehunt badges

Running DEBUG=emittery node x.js or DEBUG=* node x.js should output debug information/trace of events.

Events can be hard to debug, so this is important.

I'm looking for feedback on what the trace output should look like.


I don't want to depend on the debug module, but we could easily detect the environment variable manually.


IssueHunt Summary

sbencoding sbencoding has been rewarded.

Backers (Total: $40.00)

Submitted pull Requests


Tips

TS Error when using .events() with multiple event names parameter

Example:

import EventEmitter from 'emittery';

const EE = new EventEmitter.Typed<{
  'event-name-A': string;
  'event-name-B: string;
}>();

const iteratorSingle = EE.events('event-name-A'); // This works and has correct type info
const iteratorMultiple = EE.events(['event-name-A', 'event-name-B']); // This doesn't compile. See error below.

This is the typescript error:

Argument of type 'string[]' is not assignable to parameter of type '"event-name-A" | "event-name-B".
  Type 'string[]' is not assignable to type '"event-name-A"'.

I'm not such a typescript whiz but I assume the type discrimation between single and multi value .events isn't working correctly.

Type-check event names?

Despite the intent communicated in the documentation and tests, Emittery's use of Map means event names can be any kind of JavaScript value โ€”ย not just strings.

I think we should limit this to strings, though it might be nice to also support symbols. That said any TypeScript / Flow interfaces will be easier to use if we only support strings.

Support event namespace

Issuehunt badges

It would be rad if it's possible for creators of libs to create a namespace for their events, it's well used in jQuery for example in Bootstrap.

Bootstrap Modal emit show.bs.modal event, it's allow user to be sure the show event come from the Bootstrap Modal and not an other lib or a general show event.

Currently, except jQuery I don't know an other libs which handle event namespace

See:


Note: This issue has a bounty, so it's expected that you are an experienced programmer and that you give it your best effort if you intend to tackle this. Don't forget, if applicable, to add tests, docs (double-check for typos), and update TypeScript definitions. And don't be sloppy. Review your own diff multiple times and try to find ways to improve and simplify your code. Instead of asking too many questions, present solutions. The point of an issue bounty is to reduce my workload, not give me more. Include a ๐Ÿฆ„ in your PR description to indicate that you've read this. Thanks for helping out ๐Ÿ™Œ - @sindresorhus


IssueHunt Summary

Backers (Total: $60.00)

Become a backer now!

Or submit a pull request to get the deposits!

Tips


IssueHunt has been backed by the following sponsors. Become a sponsor

Writing async code using Emittery. How do I tell my while loop to stop?

I'm new to writing async code. I want to use a while loop to spawn a bunch of async requests until something happens (e.g., the async function createRandomNumber returns a number > 50). I'm trying out the Emittery library, but I'm wondering whether this is right way of accomplishing what I want... Is there a better way to tell the while loop to stop creating random numbers?

const Emittery = require('emittery');
const SECONDS_MILLISECONDS = 1000;
let shouldSendEvents = true;
 
const emitter = new Emittery();
emitter.on(['numberGenerated'], logInfo);
 
function logInfo(data) {
  console.log(data);
  if (data.randomNumber > 50) {
    // emitter.off(['numberGenerated'], logInfo);
    console.log('Stop creating new random numbers...');
    shouldSendEvents = false;
  }
}
 
async function startLoop() {
  while (shouldSendEvents) {
    console.log(`Creating a random number...`);
    createRandomNumber();
    await sleep(100);
  }
}
 
async function createRandomNumber() {
  await sleep(Math.random() * 1 * SECONDS_MILLISECONDS);
  emitter.emit('numberGenerated', { randomNumber: Math.floor(Math.random() * 100) });
}
 
async function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
 
startLoop();

`Emittery.mixin` type is no longer valid for classes with args in their constructor

TypeScript v4.1.5
Emittery v0.8.1

@Emittery.mixin("emittery")
class MyClass {
  constructor(arg: string){
    
  }
}

errors with:

Argument of type 'typeof MyClass' is not assignable to parameter of type 'new () => any'.
  Types of construct signatures are incompatible.
    Type 'new (arg: string) => MyClass' is not assignable to type 'new () => any'.ts(2345)

Maybe this type should be more like:

	static mixin(
		emitteryPropertyName: string | symbol,
		methodNames?: readonly string[]
	): <T extends { new (...args: any): any }>(klass: T) => T; 

(I haven't really tested this type)

Arbitrary listener return type (Typescript)

Right now, the listener can only return void or Promise<void>, but it shouldn't matter since the returned value is unused.
This applies especially to arrow functions:

client.on('disconnect', () => test());
function test() {
  // ...
  return 1;
}

Type 'number' is not assignable to type 'void | Promise'

Maybe use () => unknown or any for the listener instead as it works with void and all other return types

Breaking change in 0.8.1

I used to do this in 0.8.0:

    this.on('app-data:update-state', this.updateAppState);
    this.on('tray:get-options', this.renderTrayOptions);

in a class extends Emittery

but now it will throw.

Revisit off() and offAny() without listener

It seems to me the current behavior of off() and offAny() is a bit of a footgun:

emitter.off('event', myAccidentallyUndefinedListener)

This ends up unsubscribing all listeners for event.

If you view emitters as producers, what is the use case for one event consumer to modify the subscriptions of another? Is this more a feature for the code that created the emitter, and if so perhaps this could be replaced by a generic emitter.dispose() function which unsubscribes all listeners, and prevents new subscriptions?

Legacy Node.js support

I'd be interested in using this for projects that still need to target Node.js 4 and 5 (cough transitive AVA dependencies cough).

One idea might be to transpile index.js into a legacy.js file when publishing (and installing from GitHub). We could muck about with the tests too so we can test that file without too much effort.

This way modern codebases can simply require emittery, but others can require emittery/legacy for compatibility. And index.js stays nice and simple, with native async/await.

@sindresorhus would you be open to this?

Allow setting up emitter

Just like how Cold Observables set up their emitter when they are first subscribed to, something similar should be done here. Using Emittery.listenerAdded, it is possible to detect when events are first listened to so it should be easy to invoke a callback from there.

For example:

const Emittery = require('emittery');

const emitter = new Emittery();

emitter.setup('name', () => {
    // Prepare emitter...
});

// Invoke preparation logic
emitter.on('name', () => {
    //...
})

// @sindresorhus

[TypeScript] type of ofAny doesn't match

typescript: 2.9.1
emittery: 0.4.0 OK
emittery 0.3.0 ERROR
node_modules/emittery/Emittery.d.ts:123:3 - error TS2416: Property 'offAny' in type 'Typed<EventDataMap, EmptyEvents>' is not assignable to the same property in base type 'Emittery'.
  Type '(listener: (eventName: EmptyEvents | keyof EventDataMap, eventData?: EventDataMap[keyof EventData...' is not assignable to type '(listener:(eventName: string, eventData?: any) => any) => void'.
    Types of parameters 'listener' and 'listener' are incompatible.
      Types of parameters 'eventName' and 'eventName' are incompatible.
        Type 'EmptyEvents | keyof EventDataMap' is not assignable to type 'string'.
          Type 'keyof EventDataMap' is not assignable to type 'string'.

123     offAny(listener: (eventName: keyof EventDataMap | EmptyEvents, eventData?: EventDataMap[keyof EventDataMap]) => any): void;

`emit('eventName')` returns as `emit(undefined)`

My emitter:

import Emittery from 'emittery'
import { logger } from './'

const eventBus: Emittery = new Emittery({
  debug: {
    name: 'eventBus',
    enabled: process.env.ENVIRONMENT !== 'production',
    logger: (type, debugName, eventName, eventData) => {
      logger.debug(`${debugName}: ${type} ${JSON.stringify(eventName)}`, eventData)
    }
  }
})

export default eventBus

My handler

eventBus.on('closeMailer', () => {
  logger.debug('Closing mailer')
  mailer.close()
})

My event

eventBus.emit('closeMailer')

The result:

[1630369974586] DEBUG (20026 on selfagency-m1mini.local): eventBus: subscribe "closeMailer"
[1630369974587] DEBUG (20026 on selfagency-m1mini.local): eventBus: emit undefined

When I try to use a Symbol instead of a string, as specified in the README, TypeScript says it's an invalid type for the parameter.

Support listening for listener changes

EventEmitter's implementation:

const EventEmitter = require("events")

const emitter = new EventEmitter()

emitter.on("newListener", (data, callback) => {

})

emitter.on("removeListener", (data, callback) => {

})

cc @sindresorhus

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.