Finsemble: Using The Event Router

Using the Finsemble Router

The Finsemble Router sends and receives messages between windows, enabling communication between all Finsemble components and services. The Router Client API specifies the Router's interface, but a higher level introduction is given here, describing the three types of messaging models supported.

The diagram below illustrates the Router as the center point of all Finsemble communication. Again, all communication between components and services takes place through the Router.

Router Overview Diagram

There are three distinct models within the Finsemble 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—there is no cross talk. Also, because these message exchanges are usually between different windows, they are asynchronous.


First supported model: Listen/Transmit

The diagram below shows the basic usage of Listen/Transmit, where one transmitter (or more) can communicate 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 event won't be received without at least one corresponding listener in place—there is no message queuing. 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 through 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 event 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 set up listeners either during initialization or when a specific service is invoked.

Data Manager Diagram

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, triggering events in Clients B and C in this instance. Any data consistent with JSON can be sent in the transmit event.

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, in which 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 sent from within the responder callback; and 4) the query response is routed back to the query client's callback. The two main distinctions are that only one responder can exist 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

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 respond 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 that 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 subscriber will always receive an initial notification with the current state for a topic. Then, each time a 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 event 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's different for the Finsemble PubSub model is that 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 provide 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 event.

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 request 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 request (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

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 subscribe or publish callbacks. 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 subscribe and publish callbacks 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 publishers and subscribers are decoupled, the commands can be issued before or after the subscribe callbacks; the only difference is what the current state will be when the subscribe callback occurs.

Note: The state data is completely free-form—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 events 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 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, you 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

This essentially defines any PubSub responder starting with the topic substring topicB, enabling any of the following subscriptions or publish events 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 SharedWorker. The more flexible OpenFin IAB is automatically selected for cross-domain windows; the faster SharedWorker 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.


Further Reading

See Router Transport for more information of how the transports work or adding a new transport.

The Finsemble Router is an important part of the Finsemble library, as discussed in Finsemble Lifecycle EVents.