Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a reusable Socket.IO module

I have troubles for creating a module which exposes functionalities for my Socket.IO library:

const sio = require('socket.io');
module.exports = function(server) {
  const io = sio(server);
  return {
    register: function(namespace) {
      let nsp = io.of(namespace);
      nsp.on('connect', function(socket) {
        // ...
      }
    }
  }
}

The problem is now how do I make use of this in other modules? In my app.js

I create the server with Express and can instantiate the module with require('./mysocketio')(server) but not in other modules because server is not available there. What's a nice way to resolve these circular dependencies?

like image 790
Mahoni Avatar asked May 16 '16 20:05

Mahoni


4 Answers

Well you can achieve those in various ways, like:

  • setting objects to global namespace. (altering global needs care)
  • Use module.exports and require the object in the other files. (can lead to circular dependency issues if not done properly)
  • pass the instance as arguments to the controllers, while requiring them in routes.

myModule.js Module which exposes functionalities of your Socket.IO library

const sio = require('socket.io');
module.exports = function(server) {
  const io = sio(server);
  return {
    register: function(namespace) {
      let nsp = io.of(namespace);
      nsp.on('connect', function(socket) {
        // ...
      }
    }
  }
}

FLow 1: set the module in global namespace.

app.js

var app = require('express').createServer();
var io = require('./myModule')(app);
global._io = io;

app.listen(80)

controller.js

module.exports = function(io){
    var that={};
    /*
     * Private local variable
     * made const so that 
     * one does not alter it by mistake
     * later on.
     */
    const _io = global._io; 

    that.myAction = function(req,res){

        _io.register('newRoom');
        res.send('Done');   
    }
    return that;
}

Flow 2: passing module as arguments.

app.js

var app = require('express').createServer();
var io = require('./myModule')(app);

require(./router.js)(app,io);

app.listen(80);

router.js

/*
 * Contains the routing logic
 */
module.exports = function (app,io) {
//passing while creating the instance of controller for the first time.
var controller = require("./controller")(io);

app.get('/test/about',controller.myAction);
};

controller.js

module.exports = function(io){
    var that={};

    const _io = io; 

    that.myAction = function(req,res){

        _io.register('newsRoom');
        res.send('Done');   
    }

    // everything attached to that will be exposed
    // more like making public member functions and properties.
    return that;
}

Flow 3: Setting io to global. Thus no need to pass server every time.

app.js

var app = require('express').createServer();
require('./myModule')(app);

require(./router.js)(app);

app.listen(80);

controller.js

// no need to pass the server as io is already initialized
const _io = require('./myModule')();

module.exports = function(io){
    var that={};

    that.myAction = function(req,res){

        _io.register('newsRoom');
        res.send('Done');   
    }
    return that;
}

myModule.js

module.exports = function( server ) {

  const _io = global._io || require('socket.io')(server);

  if(global._io === undefined){
    //initializing io for future use
    global._io = _io;
  }

  return {
    register: function(namespace) {
      let nsp = _io.of(namespace);
      nsp.on('connect', function(socket) {
        // ...
      }
    }
  }
}

Probably, the cleanest way is to pass is as arguments to the controllers, while requiring them in routes. Although 3rd flow seems promising but one should be care full while altering the global namespace.

like image 191
Nivesh Avatar answered Nov 15 '22 01:11

Nivesh


It's not really a circular dependency; It's just that your module a) depends on another module that's not globally available and b) your module is presumably used in many places in your code.

Global

A possible solution (with downsides), is to just load your module once, and attach it to a global: global.mysocketio = require('./mysocketio')(server);

This allows you to access global.mysocketio anywhere in your project, once it has been loaded. This is a construction that I personally use for an own logger construction; My logger is used in many places around my code, so I just keep it attached to global.log.

However, usage of globals is a bit dirty; It gives problems with namespace-separation (what is somewhere some code decides to use global.mysocketio itself), and it creates an 'invisible' dependency; Other code just assumes that a certain global will exist, and it's not that easy to find these dependencies.

Export

A nicer solution is to just pass the variable wherever needed. There are many ways to do this. I understand that your app.js doesn't have the server variable available, but it surely is including your express-code in some way. If you need the 'server' or 'mysocketio' available from app.js, just export it from your module where you are creating 'server'. Like:

module.exports.expressServerVar = server;

Just my 2 cents; Do you strongly disagree with me or am I missing something important? Let me know!

like image 39
TinkerTank Avatar answered Nov 15 '22 00:11

TinkerTank


I'd use a factory or dependency injection. You could use something like jimple.

But here's an example without using any external dependencies. This is by no means the best code example but it should hopefully get the point across. I'd still recommend using jimple rather than this.

// app.js

var express = require('express');
var app = express();

var factory = require('./factory.js');
factory.setExpress(app); // This could also be done in the factory constructor. Or you could instanciate your express app in the factory.js class.


// factory.js
var socketIoModule = require('./your-socket-io-module.js')

function Factory() {

}

Factory.prototype.setExpress = function(app) {
    this.app = app;
}

Factory.prototype.getSocketIOModule = function() {
    return socketIoModule(this.app);
}

// By exporting it this way we are making it a singleton
// This means that each module that requires this file will
// get the same instance of factory.
module.exports = new Factory();


// some code that needs socket io module
var factory = require('./factory.js');

function() {
    var socketIo = factory.getSocketIOModule();

    socketIo.doStuff();
}
like image 1
grimurd Avatar answered Nov 14 '22 23:11

grimurd


Approach that I use in my applications is exposing server and io instances from start script and reusing them in modules

// Setup servers.
var http = require('http').Server(app);
var io = require('socket.io')(http);

// Setup application.
require('./server/app')(app, express, io);

// Start listening on port.
http.listen(configs.PORT, function() {
    console.log("Listening on " + configs.PORT);
});

Inside your modules you can use io instance to setup event handlers or emit events, something like this

module.exports = {
  contest: function(io, contest) {
    var namespace = io.of('/' + contest.id);
    namespace.on('connection', function(socket) {
        socket.on('word', function(data) {
            ...
        });
    });
  }
};

For your sample

I would put this part in app.js or in js file that is used to start server

const sio = require('socket.io');
const io = sio(server);

and will have Socket.IO module like this

module.exports = function(server, io) {
  return {
    register: function(namespace) {
      let nsp = io.of(namespace);
      nsp.on('connect', function(socket) {
        // ...
      }
    }
  }
}

My sample

  • https://github.com/gevorg/typeitquick/blob/master/web.js
  • https://github.com/gevorg/typeitquick/blob/master/server/contest.coffee
  • https://github.com/gevorg/typeitquick/blob/master/server/io.coffee
like image 1
gevorg Avatar answered Nov 14 '22 23:11

gevorg