Data Sharing Between Apps

Overview

The Interop API enables apps to:

  • offer functionality to other apps in the same Glue42 Core project by registering Interop methods;
  • discover apps in the same Glue42 Core project which offer methods;
  • invoke registered Interop methods;
  • stream and subscribe for real-time data using the streaming methods of the Interop API;

Apps which offer methods and streams are called Interop servers, and apps which consume them - Interop clients, and collectively - Interop instances.

The Live Examples section demonstrates using the Interop API. To see the code and experiment with it, open the embedded examples directly in CodeSandbox.

Method Registration

The Interop API is accessible through the glue.interop object.

To register an Interop method that will be available to all other Glue42 enabled apps, use the register() method. Provide a name for the method (or a MethodDefinition object) and a callback that will handle invocations from client apps:

// Required name for the method to register.
const methodName = "Addition";
// Required callback that will handle client invocations.
const handler = ({ a, b }) => {
    const result = { sum: a + b };

    return result;
};

await glue.interop.register(methodName, handler);

After registration, the "Addition" Interop method will be available to all other Glue42 enabled apps and any of them will be able to invoke it with custom arguments at any time, as long the server offering it is running or until it unregisters it (with the unregister() method).

Interop methods with the same name may be registered by different servers. An Interop method is considered the same as another Interop method if their names are the same and if the accepts and returns properties of their MethodDefinition objects have identical values. The implementation of the handler function, however, may differ for each server.

Method Definition

When registering an Interop method, it is required to pass either a string for a method name or a MethodDefinition object. The MethodDefinition object describes the Interop method your app is offering. It has the following properties:

Property Type Description Required
name string A name for the method. Yes
accepts string Signature describing the parameters that the method expects (see Input and Output Signature). No
returns string Signature describing the return value of the method (see Input and Output Signature). No
displayName string User-friendly name for the method that may be displayed in UIs. No
description string Description of the functionality the method provides. No
objectTypes string Predefined data structures (e.g., "Instrument", "Client", etc.) with which the method works (see Object Types). No
supportsStreaming boolean Whether the method is an Interop stream. No
version number Method version. No
// Method definition.
const methodDefinition = {
    name: "Addition",
    accepts: "Int a, Int b, Int? c",
    returns: "Int sum",
    displayName: "Calculate Sum",
    description: "Calculates the sum of the input numbers."
};
const handler = ({ a, b, c }) => {
    const result = {
        sum: a + b + (c ? c : 0)
    };

    return result;
};

await glue.interop.register(methodDefinition, handler);

Input and Output Signature

To describe the parameters that your Interop method expects and the value it returns, use the accepts and returns properties of the MethodDefinition object. Both properties accept a comma-delimited string of parameters. Each parameter described in the string must use the following format:

type <array-modifier> <optional-modifier> parameter-name (<description>)

// The `type` is one of:
type = "bool" | "int" | "double" | "long" | "string" | "datetime" | "tuple: {<schema>}" | "composite: {<schema>}"

// The `<schema>` represents any value(s) in the same format.

"Composite" is a structure which may contain one or more fields of scalar type, array of scalars, a nested composite or an array of composites. A "Composite" allows you to define almost any non-recursive structure.

Examples:

  • "string name, string[]? titles" - name is required, titles is an optional string array;
  • tuple: { string name, int age } personalDetails - personalDetails is a required tuple value containing two required values - name as a string and age as an integer;
  • "composite: { string first, string? middle, string last } name" - name is a composite parameter and its schema is defined by 2 required string fields - first and last, and an optional string field - middle;

Returning Results

When returning results from you Interop methods, wrap the return value in an object:

({ a, b }) => {
    // Return an object.
    return { sum: a + b };
};

Otherwise, the result will be automatically wrapped in an object with a single _value property which will hold your return value:

({ a, b }) => {
    // This will be automatically wrapped in an object.
    return a + b;
};

// If a=2 and b=3, the resulting value will look like this:
// { _value: 5 }

Asynchronous Results

Interop methods can return asynchronous results as well. Use the register() method to register an asynchronous Interop method:

const asyncMethodName = "MyAsyncMethod";
const asyncHandler = async () => {
    const response = await fetch("https://docs.glue42.com");

    if (response.ok) {
        return 42;
    } else {
        throw new Error("The doc site is down!");
    };
};

await glue.interop.register(asyncMethodName, asyncHandler);

Method Invocation

To invoke an Interop method, use the invoke() method. The only required argument for invoke() is a method name or a MethodDefinition object. You can also specify arguments, target and other invocation options:

const methodName = "Addition";
const args = { a: 2, b: 3 };
const target = "all";
const options = {
    waitTimeoutMs: 5000,
    methodResponseTimeoutMs: 8000
};

const result = await glue.interop.invoke(methodName, args, target, options);
  • args - as a second parameter, invoke() accepts an object containing arguments for the method invocation;
  • target - as a third parameter, invoke() accepts a value specifying which Interop servers offering the method to target (see Targeting).
  • options - as a fourth parameter, invoke() accepts an InvokeOptions object with optional properties, described in the following table.
Value Description
waitTimeoutMs In ms, default is 30 000. Timeout to discover the method if not immediately available.
methodResponseTimeoutMs In ms, default is 30 000. Timeout to wait for a reply from the method invocation.

Targeting

If multiple apps offer the same Interop method, you can choose to invoke it on the "best" app instance (this is the default behavior, if no target is passed), on a specific Interop instance, on a set of instances, or on all instances.

The following table describes the values accepted by the target property of the MethodDefinition object when invoking an Interop method:

Value Description
"best" Default. Executes the method on the best (first) server (the Glue42 runtime determines the appropriate instance).
"all" Executes the method on all Interop servers offering it.
"skipMine" Like "all", but skips the current server.
Instance An object describing an Interop instance. It is also possible to provide only a subset of the Interop instance object properties as a filter - e.g., { application: "appName" }.
Instance[] Array of Interop Instance objects (or subset filters).

Note that the properties of an Interop Instance can have both a string or a regular expression as a value.

App instances are ranked internally. The "best" instance is the first one running on the user's desktop and under the user's name. If there are multiple apps matching these criteria, the first instance is used.

To invoke a method on a preferred set of apps, pass a target as a third argument.

If nothing is passed, "best" is default:

await glue.interop.invoke("Addition", { a: 2, b: 3 });

To target all Interop instances offering the same method:

const target = "all";

await glue.interop.invoke("Addition", { a: 2, b: 3 }, target);

To target all instances, except the current one:

const target = "skipMine";

await glue.interop.invoke("Addition", { a: 2, b: 3 }, target);

To target a specific instance:

const target = { application: "Calculator" };

await glue.interop.invoke("Addition", { a: 2, b: 3 }, target);

To target a set of instances (for more information on finding Interop instances, see Discovery):

const targets = glue.interop.servers()
    .filter(server => server.application.startsWith("Calculator"));

await glue.interop.invoke("Addition", { a: 2, b: 3 }, targets);

Consuming Results

The invoke() method is asynchronous and resolves with an InvocationResult object. Use the returned property of the InvocationResult object to extract the returned result:

const invocationResult = await glue.interop.invoke("Addition", { a: 2, b: 3 });

// The method returns an object with a `sum` property.
const sum = invocationResult.returned.sum;

Multiple Results

Invoking a method on multiple Interop instances produces multiple results. Use the all_return_values property of the InvocationResult object to obtain an array of all invocation results:

const invocationResult = await glue.interop.invoke("Addition", { a: 2, b: 3 }, "all");

invocationResult.all_return_values
    .forEach(result => console.log(result.returned.sum));

Object Types

Use the objectTypes property of the MethodDefinition when registering an Interop method to specify what predefined data structures the method expects - e.g., "Instrument", "Client", etc. Specifying the object types in a method definition is useful for determining at runtime the methods applicable to the currently handled object. For the object types to function in a generic manner, all apps must follow the same data format and pass the respective objects to the respective Interop methods.

To register a method with object type specifications:

const methodDefinition = {
    name: "SetClient",
    objectTypes: ["Client"]
};

const handler = (client) => {
    console.log(client.id, client.name);
};

await glue.interop.register(methodDefinition, handler);

To find all methods working with a specific object type:

const clientMethods = glue.interop.methods()
    .filter(method => method.objectTypes?.includes("Client"));

To invoke a method working with a specific object type:

const methodDefinition = {
    name: "SetClient",
    objectTypes: ["Client"]
};

await glue.interop.invoke(methodDefinition);

Discovery

Methods

To get a collection of all available Interop methods, use the methods() method:

const allMethods = glue.interop.methods();

To find a specific method or a set of methods, pass a string or a MethodFilter object:

const methodFilter = { name: "Addition" };
const filteredMethods = glue.interop.methods(methodFilter);

To find all methods of an Interop instance:

const instance = { application: "appName" };
const methods = glue.interop.methodsForInstance(instance);

If you have a reference to an Interop instance, use its getMethods() and getStreams() methods:

// Get the current Interop instance of the app.
const myInstance = glue.interop.instance;
// Get the Interop methods registered by the instance.
const methods = myInstance.getMethods();
// Get the Interop streams registered by the instance.
const streams = myInstance.getStreams();

Servers

To get a collection of all Interop servers, use the servers() method:

const servers = glue.interop.servers();

To find the servers offering a specific method, pass a MethodFilter object:

const methodFilter = { name: "Addition" };
const serversForMethod = glue.interop.servers(methodFilter);

If you have a reference to a Method object, use its getServers() method:

const method = glue.interop.methods("Addition")[0];
const servers = method.getServers();

Interop Events

The Interop API offers means for notifying you when a method has been added/removed or when an app offering methods becomes available/unavailable. All methods for listening for events return an unsubscribe function. Use it to stop receiving event notifications.

To get notified when a method has been added for the first time by any app, use methodAdded():

const handler = (method) => {
    console.log(`Method "${method.name}" was added.`);
};

glue.interop.methodAdded(handler);

To get notified when a method has been removed from the last app offering it, use methodRemoved():

const handler = (method) => {
    console.log(`Method "${method.name}" was removed.`);
};

glue.interop.methodRemoved(handler);

To get notified when an app offering methods has been discovered, use serverAdded():

const handler = (instance) => {
    console.log(`Interop server was discovered: "${instance.application}".`);
};

glue.interop.serverAdded(handler);

To get notified when an app stops offering methods or is closed, use serverRemoved():

const handler = (instance) => {
    console.log(`Interop server was removed: "${instance.application}".`);
};

glue.interop.serverRemoved(handler);

To get notified every time a method is offered by any app, use serverMethodAdded(). This event fires every time any app starts offering a method, while methodAdded() fires only for the first app which starts to offer the method:

const handler = (info) => {
    const serverName = info.server.application;
    const methodName = info.method.name;
    console.log(`Interop server "${serverName}" now offers method "${methodName}".`);
};

glue.interop.serverMethodAdded(handler);

To get notified every time a method is removed from any app, use serverMethodRemoved(). This event fires every time any app stops offering a method, while methodRemoved() fires only when the method has been removed from the last app offering it:

const handler = (info) => {
    const serverName = info.server.application;
    const methodName = info.method.name;
    console.log(`Interop server "${serverName}" has removed method "${methodName}".`);
};

glue.interop.serverMethodRemoved(handler);

Streaming

Overview

Your app can publish events that can be observed by other apps and can provide real-time data (e.g., market data, news alerts, notifications, etc.) to other apps by publishing an Interop stream. It can also receive and react to these events and data by creating an Interop stream subscription.

Apps that create and publish to Interop streams are called publishers, and apps that subscribe to Interop Streams are called subscribers. An app can be both.

Publishing Stream Data

Creating Streams

To start publishing data, create an Interop stream by using the createStream() method. This registers an Interop method similar to the one created by register(), but with streaming semantics. The createStream() method accepts a string or a MethodDefinition object as a first parameter and a StreamOptions object as a second.

The MethodDefinition is identical to the Interop method definition for the register() method. If you pass a string, it will be used as a stream name:

const stream = await glue.interop.createStream("MarketData.LastTrades");

Which is identical to:

const streamDefinition = { name: "MarketData.LastTrades" };
const stream = await glue.interop.createStream(streamDefinition);

The StreamOptions object allows you to pass several optional callbacks which let your app handle subscriptions in a more detailed manner:

  • to identify individual subscribers/clients;
  • to accept or reject subscriptions based on the subscription arguments;
  • to unicast data as soon as a client subscribes to the stream;
  • to group subscribers which use the same subscription arguments on a stream branch and then publish to that branch, multicasting data to all subscribers;

StreamOptions object example:

const streamOptions = {
    subscriptionRequestHandler: subscriptionRequest => {},
    subscriptionAddedHandler: streamSubscription => {},
    subscriptionRemovedHandler: streamSubscription => {}
};

Example of creating a stream:

// Stream definition.
const streamDefinition = {
    name: "MarketData.LastTrades",
    displayName: "Market Data - Last Trades",
    accepts: "String symbol",
    returns: "String symbol, Double lastTradePrice"
};

// Stream options object containing subscription request handlers.
const streamOptions = {
    subscriptionRequestHandler: subscriptionRequest => subscriptionRequest.accept(),
    subscriptionAddedHandler: console.log,
    subscriptionRemovedHandler: console.log
};

// Creating the stream.
let stream;

async function initiateStream() {
    stream = await glue.interop.createStream(streamDefinition, streamOptions);
    console.log(`Stream "${stream.definition.displayName}" created successfully.`);
};

initiateStream().catch(console.error);

Accepting or Rejecting Subscriptions

Subscriptions are auto accepted by default. You can control this behavior by passing a subscriptionRequestHandler in the StreamOptions object. Note that this handler is called before the subscriptionAddedHandler, so if you reject the request, the subscriptionAddedHandler won't be called.

The SubscriptionRequest object, passed as an argument to the subscription request handler, has the following properties and methods:

Name Description
instance The Interop Instance of the subscriber app.
arguments An object containing the subscription arguments, e.g. { symbol: "GOOG" }.
accept() Accepts the instance subscription.
acceptOnBranch() Accepts the subscription on a branch with the provided string argument as a name. Pushing data to that branch will multicast it to all subscriptions associated with the branch.
reject() Rejects the subscription and returns the provided string argument as a reason for the rejection.

Example of a subscription request handler:

function onSubscriptionRequest(subscriptionRequest) {

    // Here you can identify, accept or reject subscribers,
    // group subscribers on a shared stream branch, access the subscription arguments.

    const application = subscriptionRequest.instance.application;
    const symbol = subscriptionRequest.arguments.symbol;

    // If the subscription request contains a `symbol` property in the its `arguments` object,
    // accept it on a stream branch with the provided symbol as a branch key,
    // otherwise, reject the subscription.
    if (symbol) {
        subscriptionRequest.acceptOnBranch(symbol);
        console.log(`Accepted subscription by "${application}" on branch "${symbol}".`);
    } else {
        subscriptionRequest.reject("Subscription rejected: missing `symbol` argument.");
        console.warn(`Rejected subscription by "${application}". Symbol not specified.`);
    };
};

Added and Removed Subscriptions

By default, nothing happens when a new subscription is added or removed. You may, however, want to push data to the subscriber, if such is available, or unsubscribe from the underlying data source when the last subscriber for that data is removed. Use the subscriptionAddedHandler and the subscriptionRemovedHandler in the StreamOptions object to achieve this.

Handling New Subscriptions

Example of a handler for added subscriptions:

const symbolPriceCache = {
    "GOOG": {
        price: 123.456
    }
};

function onSubscriptionAdded(streamSubscription) {

    const symbol = streamSubscription.arguments.symbol;
    const isFirstSubscription = symbolPriceCache[symbol] ? false : true;

    if (isFirstSubscription) {
        // If this is a first subsription for that symbol,
        // start requesting data for it and cache it.
        symbolPriceCache[symbol] = {};
        startDataRequests(symbol);
        console.log(`First subscription for symbol "${symbol}" created.`);
    } else {
        // If there is already an existing subscription for that symbol,
        // send a snapshot of the available price to the new subscriber.
        const price = symbolPriceCache[symbol].price;

        // Check first whether a price is available.
        if (price) {
            const data = { symbol, price };

            // Unicast data directly to this subscriber.
            streamSubscription.push(data);
            console.log(`Sent snapshot price for symbol "${symbol}".`);
        };
    };
};

function startDataRequests(symbol) {
    // Here you can make requests to a real-time data source.
};

Handling Last Subscription Removal

Example of a handler for removed subscriptions:

function onSubscriptionRemoved(streamSubscription) {

    const symbol = streamSubscription.arguments.symbol;
    const branch = streamSubscription.stream.branch(symbol);

    // If there are no more subscriptions for that symbol,
    // stop requesting data and remove the symbol from the cache.
    if (branch === undefined) {
        stopDataRequests(symbol);
        delete symbolPriceCache[symbol];
        console.warn(`Branch was closed, no more active subscriptions for symbol "${symbol}".`);
    };
};

function stopDataRequests(symbol) {
    // Terminate the requests to the data source.
};

Using Stream Branches

If your stream publishing code uses branches (e.g., creates a branch for each unique set of subscription arguments and associates the subscriptions with that branch), whenever a data arrives from your underlying source, you can use the branch to publish the data to the subscribers on that branch instead of manually going over all subscriptions and pushing data to the interested clients.

Example:

// Extract the data returned in the response from the data source, e.g.:
// const symbol = responseData.symbol;
// const price = responseData.price;
const data = { symbol, price };

// The subscriptions have been accepted on branches with the `symbol`
// provided in the subscription requests as a branch key,
// so now the same `symbol` is used to identify the branch to which to push data.
stream.push(data, symbol);

Server Side Subscription Object

The StreamSubscription object has the following properties and methods:

Name Description
arguments The arguments used by the client app to subscribe.
stream The stream object you have registered, so you don't need to keep track of it.
branchKey The key of the branch (if any) with which the stream publisher has associated the client subscription.
instance The instance of the subscriber.
push() A method to push data directly to a subscription (unicast).
close() method which closes the subscription forcefully on the publisher side, e.g. if the publisher shuts down.

Stream Object

The Stream object has the following properties and methods:

Name Description
definition The definition object with which the stream was created.
name The name of the stream as specified in the definition object.
subscriptions() Returns a list of all subscriptions.
branches() Returns a list of all branches.
close() Closes the stream and unregisters the corresponding Interop method.

Branch Object

The StreamBranch object has the following properties and methods:

Name Description
key The key with which the branch was created.
subscriptions() Returns all subscriptions which are associated with this branch.
close() Closes the branch (and drops all subscriptions on it).
push() Multicasts data to all subscriptions on the branch. This is always more efficient than keeping track of the subscriptions manually and doing it yourself.

Consuming Stream Data

Subscribing to a Stream

Streams are simply special Interop methods, so subscribing to a stream resembles very much invoking a method. To subscribe, create a subscription using the subscribe() method. It accepts a string or a MethodDefinition object as a first required parameter and a SubscriptionParams object as a second optional one:

const subscriptionOptions = {
    arguments: { symbol: "GOOG" }
};

// Creating the subscription.
let subscription;

async function createSubscription() {
    subscription = await glue.interop.subscribe("MarketData.LastTrades", subscriptionOptions);
};

createSubscription().catch(console.error);

// Use subscription here.

The SubscriptionParams object has the following properties:

Property Description
arguments Object containing arguments for the stream subscription. Passing arguments enables you to group subscribers that use the same arguments on a stream branch (see Publishing Stream Data), and/or use these as a filter on the publisher side.
target An InstanceTarget object that can be one of "best", "all", "skipMine", Instance or Instance[] (see Invoking Methods).
waitTimeoutMs Timeout to discover the stream if not immediately available.
methodResponseTimeout Timeout to wait for the stream reply.
onData Callback for handling new data.
onClosed Callback to handle the event when the subscription is closed by the server.
onConnected Callback to handle the event when the subscription is connected to a server.

Handling Subscriptions Client Side

The client side Subscription object has several useful properties providing information about the subscription instance:

Property Description
requestArguments Arguments used for the subscription.
serverInstance Instance of the app providing the stream.
stream The stream definition object.

Once you have a subscription, use its onData() method to handle stream data. The callback you register with the onData() method of the Subscription object will fire every time new stream data is received:

subscription.onData((streamData) => {
    // Use stream data here.
});

The StreamData object has the following properties:

Property Description
requestArguments The subscription request arguments.
data The data object sent by the stream publisher.
private A flag indicating whether the data was unicast to this subscription (false, if multicast from a stream or a stream branch).
server The Interop instance which pushed the data.
message Message from the publisher of the stream.

Closed or Rejected Subscriptions

A stream subscription can be closed at any time due to the publisher shutting down or due to an error. Two methods handle these events:

subscription.onClosed(() => {
    // Closed gracefully by the publisher.
});

subscription.onFailed((error) => {
    // Unexpected error in the publisher.
});

Stream Discovery

Streams are special Interop methods, so you can use the Interop discovery methods to find available streams. The only difference is that streaming methods are flagged with a property supportsStreaming: true.

Finding all streams:

const streams = glue.interop.methods().filter(method => method.supportsStreaming === true);

Finding a known stream:

const stream = glue.interop.methods().find(method => method.name === "MarketData.LastTrades");

Live Examples

Registering and Invoking Methods

The apps below demonstrate how to register and invoke Interop methods using the register() and invoke() methods of the Interop API.

On load, App B registers a method called "G42Core.Basic". Click the "Invoke" button in App A to invoke this method and print the result from the method invocation.

Open in CodeSandbox

Targeting

The apps below demonstrate targeting Interop servers when invoking Interop methods.

On load, Apps B and C register a method with the same name. Click one of the buttons in App A to invoke this method and print the result from the method invocation. There are four buttons - "Invoke Default" (invokes the method by targeting the server that has registered it first), "Invoke All" (invokes the method by targeting all servers offering it), "Invoke App B" (invokes the method by targeting App B) and "Invoke App C" (invokes the method by targeting App C).

Open in CodeSandbox

Discovery

Methods

The apps below demonstrate discovering Interop methods by a method name.

Use App B and App C to register Interop methods by providing a method name. Input a method name in App A and click the "Invoke" button to invoke the method and print the result from the method invocation.

Open in CodeSandbox

The apps below demonstrate discovering Interop methods by subscribing to the serverMethodAdded() and the serverMethodRemoved() events of the Interop API.

On load, App A subscribes to the serverMethodAdded() and serverMethodRemoved() events and will print the names of the newly registered method and the server offering it. Use App B and App C to register Interop methods by providing a method name.

Open in CodeSandbox

Servers

The apps below demonstrate discovering Interop servers by a method name.

Use App B and App C to register Interop methods by providing a method name. Input a method name in App A and click the "Find Servers" button to print the Interop servers that provide the method.

Open in CodeSandbox

Streaming

Publishing and Subscribing

The apps below demonstrate publishing and subscribing for Interop streams.

On load, App B registers an Interop stream called "G42Core.Stream.Basic". Click the "Subscribe" button in App A to subscribe to the registered stream. Each time App A receives data, it will be printed on the page (time stamp and a message). Click the "Start Publishing" button in App B to start publishing data to the stream every 3 seconds.

Open in CodeSandbox

Events

The apps below demonstrate handling streaming events - adding/removing subscribers and closing the stream.

Click the "Create Stream" button in App B to register an Interop stream called "G42Core.Stream.Basic". Click the "Subscribe" button in App A to subscribe to the registered stream - App B will print to the page when a new subscriber is added. Each time App A receives data, it will be printed on the page (time stamp and a message).

Click the "Unsubscribe" button in App A to close the subscription to the stream - App B will print to the page when a subscriber is removed. Click the "Close Stream" button in App B to close the stream - App A will print to the page when the stream is closed.

Open in CodeSandbox

Managing Subscriptions

The apps below demonstrate handling stream subscriptions - accepting/rejecting subscriptions, grouping subscribers on branches, pushing data to all subscribers or to a specific stream branch.

On load, App C registers an Interop stream called "G42Core.Stream.Basic". Click the "Subscribe" button in App A and App B to subscribe to the registered stream. App A and App B will print to the page subscription success or error messages, as well as the received data from the stream (time stamp and a message).

When App C receives a new subscription request, it will print the subscription info on the page and show three buttons for the subscription: "Accept", "Accept on Private" and "Reject".

  • "Accept" - accepts the subscription on the default branch.
  • "Accept on Private" - accepts the subscription on a branch called "Private".
  • "Reject" - rejects the subscription.

Use the "Push" and "Push to Private" buttons to push stream data to the default streaming branch (to all subscriptions) or to the "Private" branch.

Open in CodeSandbox

Reference

For a complete list of the available Interop API methods and properties, see the Interop API Reference Documentation.