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 messageOut
Controllers 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 ornull
when the message failed.
Note: When using
messageOut
, ALL subscribers to themessageIn
event 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
command
field 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
parameters
payload 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
variables
payload is handled differently from theparameters
payload 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:
key4
was a numerical value that was converted to a string.key3
was a JavaScript object that was completely lost, replaced by an empty string.The data from the
variables
argument was included as two separate, key-value objects.The values for the
variables
objects 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