Pular para o conteúdo

Assinaturas em Tempo Real

O SDK Cliente da Rebase fornece assinaturas de dados em tempo real via WebSocket. Quando os registros mudam no servidor, seus callbacks assinados disparam imediatamente com os dados atualizados.

A conexão WebSocket é estabelecida automaticamente quando uma websocketUrl está disponível (derivada de baseUrl por padrão). A reconexão e a atualização de tokens são tratadas de forma transparente.

Use listen() para assinar uma consulta de coleção. O callback dispara sempre que o conjunto de dados correspondente muda:

const unsubscribe = client.data.products.listen(
{ where: { active: ["==", true] }, limit: 50 },
(response) => {
console.log("Products updated:", response.data);
console.log("Total:", response.meta.total);
}
);
// Stop listening when done
unsubscribe();

O método listen() aceita os mesmos FindParams que find() — você pode filtrar, ordenar e paginar sua assinatura:

const unsubscribe = client.data.orders.listen(
{
where: { status: ["==", "pending"] },
orderBy: ["created_at", "desc"],
limit: 20
},
(response) => {
renderOrders(response.data);
},
(error) => {
console.error("Subscription error:", error);
}
);
listen(
params: FindParams | undefined,
onUpdate: (response: FindResponse<M>) => void,
onError?: (error: Error) => void
): () => void // returns unsubscribe function

Quando listen() dispara, ele emite atualizações em até duas fases:

  1. Imediata (estimada): O primeiro callback dispara instantaneamente com as entidades e metadados de paginação heurísticos (total = número de entidades retornadas, hasMore = se a contagem é igual ao limite solicitado). Essa emissão carrega meta.total: true.

  2. Autoritativa (opcional): Uma consulta de contagem assíncrona é executada em segundo plano. Se o total ou hasMore autoritativo diferir da estimativa, um segundo callback dispara com metadados corrigidos e sem o sinalizador estimated. Se os valores coincidirem, a segunda emissão é totalmente ignorada — seu callback dispara apenas uma vez.

Se a consulta de contagem falhar, nenhuma segunda emissão ocorre. O sinalizador estimated: true da primeira emissão permanece como sinal de que os metadados são heurísticos. Isso não é tratado como um erro de assinatura.

client.data.products.listen(
{ where: { active: ["==", true] }, limit: 50 },
(response) => {
if (response.meta.total) {
// First-paint: render immediately, total/hasMore may change
renderProducts(response.data, { loading: true });
} else {
// Authoritative: safe to render final pagination controls
renderProducts(response.data, { loading: false });
}
}
);

Dica: Se você não precisar distinguir entre metadados estimados e autoritativos, pode ignorar o sinalizador estimated — ambas as emissões carregam o mesmo array data.

Use listenById() para observar um registro específico pelo seu ID:

const unsubscribe = client.data.products.listenById(
42,
(entity) => {
if (entity) {
console.log("Product changed:", entity.values.name);
} else {
console.log("Product was deleted");
}
},
(error) => {
console.error("Subscription error:", error);
}
);
listenById(
id: string | number,
onUpdate: (entity: Entity<M> | undefined) => void,
onError?: (error: Error) => void
): () => void // returns unsubscribe function

O callback recebe undefined quando a entidade é excluída.

Você também pode assinar através do construtor de consultas fluente. Isso é equivalente a chamar listen() com parâmetros, mas permite encadear .where(), .orderBy(), etc.:

const unsubscribe = client.data.products
.where("active", "==", true)
.orderBy("created_at", "desc")
.limit(20)
.listen(
(response) => console.log("Updated:", response.data),
(error) => console.error("Error:", error)
);

Toda assinatura retorna uma função unsubscribe. Chame-a para parar de receber atualizações e limpar o listener do WebSocket:

const unsubscribe = client.data.products.listen(
undefined,
(response) => { /* ... */ }
);
// Later, when the component unmounts or you no longer need updates:
unsubscribe();

No React, use a limpeza do useEffect:

useEffect(() => {
const unsubscribe = client.data.products.listen(
{ where: { active: ["==", true] } },
(response) => setProducts(response.data)
);
return () => unsubscribe();
}, []);

O cliente WebSocket cuida da autenticação automaticamente:

  • No login ou na atualização do token, o novo token é enviado ao servidor WebSocket através de uma mensagem authenticate.
  • No logout, a conexão WebSocket é desconectada.
  • Se a conexão cair, o cliente se reconecta automaticamente e restabelece todas as assinaturas ativas.

Nenhum gerenciamento manual de tokens é necessário — a integração entre client.auth e a camada WebSocket é tratada internamente.

Os canais de broadcast permitem enviar mensagens arbitrárias entre clientes conectados — ideal para chat, notificações ou recursos colaborativos:

// Obtain a channel. This alone opens no connection.
const channel = client.realtime.channel("chat-room");
// Listen for broadcasts. Pass an event name to filter, or omit it for all.
channel.onBroadcast("message", (payload) => {
console.log("New message:", payload);
});
// Send to every other member — the sender never receives its own message.
await channel.broadcast("message", {
text: "Hello, world!",
userId: currentUser.id
});
// Leave, releasing handlers and timers.
await channel.leave();

Os canais são leves e efêmeros — existem enquanto pelo menos um cliente estiver assinado.

Por padrão, os broadcasts não são reenviados. Eles alcançam apenas os membros conectados no momento. É o que se quer para notificações que se autocorrigem — um aviso de «alguém salvou» é substituído pelo salvamento seguinte — e não custa nada. Para um fluxo de operações, onde uma lacuna silenciosa causa divergência, ative o histórico de mensagens no canal.

Um canal pode ser configurado para reter seus broadcasts, de modo que um cliente que se reconecta recupere o que perdeu em vez de ressincronizar do zero. É isso que torna os canais utilizáveis como transporte para edição colaborativa.

A retenção é configurada no servidor, por padrão de canal — veja Backend Realtime. Um cliente não pode ativá-la por conta própria, porque um canal é criado por quem o nomeia, e uma profundidade de histórico escolhida pelo cliente permitiria a qualquer visitante comprometer seu backend com armazenamento ilimitado.

Em um canal com retenção, passe { history: true } e o SDK faz o resto:

const channel = client.realtime.channel("doc:42", { history: true });
// Handlers receive replayed messages exactly like live ones, in order.
channel.onBroadcast("op", (payload) => {
applyOperation(payload);
});
await channel.join();

No join() e após cada reconexão, o SDK pede ao servidor tudo o que vem depois do último número de sequência que viu, e entrega o resultado pelos mesmos handlers. Não há um segundo caminho de código a escrever: um handler que aplica uma operação corretamente ao vivo a aplica corretamente na recuperação.

Todo broadcast em um canal com retenção carrega um seq — por canal, sem lacunas e crescente. É o ponto de retomada do cliente.

channel.onBroadcast((event) => {
console.log(event.seq); // 1, 2, 3, …
console.log(event.replayed); // true when delivered by catch-up
});
console.log(channel.sequence); // highest seq delivered so far

Persista channel.sequence se quiser que a recuperação sobreviva também a um recarregamento de página, e devolva-o via history({ sinceSeq }).

const { messages, retained, latestSeq } = await channel.history({
sinceSeq: 0,
limit: 100
});

retained: false significa que o canal não guarda histórico e nunca guardará — uma resposta explícita, para que você possa distinguir «você não perdeu nada» de «este canal não tem regra de retenção». No segundo caso, um cliente que precisa convergir tem de recorrer a uma ressincronização completa.

latestSeq é a maior sequência que o servidor possui, tenha este lote chegado a ela ou não. Se estiver muito além do seu último seq entregue, você está mais atrasado do que uma página e ressincronizar pode sair mais barato que paginar.

A presença permite rastrear quais usuários estão online e sincronizar o estado compartilhado entre todos os participantes:

const channel = client.realtime.channel("editors");
// Publish your presence. This is also what opens the connection.
await channel.track({
userId: currentUser.id,
status: "editing",
cursor: { x: 100, y: 200 }
});
// One handler for every change. `presences` is always the full roster;
// `diff` is what changed, when you only care about the delta.
channel.onPresence((presences, diff) => {
console.log("Online users:", Object.keys(presences));
if (diff) {
console.log("joined:", Object.keys(diff.joins));
console.log("left:", Object.keys(diff.leaves));
}
});
// Calling track() again replaces your state — this is how you publish a
// moving cursor.
await channel.track({ userId: currentUser.id, status: "idle" });
// Stop publishing without leaving the channel.
await channel.untrack();

A presença é construída sobre os canais de broadcast com diferenciação automática de estado — apenas as mudanças são transmitidas.

Caso de Uso Método
Painel com dados ao vivo listen() com filtros
Chat ou mensagens channel.broadcast()
Edição colaborativa / fluxos de operações channel(name, { history: true })
Indicadores de digitação / status online channel.track() + channel.onPresence()
Página de detalhe com atualizações ao vivo listenById()
Monitoramento do painel de administração listen() com orderBy e limit

Dica: Para buscas de dados pontuais, use find() ou findById(). As assinaturas são ideais para dados que mudam com frequência e precisam ser refletidos na interface imediatamente.