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 the connectOptions.

📘

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:

  1. Message dispatched over the channel for a specific topic to the corresponding provider
  2. Provider callback for that registered action is invoked
  3. 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)
  4. 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'] });