Know the events you can publish | Know the data you will receive after subscribing to specific event |
---|---|
Typical PubSub, EventBus, EventEmitter (whatever you call it), that you can expect, but fully and hardly typed with full type inference, which means that you will be able to get all autocomplete and autovalidation. It's scalable, very-performant and feature packed with all batteries included with Zero dependencies
NPM: https://www.npmjs.com/package/@kamyil/typed-pubsub
GitHub: https://github.com/Kamyil/typed-pubsub
- Typed PubSub
- How to use it?
- Do I have to declare values on initialization?
- ... so do the values even matter?
- Performance test
- Optional logging
- Using it with JavaScript
- What is PubSub?
- In which way this library is fast?
- I want to unsubscribe specific subscriber. How to do it?
- I want to subscribe for one event publish only
- I want to clear all subscribers
- I want to clear subscribers from specific event
- I want to check if there are any active subscribers for specific event
- I prefer other method names like f.e.
emit()
&listen()
rather thanpublish()
&subscribe()
- I want to publish/subscribe asynchronously. How to do it?
- I want to check history of all publishes and subscribes
- I want to extend the functionality of it
- I want to count how many subscribers specific event has
- Simply import
PubSub
from package (since it's named import, VSCode WebStorm and other IDEs should be able to auto-import it easily)
import { PubSub } from '@kamyil/typed-pubsub';
- Declare some
Events
. Doesn't matter how you call them. The only one important thing is to keep them in one-level nested object. In which way you will declare event names and what data you will pass to them - it's completely up to you
// 1
import { PubSub } from '@kamyil/typed-pubsub';
// 2
const Events = {
'user:registered': { firstName: '', lastName: '', age: 22 },
'age:changed': 40,
'firstName:changed': 'Matt',
'lastName:changed': 'Murdock'
}
- Create
PubSub
instance withEvents
passed
// 1
import { PubSub } from '@kamyil/typed-pubsub';
// 2
const Events = {
'user:registered': { firstName: '', lastName: '', age: 22 },
'age:changed': 40,
'firstName:changed': 'Matt',
'lastName:changed': 'Murdock'
};
// 3
const pubSub = new PubSub({
events: Events
});
- And voilá! You have your PubSub up and running. You can start publishing the events and subscribing to them. With all types auto-infered
You actually don't. Those values just helps TypeScript to infer the types of your data, so you don't need to create any types for them manually. But if you prefer to declare a types manually, then you can pass the type directly as a generic into the PubSub class like here:
import { PubSub } from '@kamyil/typed-pubsub';
type TEvents = {
'user:registered': { firstName: string, lastName: string, age: number },
'age:changed': number,
'firstName:changed': string,
'lastName:changed': string
}
const pubSub = new PubSub<TEvents>({
events: {} as TEvents
});
But remember that you don't need to do it, since TypeScript will nicely auto infer everything from your usage :)
Their only role and purpose is to give TypeScript informations to infer, to allow you to have nice type checking, type inference, error validation and autocompletion. As mentioned in previous point, you can even pass the empty object there and it will work in the runtime, but you will loose all of those convinient TypeScript features. If you prefer to declare events and data model as a types - not runtime values, you can declare them like in example in previous point: Do I have to declare values on initialization?
You can check how this library is performing by checking this link: https://stackblitz.com/edit/typescript-v2k7gx?file=index.ts Test essentially creates new subscriber on every 10ms, while updating value to all subscribers on every 20ms
For debugging purposes you can enable non-verbose logs
const pubSub = new PubSub({
events: Events,
enableLogs: true
});
You can also use this library in normal JavaScript files. If you're using VSCode, you should also have type autocompletion enabled by default, even in JS files
PubSub
is extremely common publish-subscribe
design pattern that allows you to listen on specific events and retrieve data. Every time you call the .publish()
of specific event with some data passed in there, every listener to that event will receive the call and the data you've passed.
You can get more info about this here:
https://www.enjoyalgorithms.com/blog/publisher-subscriber-pattern
- In compare to other PubSub libraries, this one does not store event listeners in the
Map
,Array
orSet
but simple object instead with following model:
{
'event1': {
1: { eventHandler: someSpecificCallback },
2: { eventHandler: differentCallback },
// etc.
},
'event2': {
1: { eventHandler: someSpecificCallback },
2: { eventHandler: differentCallback },
// etc.
}
// etc.
}
It's made this way, because objects are the best store dictionaries for performing heavy reads, since JavaScript engines compile them down to C++ classes which later are being cached. As it's stated here: https://stackoverflow.com/a/49164774
So if you have a write-once read-heavy workload with string keys then you can use an object as a high-performance dictionary
Since PubSub is a pattern where there are not a lot of writes, but there is actually a heavy reading (by using string keys) from it, object seems to be a perfect fit
- Also getting all subscribed event listeners is not performed by using
Array.filter()
orArray.find()
but rather by pointing directly into concrete object, where eventName is a key. So there are no unnecessary loops going on while finding all subscribed event listeners
Every subscribe()
call returns an unsubscribe()
function
const pubSub = new PubSub({events: {testEvent: ''}});
// you can name it whatever you want
const unsubscribeTestEvent = pubSub.subscribe('testEvent', () => { /* ... */ });
// and you can call it whenever you want
unsubscribeTestEvent();
It's made this way, because this returned unsubsribe function contains id of given event listener so it's the most proper way to remove this specific listener from the memory
You can also set a subscribe listener for only one event publish if needed
const pubSub = new PubSub({ events: { testEvent: '' }});
pubSub.subscribeForOnePublishOnly('testEvent', (data) => {/** some callback with data */});
You can do it by using clearAllSubscribers()
method
pubSub.clearAllSubscribers();
You can do it by using clearAllSubscribersFromEvent(eventName)
method,
where you can pass the event name as parameter and it will clear all listeners
of that event
pubSub.clearAllSubscribersFromEvent('name of your event');
You can do it by using hasSubscribers()
method, which returns true
if there are subscribers
for passed eventName, and false
if not.
pubSub.hasSubscribers('name of your event');
Since this library relies hardly on types, it's hard to add some method rename function,
because even if you could run such rename() and start using f.e. pubSub.listen()
or pubSub.emit()
in the runtime,
it unfortunetly wouldn't work properly for types, since TS cannot resolve types dynamically
depending on your runtime usage. So the solution here (not-ideal but it somewhat solves the problem)
would be to create new class that extends this class and remap the names like so:
import { PubSub } from '@kamyil/typed-pubsub';
// Remember to add and pass generic here, to let type inference keep working properly
class EventEmitter<Events> extends PubSub<Events> {
emit = this.publish;
listen = this.subscribe;
hasListeners = this.hasSubscribers;
//... and so on
}
Solution is far from ideal, because you will still get publish()
and subscribe()
and other
non-renamed methods in the autocompletion, but at least you will also be able to use those methods
with a new names that you set which fits your preference, while keeping same functionality (in both types and fast execution in the runtime)
Probably the better solution would be to copy the source code from index.ts
, since whole functionality self-contained within one file and
rename the methods manually. But if you want to get updates, you will also need to manually update the new code within your copied file
If you like this library and decide to copy the source code, please at least leave a star on GitHub or install&uninstall it via npm - it will make me sure
that this library is used by people and it makes sense to further develop it :)
Since it doesn't make really sense for subscribe()
to be async, it is designed to be synchronous only.
If you want to await for some things within subscribe, you can simply mark it's callback/eventHandler as async
pubSub.subscribe('someEvent', async () => {
await something();
});
However, it is not the case for publish, since there might be some edgy cases where you would want to "pause" further code execution
to be completely sure that your publish()
call will be noticed by all subscribers
and all of those subscribers will perform their callbacks before code runs further.
publish()
method has it's async equivalent named publishAsync()
It also accepts eventName and data as it's arguments, but in opposite to normal synchronous publish()
it also returns
boolean, that indicates if publish finished successfully, which means - it found all of it's subscribers and (by default, but can be disabled) all of them
finished their callbacks.
However!, if you don't want to await for all subscribers to finish their callbacks, you can disable it by passing
awaitAllSubscribersFinish
as false
in options
param
Example:
async function onDataFetched(data) {
// It will return true or false when all subscribers receive message and all of them finish their callbacks
const isDataPublished = await pubSub.publishAsync('data:fetched', data);
if (isDataPublished) {
await doSomethingAfterThePublishIsComplete();
await doSomethingElseAfterThePublishIsComplete();
}
}
async function onDataFetched(data) {
// It will return true or false when all subscribers receive message but may not all of them finish their callbacks
const isDataPublished = await pubSub.publishAsync('data:fetched', data, {
awaitAllSubscribersFinish: false
});
if (isDataPublished) {
await doSomethingAfterThePublishIsComplete();
await doSomethingElseAfterThePublishIsComplete();
}
}
You can enable history tracking, by enabling keepHistory
option while instantiating PubSub
const pubSub = new PubSub({
events: Events
keepHistory: true
});
It's a great tool for debugging events. This way you can check history of all publishes, async publishes, subscribes and unsubscribes. And you can log the whole history by running
pubSub.logHistory();
Of course, this option is disabled by default for performance reasons. Not saying that it will badly impact performance
if it will be enabled fe. on production, but it will unnecessarily keep a lot of objects in history
array for no reason
Since it's a simple class, you can easily extend it by using an extends
keyword
export class CustomPubSub extends PubSub {
someNewProperty = '';
someNewFunctionality() {
// ...
}
}
You can do it by using countSubscribers()
method
pubSub.subscribe('someEvent', () => {});
pubSub.subscribe('someEvent', () => {});
const subsAmount = pubSub.countSubscribers();
console.log(subsAmount) // => 2