Ir al contenido

Suscripciones en Tiempo Real

El SDK del Cliente de Rebase proporciona suscripciones a datos en tiempo real mediante WebSocket. Cuando los registros cambian en el servidor, sus callbacks suscritos se disparan de inmediato con los datos actualizados.

La conexión WebSocket se establece automáticamente cuando hay una websocketUrl disponible (derivada de baseUrl de forma predeterminada). La reconexión y el refresco de tokens se gestionan de forma transparente.

Use listen() para suscribirse a una consulta de colección. El callback se dispara cada vez que cambia el conjunto de datos coincidente:

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();

El método listen() acepta los mismos FindParams que find() — puede filtrar, ordenar y paginar su suscripción:

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

Cuando listen() se dispara, emite actualizaciones en hasta dos fases:

  1. Inmediata (estimada): El primer callback se dispara al instante con las entidades y metadatos de paginación heurísticos (total = número de entidades devueltas, hasMore = si el conteo es igual al límite solicitado). Esta emisión lleva meta.total: true.

  2. Autoritativa (opcional): Una consulta de conteo asíncrona se ejecuta en segundo plano. Si el total o hasMore autoritativo difiere de la estimación, se dispara un segundo callback con metadatos corregidos y sin la marca estimated. Si los valores coinciden, la segunda emisión se omite por completo — su callback se dispara solo una vez.

Si la consulta de conteo falla, no se produce una segunda emisión. La marca estimated: true de la primera emisión permanece como señal de que los metadatos son heurísticos. Esto no se trata como un error de suscripción.

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 });
}
}
);

Consejo: Si no necesita distinguir entre metadatos estimados y autoritativos, puede ignorar la marca estimated — ambas emisiones llevan el mismo array data.

Use listenById() para observar un registro específico por su 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

El callback recibe undefined cuando la entidad se elimina.

También puede suscribirse a través del constructor de consultas fluido. Es equivalente a llamar a listen() con parámetros, pero permite encadenar .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)
);

Cada suscripción devuelve una función unsubscribe. Llámela para dejar de recibir actualizaciones y limpiar el listener de WebSocket:

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

En React, use la limpieza de useEffect:

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

El cliente WebSocket gestiona la autenticación automáticamente:

  • Al iniciar sesión o refrescar el token, el nuevo token se envía al servidor WebSocket mediante un mensaje authenticate.
  • Al cerrar sesión, la conexión WebSocket se desconecta.
  • Si la conexión se cae, el cliente se reconecta automáticamente y restablece todas las suscripciones activas.

No se necesita gestión manual de tokens — la integración entre client.auth y la capa WebSocket se gestiona internamente.

Los canales de difusión le permiten enviar mensajes arbitrarios entre clientes conectados — ideal para chat, notificaciones o funciones colaborativas:

// 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();

Los canales son ligeros y efímeros — existen mientras al menos un cliente esté suscrito. Las llamadas repetidas a channel() con el mismo nombre devuelven el mismo objeto, por lo que dos componentes pueden adjuntar manejadores de forma independiente sin que uno corte al otro al salir.

Las tramas de canal y presencia no requieren una cuenta: los visitantes anónimos pueden unirse a canales públicos, y el servidor sigue autorizando cada trama.

Por defecto, las difusiones no se reproducen. Solo llegan a los miembros conectados en ese momento. Eso es lo que se quiere para notificaciones que se autocorrigen — un aviso de «alguien ha guardado» queda sustituido por el siguiente guardado — y no cuesta nada. Para un flujo de operaciones, donde un hueco silencioso causa divergencia, active el historial de mensajes en el canal.

Un canal puede configurarse para conservar sus difusiones, de modo que un cliente que se reconecta recupere lo que se perdió en lugar de resincronizarse desde cero. Esto es lo que hace que los canales sirvan como transporte para la edición colaborativa.

La retención se configura en el servidor, por patrón de canal — consulte Backend de Tiempo Real. Un cliente no puede activarla por su cuenta, porque un canal lo crea quien lo nombra, y una profundidad de historial elegida por el cliente permitiría a cualquier visitante comprometer su backend con almacenamiento ilimitado.

En un canal con retención, pase { history: true } y el SDK hace el 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();

Al hacer join() y tras cada reconexión, el SDK pide al servidor todo lo posterior al último número de secuencia que vio, y entrega el resultado a través de los mismos manejadores. No hay un segundo camino de código que escribir: un manejador que aplica una operación correctamente en vivo la aplica correctamente al recuperar.

Cada difusión en un canal con retención lleva un seq — por canal, sin huecos y creciente. Es el punto de reanudación del 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

Guarde channel.sequence si quiere que la recuperación sobreviva también a una recarga de página, y devuélvalo mediante history({ sinceSeq }).

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

retained: false significa que el canal no guarda historial y nunca lo hará — una respuesta explícita, para que pueda distinguir «no se perdió nada» de «este canal no tiene regla de retención». En el segundo caso, un cliente que necesite converger debe recurrir a una resincronización completa.

latestSeq es la secuencia más alta que tiene el servidor, haya llegado o no este lote hasta ella. Si está muy por delante de su último seq entregado, va más atrasado que una página y resincronizar puede salir más barato que paginar.

La presencia le permite rastrear qué usuarios están en línea y sincronizar el estado compartido entre todos los 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();

La presencia se construye sobre los canales de difusión con diferenciación automática del estado — solo se transmiten los cambios.

Caso de Uso Método
Panel con datos en vivo listen() con filtros
Chat o mensajería channel.broadcast()
Edición colaborativa / flujos de operaciones channel(name, { history: true })
Indicadores de escritura / estado en línea channel.track() + channel.onPresence()
Página de detalle con actualizaciones en vivo listenById()
Monitorización del panel de administración listen() con orderBy y limit

Consejo: Para obtener datos una sola vez, use find() o findById() en su lugar. Las suscripciones son mejores para datos que cambian con frecuencia y deben reflejarse en la UI de inmediato.