Messaging Guide
Overview
The Messaging API allows developers to easily send simple messages between the app and its companion using a synchronous socket based communications channel.
This API has an identical implementation in the Device API and the Companion API, so code examples work the same in both locations. This API is a subset of the WebSocket API.
How It Works
The application and companion are commonly referred to as "peers". Each peer
must successfully open a MessageSocket connection before any messages can be
sent. Connections are established automatically, and the open
event will be
emitted upon connection.
Once a connection has been established, peers may send and receive simple messages. For example, the device peer can ask the companion peer to request data from a web service, then the companion can send that response to the device.
The data is automatically encoded using CBOR before it is sent. This allows developers to easily send and receive the following JavaScript data types:
- Number
- String
- ArrayBuffer or ArrayBufferView
- true, false, null, undefined
- Array Object
[1,2]
- Object
{"a": "1"}
Opening a Connection
A connection must be successfully opened before any messages can be sent or received.
import * as messaging from "messaging";
messaging.peerSocket.addEventListener("open", (evt) => {
console.log("Ready to send or receive messages");
});
Error Handling
The error
event will be emitted whenever the peer connection is lost, this
usually means that one or more messages have been lost. You will receive another
open
event before the connection can be used again. You should always check
the readyState
of the connection before attempting to send a message.
Some connectivity issues may be temporary, like the device going out of Bluetooth range, but others may be more fatal, like the mobile device running out of battery.
messaging.peerSocket.addEventListener("error", (err) => {
console.error(`Connection error: ${err.code} - ${err.message}`);
});
Sending a Message
Once a connection has been established, you can send a message to the connected peer.
import * as messaging from "messaging";
messaging.peerSocket.addEventListener("open", (evt) => {
sendMessage();
});
messaging.peerSocket.addEventListener("error", (err) => {
console.error(`Connection error: ${err.code} - ${err.message}`);
});
function sendMessage() {
// Sample data
const data = {
title: 'My test data',
isTest: true,
records: [1, 2, 3, 4]
}
if (messaging.peerSocket.readyState === messaging.peerSocket.OPEN) {
// Send the data to peer as a message
messaging.peerSocket.send(data);
}
}
Receiving a Message
To receive a message from the connected peer, simply subscribe to the
message
event.
import * as messaging from "messaging";
messaging.peerSocket.addEventListener("message", (evt) => {
console.error(JSON.stringify(evt.data));
});
Checking Connection State
The Messaging API provides a method to check the state of the current
connection. You can use the readyState
property to confirm that the connection
is OPEN
before sending a message, or display an error if the connection is
CLOSED
.
if (messaging.peerSocket.readyState === messaging.peerSocket.OPEN) {
// Send a message
}
if (messaging.peerSocket.readyState === messaging.peerSocket.CLOSED) {
// Display error message
}
Maximum Message Size
It is important to note that the maximum size of a single serialized message is
1027
bytes (MAX_MESSAGE_SIZE
). This means that due to serialization, the
largest blob of data using Uint8Array
is 1024
bytes. Sending a message
which exceeds this size limit will trigger an exception.
If you need to send messages which exceed the maximum size, you would need to split the messages into multiple parts before sending, or alternatively, use the File Transfer API.
Buffering Data
When sending multiple messages, it is important to avoid overflowing the buffer.
The actual buffer size will vary depending upon the application's available
memory. The bufferedAmount
property can be used to determine how much data
currently resides within the buffer. By subscribing to the
bufferedamountdecrease
event, the sending of data can be resumed once there
is available space in the buffer.
In a maximum throughput application, developers can pre-fill the buffer with a few messages in advance. Or, in a minimal latency application, developers can wait for the buffer to be empty, in order to send the most recent data.
// Counter used to limit the amount of messages sent
let counter = 0;
function sendMoreData() {
// Artificial limit of 100 messages
if (counter < 100) {
// Send data only while the buffer contains less than 128 bytes
while (messaging.peerSocket.bufferedAmount < 128) {
// Send the incremented counter value as the message
messaging.peerSocket.send(counter++);
}
}
}
messaging.peerSocket.addEventListener("bufferedamountdecrease", (evt) => {
sendMoreData();
});
sendMoreData();
Fetching Weather Example
In this example, the device will send a message to the companion to request the current temperature for San Francisco. The companion will query the Open Weather API, then send the data back to the device.
The OpenWeather API requires an API key. See here for more information.
Device JavaScript
The following JavaScript runs on the device. When the application is launched
and the MessageSocket is opened, a message is sent to the companion requesting
weather data. The application will then wait to receive the weather data from
the companion. Every 30
minutes after launch, it will request weather data
again.
import * as messaging from "messaging";
function fetchWeather() {
if (messaging.peerSocket.readyState === messaging.peerSocket.OPEN) {
// Send a command to the companion
messaging.peerSocket.send({
command: "weather"
});
}
}
function processWeatherData(data) {
console.log(`The temperature is: ${data.temperature}`);
}
messaging.peerSocket.addEventListener("open", (evt) => {
fetchWeather();
});
messaging.peerSocket.addEventListener("message", (evt) => {
if (evt.data) {
processWeatherData(evt.data);
}
});
messaging.peerSocket.addEventListener("error", (err) => {
console.error(`Connection error: ${err.code} - ${err.message}`);
});
// Fetch the weather every 30 minutes
setInterval(fetchWeather, 30 * 1000 * 60);
Companion JavaScript
The following JavaScript runs on the companion. The companion will wait for a command from the device, then query the Open Weather API. The weather data is then sent to the device.
import * as messaging from "messaging";
var API_KEY = "your-key-goes-here";
var ENDPOINT = "https://api.openweathermap.org/data/2.5/weather" +
"?q=San%20Francisco,USA&units=imperial";
function queryOpenWeather() {
fetch(ENDPOINT + "&APPID=" + API_KEY)
.then(function (response) {
response.json()
.then(function(data) {
// We just want the current temperature
var weather = {
temperature: data["main"]["temp"]
}
// Send the weather data to the device
returnWeatherData(weather);
});
})
.catch(function (err) {
console.error(`Error fetching weather: ${err}`);
});
}
function returnWeatherData(data) {
if (messaging.peerSocket.readyState === messaging.peerSocket.OPEN) {
messaging.peerSocket.send(data);
} else {
console.error("Error: Connection is not open");
}
}
messaging.peerSocket.addEventListener("message", (evt) => {
if (evt.data && evt.data.command === "weather") {
queryOpenWeather();
}
});
messaging.peerSocket.addEventListener("error", (err) => {
console.error(`Connection error: ${err.code} - ${err.message}`);
});
If you're interested in retrieving weather data for the current location, you'll want to read our Geolocation guide.
Best Practices
- Use the
open
event to trigger a message when the connection is opened. - Don't exceed the maximum message size (
MAX_MESSAGE_SIZE
). - Implement buffering when sending multiple messages.
- You can use the
readyState
property to check the connection state.
Messaging in Action
If you're interested in using the Messaging API within your application, why not check out the "Bay Area Rapid Transit (BART)" example app or review the Messaging API reference documentation.