Como ativar recursos off-line no JavaScript

Os aplicativos do Firebase funcionam mesmo se o app perder temporariamente a conexão com a rede. Neste documento, apresentamos várias ferramentas que oferecemos para monitorar a presença e sincronizar o estado local com o estado do servidor.

Como gerenciar a presença

Nos apps em tempo real, costuma ser útil detectar quando os clientes se conectam e desconectam. Por exemplo, para marcar um usuário como off-line quando o cliente dele se desconecta.

Os clientes do Firebase Database oferecem primitivos simples que você usa para gravar no banco de dados quando um cliente se desconecta dos servidores do Firebase Database. Podemos confiar nessas atualizações para limpar os dados quando uma conexão é perdida ou quando ocorre uma falha no cliente porque elas são executadas mesmo quando o cliente se desconecta incorretamente. É possível executar todas as operações de gravação, como configuração, atualização e remoção, após uma desconexão.

Este é um exemplo simples de gravação de dados após a desconexão usando o primitivo onDisconnect:

API modular da Web

import { getDatabase, ref, onDisconnect } from "firebase/database";

const db = getDatabase();
const presenceRef = ref(db, "disconnectmessage");
// Write a string when this client loses connection
onDisconnect(presenceRef).set("I disconnected!");

API com namespace da Web

var presenceRef = firebase.database().ref("disconnectmessage");
// Write a string when this client loses connection
presenceRef.onDisconnect().set("I disconnected!");

Como o onDisconnect funciona

Quando você estabelece uma operação onDisconnect(), ela reside no servidor do Firebase Realtime Database. O servidor verifica a segurança para garantir que o usuário possa executar o evento de gravação solicitado e informa ao app se ele for inválido. Em seguida, o servidor monitora a conexão. Se, a qualquer momento, o tempo limite da conexão se esgotar ou se ela for ativamente fechada pelo cliente do Realtime Database, o servidor verificará a segurança mais uma vez para garantir que a operação ainda seja válida e depois invocará o evento.

O app usa o retorno de chamada na operação de gravação para garantir que o onDisconnect foi corretamente anexado:

API modular da Web

onDisconnect(presenceRef).remove().catch((err) => {
  if (err) {
    console.error("could not establish onDisconnect event", err);
  }
});

API com namespace da Web

presenceRef.onDisconnect().remove((err) => {
  if (err) {
    console.error("could not establish onDisconnect event", err);
  }
});

Um evento onDisconnect também pode ser cancelado ao chamar .cancel():

API modular da Web

const onDisconnectRef = onDisconnect(presenceRef);
onDisconnectRef.set("I disconnected");
// some time later when we change our minds
onDisconnectRef.cancel();

API com namespace da Web

var onDisconnectRef = presenceRef.onDisconnect();
onDisconnectRef.set("I disconnected");
// some time later when we change our minds
onDisconnectRef.cancel();

Como detectar um estado de conexão

Para muitos recursos relacionados à presença, é útil que o app saiba quando está on-line ou off-line. O Firebase Realtime Database dispõe de um local especial em /.info/connected, que é atualizado sempre que o estado da conexão do cliente do Firebase Realtime Database muda. Confira um exemplo:

API modular da Web

import { getDatabase, ref, onValue } from "firebase/database";

const db = getDatabase();
const connectedRef = ref(db, ".info/connected");
onValue(connectedRef, (snap) => {
  if (snap.val() === true) {
    console.log("connected");
  } else {
    console.log("not connected");
  }
});

API com namespace da Web

var connectedRef = firebase.database().ref(".info/connected");
connectedRef.on("value", (snap) => {
  if (snap.val() === true) {
    console.log("connected");
  } else {
    console.log("not connected");
  }
});

/.info/connected é um valor booleano não sincronizado entre os clientes do Realtime Database, porque esse valor depende do estado deles. Em outras palavras, se um cliente lê /.info/connected como falso, isso não garante que outro cliente também faça a mesma leitura.

Como gerenciar a latência

Carimbos de data/hora do servidor

Os servidores do Firebase Realtime Database têm um mecanismo para inserir carimbos de data/hora gerados no servidor como dados. Combinado com o onDisconnect, esse recurso é uma maneira fácil de armazenar o horário em que um cliente do Realtime Database se desconectou:

API modular da Web

import { getDatabase, ref, onDisconnect, serverTimestamp } from "firebase/database";

const db = getDatabase();
const userLastOnlineRef = ref(db, "users/joe/lastOnline");
onDisconnect(userLastOnlineRef).set(serverTimestamp());

API com namespace da Web

var userLastOnlineRef = firebase.database().ref("users/joe/lastOnline");
userLastOnlineRef.onDisconnect().set(firebase.database.ServerValue.TIMESTAMP);

Defasagem horária

Embora firebase.database.ServerValue.TIMESTAMP seja muito mais preciso e indicado para a maioria das operações de leitura/gravação, em algumas situações é importante estimar a defasagem horária dos clientes em relação aos servidores do Firebase Realtime Database. Anexe um retorno de chamada ao local /.info/serverTimeOffset para receber o valor, em milissegundos, que os clientes do Firebase Realtime Database adicionam ao horário local informado (tempo de época em milissegundos) para estimar o horário do servidor. A precisão dessa diferença pode ser afetada pela latência da rede. Portanto, ela é útil principalmente para descobrir grandes discrepâncias de mais de um segundo no horário do relógio.

API modular da Web

import { getDatabase, ref, onValue } from "firebase/database";

const db = getDatabase();
const offsetRef = ref(db, ".info/serverTimeOffset");
onValue(offsetRef, (snap) => {
  const offset = snap.val();
  const estimatedServerTimeMs = new Date().getTime() + offset;
});

API com namespace da Web

var offsetRef = firebase.database().ref(".info/serverTimeOffset");
offsetRef.on("value", (snap) => {
  var offset = snap.val();
  var estimatedServerTimeMs = new Date().getTime() + offset;
});

Exemplo de app de presença

Combine as operações de desconexão com o monitoramento de estado da conexão e os carimbos de data/hora do servidor para criar um sistema de presença do usuário. Nesse sistema, cada usuário armazena dados em um local do banco de dados para indicar se um cliente do Realtime Database está on-line. Os clientes definem esse local como verdadeiro quando ficam on-line e um carimbo de data/hora quando se desconectam. Esse carimbo de data/hora indica a última vez em que o usuário esteve on-line.

Observe que o app precisa colocar as operações de desconexão em fila antes que um usuário seja marcado como on-line, para evitar quaisquer disputas se a conexão de rede do cliente for perdida antes que os dois comandos possam ser enviados ao servidor.

Confira um sistema simples de presença do usuário:

API modular da Web

import { getDatabase, ref, onValue, push, onDisconnect, set, serverTimestamp } from "firebase/database";

// Since I can connect from multiple devices or browser tabs, we store each connection instance separately
// any time that connectionsRef's value is null (i.e. has no children) I am offline
const db = getDatabase();
const myConnectionsRef = ref(db, 'users/joe/connections');

// stores the timestamp of my last disconnect (the last time I was seen online)
const lastOnlineRef = ref(db, 'users/joe/lastOnline');

const connectedRef = ref(db, '.info/connected');
onValue(connectedRef, (snap) => {
  if (snap.val() === true) {
    // We're connected (or reconnected)! Do anything here that should happen only if online (or on reconnect)
    const con = push(myConnectionsRef);

    // When I disconnect, remove this device
    onDisconnect(con).remove();

    // Add this device to my connections list
    // this value could contain info about the device or a timestamp too
    set(con, true);

    // When I disconnect, update the last time I was seen online
    onDisconnect(lastOnlineRef).set(serverTimestamp());
  }
});

API com namespace da Web

// Since I can connect from multiple devices or browser tabs, we store each connection instance separately
// any time that connectionsRef's value is null (i.e. has no children) I am offline
var myConnectionsRef = firebase.database().ref('users/joe/connections');

// stores the timestamp of my last disconnect (the last time I was seen online)
var lastOnlineRef = firebase.database().ref('users/joe/lastOnline');

var connectedRef = firebase.database().ref('.info/connected');
connectedRef.on('value', (snap) => {
  if (snap.val() === true) {
    // We're connected (or reconnected)! Do anything here that should happen only if online (or on reconnect)
    var con = myConnectionsRef.push();

    // When I disconnect, remove this device
    con.onDisconnect().remove();

    // Add this device to my connections list
    // this value could contain info about the device or a timestamp too
    con.set(true);

    // When I disconnect, update the last time I was seen online
    lastOnlineRef.onDisconnect().set(firebase.database.ServerValue.TIMESTAMP);
  }
});