SDK Tiempo Real y Canales Nuevo
El módulo realtime te permite suscribirte a cambios en la base de datos, enviar mensajes entre clientes, rastrear quién está online y construir sistemas de chat completos con canales persistentes — todo mediante una única conexión WebSocket gestionada por el SDK.
Cambios en la Base de Datos
Recibe notificaciones al instante cuando se insertan, actualizan o eliminan filas:
const channel = dypai.realtime.channel('orders-updates')
channel
.on('postgres_changes', { event: '*', table: 'orders' }, (payload) => {
console.log('Evento:', payload.eventType) // INSERT, UPDATE o DELETE
console.log('Nuevos datos:', payload.new)
console.log('Datos anteriores:', payload.old)
})
.subscribe()
Filtrar por tipo de evento
// Solo pedidos nuevos
channel.on('postgres_changes', { event: 'INSERT', table: 'orders' }, callback)
// Solo cambios de estado
channel.on('postgres_changes', {
event: 'UPDATE',
table: 'orders',
filter: 'status=eq.completed'
}, callback)
Broadcast
Envía mensajes efímeros entre clientes conectados — sin base de datos:
const channel = dypai.realtime.channel('room-1')
// Escuchar
channel.on('broadcast', { event: 'typing' }, (payload) => {
console.log(`${payload.user} está escribiendo...`)
})
channel.subscribe()
// Enviar
channel.send('typing', { user: 'Juan' })
Presencia
Rastrea quién está online en tiempo real:
const channel = dypai.realtime.channel('room-1')
channel.on('presence', { event: 'sync' }, () => {
const users = channel.presenceState()
console.log('Online:', Object.keys(users))
})
channel.subscribe()
// Anunciar tu presencia
channel.track({ user_id: '123', name: 'Juan' })
React Hooks
useRealtime — Cambios en la base de datos
import { useRealtime } from '@dypai-ai/client-sdk/react'
function ListaPedidos() {
const { data: pedidos, refetch } = useEndpoint('listar_pedidos')
useRealtime('pedidos', {
onInsert: () => refetch(),
onUpdate: () => refetch(),
onDelete: () => refetch(),
})
return <ul>{pedidos?.map(p => <li key={p.id}>{p.estado}</li>)}</ul>
}
| Parameter | Type | Description |
|---|---|---|
tablerequired | string | Nombre de la tabla a suscribirse |
event | '*' | 'INSERT' | 'UPDATE' | 'DELETE'= '*' | Filtro de tipo de evento |
filter | object | Filtrar por valores de campo, ej. { status: 'active' } |
onInsert(record) | function | Se ejecuta al insertar una fila |
onUpdate(record, old) | function | Se ejecuta al actualizar |
onDelete(old) | function | Se ejecuta al eliminar |
onChange(change) | function | Se ejecuta con cualquier cambio |
enabled | boolean= true | Activar/desactivar la suscripción |
Devuelve: changes, isConnected, status, error
useChannel — Broadcast y Presencia
import { useChannel } from '@dypai-ai/client-sdk/react'
function Editor() {
const { presences, broadcast, memberCount } = useChannel('doc:abc', {
presence: { name: 'Alicia' },
onBroadcast: (msg) => {
if (msg.event === 'cursor') actualizarCursor(msg.sender, msg.payload)
},
})
return <span>{memberCount} online</span>
}
| Parameter | Type | Description |
|---|---|---|
channelrequired | string | Nombre del canal (cualquier string) |
presence | boolean | object | Rastrear presencia. Pasa un objeto con datos custom |
onBroadcast(msg) | function | Se ejecuta con mensajes broadcast |
onJoin(userId, data) | function | Se ejecuta cuando alguien se une |
onLeave(userId, data) | function | Se ejecuta cuando alguien se va |
enabled | boolean= true | Activar/desactivar |
Devuelve: broadcast(event, payload?), presences, isConnected, memberCount
useChannelMessages — Chat Persistente
Mensajería completa con almacenamiento en base de datos y entrega en tiempo real:
import { useChannelMessages } from '@dypai-ai/client-sdk/react'
function ChatEquipo({ channelId }) {
const { messages, send, isLoading, isConnected } = useChannelMessages(channelId)
return (
<div>
{messages.map(m => (
<div key={m.id}>
<b>{m.sender_email}</b>: {m.content}
</div>
))}
<input onKeyDown={e => {
if (e.key === 'Enter') { send(e.target.value); e.target.value = '' }
}} />
</div>
)
}
| Parameter | Type | Description |
|---|---|---|
channelIdrequired | string | ID del canal (ej. 'ch_abc123') |
loadHistory | boolean= true | Cargar historial al montar |
limit | number= 50 | Máximo de mensajes a cargar |
Devuelve: messages, send(content, metadata?), isLoading, isConnected, error
useChannels — Gestión de Canales
import { useChannels } from '@dypai-ai/client-sdk/react'
function ListaCanales() {
const { channels, createChannel } = useChannels()
return (
<div>
{channels.map(ch => <div key={ch.id}>{ch.name} ({ch.member_count})</div>)}
<button onClick={() => createChannel('General', [], 'public')}>Nuevo</button>
</div>
)
}
| Parameter | Type | Description |
|---|---|---|
channels | Channel[] | Canales del usuario + canales públicos |
createChannel(name, members?, type?) | async | Crear canal. Tipo: 'group', 'dm' o 'public' |
isLoading | boolean | Estado de carga |
refetch() | async | Recargar lista de canales |
Tipos de Canal
| Tipo | Descripción | Quién puede unirse | Auto-dedup |
|---|---|---|---|
group | Chat privado de grupo | Solo miembros invitados | No |
dm | Mensaje directo (1 a 1) | Los 2 participantes | Sí — no crea duplicados |
public | Canal abierto | Cualquiera (auto-join al enviar mensaje) | No |
// DM — llamar dos veces con los mismos usuarios devuelve el canal existente
const dm = await createChannel('Alicia ↔ Roberto', ['roberto-user-id'], 'dm')
Roles de Miembros
| Acción | admin | member |
|---|---|---|
| Enviar/leer mensajes | ✅ | ✅ |
| Editar/borrar propios | ✅ | ✅ |
| Borrar mensajes ajenos | ✅ | ❌ |
| Añadir/quitar miembros | ✅ | ❌ |
| Cambiar roles | ✅ | ❌ |
El creador del canal recibe el rol admin automáticamente.
API REST de Canales
Endpoints built-in en /api/v0/channels — no necesitas crear workflows:
| Método | Ruta | Descripción |
|---|---|---|
POST | /channels | Crear canal |
GET | /channels | Listar canales |
GET | /channels/:id | Detalle + miembros |
PATCH | /channels/:id | Actualizar nombre |
DELETE | /channels/:id | Eliminar canal |
GET | /channels/users | Listar usuarios del proyecto |
POST | /channels/:id/messages | Enviar mensaje |
GET | /channels/:id/messages | Historial (?limit=50&before=timestamp) |
PATCH | /channels/:id/messages/:msgId | Editar mensaje |
DELETE | /channels/:id/messages/:msgId | Borrar (soft delete) |
GET | /channels/:id/unread | Contador de no leídos |
POST | /channels/:id/read | Marcar como leído |
POST | /channels/:id/join | Unirse a canal público |
POST | /channels/:id/leave | Salir del canal |
POST | /channels/:id/members | Añadir miembros |
DELETE | /channels/:id/members/:userId | Quitar miembro |
PATCH | /channels/:id/members/:userId | Cambiar rol |
GET | /channels/:id/members | Listar miembros |
Comportamiento de la Conexión
- WebSocket único — todos los hooks comparten una conexión. Sin duplicados.
- Reconexión automática — backoff exponencial (1s → 2s → 5s → 10s → 30s).
- Heartbeat — ping cada 25s para mantener la conexión viva.
- Auto-replay — suscripciones y joins se reenvían al reconectar.
- Multi-tab — cada pestaña tiene su propia conexión. La presencia las rastrea por separado.
Ejemplo Completo — Chat de Equipo
import {
DypaiProvider, useAuth, useChannelMessages, useChannels, useChannel
} from '@dypai-ai/client-sdk/react'
import { dypai } from './lib/dypai'
function App() {
return (
<DypaiProvider client={dypai}>
<Chat />
</DypaiProvider>
)
}
function Chat() {
const { user } = useAuth()
const miNombre = user?.email?.split('@')[0] || 'anónimo'
const { channels, createChannel } = useChannels()
const [activeId, setActiveId] = useState(channels[0]?.id)
return (
<div className="flex">
<aside className="w-56">
{channels.map(ch => (
<button key={ch.id} onClick={() => setActiveId(ch.id)}>
# {ch.name}
</button>
))}
<button onClick={() => createChannel('Nuevo Canal')}>+</button>
</aside>
{activeId && <ChatRoom channelId={activeId} myName={miNombre} />}
</div>
)
}
function ChatRoom({ channelId, myName }) {
const { messages, send, isLoading, isConnected } = useChannelMessages(channelId)
const { presences } = useChannel(`channel:${channelId}`, {
presence: { name: myName },
})
// Indicador de escritura
const [escribiendo, setEscribiendo] = useState(new Set())
const { broadcast } = useChannel(`channel:${channelId}:typing`, {
onBroadcast: (msg) => {
if (msg.event === 'typing' && msg.payload.name !== myName) {
setEscribiendo(prev => new Set(prev).add(msg.payload.name))
setTimeout(() => setEscribiendo(prev => {
const next = new Set(prev); next.delete(msg.payload.name); return next
}), 2000)
}
},
})
const [input, setInput] = useState('')
return (
<div className="flex-1 flex flex-col">
<div className="p-2 border-b flex justify-between">
<span>{isConnected ? '● En vivo' : '○ Desconectado'}</span>
<span>{Object.values(presences).map(p => p.data?.name).join(', ')}</span>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-2">
{isLoading && <p>Cargando...</p>}
{messages.map(m => (
<div key={m.id}>
<b>{m.sender_email}</b>: {m.content}
</div>
))}
{escribiendo.size > 0 && (
<p className="text-sm italic">{[...escribiendo].join(', ')} escribiendo...</p>
)}
</div>
<div className="p-3 border-t">
<input
value={input}
onChange={e => { setInput(e.target.value); broadcast('typing', { name: myName }) }}
onKeyDown={e => { if (e.key === 'Enter') { send(input); setInput('') } }}
placeholder="Escribe un mensaje..."
className="w-full border rounded px-3 py-2"
/>
</div>
</div>
)
}
Limpieza
// Desuscribirse de un canal específico
channel.unsubscribe()
// Eliminar todas las suscripciones
dypai.realtime.removeAllChannels()