Finsemble: Using The Event Router

Finsemble Event Router


The Finsemble Event Router sends and receives event messages between windows, enabling communication between all Finsemble components and services. Router Client API specifies the interface to the event router, but here a higher level introduction is given, describing the three types of event-messaging models supported along with examples for each.

The diagram below illustrates the Event Router as the center point of all Finsemble communication. Again, all communication between components and services (each a Chromium window) takes place through the Event Router.

Router Overview Diagram

There are three distinct models within the Event Router for exchanging event messages:

  1. Listen/Transmit one-way communication on a channel between one or more transmitters to one or more listeners
  2. Query/Response two-way communication between one or more "clients" querying a channel "responder"
  3. PubSub a subscribe-publish-notify exchange where the subscribers (which receive state notifications) have no couplings to the publishers of state, which is maintained within a PubSub responder that bridges communication.

Note: the messaging models are separated, which primarily means a message channel (or topic) used in one model is distinct from the same channel name used in another model (i.e. there is no cross talk). Also, given these message exchanges are usually between different windows, they are asynchronous. Each of these messaging models are further explained below with sample code provided.


First Supported Model: Listen/Transmit

The diagram below shows the basic usage of Listen/Transmit, where one transmitter (or more) can communication with one or more listeners over a specific virtual channel. More specifically, a transmit event is duplicated by the router service and sent to each of the corresponding listeners, whose callback is then invoked with the transmit data.

Channels do not have to be predefined, but a transmit won't be received without at least one corresponding listener in place -- there is no message queuing in the service router (although something like a message queue could be built using the API). It is up to a service or component to document the specific channel names it uses when building new functionality; although, channel names can also be dynamically defined and pass to clients (though other established channels) -- it all depends on the functionality being provided. Also, once a listener is added for a channel, it will stay in place receiving incoming transmits to the channel until the listener is removed.

If a transmit occurs to a channel without any listeners, a diagnostic message will be logged (when debug is enabled) by the router service, but no run-time error is passed back to the one-way transmitter -- it is the responsibility of services and components to properly setup listeners either during initialization or when a specific service is invoked.

Data Manager Diagram

A Corresponding Listen/Transmit Example

Clients B and C in the above diagram can use something like the following code snippet to create a listener for ChannelA -- with this code events will be passed to the callback function for all incoming transmits to "ChannelA". As long as the error parameter is null, the response parameter will contain the incoming transmit event.

FSBL.Clients.RouterClient.addListener("ChannelA", function(error,response) {
    if (error) {
        console.log("ChannelA Error: ' + JSON.stringify(error));
    } else {
        console.log("ChannelA Response: ' + JSON.stringify(response));
    }
});

Client A in the following snippet transmits to all ChannelA listeners, in the above case triggering events in Clients B and C. Any data consistent with JSON can be sent in the transmit.

FSBL.Clients.RouterClient.transmit("ChannelA", { "dataFieldOneA": 1, "dataFieldTwoA": { "subfieldA": 2 } }); 

In summary, any channel can have multiple transmitters and listeners. If there are no listeners for a channel, the corresponding transmit will be discarded (with a warning sent to the router-service log). The same router client can transmit and listen on the same channel. The same router client can also add multiple listeners for the same channel. Often channel listeners are defined during initialization for components and service, but they can also be dynamically defined (with their names distributed to other clients over other channels using one of the messaging models).


Second Supported Model: Query/Response

The diagram below shows the basic usage of the two-way Query/Response model, where a query is routed to a corresponding responder for a specific virtual channel and a corresponding response is returned. More specifically: 1) a query is routed to the corresponding responder; 2) the responder's callback is invoked; 3) a query response is send from within the responder callback; and 4) the query response is routed back to the query client's callback. The two main distinctions are only one responder can exists for a given virtual channel and each time a responder receives an incoming query, it is required to send back a corresponding response. As with listeners, once a responder is added it stays in place, receiving, and responding to incoming queries until it is removed.

If a responder is added to a channel that already has a responder, an error is returned. If a query is sent to a channel without a responder, an error is returned.

Data Manager Diagram

A Corresponding Query/Response Example

Client B in the diagram above can use something like the following snippet to create "ResponderA" to catch corresponding incoming queries and responds to them. Note the queryMessage passed into the addResponder's callback function contains the function required to send back the corresponding response -- this sendQueryResponse() function should always be called within the responder's callback function.

FSBL.Clients.RouterClient.addResponder("ResponderA", function(error, queryMessage) {
    if (!error) {
        console.log("Responder A Query: ' + JSON.stringify(queryMessage));
        queryMessage.sendQueryResponse(null, { field1: "query response data", field2: queryMessage.data.count } );
    }    
});

For the other half, Client A in the following snippet sends a query to "ResponderA" and processes the corresponding query response when it is received. Note optional data can be included in the query request.

FSBL.Clients.RouterClient.query("ResponderA", { "count": 1 }, function(error, response) {
    if (!error) {
        console.log('Responder A Query Response Response: ' + JSON.stringify(response));
    }
});



Third Supported Model: PubSub

The diagram below illustrates the usage of the PubSub model. PubSub has three primary players:

  1. Subscribers which receive state (i.e., data) updates as published to a specific topic -- a topic is essentially a virtual channel. Each subscribe will always receive an initial notification with the current state for a topic. Then, each time new state is published to a topic, all the subscribers receive notifications with the new state data.
  2. Publishers of state data for a specific topic. Again, each publish will result in a corresponding notification being sent to all of the subscribers.
  3. A topic's Responder must always be defined, but comes with sufficient default built-in functionality to handle most cases. The responder provides a bridge between subscribers and publishers (i.e., subscribers have no knowledge of publishers and visa versa).

What is different for the Finsemble PubSub model is the responder isn't automatically "built-in" for all topics like in some PubSub implementations. By explicitly requiring that a responder be added for each topic, better diagnostics can be provided. Also, when a responder is added it allows the responder client to provided optional callbacks to supplement the default behavior -- default behavior includes delivering to each new subscriber an initial notification with the current state and then a new notification and state for each subsequent publish. (There are also cases where it is useful to define a block of PubSub topics, which is covered below under PubSub with a RegEx Topic.)

Three optional callbacks can be provided when a PubSub responder is added.

  1. A subscribe callback for invocation each time the responder receives a new subscription request. The callback can either "accept" or "reject" the subscription. The default behavior is to accept all subscriptions.
  2. A publish callback for invocation each time the responder receives a new publish request. The callback can either "accept" the publish as is resulting in sent notifications, "reject" it with an error causing the publish to be discarded, or "modify/augment" the publish state data before accepting the publish (with notifications then sent). The default behavior is to accept the new state data and send notifications to all subscribers.
  3. A unsubscribe callback for invocation each time the responder receives a new unsubscribe request. In this case, the default behavior cannot be modified by the callback, but it provides a hook to take additional action before the unsubscribe is completed.

NOTE: if the optional subscribe or publish callbacks are defined, then it is up to PubSub responder client to maintain the internal state for the given topic so it can be returned in the callback responses.

Data Manager Diagram

A Corresponding PubSub Example

Client E in the diagram above can use the command below to define a PubSub responder for "TopicA". This step must be done before any subscribes or publishes. Only the topic and starting state are defined in this snippet (i.e., no optional callbacks are defined) so the default functionality will be provided for all subscribes and publishes to "TopicA".

FSBL.Clients.RouterClient.addPubSubResponder("topicA", { "State": "start" });

Client A and Client B both publish to "TopicA" using the following snippets, each defining new states for "TopicA". Since publisher and subscribers are decoupled, the commands can be issued before or after the subscribes, the only difference being what the current state will be when the subscribe occurs.

NOTE: the state data is completely free format -- it is up to PubSub responder for "TopicA" to define the formats of state data it expects.

FSBL.Clients.RouterClient.publish("topicA", { "NOTIFICATION-STATE": "One" });
FSBL.Clients.RouterClient.publish("topicA", { "NOTIFICATION-STATE": "Two" });

Clients C and D can both use something like the following snippet to subscribe to "TopicA". An initial state is always returned in an underlying notification message causing the defined subscribe callback to be invoked. Subsequent publishes sent to the PubSub responder will result in the same subscribe callback being invoked as each corresponding notifications are received.

FSBL.Clients.RouterClient.subscribe("topicA", function(error, notify) {
    if (error) {
        console.log("Topic A Subscribe Error:" + JSON.stringify(error));
    } else {
        console.log("Topic A Notification:" + JSON.stringify(notify));
    }
});

As outlined earlier, the PubSub responder can optionally catch the incoming events for Subscribe, Publish, or Unsubscribe using something like the following snippet -- the callbacks are defined when the PubSub responder is added. Notice that each callback message includes a callback-response function (e.g., sendNotifyToSubscriber, sendNotifyToAllSubscribers, or removeSubscriber). If a callback-response has the error parameter set, then it essentially rejects the incoming request.

As previously noted, when callbacks for subscribe or publish are defined, it is up to the corresponding client code to maintain the topic's state and return it in the callback (or just pass it though from the incoming message in the publish case).

function subscribeCallback(error, subscribe) {
    if (!error) {
        subscribe.sendNotifyToSubscriber(null, { "NOTIFICATION-STATE": "One" });
    }
}
function publishCallback(error, publish) {
    if (!error) {
        publish.sendNotifyToAllSubscribers(null, publish.data); 
    }
}
function unsubscribeCallback(error, unsubscribe) {
    if (!error) {
        unsubscribe.removeSubscriber();
    }
}

FSBL.Clients.RouterClient.addPubSubResponder("topicA", { "State": "start" }, subscribeCallback, publishCallback, unsubscribeCallback);

PubSub with a RegEx Topic

Occasionally, a client may want to define a range of PubSub topics all at once, typically with built-in functionality for each, as shown in the following PubSub responder:

RouterClient.addPubSubResponder(/topicB.*/, { "NOTIFICATION": "start" }); // RegEx Topic for new PubSubResponder

The above essentially defines any PubSub responder starting with the topic substring "topicB", enabling any of the following subscriptions or publishes along with the resulting default functionality for each individual topic.

    FSBL.Clients.RouterClient.subscribe("topicB12345", notifyCallback);
    FSBL.Clients.RouterClient.subscribe("topicB123", notifyCallback);
    FSBL.Clients.RouterClient.subscribe("topicB2", notifyCallback);
    FSBL.Clients.RouterClient.publish("topicB12345", { "NOTIFICATION": "one" });
    FSBL.Clients.RouterClient.publish("topicB123", { "NOTIFICATION": "one" });
    FSBL.Clients.RouterClient.publish("topicB2", { "NOTIFICATION": "one" });

The Underlying Transport of Event Messages

For extensibility the router design is decoupled from the underlying point-to-point transports between router clients and the central router service. Many transports can be supported depending on the context, but currently there are just two: the OpenFin Inter-Application Bus (IAB) and a HTML5 Shared Worker. The more flexible OpenFin IAB is automatically selected for cross-domain windows; the faster Shared-Worker transport is automatically selected for all local windows (i.e., local components and services) -- any window's location that matches the domain of the Finsemble Application is considered local. See Router Transport for more information of how the transports work or adding a new transport.