Andreas Johansson
Andreas Johansson

Reputation: 161

Socket.io emitting values inside ES6 class

I wonder if any smart individuals could show me how to implement Socket.IO in an OOP environment with ES6 classes. The main problem I keep running into with Socket.io is passing around the server object, in my case called 'io'. Almost every example I've seen of socket.io has been pure spaghetti code, one file with many socket related events and logic. First I tried to pass the server object, io, to new class's constructor, but for some reason you end up with a nasty "RangeError: Maximum call stack size exceeded" error message. Then I've tried to wrap my classes in module.exports function which parameter should contain the io object. Which is fine for the first class. Let's say I pass the io object into my Game, great works as expected. But when I try to reference the io object down to the Round class(Game holds an array of Rounds) I can't. Because that is one hell of a bad practice in NodeJS, require should be global and not inside the modules/functions. So I'm once again back with the same issue.

app.js(where I require the main sockets file)

const io = socketio(server, { origins: '*:*' });
...
require('./sockets')(io);

sockets/index.js(where I initialize my game server, and handle incoming messages from client sockets)

const actions = require('../actions.js');
const chatSockets = require('./chat-sockets');
const climbServer = require('./climb-server');
const authFunctions = require('../auth-functions');

module.exports = (io) => {
    io.on('connection', (client) => {
        console.log('client connected...');
        // Standard join, verify the requested room; if it exists let the client join it.
        client.on('join', (data) => {
            console.log(data);
            console.log(`User ${data.username} tries to join ${data.room}`);
            console.log(`Client joined ${data.room}`);
            client.join(data.room);
        });

        client.on('disconnect', () => {
            console.log('Client disconnected');
        });

        client.on(actions.CREATE_GAME, (hostParticipant) => {
            console.log('CREATE_GAME', hostParticipant);

            // Authorize socket sender by token?
            // Create a new game, and set the host to the host participant
            climbServer.createGame(io, hostParticipant);
        });

        client.on(actions.JOIN_GAME, (tokenizedGameId) => {
            console.log('JOIN_GAME');

            const user = authFunctions.getPayload(tokenizedGameId.token);

            // Authorize socket sender by token?
            // Create a new game, and set the host to the host participant
            const game = climbServer.findGame(tokenizedGameId.content);
            game.joinGame(user);
        });
    });
};

climbServer.js(My game server that keeps track of active games)

const actions = require('../actions.js');
const Game = require('../models/game');

const climbServer = { games: { }, gameCount: 0 };

climbServer.createGame = (io, hostParticipant) => {
    // Create a new game instance
    const newGame = new Game(hostParticipant);
    console.log('New game object created', newGame);

    // Store it in the list of game
    climbServer.games[newGame.id] = newGame;

    // Keep track
    climbServer.gameCount += 1;

    // Notify clients that a new game was created
    io.sockets.in('climb').emit(actions.CLIMB_GAME_CREATED, newGame);
};

climbServer.findGame = gameId => climbServer.games[gameId];

module.exports = climbServer;

Game.js(ES6 class that SHOULD be able to emit to all connected sockets)

const UUID = require('uuid');
const Round = require('./round');

class Game {
    // Constructor
    constructor(hostParticipant) {
        this.id = UUID();
        this.playerHost = hostParticipant;
        this.playerClient = null;
        this.playerCount = 1;
        this.rounds = [];
        this.timestamp = Date.now();
    }

    joinGame(clientParticipant) {
        console.log('Joining game', clientParticipant);
        this.playerClient = clientParticipant;
        this.playerCount += 1;

        // Start the game by creating the first round
        return this.createRound();
    }

    createRound() {
        console.log('Creating new round at Game: ', this.id);
        const newRound = new Round(this.id);

        return this.rounds.push(newRound);
    }
}

module.exports = Game;

Round.js(ES6 class that is used by the Game class(stored in a rounds array))

const actions = require('../actions.js');

class Round {
    constructor(gameId) {
        console.log('Initializing round of gameId', gameId);
        this.timeLeft = 60;
        this.gameId = gameId;
        this.winner = null;
        this.timestamp = Date.now();

        // Start countdown when class is instantiated
        this.startCountdown();
    }

    startCountdown() {
        const countdown = setInterval(() => {
            // broadcast to every client
            io.sockets.in(this.gameId).emit(actions.ROUND_TIMER, { gameId: this.gameId, timeLeft: this.timeLeft });
            if (this.timeLeft === 0) {
                // when no time left, stop counting down
                clearInterval(countdown);
                this.onRoundEnd();
            } else {
                // Countdown
                this.timeLeft -= 1;
                console.log('Countdown', this.timeLeft);
            }
        }, 1000);
    }

    onRoundEnd() {
        // Evaluate who won
        console.log('onRoundEnd: ', this.gameId);
    }
}

module.exports = Round;

TO SUMMARIZE with a question: How can I pass a reference of io to my classes so that I'm able to emit to connected sockets within these classes? This doesn't necessarily have to be ES6 classes, it can be NodeJS objects using the .prototype property. I just want a mainatainable way to handle my game server with sockets... ANY HELP IS APPRECIATED!

Upvotes: 0

Views: 3581

Answers (1)

Andreas Johansson
Andreas Johansson

Reputation: 161

After hours upon hours I figured out a solution. If anyone runs into the same thing check my solution out below. Not the best, but much better than putting all socket related code in one file...

Game.js(ES6 Class). Focus on the first line containing 'module.exports'.

const GameFactory = require('../models/game');

const climbServer = { games: { }, gameCount: 0 };

climbServer.createGame = (io, hostParticipant) => {
    // Create a new game instance
    const Game = GameFactory(io);
    const newGame = new Game(hostParticipant);
    console.log('New game object created', newGame);

    // Store it in the list of game
    climbServer.games[newGame.id] = newGame;

    // Keep track
    climbServer.gameCount += 1;

    return newGame;
};

climbServer.findGame = gameId => climbServer.games[gameId];

module.exports = climbServer;

The trick is to use this factory pattern where you first declare:

const GameFactory = require('../models/game');

Then initialize the factory with passing in the Socket.io server object, in my case 'io'. IF YOU pass it in via the constructor you end up with a RangeError, therefore this is the only way. Once again not certain how this code performs in comparison to spaghetti code.

const Game = GameFactory(io);

Finally, you can now instantiate instances of your class:

const newGame = new Game(hostParticipant);

If anyone have improvements or thoughts, please leave me a comment. Still uncertain about the quality of this code.

Upvotes: 4

Related Questions