# WebSockets - Connection Management

Detailed guide for managing WebSocket connections to Zoom.

## Connection Lifecycle

```
1. Generate access token (S2S OAuth)
         ↓
2. Open WebSocket connection with token
         ↓
3. Receive events in real-time
         ↓
4. Handle disconnects and reconnect
         ↓
5. Close connection when done
```

## Authentication

WebSocket connections require a valid Server-to-Server OAuth access token.

### Generate Access Token

```javascript
const axios = require('axios');

async function getAccessToken(accountId, clientId, clientSecret) {
  const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
  
  const response = await axios.post(
    'https://zoom.us/oauth/token',
    new URLSearchParams({
      grant_type: 'account_credentials',
      account_id: accountId
    }),
    {
      headers: {
        'Authorization': `Basic ${credentials}`,
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    }
  );
  
  return {
    accessToken: response.data.access_token,
    expiresIn: response.data.expires_in // Usually 3600 seconds (1 hour)
  };
}
```

### Token Refresh

Access tokens expire after 1 hour. Implement token refresh before expiration:

```javascript
class ZoomWebSocketClient {
  constructor(accountId, clientId, clientSecret, subscriptionId) {
    this.accountId = accountId;
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.subscriptionId = subscriptionId;
    this.ws = null;
    this.tokenExpiry = null;
  }
  
  async refreshTokenIfNeeded() {
    const now = Date.now();
    const bufferTime = 5 * 60 * 1000; // 5 minutes before expiry
    
    if (!this.tokenExpiry || now >= this.tokenExpiry - bufferTime) {
      const { accessToken, expiresIn } = await getAccessToken(
        this.accountId, this.clientId, this.clientSecret
      );
      this.accessToken = accessToken;
      this.tokenExpiry = now + (expiresIn * 1000);
      
      // Reconnect with new token
      if (this.ws) {
        this.ws.close();
        await this.connect();
      }
    }
  }
  
  async connect() {
    await this.refreshTokenIfNeeded();
    
    const wsUrl = `wss://ws.zoom.us/ws?subscriptionId=${this.subscriptionId}&access_token=${this.accessToken}`;
    this.ws = new WebSocket(wsUrl);
    
    // Set up event handlers...
  }
}
```

## Connection URL

```
wss://ws.zoom.us/ws?subscriptionId={SUBSCRIPTION_ID}&access_token={ACCESS_TOKEN}
```

| Parameter | Description |
|-----------|-------------|
| `subscriptionId` | Your WebSocket subscription ID from Marketplace |
| `access_token` | Valid S2S OAuth access token |

## Connection Limits

| Limit | Value |
|-------|-------|
| **Connections per subscription** | 1 (opening new connection closes existing) |
| **Connection timeout** | Varies (implement keep-alive) |
| **Message size** | Check Zoom docs for current limits |

## Keep-Alive / Heartbeat

Maintain connection with periodic pings:

```javascript
class WebSocketManager {
  constructor() {
    this.ws = null;
    this.pingInterval = null;
  }
  
  startHeartbeat() {
    // Ping every 30 seconds
    this.pingInterval = setInterval(() => {
      if (this.ws && this.ws.readyState === WebSocket.OPEN) {
        this.ws.ping();
        console.log('Ping sent');
      }
    }, 30000);
  }
  
  stopHeartbeat() {
    if (this.pingInterval) {
      clearInterval(this.pingInterval);
      this.pingInterval = null;
    }
  }
  
  connect(url) {
    this.ws = new WebSocket(url);
    
    this.ws.on('open', () => {
      console.log('Connected');
      this.startHeartbeat();
    });
    
    this.ws.on('pong', () => {
      console.log('Pong received - connection alive');
    });
    
    this.ws.on('close', () => {
      this.stopHeartbeat();
    });
  }
}
```

## Reconnection Strategy

Implement exponential backoff for reconnection:

```javascript
class ReconnectingWebSocket {
  constructor(config) {
    this.config = config;
    this.ws = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 10;
    this.baseDelay = 1000; // 1 second
    this.maxDelay = 30000; // 30 seconds
  }
  
  async connect() {
    try {
      const token = await getAccessToken(
        this.config.accountId,
        this.config.clientId,
        this.config.clientSecret
      );
      
      const url = `wss://ws.zoom.us/ws?subscriptionId=${this.config.subscriptionId}&access_token=${token.accessToken}`;
      
      this.ws = new WebSocket(url);
      
      this.ws.on('open', () => {
        console.log('Connected successfully');
        this.reconnectAttempts = 0; // Reset on successful connection
      });
      
      this.ws.on('close', (code, reason) => {
        console.log(`Disconnected: ${code} - ${reason}`);
        this.scheduleReconnect();
      });
      
      this.ws.on('error', (error) => {
        console.error('WebSocket error:', error.message);
      });
      
      this.ws.on('message', (data) => {
        this.handleMessage(JSON.parse(data));
      });
      
    } catch (error) {
      console.error('Connection failed:', error.message);
      this.scheduleReconnect();
    }
  }
  
  scheduleReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('Max reconnection attempts reached');
      return;
    }
    
    // Exponential backoff with jitter
    const delay = Math.min(
      this.baseDelay * Math.pow(2, this.reconnectAttempts) + Math.random() * 1000,
      this.maxDelay
    );
    
    console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`);
    
    setTimeout(() => {
      this.reconnectAttempts++;
      this.connect();
    }, delay);
  }
  
  handleMessage(event) {
    // Override this method to handle events
    console.log('Event:', event.event, event.payload);
  }
  
  close() {
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }
}
```

## Error Handling

### Common Error Codes

| Code | Meaning | Action |
|------|---------|--------|
| 1000 | Normal closure | Clean shutdown |
| 1001 | Going away | Server shutting down, reconnect |
| 1006 | Abnormal closure | Network issue, reconnect |
| 1008 | Policy violation | Check token validity |
| 1011 | Internal error | Server error, retry later |

### Error Handling Example

```javascript
ws.on('close', (code, reason) => {
  switch (code) {
    case 1000:
      console.log('Connection closed normally');
      break;
    case 1001:
    case 1006:
      console.log('Connection lost, reconnecting...');
      scheduleReconnect();
      break;
    case 1008:
      console.log('Auth error - refreshing token');
      refreshTokenAndReconnect();
      break;
    default:
      console.log(`Unexpected close: ${code} - ${reason}`);
      scheduleReconnect();
  }
});

ws.on('error', (error) => {
  console.error('WebSocket error:', error);
  // The 'close' event will follow, handle reconnection there
});
```

## Complete Example

```javascript
const WebSocket = require('ws');
const axios = require('axios');

class ZoomWebSocketClient {
  constructor(config) {
    this.config = config;
    this.ws = null;
    this.accessToken = null;
    this.tokenExpiry = null;
    this.pingInterval = null;
    this.reconnectAttempts = 0;
    this.handlers = new Map();
  }
  
  on(event, handler) {
    this.handlers.set(event, handler);
  }
  
  async getAccessToken() {
    const credentials = Buffer.from(
      `${this.config.clientId}:${this.config.clientSecret}`
    ).toString('base64');
    
    const response = await axios.post(
      'https://zoom.us/oauth/token',
      new URLSearchParams({
        grant_type: 'account_credentials',
        account_id: this.config.accountId
      }),
      {
        headers: {
          'Authorization': `Basic ${credentials}`,
          'Content-Type': 'application/x-www-form-urlencoded'
        }
      }
    );
    
    this.accessToken = response.data.access_token;
    this.tokenExpiry = Date.now() + (response.data.expires_in * 1000);
    
    return this.accessToken;
  }
  
  async connect() {
    await this.getAccessToken();
    
    const url = `wss://ws.zoom.us/ws?subscriptionId=${this.config.subscriptionId}&access_token=${this.accessToken}`;
    
    this.ws = new WebSocket(url);
    
    this.ws.on('open', () => {
      console.log('WebSocket connected');
      this.reconnectAttempts = 0;
      this.startPing();
      this.scheduleTokenRefresh();
    });
    
    this.ws.on('message', (data) => {
      const event = JSON.parse(data);
      const handler = this.handlers.get(event.event);
      if (handler) {
        handler(event.payload);
      }
    });
    
    this.ws.on('close', (code, reason) => {
      console.log(`Disconnected: ${code}`);
      this.stopPing();
      if (code !== 1000) {
        this.reconnect();
      }
    });
    
    this.ws.on('error', (error) => {
      console.error('Error:', error.message);
    });
  }
  
  startPing() {
    this.pingInterval = setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        this.ws.ping();
      }
    }, 30000);
  }
  
  stopPing() {
    if (this.pingInterval) {
      clearInterval(this.pingInterval);
    }
  }
  
  scheduleTokenRefresh() {
    const refreshIn = this.tokenExpiry - Date.now() - 300000; // 5 min before expiry
    setTimeout(() => this.refreshToken(), refreshIn);
  }
  
  async refreshToken() {
    await this.getAccessToken();
    // Close and reconnect with new token
    this.ws?.close(1000);
    await this.connect();
  }
  
  reconnect() {
    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
    this.reconnectAttempts++;
    console.log(`Reconnecting in ${delay}ms...`);
    setTimeout(() => this.connect(), delay);
  }
  
  disconnect() {
    this.stopPing();
    this.ws?.close(1000);
  }
}

// Usage
const client = new ZoomWebSocketClient({
  accountId: process.env.ZOOM_ACCOUNT_ID,
  clientId: process.env.ZOOM_CLIENT_ID,
  clientSecret: process.env.ZOOM_CLIENT_SECRET,
  subscriptionId: process.env.ZOOM_SUBSCRIPTION_ID
});

client.on('meeting.started', (payload) => {
  console.log(`Meeting started: ${payload.object.topic}`);
});

client.on('meeting.ended', (payload) => {
  console.log(`Meeting ended: ${payload.object.uuid}`);
});

client.on('meeting.participant_joined', (payload) => {
  console.log(`Participant joined: ${payload.object.participant.user_name}`);
});

client.connect();
```

## Resources

- **WebSockets docs**: https://developers.zoom.us/docs/api/websockets/
- **S2S OAuth guide**: https://developers.zoom.us/docs/internal-apps/s2s-oauth/
