Server-side WebSocket — Create a Connect 4 online training

In this chapter we will implement server-side WebSockets logic to manage game synchronization between players. For this we will rely on the package @fastify/websocket which will allow easy integration with what has already been done.

To manage our games and our connections we will use a repository system to abstract the game fetching and creation logic.

  • the ConnectionRepository will memorize our connections by organizing them by player id using a Map new Map<Player["id"], Map<GameId, SocketStream>>
  • the GameRepository will take care of saving the different parts of the players new Map<GameId, Machine>

Once these 2 classes have been created, they can easily be used during connections.

const connections = new ConnectionRepository()
const games = new GameRepository(connections)

fastify.register(FastifyWebsocket)
fastify.register(async (f) => {
  f.get('/ws', {websocket: true}, (connection, req) => {
    const query = req.query as Record<string, string>
    const playerId = query.id ?? ''
    const signature = query.signature ?? ''
    const playerName = query.name || 'John Doe'
    const gameId = query.gameId

    if (!gameId) {
      connection.end()
      f.log.error('Pas de gameId')
      return;
    }

    if (!verify(playerId, signature)) {
      f.log.error(`Erreur d'authentification`)
      connection.socket.send(JSON.stringify({
        type: 'error', code: ServerErrors.AuthError
      }))
      return;
    }

    const game = games.find(gameId) ?? games.create(gameId)
    connections.persist(playerId, gameId, connection)
    game.send(GameModel.events.join(playerId, playerName))
    publishMachine(game.state, connection)

    connection.socket.on('message', (rawMessage) => {
      const message = JSON.parse(rawMessage.toLocaleString())
      if (message.type === 'gameUpdate') {
        game.send(message.event)
      }
    })

    connection.socket.on('close', () => {
      connections.remove(playerId, gameId)
      game.send(GameModel.events.leave(playerId))
      games.clean(gameId)
    })
  })
})