Create your own service

A service is a headless OpenFin application. The main difference between a service and an application is a service’s lifecycle is typically managed by the Runtime Version Manager rather than by a user, and its main window (since all applications must have a window) will typically remain hidden from the user.

For an overview of the lifecycle of a service, see the existing OpenFin Desktop Services docs. This headless application will implement the services functionality. If the service wishes to expose an API to applications then it may do so using any communication method available to it.

By convention, OpenFin-developed services use the Channels API to provide an API to other applications running on the desktop. The service application (known as the service "provider") creates an IAB channel, which clients can then connect to and send messages across. To make using the service as simple as possible, each service comes with a small NPM library that provides a strongly-typed and documented API to applications, and translates each call into a message to be sent over the IAB channel that the provider can understand.

Service example

Provider

This is an example of an application running on the user's desktop, created with {autoShow: false} so that the window is hidden from the user.

// This is the name of the channel that the service 
// will use to communicate with other applications.
// These channels can be connected to by any application. 
// It is a good idea to "namespace" the channel name, to 
// avoid conflicts.
const CHANNEL_NAME = 'myorg-myservice-v1';

async function init() {
    // Create IAB Channel for receiving messages from applications
    const channel = await fin.InterApplicationBus.Channel.create(CHANNEL_NAME);
    
    // Add listeners for each client-callable API
    channel.register('echo', doEcho);
}

// Implementation of the "echo" API
async function doEcho(msg, level) {
    // Validate inputs passed from client
    if (['log', 'info', 'warn', 'error'].indexOf(level) === -1) {
        throw new Error(`Invalid value for level: ${level}`);
    }

    // Log the given message, at the requested log-level
    console[level](msg);
}

// Initialise the service provider
init();

Client

This is an example of what a client would embed within each application that wishes to communicate with the service provider.

// Connect to IAB Channel for sending messages to the provider
const channelPromise = fin.InterApplicationBus.Channel.connect(CHANNEL_NAME).then((channel) => {
    // Add any listeners for provider-initiated messages here
    channel.register('WARN', (payload) => console.warn(payload));

    return channel;
});

/**
 * Basic API call - takes two parameters, and will receive a timestamp from
 * the service provider in the case of a successful call.
 *
 * If the provider has a problem handling the request, the promise returned 
 * by this function will reject with details of the problem.
 */
export async function echo(msg, level) {
    const connection = await channelPromise;

    // The action names and message payloads are up to the 
    // developer/maintainer of the service.
    return connection.dispatch('echo', {data: msg});
}

Application

An application that wishes to use the service must declare the service within its manifest, and lists the service’s client module as a dependency.

import {echo} from 'myservice';

echo('Hello Service', 'info').then((timestamp) => {
    console.log(`Message processed at time: ${timestamp}`);
})
.catch((error) => {
    console.error('Error whilst making call to service:', error);
});

Hosting a service

Hosting a service is no different from hosting an ordinary OpenFin app. Create the service code and host on a http/https server under your control, reachable without authentication from the machines the end users will be running the service on.

To use a custom service location, the RVM needs to be instructed to look for the service in the new location. This can be done in 2 different ways:

Specifying the service location in the application config file (to be used for test/dev only)

"services": [{
   "name": "yourservicename",
   "manifestUrl": "https://yourapplication.com/app.json"
}]

Specifying the service location in the DesktopOwnerSettings

"services": {
    "yourservicename": {
        "manifestUrl": "https://yourapplication.com/app.json"
    }
}

Controlling the provider location through Desktop Owner Settings ensures that the provider used will not be dependent upon which applications are run by the user, or the order in which those applications are started.

The RVM ensures only one instance of a service can run at any one time, using per-application manifestUrl’s may result in user-visible warnings about the service provider already running.

Making changes to a service

If it is not possible to always deploy applications that use the service at the same time as the service itself, then some additional considerations must be taken into account. We would like an application that uses the service to remain functional, even as the service itself is updated and expanded.

One useful mechanism for ensuring simple backward compatibility (and a mechanism that is built into each OpenFin-developed service) is to version the channels that are used for client-provider communication.

There are many general purpose ways of handling backward compatibility. The method described here is a good use-case of an additional OpenFin-specific behavior that app developers may want to take advantage of.

If a breaking change must be made to the services API, rather than modifying the messages sent over the IAB channel, instead create a new channel. For any API calls that haven't changed, attach the same listeners to both the old and new channels. For any APIs that have breaking changes, leave the existing channel as-is, and add a modified listener to the new channel. This will allow the provider to maintain compatibility with old client implementations.

Consider the following example, which makes a breaking change to replace a “flat” API, with an API that returns “handle” objects that allow for further interaction. That means that newer client code will exist that deals with different types.

Provider with multiple channels

First, here is the updated provider, capable of communicating with both “old” and “new” clients. Notice the two channel names.

const CHANNEL_NAMES = [
    'myorg-myservice-v1',
    'myorg-myservice-v2'
];

async function init() {
    let channel;

    channel = await fin.InterApplicationBus.Channel.create(CHANNEL_NAMES[0]);
    // Legacy API, replaced by new version
    channel.register('createNotification', createNotificationLegacy);
    // Deprecated API, unsupported by new version
    channel.register('updateNotification', updateNotification);
    // Unmodified API, same behaviour in both versions
    channel.register('removeNotification', removeNotification);
  
    
    channel = await fin.InterApplicationBus.Channel.create(CHANNEL_NAMES[1]);
    // Updated API, same action name but new handler
    channel.register('createNotification', createNotification);
    // Unmodified API, same behaviour in both versions
    channel.register('removeNotification', removeNotification);
}

// Function implementations omitted...
async function createNotificationLegacy(options);
async function createNotification(options);
async function updateNotification(options);
async function removeNotification(options);

Client (legacy)

Some client code will have been developed and released some time ago and cannot be updated. The provider must remain compatible with any clients running this code.

const CHANNEL_NAME = 'myorg-myservice-v1';
const channelPromise = fin.InterApplicationBus.Channel.connect(CHANNEL_NAME).then((channel) => {
    // Add any listeners for provider-initiated messages here
    channel.register('WARN', (payload) => console.warn(payload));

    return channel;
});

/**
 * Create a new notification
 *
 * Will return the (new) number of active notifications
 */
export async function createNotification(options): Promise<number> {
    const connection = await channelPromise;
    return connection.dispatch('createNotification', options);
}

/**
 * Update an existing notification
 */
export async function updateNotification(options): Promise<boolean> {
    const connection = await channelPromise;
    return connection.dispatch('updateNotification', options);
}

/**
 * Removes an existing notification
 */
export async function removeNotification(notificationId): Promise<boolean> {
    const connection = await channelPromise;
    return connection.dispatch('removeNotification', notificationId);
}

Client (new)

However, we can provide an updated client with new changes. Applications will need to make code changes to integrate this version of the client (making it a breaking change), but because the provider is capable of supporting both clients at once, this will be safe to deploy.

const CHANNEL_NAME = 'myorg-myservice-v2';
const channelPromise = fin.InterApplicationBus.Channel.connect(CHANNEL_NAME).then((channel) => {
    // Add any listeners for provider-initiated messages here
    channel.register('WARN', (payload) => console.warn(payload));

    return channel;
});

interface Notification {
   addEventListener(type: string, handler: Function);
   removeEventListener(type: string, handler: Function);

   update(options): Promise<void>;
   remove(): Promise<void>;
}

/**
 * Create a new notification
 *
 * Will return an object that can be used to interact with the notification
 */
export async function createNotification(options): Promise<Notification> {
   const connection = await channelPromise;
    return connection.dispatch('createNotification', options);
}

/**
 * Removes an existing notification
 */
export async function removeNotification(notificationId): Promise<boolean> {
   const connection = await channelPromise;
    return connection.dispatch('removeNotification', notificationId);
}