Channel API
OpenFin’s Channel API on the InterApplicationBus (IAB) provides optionality for secure desktop messaging between your application and other OpenFin applications. These approaches enable an end-user to send/receive data or events across applications without the need to leave the desktop or rekey in information.
The OpenFin Channel API permits communication between OpenFin applications via an asynchronous request-response messaging channel. It offers asynchronous methods for discovery, transfer of data, and lifecycle management, allowing applications to complete events outside of the main process flow while permitting multiple actions at the same time. There is only one owner or channelProvider
per channel, but multiple clients or channelClient
can connect to a channel. A channel creates a pathway between the channelClient
and channelProvider
for messages to be dispatched bidirectionally.
Why use the Channel API
The Channels API is recommended for developers who want to employ asynchronous calls into their workflow, employ two-way, secure communication between themselves and clients, or provide a service on OpenFin. By convention, OpenFin-developed system apps use the Channels API to provide an API to other applications running on the desktop. This is an ideal case for where one wants to expose their own client-side API. The Provider creates a channel which clients can then connect to and send messages across.
Provider
A channel can be created with a unique channel name by calling Channel.create
. The create method returns a promise that resolves to an instance of the channelProvider
bus. The caller then becomes the “channel provider” and can use the channelProvider
bus to register actions and middleware. The caller can also set an onConnection
and/or onDisconnection
listener that executes on any new channel connection/disconnection attempt from a channel client.
To reject a connection, simply throw an error in the onConnection
listener. A map of client connections is updated automatically on client connection and disconnection and saved in the [read-only] connections property on the channelProvider
bus. The channel exists until the provider destroys it or disconnects by closing or destroying the context (navigating or reloading).
Provider Setup
async function makeProvider() {
// entity creates a channel and becomes the channelProvider
const providerBus = await fin.InterApplicationBus.Channel.create('channelName');
providerBus.onConnection((identity, payload) => {
// can reject a connection here by throwing an error
console.log('Client connection request identity: ', JSON.stringify(identity));
console.log('Client connection request payload: ', JSON.stringify(payload));
});
providerBus.register('topic', (payload, identity) => {
// register a callback for a 'topic' to which clients can dispatch an action
console.log('Action dispatched by client: ', JSON.stringify(identity));
console.log('Payload sent in dispatch: ', JSON.stringify(payload));
});
}
static async void MakeProvider()
{
var fin = GetRuntime();
var providerBus = fin.InterApplicationBus.Channel.CreateProvider("channelName");
await providerBus.OpenAsync();
providerBus.Opened += (identity, payload) =>
{
Console.WriteLine("Client connection request identity: ", identity?.ToString());
Console.WriteLine("Client connection request payload: ", payload?.ToString());
};
providerBus.RegisterTopic<Object>("topic", (payload, client) =>
{
Console.WriteLine("Action dispatched by client: ", client.RemoteEndpoint);
Console.WriteLine("Payload sent in dispatch: ", payload?.ToString());
});
}
public void makeProvider() {
desktopConnection.getChannel(CHANNEL_NAME).createAsync().thenAccept(provider -> {
provider.addProviderListener(new ChannelProviderListener() {
@Override
public void onClientConnect(ChannelClientConnectEvent connectionEvent) throws Exception {
logger.info(String.format("provider receives client connect event from %s ", connectionEvent.getUuid()));
JSONObject payload = (JSONObject) connectionEvent.getPayload();
if (payload != null) {
String name = payload.optString("name");
if ("badguy".equals(name)) {
// throw exception here to reject the connection
throw new Exception("stay out");
}
}
}
@Override
public void onClientDisconnect(ChannelClientConnectEvent connectionEvent) {
logger.info(String.format("provider receives channel disconnect event from %s ", connectionEvent.getUuid()));
}
});
provider.register("topic", new ChannelAction() {
@Override
public JSONObject invoke(String action, Object payload, JSONObject senderIdentity) {
logger.info(String.format("provider processing action %s, payload=%s", action, payload.toString()));
JSONObject obj = new JSONObject();
obj.put("response", "123");
return obj;
}
});
});
}
Client
A connection can be made to a channel as a channelClient
by calling Channel.connect
with a given channel name. The connection request is routed to the channelProvider
. The connect call returns a promise that will resolve with a channelClient
bus if accepted by the channelProvider
, or reject if the channelProvider
throws an error to reject the connection. This bus can communicate with the Provider, but not to other clients on the channel. Using the bus, the channelClient
can register actions and middleware. Channel lifecycle can also be handled with an onDisconnection
listener.
NOTE
If the connection request is sent prior to creation, the promise will not resolve or reject until the channel is created by a
channelProvider
. Whether to wait for creation is configurable in theconnectOptions
.
NOTE
The .NET client is expected to persist references to its channel providers. If a channel Close event is received, the .NET client should have reconnection logic in the channel Close event handler to reconnect to the channel provider.
Client Setup
async function makeClient(channelName) {
// A payload can be sent along with channel connection requests to help with authentication
const connectPayload = { payload: 'token' };
// If the channel has been created this request will be sent to the provider. If not, the
// promise will not be resolved or rejected until the channel has been created.
const clientBus = await fin.InterApplicationBus.Channel.connect('channelName', connectPayload);
clientBus.onDisconnection(channelInfo => {
// handle the channel lifecycle here - we can connect again which will return a promise
// that will resolve if/when the channel is re-created.
makeClient(channelInfo.channelName);
})
clientBus.register('topic', (payload, identity) => {
// register a callback for a topic to which the channel provider can dispatch an action
console.log('Action dispatched by provider: ', JSON.stringify(identity));
console.log('Payload sent in dispatch: ', JSON.stringify(payload));
return {
echo: payload
};
});
}
static async void MakeClient()
{
var fin = GetRuntime();
var opts = new ChannelConnectOptions("channelName")
{
// A payload can be sent along with channel connection requests to help with authentication
Payload = "token"
};
var clientBus = fin.InterApplicationBus.Channel.CreateClient(opts);
// If the channel has been created this request will be sent to the provider. If not, the
// promise will not be resolved or rejected until the channel has been created.
await clientBus.ConnectAsync();
clientBus.Closed += (identity, payload) =>
{
// handle the channel lifecycle here - we could attempt re-open a closed channel
// or clean up after it, or else write to logs
Console.WriteLine("Channel closed by: ", identity);
Console.WriteLine("Channel closed payload: ", payload);
};
// register a callback for a topic to which the channel provider can dispatch an action
clientBus.RegisterTopic<Object>("topic", (payload) =>
{
Console.WriteLine("Payload sent in dispatch: ", payload);
});
}
public void MakeClient() {
JSONObject payload = new JSONObject();
payload.put("name", "java example");
desktopConnection.getChannel(CHANNEL_NAME).connectAsync(payload).thenAccept(client -> {
client.register("event", new ChannelAction() {
@Override
public JSONObject invoke(String action, Object payload, JSONObject senderIdentity) {
logger.info("channel event {}", action);
return null;
}
});
client.register("topic", new ChannelAction() {
@Override
public Object invoke(String action, Object payload, JSONObject jsonObject) {
logger.info(String.format("provider processing action %s, payload=%s", action, payload.toString()));
return null;
}
});
});
}
Messaging
Messages can be dispatched from the channelProvider
to any channelClient
or from any channelClient
to the channelProvider
. The use case below demonstrates the usage of a channel, in which a message is dispatched from a client to the provider for a specific topic and a corresponding acknowledgement is returned to the client:
- Message dispatched over the channel for a specific topic to the corresponding provider
- Provider callback for that registered action is invoked
- Return value of the registered action is sent from the provider as a payload to the acknowledgement (ack) response or the error message to a negative-acknowledgement (nack)
- Ack or nack is routed back to the client and the promise resolves with this payload
Messaging Example
// previously created channelBus here could be a client or provider
channelBus.register('topic1', (payload, identity) => {
// register a callback for a given topic to which the sender can dispatch an action
const somePayload = someAction(payload, identity);
// The return value will be returned as a payload in response to the sender
return somePayload;
});
channelBus.setDefaultAction((topic, payload, senderIdentity) => {
// register a callback for topics that do not have a registered action
return 'Dispatch will now resolve with this payload instead of reject on sender side.';
});
// if channelBus is a providerBus, there are three arguments to dispatch
// and the first argument determines which client to target.
const response = await channelBus.dispatch('topic2', 'payload');
// the dispatch method returns a promise that resolves to the return value of the
// register method's listener of the channel counterpart.
document.querySelector('#someId').innerText = response;
static async void MakeClient()
{
var fin = GetRuntime();
var opts = new ChannelConnectOptions("channelName")
{
// A payload can be sent along with channel connection requests to help with authentication
Payload = "token"
};
var clientBus = fin.InterApplicationBus.Channel.CreateClient(opts);
// If the channel has been created this request will be sent to the provider. If not, the
// promise will not be resolved or rejected until the channel has been created.
await clientBus.ConnectAsync();
clientBus.Closed += (identity, payload) =>
{
// handle the channel lifecycle here - we could attempt re-open a closed channel
// or clean up after it, or else write to logs
Console.WriteLine("Channel closed by: ", identity);
Console.WriteLine("Channel closed payload: ", payload);
};
// register a callback for a topic to which the channel provider can dispatch an action
clientBus.RegisterTopic<Object>("topic", (payload) =>
{
Console.WriteLine("Payload sent in dispatch: ", payload);
});
}
/**
* Create a provider that supports "getValue", "increment" and "incrementBy n" actions
*/
public void createChannelProvider() {
// Create the channel provider.
desktopConnection.getChannel(CHANNEL_NAME).createAsync().thenAccept(provider -> {
provider.addProviderListener(new ChannelProviderListener() {
// Create the onChannelConnect event handler.
@Override
public void onClientConnect(ChannelClientConnectEvent connectionEvent) throws Exception {
// Add a line to the log file to identify the UUID of the caller.
logger.info(String.format("provider receives client connect event from %s ", connectionEvent.getUuid()));
// Extract the JSON payload.
JSONObject payload = (JSONObject) connectionEvent.getPayload();
// If the "name" element of the payload says the client is invalid, reject the request.
if (payload != null) {
String name = payload.optString("name");
if ("Invalid Client".equals(name)) {
throw new Exception("request rejected");
}
}
}
// Create the onChannelDisconnect event handler.
@Override
public void onClientDisconnect(ChannelClientConnectEvent connectionEvent) {
// Add a line to the log file identifying the UUID of the caller.
logger.info(String.format("provider receives channel disconnect event from %s ", connectionEvent.getUuid()));
}
});
// The provider was created. Now to register the actions.
// ------------------------------------------------------
// This variable is used as the "value" element for the getValue, increment, and incrementBy actions.
AtomicInteger localInteger = new AtomicInteger(0);
// Register the "getValue" action.
// This action will return the value of the localInteger variable.
provider.register("getValue", new ChannelAction() {
// This is the logic for the "getValue" action.
@Override
public JSONObject invoke(String action, Object payload, JSONObject senderIdentity) {
// Write a string to the logfile that shows the requested action and payload.
logger.info(String.format("provider processing action %s, payload=%s", action, payload.toString()));
// Create a JSON object to return to the channel client.
JSONObject obj = new JSONObject();
// Set the "value" JSON element to the value of the localInteger variable.
obj.put("value", localInteger.get());
// Return the JSON object to the channel client.
return obj;
}
});
// Register the "increment" action.
// This action will increment the value of the localInteger variable by one.
provider.register("increment", new ChannelAction() {
// This is the logic for the "increment" action.
@Override
public JSONObject invoke(String action, Object payload, JSONObject senderIdentity) {
// Write a string to the logfile that identifies the action and payload.
logger.info(String.format("provider processing action %s, payload=%s", action, payload.toString()));
// Create a JSON object to return to the channel client.
JSONObject obj = new JSONObject();
// Increment localInteger and set the "value" JSON element to the new value of localInteger.
obj.put("value", localInteger.incrementAndGet());
provider.publish("event", obj, null);
// Return the JSON object to the channel client.
return obj;
}
});
// Register the "incrementBy" action.
// This action will increment the value of the localInteger variable by a specified amount.
provider.register("incrementBy", new ChannelAction() {
// This is the logic for the "incrementBy" action.
@Override
public JSONObject invoke(String action, Object payload, JSONObject senderIdentity) {
// Write a string to the logfile that identifies the action and payload.
logger.info(String.format("provider processing action %s, payload=%s", action, payload.toString()));
// Extract the increment amount (delta) from the payload JSON object.
int delta = ((JSONObject)payload).getInt("delta");
// Create a new JSON object to return to the channel client.
JSONObject obj = new JSONObject();
// Increase localInteger by the delta amount and set the "value" JSON element to the new value of localInteger.
obj.put("value", localInteger.addAndGet(delta));
// Return the new JSON object to the channel client.
return obj;
}
});
});
}
Each time a provider receives an incoming dispatch, it is required to send back a corresponding acknowledgement (can be undefined). Once a listener is registered for a particular action, it stays in place receiving and responding to incoming messages until it is removed. This messaging mechanism works exactly the same when messages are dispatched from the provider to a client. However, the provider has an additional publish method that sends messages to all connected clients.
If a message is dispatched to an action that does not have a callback registered, an error is returned. This default error can be overwritten on the channelBus
by setting a default action.
Messaging Protocols
Channels may use one or both of the following protocols:
NOTE
Support for the
rtc
protocol starts in OpenFin version 23.
The OpenFin IPC provides connectivity between applications. It is, however, not intended to support low-latency, high-frequency data streams. Attempting to use the IPC in this way can negatively impact the performance and response time of all applications on the desktop.
OpenFin now provides the rtc
protocol option to isolate channel messaging from the underlying OpenFin API IPC usage to improve the performance of channels. The rtc
protocol also supports low-latency, high-frequency data streams by using the webRTC protocol.
A channelProvider
may support one or all protocols and likewise a channelClient
may try to connect with one or all protocols. If the channelProvider
does not support the channelClient
's protocol request, the client will fail to connect.
Messaging Protocol Example
// provider with support for both protocols
const provider = await fin.InterApplicationBus.Channel.create('channelName', { protocols: ['rtc', 'classic'] });
// connect as an rtc client
const rtcClient = await fin.InterApplicationBus.Channel.connect('channelName', { protocols: ['rtc'] });
// connect as a classic client
const classicClient = await fin.InterApplicationBus.Channel.connect('channelName', { protocols: ['classic'] });
Updated 9 months ago