Skip to content

Asynchronous message subscription

Notifications You must be signed in to change notification settings

janjaap/eventbus

Repository files navigation

CircleCI Greenkeeper badge

Eventbus

The EventBus package enables asynchronous message handling and features two different modules; the PubSub module and the Middleware module. Asynchronous messages can be used to overcome dependencies between (stateful) modules on a page or to prevent callback hell.

PubSub module

This module consists of three core classes; EventBus, Publisher, Subscriber and a helper class; MessageStore. The EventBus is responsible for registering topics (or message categories), keeping track of subscribers and routing published messages to those subscribers that have a subscription that matches the published message.

A publisher is nothing more than a topic owner. All messages that are published by a particular publisher, will only be available in the topic that is owned by that publisher.

A subscriber can subscribe its callback to any number of topics and any number of messages.

The MessageStore can record messages and can be used as middleware for the EventBus.


Example (synchronous messaging)

Flow

- Instantiate EventBus
- Create topic
- Subscribe to topic
- Publish message
- Execute callback

              process message
      +-----------------------------+
      |                             |
+-----v------+                      |
|            |  subscribe           |
| Subscriber +-------------+        |
|            |             |        |
+------------+             |  +-----+----+
                           |  |          |
                           +--+ EventBus |
                              |          |
                              +---^--^---+
                                  |  |
   +-----------+  create topic    |  |
   |           +------------------+  |
   | Publisher |                     |
   |           +---------------------+
   +-----------+  publish message

1. Instantiate the EventBus

const eventBus = new PubSub.EventBus();

2. Create topics

/**
 * A topic can be any string and will act as a container in which messages can be published
 */
const POLYFILLS_LOADED_TOPIC = '✨polyfills✨';
const THIRD_PARTY_LOADED_TOPIC = '3rd-party';
const UI_TOPIC = 'user_interactions';

3. Create publishers

/**
 * A Publisher object takes two arguments; the topic and an EventBus instance. The Publisher will
 * register the topic in the EventBus. Any messages that are published by a Publisher will only be
 * published in the topic that the Publisher registered in the event bus.
 */
const polyfillPublisher = new PubSub.Publisher(POLYFILLS_LOADED_TOPIC, eventBus);
const thirdPartyPublisher = new PubSub.Publisher(THIRD_PARTY_LOADED_TOPIC, eventBus);
const guiPublisher = new PubSub.Publisher(UI_TOPIC, eventBus);

It is not strictly necessary to use a Publisher object to have messages published with the EventBus. The above three publishers will, on instantiation, call the EventBus to create a topic and, on publishing messages, call the EventBus again. Thus, the above example could also be:

eventBus.createTopic(POLYFILLS_LOADED_TOPIC);
eventBus.createTopic(THIRD_PARTY_LOADED_TOPIC);
eventBus.createTopic(UI_TOPIC);

4. Create a subscriber

/**
 * A Subscriber can subscribe itself to one or more topics and listen for publication of one
 * or more messages that are published in those topics.
 *
 * The scrollDetectorSubscriber subscriber will:
 * - subscribe itself to the topics '✨polyfills✨' and '3rd-party'
 * - listen for the publication of the messages 'window_matchmedia' and 'scrollmonitor'
 * - execute its callback when both messages have been published
 */
const scrollDetectorSubscriber = new PubSub.Subscriber(
    ['window_matchmedia', 'scrollmonitor'],
    () => debugger,
).subscribeToMany([POLYFILLS_LOADED_TOPIC, THIRD_PARTY_LOADED_TOPIC]);

/**
 * The userClickedSubscriber subscriber will:
 * - subscribe itself to the topic 'user_interactionss'
 * - listen for the publication of the messages 'button_clicked' and 'checkbox_checked'
 * - execute its callback when each of those messages has been published
 */
const userClickedSubscriber = new PubSub.Subscriber(
    ['button_clicked', 'checkbox_checked'],
    () => { console.log('UI element clicked!'); },
).subscribeToOne(UI_TOPIC)
    .requireAllMessages(false);

Note that, in order to register a subscriber with the EventBus, the topic (or topics) that the subscriber subscribes itself to, have to already be created in the EventBus.

5. Publish messages

// feature detecting
if ('matchMedia' in window) {
    polyfillPublisher.send('window_matchmedia');
}

// 3rd-party dependency loading, in this example with loadJS
window.loadJS('//cdnjs.cloudflare.com/ajax/libs/scrollmonitor/1.2.0/scrollMonitor.js', () =>
    thirdPartyPublisher.send('scrollmonitor');
);

// interacting with GUI elements
buttonElement.addEventListener('click', () => guiPublisher.send('button_clicked'));
checkboxElement.addEventListener('change', () => guiPublisher.send('checkbox_checked'));

As with creating publishers as topic owners, it's also not required to have messages published by means of a Publisher object. The EventBus can be called directly, like so:

eventBus.publish('window_matchmedia', POLYFILLS_LOADED_TOPIC);
eventBus.publish('scrollmonitor', THIRD_PARTY_LOADED_TOPIC);

The EventBus also has a default topic (__global__) that can be subscribed to and published in. Calling eventBus.publish('window_matchmedia') will publish that message in the default topic. Subscribers still have to be subscribed to that default topic in order to receive message from it:

const defaultTopicSubscriber = new PubSub.Subscriber('message_to_listen_to',
    () => { console.log('Message has been published'); },
).subscribeToOne(eventBus.defaultTopic);

Changing the default topic is as easy as eventBus.setDefaultTopic('__default__');.

Publishing messages before subscribers have subscribed to them will not lead to the subscriber callback being called. This asynchronous behaviour can be achieved by making use of the middleware module.

Middleware module

The Middleware module has a Manager class that allows for middleware class objects to hook into other classes so that actions can be performed before or after the original class method has executed. It's also possible to completely override the class method. Next to the Manager class, the Middleware module has the MessageRecorder as well as the PublicationPoller helper classes.


Using the middleware manager

The middleware manager chains its middleware class instances; they are executed in the reverse order they are defined in the manager's use method. Each middleware class has access to the target object as well as to the next method in the chain, which can be the target's method or the following middleware class method.

Take the following (simplified) example:

// class responsible for sending messages to subscribers
class EventBus {
    constructor() {
        this.subscribers = [new Subscriber(), new Subscriber(), new Subscriber()];
    }

    publish(message) {
        this.subscribers.forEach(subscriber => subscriber.process(message));
    }
}

// class responsible for storing messages
class MessageStore {
    constructor() {
        this.messages = [];
    }

    record(message) {
        this.messages.push(message);
    }
}

// middleware class
class MessageVerifier {
    // replace invalid characters in messages
    publish(target) {
        return next => (message, topic) => {
            message = message.replace(/[\|&;\$%@"<>\(\)\+,]/g, '');

            // break the chain if the message turns out to be an empty string
            if (message.length === 0) {
                return false;
            }

            return next(message, topic);
        };
    }
}

// middleware class
class MessageRecorder {
    constructor(messageStore) {
        this.messageStore = messageStore;
    }

    // store published messages in a separate store
    publish(target) {
        return next => (message, topic) => {
            const result = next(topic);

            this.messageStore.record(message);

            return result;
        };
    }
}

const eventBus = new EventBus();

middlewareManager.use(
    eventBus,
    MessageRecorder,
    MessageVerifier,
);

Both middleware classes hook into the EventBus' publish() method. The MessageVerifier middleware class prepends its functionality by removing invalid charactes from the passed in message parameter value and then passing that value on to the next function in line. If a middleware class object is the last in line, calling next() will be equal to calling the method of the target object that the middleware class hooks into. Returning false from any method in a middleware class will break the chain and stop execution.

The MessageRecorder middleware class appends its functionality to the target method by calling next() first, storing the value for the message parameter and then returning the result of the call to next().

By using the above two middleware classes, the order of actions, when calling the publish() on the EventBus, are:

  • remove invalid characters, break the chain if the message is empty
  • publish message, calling all subscribers
  • store message for later use

Example (Asynchronous messaging)

Subscribers can subscribe to messages that have already been published in a topic that a subscriber creates a subscription for. Messages that a subscriber wants to listen to can be stored in a separate message store, so that any subscriber can be notified whenever it subscribes to a topic where messages, that it wants to listen to, have already been published.

Flow

- Instantiate EventBus
- Publish message
- Create topic
- Publish message
- Subscribe to topic
- Execute callback

                                 process message
              +----------------------------^---------------------+
              |                            |                     |
              |                            |                     |
              |                            |                     |
        +-----v------+           +---------+---------+           |
        |            | subscribe |                   |           |
        | Subscriber +-----------> PublicationPoller +--+        |
        |            |           |                   |  |        |
        +------------+           +---------+---------+  |        |
                                           |            |        |
                                           |            |   +----+-----+
                                           |            |   |          |
                                   isMessageRecorded    +---+ EventBus |
                                           |                |          |
                                           |                +--^----^--+
                                           |                   |    |
+-----------+    create topic     +--------v--------+          |    |
|           +--------------------->                 +----------+    |
| Publisher |                     | MessageRecorder |               |
|           +--------------------->                 +---------------+
+-----------+   publish message   +----+--------+---+
                                       |        |
                                     create   publish
                                     topic    message
                                       |        |
                                    +--v--------v--+
                                    |              |
                                    | MessageStore |
                                    |              |
                                    +--------------+

1. Main actors

const eventBus = new PubSub.EventBus();
const middlewareManager = new Middleware.Manager();
const messageStore = new PubSub.MessageStore(eventBus);
const publicationPoller = new Middleware.PublicationPoller(messageStore);

// topics
const POLYFILLS_LOADED_TOPIC = '✨polyfills✨';
const THIRD_PARTY_LOADED_TOPIC = '3rd-party';
const UI_TOPIC = 'user_interactions';

// publishers
const polyfillPublisher = new PubSub.Publisher(POLYFILLS_LOADED_TOPIC, eventBus);
const thirdPartyPublisher = new PubSub.Publisher(THIRD_PARTY_LOADED_TOPIC, eventBus);
const guiPublisher = new PubSub.Publisher(UI_TOPIC, eventBus);

// subscriber
const otherSubscriber = new PubSub.Subscriber(
    ['message#1', 'message#2'],
    () => debugger,
).subscribeToMany([POLYFILLS_LOADED_TOPIC, THIRD_PARTY_LOADED_TOPIC]);

2. Using the middleware manager

The middleware manager's main method is use(target, ...middlewares). The first parameter is the target that the rest of the parameters will act as middleware for.

A middleware class object should contain all those methods that it needs to hook into from the target object.

/**
 * First, we'll hook a middleware class into the EventBus class instance so that we can catch all
 * the messages that are published through the bus.
 * The MessageRecorder class uses the MessageStore instance to record all the messages
 */
middlewareManager.use(
    eventBus,
    new Middleware.MessageRecorder(messageStore),
);

/**
 * Then we hook a PublicationPoller middleware class instance into the previously created subscriber
 * and pass in the same MessageStore class instance to the PublicationPoller.
 * The responsiblity of the PublicationPoller is to check if all required messages have been
 * published when a subscriber subscribers itself to a particular topic and, when that is the case,
 * execute the subscriber's callback function.
 */
middlewareManager.use(
    otherSubscriber,
    new Middleware.PublicationPoller(messageStore),
);

The above construct will persist messages for later use. However, messages will only be persisted when the MessageRecorder middleware class instance has been hooked into the EventBus. Messages that are published before that are lost and cannot be acted upon at a later point in time.

Available bundles

This repo comes with a set of bundled modules:

app Example file that contains an example implementation
middleware Complete Middleware module containing the classes 'Manager', 'MessageRecorder' and 'PublicationPoller'
middleware_manager Just the 'Manager' class
pubsub Complete PubSub module containing the class 'EventBus', 'MessageStore', 'Publisher' and 'Subscriber'
pubsub_middleware Package containing all class from the PubSub and the MiddleWare module