Google+ Hangouts API

Data Channels: Shared State

When participants run your app in a Hangout, they are each running the app in their own separate instance of the Hangout client. Hangouts have two major data channels for sharing app-specific data between these instances. They are shared state and sendMessage. This document discusses shared state.

Shared state

Hangouts come with a shared-state object, one per Hangout. The shared state object contains data that is kept up-to-date with every instance of the Hangout client that is running your app. The object is a regular JavaScript object with paired key/value strings.

You can set a key/value pair and that change will be distributed to every instance of the Hangout client that is running your app.

In the example below, two participants are playing tic-tac-toe and participant 1 plays the first move. The app tracks the progress of the game using the shared state. The position of the first move (1) is sent to the shared state, which triggers a Hangout-wide event (2) that notifies all instances. The app for participant 2 reads the move and will write the corresponding move to the instance of the app running in participant 2's browser (3).

A diagram showing information flowing
from Hangout instances to the server and back.

Setting shared state

The main method for setting the shared state is the submitDelta method:

gapi.hangout.data.submitDelta( {key1: value, key2: anotherValue} );

If you are setting a single value, the API provides a convenience method, setValue:

gapi.hangout.data.setValue(key1, value);

Receiving shared state updates

Each time your app submits a data change, all participants in the Hangout, including you, receive a StateChangedEvent.

You can add a callback by adding a Hangout listener.

gapi.hangout.data.onStateChanged.add( function(event) {...} );

Each StateChangedEvent comes with a timestamp indicating when the event reached the server. Events will almost always be reported to your app in timestamp order. This might not be the order that it happened in the physical world, depending on the delay between your participants and the server.

You can also access the state object at any time by calling the getState() method:

var state = gapi.hangout.data.getState();
console.log(state.key1);
// Equivalent ways to get the value:
console.log(state['key1']);
console.log(gapi.hangout.data.getValue('key1'));

Recommendation: Do not keep track of the pointer for longer than the length of a function call because the object might not be the same object during every call to getState(). The same restriction applies to values: access them, but do not keep references to them. Do not write to them directly. You must use the submitDelta or setValue methods to change the shared state object.

Timing and capacity

You can send about 10 deltas (changes) to the shared state per second per Hangout. This limit is across all clients in a Hangout. For example, if you have a Hangout with 10 connected clients and each are sending changes at an equal rate, each is limited to one delta per second. If you send too many deltas per second, the Hangout will suspend changes for a second.

You can store approximately 1MB of data in the shared state object; each delta can be about 10K each.

The delay between submitting a delta and the StateChangedEvent is usually about 150ms–300ms. The time varies depending on a number of factors including the network connection and physical distance between users.

Other notes

  • If you want to store a complicated data object, such as a JavaScript object, you can use JSON.stringify() to store the object and JSON.parse() to retrieve it.
  • If you call the setValue() or submitDelta() methods with key/value pairs that are already in your local shared-state object, the data is not sent and the StateChangedEvent is not dispatched.
  • Although you are setting only a single value in this case, sometimes you might have several changes to write at once. Batch them together in a single submitDelta() call:

    function changeState() {
        // Do not do this:
        gapi.hangout.data.submitDelta( {a: value, b: anotherValue} );
        gapi.hangout.data.submitDelta( {c: value, d: anotherValue} );
        gapi.hangout.data.setValue( {e: value, f: anotherValue} );
    }
    

    Do this instead:

    function changeState() {
        // This is awesome!
        gapi.hangout.data.submitDelta( {a: value, b: anotherValue,
                                        c: value, d: anotherValue,
                                        e: value, f: anotherValue} );
    }
    

The example above reduces the number of messages that are sent, which helps to limit the chance of exceeding the rate limit. You can group changes into submitDelta calls to limit the StateChanged events or to group changes that must happen at the same time.

Common problems

A common mistake is to chain methods together that chain together calls to the submitDelta() method. Consider this code:

function onButtonClicked() {
    updateInterface();
    gapi.hangout.data.submitDelta({'something': 1});
}

function updateInterface() {
   // Visualize the shared state object in the interface.
   showLatestDataFromEvent(gapi.hangout.data.getState());

   // Emit an update, like incrementing a timer
   gapi.hangout.data.submitDelta({'timer':gapi.hangout.data.getState()['timer'] + 1});
}

gapi.hangout.data.onStateChanged( function (event) {
   updateInterface();
}

In this example, when someone clicks the button, the app updates the interface, calls submitDelta(), which in turn calls submitDelta() again, and then when you receive a change, it will update the interface and call submitDelta(). If the state values keep changing, every client will end up calling submitDelta() every 150–300ms. You will likely exceed the rate limit for deltas (10 per second), which could result in a timeout. You might also cause rapid flip-flops in your shared state and create contention issues between Hangout clients.

Here is a better implementation:

function onButtonClicked(event) {
    disableButton();

    gapi.hangout.data.submitDelta({something’: 1’});
}

gapi.hangout.data.onStateChanged(function (event ) {
    if (event.state[‘something’] == ‘1’) {
        enableButton();
    }

    updateInterface();
});

function updateInterface() {
    // No shared-state side effects.
}

In general, if you are making a change to the Hangout app's shared state, a good approach is to call the submitDelta() method, disable the button, and then reenable the button when the StateChangedEvent occurs.

  • getState: Gets the shared-state object.
  • setValue: Sets a single key/value pair.
  • submitDelta: Submits a request to update the value of the shared-state object with one or more key/value pairs.
  • onStateChanged: Add or remove callbacks for changes to the shared state.
  • stateChangedEvent: Contains information about a change in the shared state. This object is a parameter to the onStateChanged callback.

Authentication required

You need to be signed in with Google+ to do that.

Signing you in...

Google Developers needs your permission to do that.