Using Firebase for Real-time Events on App Engine

This article shows you how to connect Google App Engine to Firebase, and then use Firebase to send real-time updates for an interactive multi-player tic-tac-toe game. If you have used the deprecated Channel API, this example might be familiar, because it shows a Firebase implementation of the two-player tic-tac-toe sample. This article describes the features you need to replicate the Channel API’s real-time functionality by using Firebase, instead.

You can use App Engine in conjunction with the Firebase Realtime Database to send immediate updates to browser and mobile clients without a persistent streaming connection to the server or long polling. This functionality is useful for applications that update users about new information in real time. Example use cases include collaborative applications, multi-player games, and chat rooms.

Using Firebase is a better choice than polling in situations where updates can't be predicted or scripted, such as when relaying information between human users, or when events aren't generated systematically.

In this article you’ll learn how to complete the following tasks on the server:

  • Set up your server to use the Firebase Realtime Database.
  • Create a unique Firebase database reference for each web client that connects to your service.
  • Send real-time updates to web clients by changing data on a particular database reference.
  • Securely access messages by creating unique tokens for each web client and using Firebase database security rules.
  • Receive messages from web clients over HTTP.
  • Delete data on a particular database reference after all messages have been sent to clients.

You'll also learn how to complete the following tasks in the web browser:

  • Securely connect to Firebase using a unique token from the server.
  • Dynamically update the interface when the database reference updates.
  • Send update messages to the server so they can be passed on to remote clients.

Getting started with Firebase Realtime Database

The Firebase Realtime Database lets you build rich, collaborative applications by allowing secure access to the database directly from client-side code. Data is persisted locally, and real-time events continue to fire even while offline, giving the end user a responsive experience. When the device regains a connection, the Realtime Database synchronizes the local data changes with the remote updates that occurred while the client was offline, merging any conflicts automatically.

Setting up your Firebase account

Follow these steps:

  1. Create a Firebase project in the Firebase console, if you don't already have one.

  2. If you already have an existing Google project associated with your App Engine application, click Import Google Project. Otherwise, click Create New Project.

  3. If you already have a Firebase project, click Add App from the project overview page.

  4. Click Add Firebase to your web app.

  5. Make a copy of the initialization code snippet, which you will use later when Adding the Firebase SDK to your web page.

Setting the initial database security rules

To simplify the process of reading and writing data to Firebase for this article, you can set your initial Firebase database security rules to allow public access. You learn how to make your data more secure, with finer-grained access for both server and client, in Securing your data.

To set the security rules:

  1. In the Firebase console, in the left-hand menu, click the Rules tab in the Database section.
  2. Copy and paste the following code to allow public access:

    // These rules give anyone, even people who are not users of your app,
    // read and write access to your database
    {
      "rules": {
        ".read": true,
        ".write": true
      }
    }
    

Working with data in Firebase

All Firebase Realtime Database data is stored as JSON objects. You can think of the database as a cloud-hosted JSON tree. Unlike a SQL database, there are no tables or records. When you add data to the JSON tree, it becomes a node in the existing JSON structure with an associated key. You can provide your own keys, such as user IDs or semantic names.

This structure makes reading data straightforward because clients need only to navigate to a given path, and the entire object is returned in JSON format. No special processing of database fields is required. For more information, see how to Structure your database in the Firebase documentation.

The Firebase Realtime Database doesn't provide an SDK for Python or Go servers. We recommend using the REST API to connect to the service. We recommend that even Java backends use the REST API for Firebase, due to known issues with restarting App Engine threads running the Firebase Java SDK.

When using the REST API, Firebase handles standard HTTP PUT, PATCH, POST, GET, and DELETE methods to perform database operations. For details about how Firebase uses these operations, see Saving Data and Retrieving Data in the Firebase documentation.

Writing data and sending messages

The following code provides examples of how to write and read data. The PUT HTTP method writes or replaces data at a specific Firebase path. The following functions retrieve Application Default Credentials from App Engine with the needed authorization scopes, and use it to create an HTTP object. Subsequent requests made using that HTTP object automatically include the Authorization headers needed to make requests to Firebase.

Python

def _get_http():
    """Provides an authed http object."""
    http = httplib2.Http()
    # Use application default credentials to make the Firebase calls
    # https://firebase.google.com/docs/reference/rest/database/user-auth
    creds = GoogleCredentials.get_application_default().create_scoped(
        _FIREBASE_SCOPES)
    creds.authorize(http)
    return http
...
def firebase_put(path, value=None):
    """Writes data to Firebase.

    An HTTP PUT writes an entire object at the given database path. Updates to
    fields cannot be performed without overwriting the entire object

    Args:
        path - the url to the Firebase object to write.
        value - a json string.
    """
    response, content = _get_http().request(path, method='PUT', body=value)
    return json.loads(content)

Java

public HttpResponse firebasePut(String path, Object object) throws IOException {
  // Make requests auth'ed using Application Default Credentials
  Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
  HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);

  String json = new Gson().toJson(object);
  GenericUrl url = new GenericUrl(path);

  return requestFactory.buildPutRequest(
      url, new ByteArrayContent("application/json", json.getBytes())).execute();
}

To see sample implementations on GitHub for PATCH and POST, click the View on GitHub button in the previous code example.

To read data, make an HTTP GET request for a particular path. The response contains the JSON object at the requested location.

Python

def firebase_get(path):
    """Read the data at the given path.

    An HTTP GET request allows reading of data at a particular path.
    A successful request will be indicated by a 200 OK HTTP status code.
    The response will contain the data being retrieved.

    Args:
        path - the url to the Firebase object to read.
    """
    response, content = _get_http().request(path, method='GET')
    return json.loads(content)

Java

public HttpResponse firebaseGet(String path) throws IOException {
  // Make requests auth'ed using Application Default Credentials
  Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
  HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);

  GenericUrl url = new GenericUrl(path);

  return requestFactory.buildGetRequest(url).execute();
}

See Putting it all together in the sample app for examples that use PATCH and DELETE.

Listening to real-time events from a web browser

To listen to real-time events, you need to:

  • Add the Firebase SDK to your web page.
  • Add a reference to the Firebase path where your data is stored.
  • Add a listener.
  • Implement a callback function.

Adding the Firebase SDK to your web page

To add the Firebase SDK to your web page:

  • Go to your project in the Firebase Console
  • Click "Add Firebase to your web app"
  • Copy the given snippet, and paste it into your HTML code. For example, in this sample project:

Python

Replace the contents of the file templates/_firebase_config.html with the snippet. This file is included in the main template file fire_index.html.

Java

Replace the contents of the file src/main/webapp/WEB-INF/view/firebase_config.jspf with the snippet. This file is included in the main template file index.jsp.

Listening to changes in data

Retrieve Firebase data by attaching an asynchronous listener to a firebase.database.Reference. The listener is triggered once for the initial state of the data, and again anytime the data changes. There are two main types of changes that the client can listen for:

  • Value events: Read and listen for changes to the entire contents of a path. Value events are most commonly used to monitor changes to a single object.
  • Child events: Read and listen for changes to child elements under a particular path. Child events are most commonly used to monitor changes within lists of objects. It is possible to listen for the following child events: child_added, child_changed, child_moved, and child_removed.

This example app keeps a single game-state object in sync between the server and client. For this reason, this article focuses on using listening for value events. Child-event listeners and more advanced data retrieval techniques are described in Retrieve Data on the Web.

The following code shows how to listen for a value-change event and add a callback function to do work when the event is fired. The code first creates a database reference, which is the path for the object to which your app will listen. Next the listener is added to the reference using the reference.on() method, where the callback function is passed as an argument.

In this example, a real-time tic-tac-toe application sends game information to clients listening on particular channels, which correspond to the user’s game, and updates the UI when this information changes in the database. The variable firebase is automatically created by the JavaScript snippet that was added in Adding Firebase to your web page.

Python

// setup a database reference at path /channels/channelId
channel = firebase.database().ref('channels/' + channelId);
// add a listener to the path that fires any time the value of the data changes
channel.on('value', function(data) {
  onMessage(data.val());
});

Java

// setup a database reference at path /channels/channelId
channel = firebase.database().ref('channels/' + channelId);
// add a listener to the path that fires any time the value of the data changes
channel.on('value', function(data) {
  onMessage(data.val());
});

The implementation of the callback function, onMessage(), is shown later in this article.

Detaching listeners

You can remove callbacks by calling the off() method of your Firebase database reference.

Posting events back to App Engine

App Engine does not currently support bidirectional streaming HTTP connections. If a client needs to update the server, it must send an explicit HTTP request.

Securing your data

Firebase allows you to restrict access to data to specific users by using authorization tokens. It is recommended that only your server have write access, while authorizing web clients for read only access. The code below demonstrates how to:

  • Generate a secure credential for your App Engine server.
  • Generate secure tokens for each client.
  • Pass tokens to web clients so that they can read data securely.

Securely communicating between App Engine and Firebase

You can use App Engine’s built-in Application Default Credentials to make authenticated REST calls to your Firebase database from your App Engine server.

Python

Instantiate a GoogleCredentials object, provided by the oauth2client package, with the proper scopes to grant access to Firebase. Then, use that object to authorize an httplib2.Http object.

def _get_http():
    """Provides an authed http object."""
    http = httplib2.Http()
    # Use application default credentials to make the Firebase calls
    # https://firebase.google.com/docs/reference/rest/database/user-auth
    creds = GoogleCredentials.get_application_default().create_scoped(
        _FIREBASE_SCOPES)
    creds.authorize(http)
    return http

That object automatically includes the proper authentication token, and you can use the httplib2.Http object directly to make REST calls. The following function uses the object returned from _get_http() to send either a PATCH or DELETE request. Note that this example calls _get_http(), as part of its return statement, to retrieve the authorized HTTP object:

def _send_firebase_message(u_id, message=None):
    """Updates data in firebase. If a message is provided, then it updates
     the data at /channels/<channel_id> with the message using the PATCH
     http method. If no message is provided, then the data at this location
     is deleted using the DELETE http method
     """
    url = '{}/channels/{}.json'.format(_get_firebase_db_url(), u_id)

    if message:
        return _get_http().request(url, 'PATCH', body=message)
    else:
        return _get_http().request(url, 'DELETE')

Java

Because App Engine restricts the ability to create background threads, you need to use the Firebase API instead of the Firebase Server SDK. directly.

First, retrieve the Application Default Credentials and initialize the UrlFetchTransport object.

credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
      httpTransport = UrlFetchTransport.getDefaultInstance();

Then, use that credential to create an authorized HttpRequestFactory object, using the Url Fetch service that App Engine requires for outbound HTTP requests. To make calls to your Firebase database, you construct a path, and perform the request:

public void sendFirebaseMessage(String channelKey, Game game)
    throws IOException {
  // Make requests auth'ed using Application Default Credentials
  HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);
  GenericUrl url = new GenericUrl(
      String.format("%s/channels/%s.json", firebaseDbUrl, channelKey));
  HttpResponse response = null;

  try {
    if (null == game) {
      response = requestFactory.buildDeleteRequest(url).execute();
    } else {
      String gameJson = new Gson().toJson(game);
      response = requestFactory.buildPatchRequest(
          url, new ByteArrayContent("application/json", gameJson.getBytes())).execute();
    }

    if (response.getStatusCode() != 200) {
      throw new RuntimeException(
          "Error code while updating Firebase: " + response.getStatusCode());
    }

  } finally {
    if (null != response) {
      response.disconnect();
    }
  }
}

Generating the Client Authentication Token(s)

Firebase uses JSON Web Tokens (JWTs) to authorize users. To create JWTs, the app takes advantage of App Engine’s built in App Identity service to securely sign the claims using App Engine’s Application Default Credentials.

The following method demonstrates how to create secure, custom JWTs:

  1. The App Identity service is used to automatically retrieve the service account email.
  2. The header of the token is constructed.
  3. The claims are added to the token payload according to the Firebase authentication documentation. The most important of these claims is uid, which is the unique channel identifier that restricts access to a single user.
  4. Finally, the App Identity service is used again to sign the token.

Python

def create_custom_token(uid, valid_minutes=60):
    """Create a secure token for the given id.

    This method is used to create secure custom JWT tokens to be passed to
    clients. It takes a unique id (uid) that will be used by Firebase's
    security rules to prevent unauthorized access. In this case, the uid will
    be the channel id which is a combination of user_id and game_key
    """

    # use the app_identity service from google.appengine.api to get the
    # project's service account email automatically
    client_email = app_identity.get_service_account_name()

    now = int(time.time())
    # encode the required claims
    # per https://firebase.google.com/docs/auth/server/create-custom-tokens
    payload = base64.b64encode(json.dumps({
        'iss': client_email,
        'sub': client_email,
        'aud': _IDENTITY_ENDPOINT,
        'uid': uid,  # the important parameter, as it will be the channel id
        'iat': now,
        'exp': now + (valid_minutes * 60),
    }))
    # add standard header to identify this as a JWT
    header = base64.b64encode(json.dumps({'typ': 'JWT', 'alg': 'RS256'}))
    to_sign = '{}.{}'.format(header, payload)
    # Sign the jwt using the built in app_identity service
    return '{}.{}'.format(to_sign, base64.b64encode(
        app_identity.sign_blob(to_sign)[1]))

Java

public String createFirebaseToken(Game game, String userId) {
  final AppIdentityService appIdentity = AppIdentityServiceFactory.getAppIdentityService();
  final BaseEncoding base64 = BaseEncoding.base64();

  String header = base64.encode("{\"typ\":\"JWT\",\"alg\":\"RS256\"}".getBytes());

  // Construct the claim
  String channelKey = game.getChannelKey(userId);
  String clientEmail = appIdentity.getServiceAccountName();
  long epochTime = System.currentTimeMillis() / 1000;
  long expire = epochTime + 60 * 60; // an hour from now

  Map<String, Object> claims = new HashMap<String, Object>();
  claims.put("iss", clientEmail);
  claims.put("sub", clientEmail);
  claims.put("aud", IDENTITY_ENDPOINT);
  claims.put("uid", channelKey);
  claims.put("iat", epochTime);
  claims.put("exp", expire);

  String payload = base64.encode(new Gson().toJson(claims).getBytes());
  String toSign = String.format("%s.%s", header, payload);
  AppIdentityService.SigningResult result = appIdentity.signForApp(toSign.getBytes());
  return String.format("%s.%s", toSign, base64.encode(result.getSignature()));
}

The server executes the preceding function and passes the tokens into the template to be rendered on the client. In this example tic-tac-toe application, the player’s user ID and the game key comprise the uid for Firebase authorization.

Authenticating the JavaScript client

After the tokens have been generated on the server and passed into the client HTML, the tokens can be used to authorize read and write commands by the client. It is only necessary to authenticate the client once, and then all subsequent calls using the firebase object will be authenticated. The sample code demonstrates how this is done:

Python

// sign into Firebase with the token passed from the server
firebase.auth().signInWithCustomToken(token).catch(function(error) {
  console.log('Login Failed!', error.code);
  console.log('Error message: ', error.message);
});

Java

// sign into Firebase with the token passed from the server
firebase.auth().signInWithCustomToken(token).catch(function(error) {
  console.log('Login Failed!', error.code);
  console.log('Error message: ', error.message);
});

Updating Firebase database security rules

In the Database section of the Firebase console, click on the Rules tab. Now update Firebase’s security rules to restrict access to only authorized connections. Specifically, restrict writes to the server and only allow reads when the user’s unique ID, uid, in the auth payload matches the unique ID in the database path.

{
    "rules": {
      ".read": false,
      ".write": false,
      "channels": {
        "$channelId": {
          // Require auth token ID to match the path the client is trying to read
          ".read": "auth.uid == $channelId",
          ".write": false
        }
      }
    }
}

Putting it all together in the sample app

To better illustrate how to use Firebase to build real-time applications, look at the sample tic-tac-toe game application. The full source code of the Python application is available at the Fire Tac Toe GitHub page. The full source code of the Java application is available at the Fire Tac Toe GitHub page.

The game allows users to create a game, invite another player by sending out a URL, and play the game together in real time. The application updates both players' views of the board, in real time, as soon as the other player makes a move.

The remainder of this article reviews major highlights that illustrate the key functionality.

Connecting to a Firebase path

When a user visits the game for the first time, two things happen:

  • The game server injects a token into the HTML page sent to the client. The client uses this token to open a connection to Firebase and listen for updates. The unique path that the user listens to is /channels/[USER_ID + GAME_KEY].
  • The game server provides the user with a URL they can share with a friend in order to invite them to join the game.

The following server-side Python code creates the Firebase path for the game app and injects it into the client’s HTML:

Python

# choose a unique identifier for channel_id
channel_id = user.user_id() + game_key
# encrypt the channel_id and send it as a custom token to the
# client
# Firebase's data security rules will be able to decrypt the
# token and prevent unauthorized access
client_auth_token = create_custom_token(channel_id)
_send_firebase_message(channel_id, message=game.to_json())

# game_link is a url that you can open in another browser to play
# against this player
game_link = '{}?g={}'.format(request.base_url, game_key)

# push all the data to the html template so the client will
# have access
template_values = {
    'token': client_auth_token,
    'channel_id': channel_id,
    'me': user.user_id(),
    'game_key': game_key,
    'game_link': game_link,
    'initial_message': urllib.unquote(game.to_json())
}

return flask.render_template('fire_index.html', **template_values)

Java

// The 'Game' object exposes a method which creates a unique string based on the game's key
// and the user's id.
String token = FirebaseChannel.getInstance(getServletContext())
    .createFirebaseToken(game, userId);
request.setAttribute("token", token);

// 4. More general template values
request.setAttribute("game_key", gameKey);
request.setAttribute("me", userId);
request.setAttribute("channel_id", game.getChannelKey(userId));
request.setAttribute("initial_message", new Gson().toJson(game));
request.setAttribute("game_link", getGameUriWithGameParam(request, gameKey));
request.getRequestDispatcher("/WEB-INF/view/index.jsp").forward(request, response);

Now the web client uses the token to listen to the path /channels/[USER_ID + GAME_KEY]. This happens in the openChannel() function. A listener is added to track changes in the value of the path. The <reference>.on() method from Firebase is used to setup a callback that executes on value changes in this case. When the value of the database path changes, the onMessage() method is called to update the game state and UI for the user.

Python

function openChannel() {
  // sign into Firebase with the token passed from the server
  firebase.auth().signInWithCustomToken(token).catch(function(error) {
    console.log('Login Failed!', error.code);
    console.log('Error message: ', error.message);
  });

  // setup a database reference at path /channels/channelId
  channel = firebase.database().ref('channels/' + channelId);
  // add a listener to the path that fires any time the value of the data changes
  channel.on('value', function(data) {
    onMessage(data.val());
  });
  onOpened();
  // let the server know that the channel is open
}
...
function onOpened() {
  $.post('/opened');
}

Java

function openChannel() {
  // sign into Firebase with the token passed from the server
  firebase.auth().signInWithCustomToken(token).catch(function(error) {
    console.log('Login Failed!', error.code);
    console.log('Error message: ', error.message);
  });

  // setup a database reference at path /channels/channelId
  channel = firebase.database().ref('channels/' + channelId);
  // add a listener to the path that fires any time the value of the data changes
  channel.on('value', function(data) {
    onMessage(data.val());
  });
  onOpened();
  // let the server know that the channel is open
}
...
function onOpened() {
  $.post('/opened');
}

Sending updates to the server

When the user makes a move, the method moveInSquare() is called, which checks to see that the move is valid and then sends a HTTP POST request to the server with the location of the move.

Python

function moveInSquare(e) {
  var id = $(e.currentTarget).index();
  if (isMyMove() && state.board[id] === ' ') {
    $.post('/move', {i: id});
  }
}

Java

function moveInSquare(e) {
  var id = $(e.currentTarget).index();
  if (isMyMove() && state.board[id] === ' ') {
    $.post('/move', {cell: id});
  }
}

The server receives the HTTP POST and the handler for the /move path is called. This handler identifies the user and pulls the location of the square off of the request. It then performs the move by calling the method on the Game object:

Python

def move():
    game = Game.get_by_id(request.args.get('g'))
    position = int(request.form.get('i'))
    if not (game and (0 <= position <= 8)):
        return 'Game not found, or invalid position', 400
    game.make_move(position, users.get_current_user())
    return ''

Java

public class MoveServlet extends HttpServlet {
  @Override
  public void doPost(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    String gameId = request.getParameter("gameKey");
    Objectify ofy = ObjectifyService.ofy();
    Game game = ofy.load().type(Game.class).id(gameId).safe();

    UserService userService = UserServiceFactory.getUserService();
    String currentUserId = userService.getCurrentUser().getUserId();

    int cell = new Integer(request.getParameter("cell"));
    if (!game.makeMove(getServletContext(), cell, currentUserId)) {
      response.sendError(HttpServletResponse.SC_FORBIDDEN);
    } else {
      ofy.save().entity(game).now();
    }
  }
}

The method that performs the move completes a final round of validation on the user’s move, checks to see if the user has won the game, and writes the game state to the datastore. Finally, it calls a method to update the game state in Firebase.

Python

def make_move(self, position, user):
    # If the user is a player, and it's their move
    if (user in (self.userX, self.userO)) and (
            self.moveX == (user == self.userX)):
        boardList = list(self.board)
        # If the spot you want to move to is blank
        if (boardList[position] == ' '):
            boardList[position] = 'X' if self.moveX else 'O'
            self.board = ''.join(boardList)
            self.moveX = not self.moveX
            self._check_win()
            self.put()
            self.send_update()
            return

Java

public boolean makeMove(ServletContext servletContext, int position, String userId) {
  String currentMovePlayer;
  char value;
  if (getMoveX()) {
    value = 'X';
    currentMovePlayer = getUserX();
  } else {
    value = 'O';
    currentMovePlayer = getUserO();
  }

  if (currentMovePlayer.equals(userId)) {
    char[] boardBytes = getBoard().toCharArray();
    boardBytes[position] = value;
    setBoard(new String(boardBytes));
    checkWin();
    setMoveX(!getMoveX());
    try {
      sendUpdateToClients(servletContext);
    } catch (IOException e) {
      LOGGER.log(Level.SEVERE, "Error sending Game update to Firebase", e);
      throw new RuntimeException(e);
    }
    return true;
  }

  return false;
}

The update method then calls the method you defined above, to securely perform the HTTP request to Firebase:

Python

def send_update(self):
    """Updates Firebase's copy of the board."""
    message = self.to_json()
    # send updated game state to user X
    _send_firebase_message(
        self.userX.user_id() + self.key.id(), message=message)
    # send updated game state to user O
    if self.userO:
        _send_firebase_message(
            self.userO.user_id() + self.key.id(), message=message)

Java

public String getChannelKey(String userId) {
  return userId + id;
}

public void deleteChannel(ServletContext servletContext, String userId)
    throws IOException {
  if (userId != null) {
    String channelKey = getChannelKey(userId);
    FirebaseChannel.getInstance(servletContext).sendFirebaseMessage(channelKey, null);
  }
}

private void sendUpdateToUser(ServletContext servletContext, String userId)
    throws IOException {
  if (userId != null) {
    String channelKey = getChannelKey(userId);
    FirebaseChannel.getInstance(servletContext).sendFirebaseMessage(channelKey, this);
  }
}

public void sendUpdateToClients(ServletContext servletContext)
    throws IOException {
  sendUpdateToUser(servletContext, userX);
  sendUpdateToUser(servletContext, userO);
}

Finally, it makes the HTTP request:

Python

def _send_firebase_message(u_id, message=None):
    """Updates data in firebase. If a message is provided, then it updates
     the data at /channels/<channel_id> with the message using the PATCH
     http method. If no message is provided, then the data at this location
     is deleted using the DELETE http method
     """
    url = '{}/channels/{}.json'.format(_get_firebase_db_url(), u_id)

    if message:
        return _get_http().request(url, 'PATCH', body=message)
    else:
        return _get_http().request(url, 'DELETE')

Java

public void sendFirebaseMessage(String channelKey, Game game)
    throws IOException {
  // Make requests auth'ed using Application Default Credentials
  HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);
  GenericUrl url = new GenericUrl(
      String.format("%s/channels/%s.json", firebaseDbUrl, channelKey));
  HttpResponse response = null;

  try {
    if (null == game) {
      response = requestFactory.buildDeleteRequest(url).execute();
    } else {
      String gameJson = new Gson().toJson(game);
      response = requestFactory.buildPatchRequest(
          url, new ByteArrayContent("application/json", gameJson.getBytes())).execute();
    }

    if (response.getStatusCode() != 200) {
      throw new RuntimeException(
          "Error code while updating Firebase: " + response.getStatusCode());
    }

  } finally {
    if (null != response) {
      response.disconnect();
    }
  }
}

After this call to Firebase, the web clients receive the updated game state immediately, without polling the server.

Removing listeners and deleting data

When a player has won the game, there is no additional information to be sent from the server. In this case, you need to remove the listeners and delete the game data from Firebase because storage is not necessarily free. To do this, call the off() method on the Firebase reference to which the clients have been listening. Finally, the client sends a message to the server to the /delete endpoint, which removes the data stored for the game that ended.

Python

function onMessage(newState) {
  updateGame(newState);

  // now check to see if there is a winner
  if (channel && state.winner && state.winningBoard) {
    channel.off(); //stop listening on this path
    deleteChannel(); //delete the data we wrote
  }
}
...
function deleteChannel() {
  $.post('/delete');
}

Java

function onMessage(newState) {
  updateGame(newState);

  // now check to see if there is a winner
  if (channel && state.winner && state.winningBoard) {
    channel.off(); //stop listening on this path
    deleteChannel(); //delete the data we wrote
  }
}
...
function deleteChannel() {
  $.post('/delete');
}

Delete data in Firebase by hitting the /delete endpoint's handler:

Python

def delete():
    game = Game.get_by_id(request.args.get('g'))
    if not game:
        return 'Game not found', 400
    user = users.get_current_user()
    _send_firebase_message(user.user_id() + game.key.id(), message=None)
    return ''

Java

public class DeleteServlet extends HttpServlet {
  @Override
  public void doPost(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    String gameId = request.getParameter("gameKey");
    Objectify ofy = ObjectifyService.ofy();
    Game game = ofy.load().type(Game.class).id(gameId).safe();

    UserService userService = UserServiceFactory.getUserService();
    String currentUserId = userService.getCurrentUser().getUserId();

    // TODO(you): In practice, first validate that the user has permission to delete the Game
    game.deleteChannel(getServletContext(), currentUserId);
  }
}

What's next

  • Try out other Google Cloud Platform features for yourself. Have a look at our tutorials.

Send feedback about...