# WebSocket Events

LVNG uses Socket.io for real-time communication. Connect to receive live messages, typing indicators, presence updates, conversation events, call transcription, and swarm session updates.

## Connection

Connect to the Socket.io server with your JWT token for authentication. The server supports both WebSocket (preferred) and HTTP long-polling as a fallback.

```typescript
import { io } from 'socket.io-client';

const socket = io('wss://api.lvng.ai', {
  auth: { token: 'YOUR_JWT_TOKEN' },
  transports: ['websocket', 'polling']
});

socket.on('connected', (data) => {
  console.log('Connected:', data);
  // { socketId, userId, organizationId, timestamp }
});

socket.on('error', (err) => {
  console.error('Socket error:', err);
  // { code, message }
});

socket.on('disconnect', (reason) => {
  console.log('Disconnected:', reason);
});
```

### Connection Parameters

| Parameter | Value |
|-----------|-------|
| Server | `wss://api.lvng.ai` |
| Auth | JWT token passed in the `auth.token` handshake field |
| Transports | WebSocket (preferred), HTTP long-polling (fallback) |
| Ping interval | 25000 ms (25s) |
| Ping timeout | 60000 ms (60s) |
| Max buffer size | 1e6 (1 MB) |
| Rate limit | 100 events per 60 seconds per socket |

> **Note:** You must join a channel with `channel:join` or a conversation with `conversation:join` before receiving events for that room.

## Room Structure

Events are scoped to rooms. The server automatically manages room membership based on your join/leave actions.

| Room Pattern | Scope |
|-------------|-------|
| `org:{organizationId}` | Organization-wide events (presence updates) |
| `channel:{channelId}` | Channel messages, typing, and member events |
| `customer:{customerId}:conversation:{conversationId}` | Multi-tenant DMs and group conversations |
| `call:{roomName}` | Call participants and live transcription |
| `swarm:{sessionId}` | Agent swarm session subscribers |
| `activity:{customerId}` | Activity feed for a customer |

## Messages

Send and receive messages in real-time. When you emit `message:send`, the server validates the payload, persists the message, and broadcasts `message:new` to the room. The sender also receives a `message:sent` acknowledgment.

```typescript
// Send a message to a channel
socket.emit('message:send', {
  channelId: '550e8400-e29b-41d4-a716-446655440000',
  content: 'Hello from WebSocket!',
  optimisticId: 'tmp_abc123',
  threadId: null,
  mentions: ['usr_8f3a2b1c-4d5e-...'],
  workspaceId: 'ws_b2c3d4e5-...'
});

// Confirmation sent back to sender only
socket.on('message:sent', (ack) => {
  // { id, optimisticId, channelId, timestamp }
});

// New message broadcast to the room
socket.on('message:new', (message) => {
  // { id, channelId, conversationId, userId, userName, content, contentType, threadId, attachments, createdAt, updatedAt, metadata }
});

// Message edited
socket.on('message:update', (update) => {
  // { messageId, content, metadata, channelId, conversationId, updatedBy, timestamp }
});

// Message deleted
socket.on('message:delete', (data) => {
  // { messageId, channelId, conversationId, deletedBy, deletedByName, timestamp }
});

// Reaction added or removed
socket.on('message:reaction', (reaction) => {
  // { messageId, channelId, emoji, userId, userName, action: 'add'|'remove', timestamp }
});
```

### Message Events

| Event | Direction | Payload |
|-------|-----------|---------|
| message:send | Client -> Server | {channelId, conversationId?, customerId?, content, threadId?, mentions?, optimisticId?, workspaceId?} |
| message:new | Server -> Client | {id, channelId, conversationId, userId, userName, content, contentType, threadId, attachments, createdAt, updatedAt, metadata} |
| message:sent | Server -> Client | {id, optimisticId, channelId, timestamp} |
| message:update | Client -> Server | {messageId, channelId, conversationId?, customerId?, content, metadata?} |
| message:update | Server -> Client | {messageId, content, metadata, channelId, conversationId, updatedBy, timestamp} |
| message:delete | Client -> Server | {messageId, channelId, conversationId?, customerId?} |
| message:delete | Server -> Client | {messageId, channelId, conversationId, deletedBy, deletedByName, timestamp} |
| message:react | Client -> Server | {messageId, channelId, emoji, action: "add" or "remove"} |
| message:reaction | Server -> Client | {messageId, channelId, emoji, userId, userName, action, timestamp} |
| message:read | Client -> Server | {messageId, channelId, conversationId?, customerId?} |
| message:read | Server -> Client | {messageId, channelId, conversationId, userId, userName, timestamp} |
| message:read:batch | Server -> Client | {messageIds: [], channelId, conversationId, userId, timestamp} |

## Typing Indicators

```typescript
socket.emit('typing:start', { channelId: '550e8400-...', userId: 'usr_8f3a2b1c-...', userName: 'Matty' });
socket.emit('typing:stop', { channelId: '550e8400-...', userId: 'usr_8f3a2b1c-...', userName: 'Matty' });

socket.on('typing:start', (data) => {
  // { channelId, conversationId, userId, userName, timestamp }
});
socket.on('typing:stop', (data) => {
  // { channelId, conversationId, userId, userName, timestamp }
});
```

### Typing Events

| Event | Direction | Payload |
|-------|-----------|---------|
| typing:start | Client -> Server | {channelId, conversationId?, customerId?, userId, userName} |
| typing:start | Server -> Client | {channelId, conversationId, userId, userName, timestamp} |
| typing:stop | Client -> Server | {channelId, conversationId?, customerId?, userId, userName} |
| typing:stop | Server -> Client | {channelId, conversationId, userId, userName, timestamp} |

## Channels

Join a channel room to receive its messages, typing indicators, and update events. On join, the server sends the last 50 messages as `channel:history`.

```typescript
socket.emit('channel:join', { channelId: '550e8400-...' }, (response) => {
  console.log('Joined channel:', response);
});

socket.on('channel:history', (data) => {
  // { channelId, messages: [...], hasMore, total }
});

socket.emit('channel:leave', { channelId: '550e8400-...' }, (response) => {
  console.log('Left channel:', response);
});
```

### Channel Events

| Event | Direction | Payload |
|-------|-----------|---------|
| channel:join | Client -> Server | {channelId} with callback |
| channel:join | Server -> Client | {channelId, userId, userName, user: {id, name}, timestamp} |
| channel:history | Server -> Client | {channelId, messages: [], hasMore, total} |
| channel:leave | Client -> Server | {channelId} with callback |
| channel:leave | Server -> Client | {channelId, userId, userName, timestamp} |
| channel:update | Client -> Server | {channelId, updates: {...}} |
| channel:update | Server -> Client | {channelId, updates, channel, updatedBy, updatedByName, timestamp} |

## Presence

Presence events are broadcast to the `org:{organizationId}` room. Send periodic heartbeats to keep your status from timing out.

```typescript
socket.emit('presence:update', {
  status: 'online',  // 'online' | 'away' | 'busy' | 'offline'
  customStatus: 'In a meeting',
  statusText: 'Back at 3pm'
});

socket.emit('presence:heartbeat');

socket.on('presence:update', (data) => {
  // { userId, userName, status, customStatus, timestamp }
});
```

### Presence Events

| Event | Direction | Payload |
|-------|-----------|---------|
| presence:update | Client -> Server | {status: "online" or "away" or "busy" or "offline", customStatus?, statusText?} |
| presence:update | Server -> Client | {userId, userName, status, customStatus, timestamp} |
| presence:heartbeat | Client -> Server | (empty) |

## Conversations

Multi-tenant conversations use a `customer:{customerId}:conversation:{conversationId}` room pattern. The `conversation:rejoin` event lets you reconnect after a disconnect and retrieve missed messages.

```typescript
socket.emit('conversation:join', {
  conversationId: 'conv_a1b2c3d4-...',
  customerId: 'cust_e5f6a7b8-...'
});

socket.on('room:joined', (data) => {
  // { room, conversationId, customerId, timestamp }
});

socket.emit('conversation:rejoin', {
  conversationId: 'conv_a1b2c3d4-...',
  customerId: 'cust_e5f6a7b8-...',
  lastMessageId: 'msg_f7e6d5c4-...',
  lastMessageTimestamp: '2026-03-19T12:00Z'
});

socket.on('conversation:rejoined', (data) => {
  // { conversationId, customerId, missed_messages: [], timestamp }
});
```

### Conversation Events

| Event | Direction | Payload |
|-------|-----------|---------|
| conversation:join | Client -> Server | {conversationId, customerId} |
| room:joined | Server -> Client | {room, conversationId, customerId, timestamp} |
| member:joined | Server -> Client | {conversationId, userId, userName, timestamp} |
| conversation:leave | Client -> Server | {conversationId, customerId} |
| member:left | Server -> Client | {conversationId, userId, userName, timestamp} |
| conversation:rejoin | Client -> Server | {conversationId, customerId, lastMessageId?, lastMessageTimestamp?} |
| conversation:rejoined | Server -> Client | {conversationId, customerId, missed_messages: [], timestamp} |

## Calls

Call rooms use the `call:{roomName}` pattern. Participants can send live transcription segments.

```typescript
socket.emit('call:join', { roomName: 'call_engineering_standup' });

socket.on('call:joined', (data) => {
  // { roomName, timestamp }
});

socket.emit('call:transcription', {
  roomName: 'call_engineering_standup',
  segment: { speaker: 'Matty', text: 'Let us review the sprint backlog.', isFinal: true, confidence: 0.97 }
});

socket.on('transcription:segment', (segment) => {
  // { speaker, text, isFinal, timestamp, confidence }
});

socket.emit('call:leave', { roomName: 'call_engineering_standup' });
```

### Call Events

| Event | Direction | Payload |
|-------|-----------|---------|
| call:join | Client -> Server | {roomName} |
| call:joined | Server -> Client | {roomName, timestamp} |
| call:participant_joined | Server -> Client | {roomName, userId, userName, timestamp} |
| call:leave | Client -> Server | {roomName} |
| call:participant_left | Server -> Client | {roomName, userId, userName, reason, timestamp} |
| call:transcription | Client -> Server | {roomName, segment} |
| transcription:segment | Server -> Client | {speaker, text, isFinal, timestamp, confidence} |

## Swarm Sessions

Subscribe to a swarm session to receive real-time updates as agents complete steps.

```typescript
socket.emit('swarm:subscribe', { sessionId: 'swarm_9a8b7c6d-...' });

socket.on('swarm:subscribed', (data) => {
  // { sessionId }
});

socket.emit('swarm:unsubscribe', { sessionId: 'swarm_9a8b7c6d-...' });
```

### Swarm Events

| Event | Direction | Payload |
|-------|-----------|---------|
| swarm:subscribe | Client -> Server | {sessionId} |
| swarm:subscribed | Server -> Client | {sessionId} |
| swarm:unsubscribe | Client -> Server | {sessionId} |

## Connection Events

| Event | Direction | Payload |
|-------|-----------|---------|
| connected | Server -> Client | {socketId, userId, organizationId, timestamp} |
| error | Server -> Client | {code, message} |

> **Reconnection:** Socket.io handles automatic reconnection with exponential backoff. After reconnecting, re-join your channels and conversations, and use `conversation:rejoin` to fetch missed messages.
