Skip to content

Very hardly typed PubSub, that gives you all autocompletion and auto type validation on both event names and data in callbacks

License

Notifications You must be signed in to change notification settings

Kamyil/typed-pubsub

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

56 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Typed PubSub

Know the events you can publish Know the data you will receive after subscribing to specific event
Zrzut ekranu 2022-11-17 o 02 01 48 Zrzut ekranu 2022-11-17 o 02 02 52

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

How to use it?

  1. 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';
  1. 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'
}
  1. Create PubSub instance with Events 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 
});
  1. And voilá! You have your PubSub up and running. You can start publishing the events and subscribing to them. With all types auto-infered

Zrzut ekranu 2022-11-17 o 02 01 42

Zrzut ekranu 2022-11-17 o 02 01 48

Zrzut ekranu 2022-11-17 o 02 02 09

Zrzut ekranu 2022-11-17 o 02 02 52

Do I have to declare values on initialization?

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 :)

... so do the values even matter?

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?

Performance test

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

Optional logging

For debugging purposes you can enable non-verbose logs

const pubSub = new PubSub({ 
  events: Events, 
  enableLogs: true 
});

Using it with JavaScript

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

What is PubSub?

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 which way this library is fast?

  1. In compare to other PubSub libraries, this one does not store event listeners in the Map, Array or Set 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

  1. Also getting all subscribed event listeners is not performed by using Array.filter() or Array.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

I want to unsubscribe specific subscriber. How to do it?

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

I want to subscribe for one event publish only

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 */});

I want to clear all subscribers

You can do it by using clearAllSubscribers() method

pubSub.clearAllSubscribers();

I want to clear subscribers from specific event

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');

I want to check if there are any active subscribers for specific 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');

I prefer other method names like f.e. emit() & listen() rather than publish() & subscribe()

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 :)

I want to publish/subscribe asynchronously. How to do 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();
  }
}

I want to check history of all publishes and subscribes

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

I want to extend the functionality of it

Since it's a simple class, you can easily extend it by using an extends keyword

export class CustomPubSub extends PubSub {
  someNewProperty = '';
  someNewFunctionality() {
    // ...
  }
}

I want to count how many subscribers specific event has

You can do it by using countSubscribers() method

pubSub.subscribe('someEvent', () => {});
pubSub.subscribe('someEvent', () => {});

const subsAmount = pubSub.countSubscribers();
console.log(subsAmount) // => 2

About

Very hardly typed PubSub, that gives you all autocompletion and auto type validation on both event names and data in callbacks

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published