Skip to main content

Real-time Communication

Real-time Communication enables seamless interaction between the Game Platform and Casino UI through PostMessage events, WebSocket connections, and synchronized data updates during tournament gameplay.

Communication Architecture

Communication Patterns

  • Game Platform � Casino UI: PostMessage for cross-frame communication
  • Game Platform � Tournament System: WebSocket for real-time updates
  • Game Platform � Event System: SQS events for external notifications
  • Event System � Casino System: Webhooks for business logic integration

PostMessage Communication

PostMessage Event Types

Tournament Events

// Tournament connected - player successfully joined tournament
{
  type: 'tournament.connected',
  tournamentId: '12345',
  playerId: 'tp_player_456',
  status: 'active',
  playerInfo: {
    displayName: 'PlayerName',
    position: 15,
    entryCount: 2
  }
}

// Tournament started - gameplay begins
{
  type: 'tournament.started',
  tournamentId: '12345',
  startTime: '2024-01-15T20:02:15Z',
  playerCount: 87,
  currentRound: 1,
  totalRounds: 5
}

// Tournament completed - final results
{
  type: 'tournament.completed',
  tournamentId: '12345',
  completedAt: '2024-01-15T21:15:00Z',
  finalResults: {
    position: 3,
    points: 1450,
    prizeAmount: 46.00
  },
  winners: [
    { position: 1, displayName: 'Winner1', prizeAmount: 115.00 }
  ]
}

Player Events

// Player eliminated
{
  type: 'player.eliminated',
  tournamentId: '12345',
  playerId: 'tp_player_456',
  eliminatedAt: '2024-01-15T20:45:00Z',
  finalPosition: 23,
  finalPoints: 850,
  eliminationRound: 3
}

// Rebuy requested
{
  type: 'rebuy.requested',
  tournamentId: '12345',
  playerId: 'tp_player_456',
  rebuyAmount: 10.00,
  currentEntries: 2,
  maxEntries: 3
}

// Player score updated
{
  type: 'player.score.updated',
  tournamentId: '12345',
  playerId: 'tp_player_456',
  score: 1250,
  position: 15,
  round: 3
}

PostMessage Implementation

Game Platform Side (Sender)

class GamePlatformMessaging {
  constructor(parentWindow) {
    this.parentWindow = parentWindow;
    this.targetOrigin = 'https://your-casino-domain.com';
  }
  
  sendTournamentEvent(eventType, data) {
    const message = {
      source: 'game-platform',
      timestamp: new Date().toISOString(),
      type: eventType,
      ...data
    };
    
    try {
      this.parentWindow.postMessage(message, this.targetOrigin);
      console.log('Sent event:', eventType, data);
    } catch (error) {
      console.error('PostMessage failed:', error);
    }
  }
  
  // Tournament lifecycle events
  onTournamentConnected(tournamentId, playerId, playerInfo) {
    this.sendTournamentEvent('tournament.connected', {
      tournamentId,
      playerId,
      status: 'active',
      playerInfo
    });
  }
  
  onTournamentStarted(tournamentId, startTime, playerCount, rounds) {
    this.sendTournamentEvent('tournament.started', {
      tournamentId,
      startTime,
      playerCount,
      currentRound: 1,
      totalRounds: rounds
    });
  }
  
  onPlayerEliminated(tournamentId, playerId, eliminationData) {
    this.sendTournamentEvent('player.eliminated', {
      tournamentId,
      playerId,
      eliminatedAt: new Date().toISOString(),
      ...eliminationData
    });
  }
  
  onRebuyRequested(tournamentId, playerId, rebuyData) {
    this.sendTournamentEvent('rebuy.requested', {
      tournamentId,
      playerId,
      ...rebuyData
    });
  }
}

Casino UI Side (Receiver)

class CasinoUIMessaging {
  constructor() {
    this.gameFrame = null;
    this.eventHandlers = new Map();
    this.setupMessageListener();
  }
  
  setupMessageListener() {
    window.addEventListener('message', (event) => {
      // Verify origin for security
      if (event.origin !== 'https://your-game-platform.com') {
        console.warn('Ignored message from unknown origin:', event.origin);
        return;
      }
      
      // Verify message structure
      if (!event.data.source || event.data.source !== 'game-platform') {
        return;
      }
      
      this.handleGameMessage(event.data);
    });
  }
  
  handleGameMessage(message) {
    const { type, ...data } = message;
    
    console.log('Received game event:', type, data);
    
    // Route to appropriate handler
    switch (type) {
      case 'tournament.connected':
        this.handleTournamentConnected(data);
        break;
        
      case 'tournament.started':
        this.handleTournamentStarted(data);
        break;
        
      case 'tournament.completed':
        this.handleTournamentCompleted(data);
        break;
        
      case 'player.eliminated':
        this.handlePlayerEliminated(data);
        break;
        
      case 'rebuy.requested':
        this.handleRebuyRequested(data);
        break;
        
      case 'player.score.updated':
        this.handleScoreUpdated(data);
        break;
        
      default:
        console.log('Unhandled event type:', type);
    }
  }
  
  // Event handlers
  handleTournamentConnected(data) {
    // Update UI to show tournament connected state
    this.updateTournamentStatus('Connected');
    this.displayPlayerInfo(data.playerInfo);
  }
  
  handleTournamentStarted(data) {
    // Show tournament started notification
    this.showNotification('Tournament started!', 'success');
    this.updateTournamentTimer(data.startTime);
    this.displayPlayerCount(data.playerCount);
  }
  
  handleRebuyRequested(data) {
    // Show rebuy confirmation dialog
    this.showRebuyDialog(data.tournamentId, data.playerId, data.rebuyAmount);
  }
  
  async handleRebuyConfirmation(tournamentId, playerId, rebuyAmount) {
    try {
      // Process rebuy via Casino API
      const response = await fetch(`/api/tournaments/${tournamentId}/players/${playerId}/rebuy`, {
        method: 'POST',
        headers: {
          'Authorization': 'Bearer casino-api-key',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ rebuyAmount })
      });
      
      const result = await response.json();
      
      if (result.rebuyProcessed) {
        // Send confirmation back to game
        this.sendToGame({
          type: 'rebuy.success',
          tournamentId,
          playerId,
          newEntryCount: result.newEntryCount,
          remainingRebuys: result.remainingRebuys
        });
        
        this.showNotification('Rebuy successful!', 'success');
        this.updatePlayerBalance(result.newBalance);
      } else {
        this.sendToGame({
          type: 'rebuy.failed',
          tournamentId,
          playerId,
          error: result.error
        });
        
        this.showNotification('Rebuy failed: ' + result.error, 'error');
      }
      
    } catch (error) {
      console.error('Rebuy processing failed:', error);
      this.showNotification('Rebuy failed due to technical error', 'error');
    }
  }
  
  sendToGame(message) {
    if (this.gameFrame && this.gameFrame.contentWindow) {
      this.gameFrame.contentWindow.postMessage({
        source: 'casino-ui',
        timestamp: new Date().toISOString(),
        ...message
      }, 'https://your-game-platform.com');
    }
  }
}

WebSocket Communication

Tournament System WebSocket

class TournamentWebSocket {
  constructor(tournamentId, internalApiKey) {
    this.tournamentId = tournamentId;
    this.apiKey = internalApiKey;
    this.ws = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 5;
    this.eventHandlers = new Map();
  }
  
  async connect() {
    const wsUrl = `wss://tournament-system.internal/ws/tournaments/${this.tournamentId}`;
    
    try {
      this.ws = new WebSocket(wsUrl, {
        headers: {
          'Authorization': `Bearer ${this.apiKey}`
        }
      });
      
      this.ws.onopen = () => {
        console.log(`WebSocket connected for tournament ${this.tournamentId}`);
        this.reconnectAttempts = 0;
        
        // Send initial subscription
        this.subscribe(['tournament.status', 'player.updates', 'game.events']);
      };
      
      this.ws.onmessage = (event) => {
        try {
          const message = JSON.parse(event.data);
          this.handleMessage(message);
        } catch (error) {
          console.error('Failed to parse WebSocket message:', error);
        }
      };
      
      this.ws.onclose = (event) => {
        console.log('WebSocket closed:', event.code, event.reason);
        this.attemptReconnect();
      };
      
      this.ws.onerror = (error) => {
        console.error('WebSocket error:', error);
      };
      
    } catch (error) {
      console.error('WebSocket connection failed:', error);
      throw error;
    }
  }
  
  subscribe(eventTypes) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({
        type: 'subscribe',
        events: eventTypes
      }));
    }
  }
  
  handleMessage(message) {
    const { type, data } = message;
    
    switch (type) {
      case 'tournament.status.changed':
        this.handleTournamentStatusChange(data);
        break;
        
      case 'player.registered':
        this.handlePlayerRegistered(data);
        break;
        
      case 'tournament.started':
        this.handleTournamentStarted(data);
        break;
        
      case 'player.eliminated':
        this.handlePlayerEliminated(data);
        break;
        
      default:
        console.log('Unhandled WebSocket message:', type, data);
    }
  }
  
  attemptReconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      
      const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
      console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
      
      setTimeout(() => {
        this.connect();
      }, delay);
    } else {
      console.error('Max WebSocket reconnection attempts exceeded');
    }
  }
}

Game Room WebSocket

class GameRoomWebSocket {
  constructor(roomId, playerId) {
    this.roomId = roomId;
    this.playerId = playerId;
    this.ws = null;
    this.heartbeatInterval = null;
  }
  
  async connect(jwtToken) {
    const wsUrl = `wss://ts.playservices.tech/ws/room/${this.roomId}?token=${jwtToken}`;
    
    this.ws = new WebSocket(wsUrl);
    
    this.ws.onopen = () => {
      console.log(`Connected to game room ${this.roomId}`);
      this.startHeartbeat();
    };
    
    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      this.handleGameMessage(message);
    };
    
    this.ws.onclose = () => {
      console.log('Game room WebSocket closed');
      this.stopHeartbeat();
    };
  }
  
  handleGameMessage(message) {
    const { type, data } = message;
    
    switch (type) {
      case 'room.state':
        this.updateRoomState(data);
        break;
        
      case 'game.round.started':
        this.handleRoundStart(data);
        break;
        
      case 'game.round.ended':
        this.handleRoundEnd(data);
        break;
        
      case 'player.action.required':
        this.handleActionRequired(data);
        break;
        
      case 'leaderboard.updated':
        this.updateLeaderboard(data);
        break;
    }
  }
  
  sendGameAction(action, data = {}) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({
        type: 'player.action',
        action: action,
        playerId: this.playerId,
        timestamp: new Date().toISOString(),
        data: data
      }));
    }
  }
  
  startHeartbeat() {
    this.heartbeatInterval = setInterval(() => {
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.send(JSON.stringify({ type: 'ping' }));
      }
    }, 30000); // Send heartbeat every 30 seconds
  }
  
  stopHeartbeat() {
    if (this.heartbeatInterval) {
      clearInterval(this.heartbeatInterval);
      this.heartbeatInterval = null;
    }
  }
}

Data Synchronization

Real-time Leaderboard Updates

class LeaderboardSync {
  constructor(messaging, tournamentId) {
    this.messaging = messaging;
    this.tournamentId = tournamentId;
    this.leaderboard = [];
    this.updateThrottle = null;
  }
  
  updatePlayerScore(playerId, score, position) {
    // Update local leaderboard
    const playerIndex = this.leaderboard.findIndex(p => p.playerId === playerId);
    
    if (playerIndex >= 0) {
      this.leaderboard[playerIndex] = { playerId, score, position };
    } else {
      this.leaderboard.push({ playerId, score, position });
    }
    
    // Sort by score descending
    this.leaderboard.sort((a, b) => b.score - a.score);
    
    // Throttle updates to avoid spam
    if (!this.updateThrottle) {
      this.updateThrottle = setTimeout(() => {
        this.syncLeaderboard();
        this.updateThrottle = null;
      }, 1000); // Update every second at most
    }
  }
  
  syncLeaderboard() {
    // Send to Casino UI
    this.messaging.sendTournamentEvent('leaderboard.updated', {
      tournamentId: this.tournamentId,
      leaderboard: this.leaderboard.slice(0, 10), // Top 10 only
      lastUpdated: new Date().toISOString()
    });
  }
}

State Persistence

class GameStateManager {
  constructor(roomId) {
    this.roomId = roomId;
    this.state = {};
    this.saveInterval = null;
  }
  
  updateState(key, value) {
    this.state[key] = {
      value: value,
      timestamp: new Date().toISOString()
    };
    
    // Schedule state persistence
    this.scheduleStateSave();
  }
  
  scheduleStateSave() {
    if (!this.saveInterval) {
      this.saveInterval = setTimeout(() => {
        this.persistState();
        this.saveInterval = null;
      }, 5000); // Save every 5 seconds
    }
  }
  
  async persistState() {
    try {
      await fetch(`/internal/rooms/${this.roomId}/state`, {
        method: 'POST',
        headers: {
          'Authorization': 'Bearer internal-api-key',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          state: this.state,
          timestamp: new Date().toISOString()
        })
      });
      
      console.log('Game state persisted successfully');
    } catch (error) {
      console.error('Failed to persist game state:', error);
    }
  }
}

Error Handling & Resilience

Connection Recovery

class CommunicationResilience {
  constructor() {
    this.connectionStates = new Map();
    this.messageQueue = [];
    this.maxQueueSize = 100;
  }
  
  trackConnection(name, connection) {
    this.connectionStates.set(name, {
      connection: connection,
      status: 'connected',
      lastActivity: new Date(),
      failureCount: 0
    });
  }
  
  handleConnectionFailure(name, error) {
    const state = this.connectionStates.get(name);
    
    if (state) {
      state.status = 'disconnected';
      state.failureCount++;
      state.lastError = error;
      
      console.error(`Connection ${name} failed:`, error);
      
      // Queue messages during downtime
      this.startMessageQueuing(name);
      
      // Attempt recovery
      this.attemptRecovery(name);
    }
  }
  
  queueMessage(connectionName, message) {
    if (this.messageQueue.length >= this.maxQueueSize) {
      // Remove oldest message if queue is full
      this.messageQueue.shift();
    }
    
    this.messageQueue.push({
      connection: connectionName,
      message: message,
      timestamp: new Date(),
      retries: 0
    });
  }
  
  async flushQueuedMessages(connectionName) {
    const queuedMessages = this.messageQueue.filter(m => m.connection === connectionName);
    
    for (const queued of queuedMessages) {
      try {
        await this.sendMessage(connectionName, queued.message);
        
        // Remove from queue on success
        const index = this.messageQueue.indexOf(queued);
        if (index >= 0) {
          this.messageQueue.splice(index, 1);
        }
      } catch (error) {
        queued.retries++;
        
        if (queued.retries >= 3) {
          // Remove failed messages after 3 retries
          const index = this.messageQueue.indexOf(queued);
          if (index >= 0) {
            this.messageQueue.splice(index, 1);
          }
        }
      }
    }
  }
}

Next Steps