Communication Between Controllers
Because a controller can operate in the Captivate browser and also in a local browser, it’s quite possible for you to have multiple instances of the same controller in two different places both communicating with Captivate. This can cause problems unless the two instances of the controller know about each other and are able to communicate.
In fact, there are many reasons why one controller would want to communicate directly with another controller, so Captivate provides two methods designed to address this specific situation. For example, if you have one controller that has one UI running in Captivate and another UI designed for a remote operator, you can keep both interfaces in sync by sending messages through Captivate.
Using messageIn and messageOut
messageIn and messageOutControllers can pass messages back and forth to other controllers or to other instances of themselves by using the messageOut method in combination with the messageIn signal.
To use, call the messageOut method with four arguments:
from- A string identifying the source of the message (usually the controller name, but may be anything).to- A string identifying the target of the message (usually the other controller name, but may be anything).data- A JavaScript object of key, value pairs. This may be an arbitrarily deeply nested object, but it must be an object. Strings, arrays, and other primitives will result in nothing being sent.callback- A JavaScript function that will be called with the stringSent to ...when the message has been sent successfully ornullwhen the message failed.
Note: When using
messageOut, ALL subscribers to themessageInevent emitter will receive the message. If you need to send data securely, consider encrypting the data before sending.
Example:
const my_id = '12345678';
const target_id = '87654321';
const scheduler = ServiceHandler.scheduler;
scheduler.messageIn.connect((from, to, json) => {
// filter out unwanted messages
if (to != my_id) return;
console.log('messageIn received');
console.log({
from,
to,
data: JSON.parse(json),
});
});
scheduler.messageOut(
my_id,
target_id,
{
hello: 'world',
list: [1, 2, 3],
nested: { level: { deep: 0 } },
},
(e) => console.log((e === `Sent to receiver`) ? 'success!' : 'failed to send');
);Using Targeted Commands
Instead of messageIn and messageOut, you can also use scheduleCommand to send targeted commands between controllers.
To turn a command message into a targeted command, add a to field to your parameters object. It’s also helpful to use a sender field as you'll see below.
Sending
Ordinarily, this command will return a large amount of data about the current project:
await ServiceHandler.scheduler.scheduleCommand('getTitleControlInfo', {}, {});However, by adding a to field to the parameters object and using it to specify the name of another input, Captivate will ignore the command internally and just pass the entire payload around to the other inputs (with a few modifications, see below). If the targeted input can’t be found, Captivate will reply with an error.
Whether the other input is found or not, the command message will always be ignored by Captivate and the payload will always get sent to all other controllers listening on the notification stream.
const me = 'My Controller: Instance 2';
const target = 'My Controller: Instance 1';
await ServiceHandler.scheduler.scheduleCommand('getTitleControlInfo', { to: target, sender: me }, {});If the specified input was found, this is the result:
{
"command": "getTitleControlInfo",
"reply": "getTitleControlInfo",
"success": true
}If the input was not found, this is the result:
{
"command": "getTitleControlInfo",
"reply": "getTitleControlInfo",
"error": "No such input",
"success": false
}Regardless of whether the input is found, the command is ignored by Captivate and the following payload will be sent through the notification system:
{
"command": "getTitleControlInfo",
"to": "My Controller: Instance 1",
"sender": "My Controller: Instance 2",
"useJson": "1"
}As a result, you usually want to use a custom command name that only your controller cares about:
Example:
async function requestControl() {
const me = 'My Controller: Instance 2';
const target = 'My Controller: Instance 1';
await ServiceHandler.scheduler.scheduleCommand('request_control', { to: target, sender: me }, {});
}Receiving
To receive targeted messages, you must be listening to the notification stream. Subscribing to notifications is described elsewhere, but here's a quick example again:
Example:
const me = 'My Controller: Instance 1';
let controlling = true;
ServiceHandler.scheduler.scheduleCommand('subscribe', {}, {});
ServiceHandler.scheduler.onNotify.connect((msg) => {
/* handle notification messages here */
const data = JSON.parse(msg);
if (data.to === me) {
switch (data.command) {
case 'request_control':
// the other instance is requesting control
controlling = false;
default:
console.log(data);
}
}
});In short, when using the to field, the command field is ignored by Captivate even if it matches a built-in command. So make you use custom command names, and that you check the to field in each notification message.
Payload Restrictions
The payload sent to the notification stream is assembled according to the following rules:
The first argument passed (command name) will populate the
commandfield in the JSON payload.All key/value pairs from the second argument, what we call
parameters, will be included as-is in the final payload with the exception that non-string values will be converted to strings.If any value from the
parameterspayload is a JavaScript object, it will not be turned into a string but dropped and replaced by an empty string:"".The third argument, what we call
variables, will be wrapped in a list of key/value pairs identified by the keyvariables.The
variablespayload is handled differently from theparameterspayload in that non-string values will be encoded to a JSON string first.
Because of this, controllers can send and receive messages with arbitrary data in the form of key/value pairs by simply adding more fields to the original message payload or by including these values in the variables argument. Here is a more thorough example:
ServiceHandler.scheduler.scheduleCommand(
'greetings',
{
to: 'API Tour: JSON Command Tester',
key1: 'hello',
key2: 'world',
key3: {
hello: 'world',
},
key4: 1,
},
{
data1: {
hello: 'world',
},
data2: 'happiness',
},
console.log
);This will be printed to the console (assuming the target controller exists in the project):
{
"command": "greetings",
"reply": "greetings",
"success": true
}...and this is the JSON payload that will propagate through the notification system:
{
"command": "greetings",
"key1": "hello",
"key2": "world",
"key3": "",
"key4": "1",
"to": "API Tour: JSON Command Tester",
"useJson": "1",
"variables": [
{
"data1": "{\n \"hello\": \"world\"\n}\n"
},
{
"data2": "happiness"
}
]
}Take note of the following:
key4was a numerical value that was converted to a string.key3was a JavaScript object that was completely lost, replaced by an empty string.The data from the
variablesargument was included as two separate, key-value objects.The values for the
variablesobjects might be encoded as pretty-printed JSON.
Therefore, to properly decode this, you will need to perform two JSON.parse operations. Once for the object as a whole, and again for the data1 value.
Last updated