> ## Índice de la documentación
> Consulta el índice completo de la documentación en: https://developer.rhombus.com/llms.txt
> Úsalo para descubrir todas las páginas disponibles antes de seguir explorando.

# Ciclo de vida de la conexión WebSocket

> Administra el ciclo de vida de una conexión WebSocket de Rhombus: realiza handshakes STOMP, envía heartbeats, maneja reconexiones y cierra correctamente.

> **Note**: Esta página se tradujo automáticamente. Si encuentras errores o tienes sugerencias, [escríbenos](mailto:support@rhombus.com).

Esta guía cubre el ciclo de vida completo de una conexión WebSocket de Rhombus: establecer la conexión, realizar el handshake STOMP, mantener la conexión con heartbeats y manejar las desconexiones adecuadamente.

## Descripción general del ciclo de vida

```text
┌─────────────────┐
│  1. WebSocket    │
│     Handshake    │──── WSS + Auth Headers
└────────┬────────┘
         ▼
┌─────────────────┐
│  2. STOMP        │
│     CONNECT      │──── Version + Heartbeat negotiation
└────────┬────────┘
         ▼
┌─────────────────┐
│  3. SUBSCRIBE    │──── Topic subscription
└────────┬────────┘
         ▼
┌─────────────────┐
│  4. MESSAGE      │◄─── Real-time events
│     Loop         │───► Heartbeat ping/pong
└────────┬────────┘
         ▼
┌─────────────────┐
│  5. DISCONNECT   │──── Graceful shutdown
└─────────────────┘
```

## Paso 1: handshake de WebSocket

Inicia una conexión WebSocket segura al endpoint de Rhombus con tu credencial. Este ejemplo usa una clave de API; los access tokens de OAuth y los tokens federados también funcionan (consulta [Autenticación de WebSocket](https://developer.rhombus.com/es/websocket/authentication)):

```python
import websocket

ws = websocket.create_connection(
    "wss://ws.rhombussystems.com:8443/websocket?x-auth-scheme=api-token",
    header={"x-auth-apikey": API_TOKEN},
    timeout=10  # 10-second handshake timeout
)
```

Un handshake exitoso actualiza la conexión HTTP a WebSocket. Si la autenticación falla, el servidor responde antes de que se complete la actualización con un error HTTP y un cuerpo JSON cuyo `errorCode` indica el motivo: 401 cuando la credencial falta, es desconocida, expiró o fue revocada, y 403 cuando es válida pero no está permitida (por ejemplo, una credencial de partner sin `x-auth-org`). Consulta [Autenticación de WebSocket](https://developer.rhombus.com/es/websocket/authentication#solución-de-problemas).

## Paso 2: STOMP CONNECT

Después de que se establezca la conexión WebSocket, envía un frame STOMP `CONNECT` para inicializar el protocolo de mensajería:

### Frame CONNECT

```text
CONNECT
accept-version:1.2
heart-beat:10000,10000

\x00
```

| Encabezado       | Valor         | Descripción                                                           |
| ---------------- | ------------- | --------------------------------------------------------------------- |
| `accept-version` | `1.2`         | Versión del protocolo STOMP                                           |
| `heart-beat`     | `10000,10000` | Intervalo de envío/recepción de heartbeat del cliente en milisegundos |

### Respuesta esperada: frame CONNECTED

```text
CONNECTED
version:1.2
heart-beat:10000,10000

\x00
```

El servidor devuelve la versión y los intervalos de heartbeat negociados.

### Construcción de frames STOMP

Los frames STOMP siguen un formato simple basado en texto:

```text
COMMAND\n
header1:value1\n
header2:value2\n
\n
body (optional)
\x00
```

- Cada frame comienza con un nombre de comando
- Los encabezados son pares clave-valor separados por dos puntos, uno por línea
- Una línea vacía separa los encabezados del cuerpo
- El frame termina con un byte nulo (`\x00`)

```python Python
def build_stomp_frame(command, headers=None, body=""):
    """Construct a STOMP protocol frame."""
    frame = command + "\n"
    if headers:
        for key, value in headers.items():
            frame += f"{key}:{value}\n"
    frame += "\n"
    frame += body
    frame += "\x00"
    return frame

# Send CONNECT
connect_frame = build_stomp_frame("CONNECT", {
    "accept-version": "1.2",
    "heart-beat": "10000,10000"
})
ws.send(connect_frame)

# Read CONNECTED response
response = ws.recv()
```

```javascript JavaScript
function buildStompFrame(command, headers = {}, body = "") {
  let frame = command + "\n";
  for (const [key, value] of Object.entries(headers)) {
    frame += `${key}:${value}\n`;
  }
  frame += "\n";
  frame += body;
  frame += "\x00";
  return frame;
}

// Send CONNECT
ws.send(
  buildStompFrame("CONNECT", {
    "accept-version": "1.2",
    "heart-beat": "10000,10000",
  })
);
```

```go Go
func buildStompFrame(command string, headers map[string]string, body string) string {
    var b strings.Builder
    b.WriteString(command + "\n")
    for k, v := range headers {
        b.WriteString(fmt.Sprintf("%s:%s\n", k, v))
    }
    b.WriteString("\n")
    b.WriteString(body)
    b.WriteByte(0x00)
    return b.String()
}

// Send CONNECT
connectFrame := buildStompFrame("CONNECT", map[string]string{
    "accept-version": "1.2",
    "heart-beat":     "10000,10000",
}, "")
conn.WriteMessage(websocket.TextMessage, []byte(connectFrame))
```

## Paso 3: suscribirse a un tópico

Después de establecer la conexión STOMP, suscríbete al tópico de cambios de la organización:

### Frame SUBSCRIBE

```text
SUBSCRIBE
id:sub-0
destination:/topic/change/{orgUuid}

\x00
```

| Encabezado    | Valor                     | Descripción                                          |
| ------------- | ------------------------- | ---------------------------------------------------- |
| `id`          | `sub-0`                   | Identificador de suscripción definido por el cliente |
| `destination` | `/topic/change/{orgUuid}` | El tópico al que suscribirse                         |

```python
subscribe_frame = build_stomp_frame("SUBSCRIBE", {
    "id": "sub-0",
    "destination": f"/topic/change/{org_uuid}"
})
ws.send(subscribe_frame)
```

> **Note**: Reemplaza `{orgUuid}` con el UUID de tu organización. Recupéralo a través del endpoint REST [`POST /api/org/getOrgV2`](https://developer.rhombus.com/api-reference/org-webservice/get-organization-details-v2).

## Paso 4: bucle de mensajes y heartbeats

Una vez suscrito, tu aplicación entra en un bucle de mensajes donde:

1. **Recibe frames MESSAGE** que contienen datos de eventos
2. **Envía heartbeats** para mantener viva la conexión
3. **Recibe heartbeats** del servidor

### Mecanismo de heartbeat

Los heartbeats son caracteres de salto de línea individuales (`\n`) enviados al intervalo negociado (10 segundos). Tanto el cliente como el servidor envían heartbeats. Si alguno de los lados deja de recibir heartbeats, la conexión se considera muerta.

```python
import threading
import time

def send_heartbeats(ws, stop_event):
    """Send STOMP heartbeats every 10 seconds."""
    while not stop_event.is_set():
        try:
            ws.send("\n")
        except Exception:
            break
        stop_event.wait(10)

# Start heartbeat sender in background
stop_heartbeat = threading.Event()
heartbeat_thread = threading.Thread(
    target=send_heartbeats,
    args=(ws, stop_heartbeat),
    daemon=True
)
heartbeat_thread.start()
```

### Análisis de frames entrantes

```python
def parse_stomp_frame(raw):
    """Parse a raw STOMP frame into command, headers, and body."""
    # Handle heartbeats (empty frames)
    raw = raw.lstrip("\n\r")
    if not raw or raw == "\x00":
        return None  # Heartbeat

    raw = raw.rstrip("\x00")
    parts = raw.split("\n\n", 1)

    lines = parts[0].split("\n")
    command = lines[0]

    headers = {}
    for line in lines[1:]:
        if ":" in line:
            key, value = line.split(":", 1)
            headers[key] = value

    body = parts[1] if len(parts) > 1 else ""
    return {"command": command, "headers": headers, "body": body}
```

### Bucle de mensajes

```python
try:
    while True:
        raw = ws.recv()
        frame = parse_stomp_frame(raw)

        if frame is None:
            continue  # Heartbeat, skip

        if frame["command"] == "MESSAGE":
            payload = json.loads(frame["body"])
            handle_event(payload)

except KeyboardInterrupt:
    print("Shutting down...")
finally:
    stop_heartbeat.set()
    heartbeat_thread.join()
```

## Paso 5: desconexión adecuada

Antes de cerrar el WebSocket, envía un frame STOMP `DISCONNECT`:

```python
# Send DISCONNECT frame
disconnect_frame = build_stomp_frame("DISCONNECT")
ws.send(disconnect_frame)

# Close WebSocket
ws.close()
```

Esto le indica al servidor que el cliente se está desconectando intencionalmente, permitiéndole liberar recursos de inmediato en lugar de esperar un timeout de heartbeat.

## Garantías de entrega

El WebSocket de eventos entrega cada mensaje **como máximo una vez**: los mensajes no se confirman, no se guardan y no se reenvían, así que lo que se publica mientras estás desconectado se pierde, y no hay garantía de orden. Después de cada reconexión, lee lo que te perdiste con la API REST (consulta [Crear una canalización de eventos](https://developer.rhombus.com/es/solutions/event-pipeline)) y ordena los eventos por sus marcas de tiempo. La [referencia de eventos](https://developer.rhombus.com/es/websocket/event-reference#entrega) tiene los detalles.

## Cuando el servidor cierra la conexión

| Cierre                                        | Causa                                                                                                                                                                           | Qué hacer                                                                                                         |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| STOMP `ERROR` y luego código de cierre `1008` | La credencial se eliminó o revocó, o su rol cambió. El servidor vuelve a verificar la credencial aproximadamente una vez por minuto, así que esto ocurre en 60 segundos o menos | Revisa la credencial antes de reconectarte; después de una eliminación o revocación, todo handshake falla con 401 |
| STOMP `ERROR` y luego código de cierre `1002` | Una suscripción al tópico de otra organización, o un frame que el servidor no acepta                                                                                            | Corrige la suscripción; reconectarte con los mismos frames falla de la misma forma                                |
| Cierre sin frame `ERROR`                      | Heartbeats perdidos, ningún `CONNECT` en 60 segundos, un cliente lento, o un reinicio de la red o del servidor                                                                  | Reconéctate con backoff                                                                                           |

## Estrategia de reconexión

Las interrupciones de red son inevitables. Implementa una reconexión automática con backoff:

```python
import time

def connect_with_retry(api_token, org_uuid, max_retries=10):
    """Connect to Rhombus WebSocket with exponential backoff."""
    for attempt in range(max_retries):
        try:
            ws = websocket.create_connection(
                "wss://ws.rhombussystems.com:8443/websocket"
                "?x-auth-scheme=api-token",
                header={"x-auth-apikey": api_token},
                timeout=10
            )
            stomp_connect(ws)
            stomp_subscribe(ws, org_uuid)
            print("Connected successfully")
            return ws
        except Exception as e:
            delay = min(5 * (2 ** attempt), 60)
            print(f"Connection failed: {e}. Retrying in {delay}s...")
            time.sleep(delay)

    raise ConnectionError("Max retries exceeded")
```

### Bucle completo de reconexión

```python
while True:
    ws = connect_with_retry(API_TOKEN, ORG_UUID)

    try:
        run_message_loop(ws)
    except websocket.WebSocketConnectionClosedException:
        print("Connection lost. Reconnecting...")
    except KeyboardInterrupt:
        print("User interrupted. Exiting.")
        break
    finally:
        try:
            ws.close()
        except Exception:
            pass
```

## Tiempos de espera de la conexión

| Timeout         | Valor                 | Descripción                                                                                              |
| --------------- | --------------------- | -------------------------------------------------------------------------------------------------------- |
| Handshake       | 10 segundos           | Tiempo máximo para completar la actualización de WebSocket                                               |
| Heartbeat       | 10 segundos           | Intervalo entre pings de keep-alive                                                                      |
| Conexión muerta | \~30 segundos         | Tiempo aproximado antes de que un heartbeat faltante dispare la desconexión                              |
| Primera trama   | 60 segundos           | Envía la trama STOMP `CONNECT` dentro de este tiempo tras abrir el socket, o se cierra                   |
| Cliente lento   | 10 segundos o 512 KiB | Se desconecta un cliente al que el servidor no puede enviar durante 10 segundos o que se retrasa 512 KiB |

## Problemas comunes

| Síntoma                              | Causa probable                                                                                                                 | Solución                                                                                                                 |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| La conexión se cae cada \~30s        | No se están enviando heartbeats                                                                                                | Implementa el thread emisor de heartbeats                                                                                |
| El frame `CONNECTED` nunca se recibe | Frame STOMP `CONNECT` mal formado                                                                                              | Verifica que el formato del frame incluya el terminador nulo `\x00`                                                      |
| No se reciben mensajes               | Tópico incorrecto o falta el UUID de la organización                                                                           | Verifica que el UUID de la organización coincida con la organización de tu credencial (o con el cliente de `x-auth-org`) |
| Bucle de reconexión                  | La credencial se eliminó, se revocó o expiró (401 en cada handshake), o la organización alcanzó el límite de solicitudes (429) | Verifica la credencial con una llamada REST; ante un 429 espera `Retry-After` y aplica backoff                           |
