12

Is it possible to make socket.io broadcast to all users of a namespace who are in both room A and room B but not those who are just in room A or room B?

If not, how would I go about implementing this myself? Is there a way to retrieve all users in a namespace who are in a given room?

I am working with socket.io 1.0 in node

Edit: If there is no native method, how would I go about to create my own syntax such as: socket.broadcast.in('room1').in('room2').emit(...)?

Firas Dib
  • 2,743
  • 19
  • 38

3 Answers3

5

You can look up all the users of a room using (ref How to update socket object for all clients in room? (socket.io) )

var clients = io.sockets.adapter.room["Room Name"]

So given two arrays for your 2 rooms' roster list, you can compute the intersection using something like the answer here (ref: Simplest code for array intersection in javascript)

And finally you can take that list of users in both rooms and emit events using (ref: How to update socket object for all clients in room? (socket.io) )

//this is the socket of each client in the room.
var clientSocket = io.sockets.connected[clientId];

//you can do whatever you need with this
clientSocket.emit('new event', "Updates");

The alternate ofcourse is to have hidden rooms, where you maintain a combination of all rooms, and add users to those rooms behind the scenes, and then you are able to just simply emit to those hidden rooms. But that suffers from an exponential growth problem.

Community
  • 1
  • 1
Archit Baweja
  • 973
  • 7
  • 18
  • Thank you! This is very valuable information. Do you know how I could implement this sort of behavior in the socket.io prototype? This is something I am going to be using throughout my code so it would really simply things if I could just call, say, `socket.broadcast.toAll([...]).emit(...)` and have the `toAll()` function grab the union of all rooms and emit only to those. – Firas Dib Jul 15 '15 at 11:41
1

There is no in-built way to do this. So first let's look up how the broadcast works:

https://github.com/Automattic/socket.io/blob/master/lib/namespace.js 206...221-224...230

this.adapter.broadcast(packet, {
    rooms: this.rooms,
    flags: this.flags
});

Now we know every broadcast creates a bunch of temp objects, indexOf lookups, arguments slices... And then calls the broadcast method of the adapter. Lets take a look at that one:

https://github.com/Automattic/socket.io-adapter/blob/master/index.js 111-151

Now we are creating even more temp objects and loop through all clients in the rooms or all clients if no room was selected. The loop happens in the encode callback. That method can be found here:

https://github.com/socketio/socket.io-parser/blob/master/index.js

But what if we are not sending our packets via broadcast but to each client separately after looping through the rooms and finding clients that exist both in room A and room B?

socket.emit is defined here: https://github.com/Automattic/socket.io/blob/master/lib/socket.js

Which brings us to the packetmethod of the client.js: https://github.com/Automattic/socket.io/blob/master/lib/client.js

Each directly emitted packet will be separately encoded, which again, is expensive. Because we are sending the exact same packet to all users.

To answer your question:

Either change the socket.io adapter class and modify the broadcast method, add your own methods to the prototype or roll your own adapter by inheriting from the adapter class). (var io = require('socket.io')(server, { adapter: yourCustomAdapter });)

Or overwrite the joinand leave methods of the socket.js. Which is rather convenient considering that those methods are not called very often and you don't have the hassle of editing through multiple files.

Socket.prototype.join = (function() {
    // the original join method
    var oldJoin = Socket.prototype.join;

    return function(room, fn) {

        // join the room as usual
        oldJoin.call(this, room, fn);

        // if we join A and are alreadymember of B, we can join C
        if(room === "A" && ~this.rooms.indexOf("B")) {
            this.join("C");
        } else if(room === "B" && ~this.rooms.indexOf("A")) {
             this.join("C");
        }  
    };
})();

Socket.prototype.leave = (function() {
    // the original leave method
    var oldLeave = Socket.prototype.leave;

    return function(room, fn) {

        // leave the room as usual
        oldLeave.call(this, room, fn);

        if(room === "A" || room === "B") {
             this.leave("C");
        }  
    };
})();

And then broadcast to C if you want to broadcast to all users in A and B. This is just an example code, you could further improve this by not hard coding the roomnames but using an array or object instead to loop over possible room combinations.

As custom Adapter to make socket.broadcast.in("A").in("B").emit()work:

var Adapter = require('socket.io-adapter');

module.exports = CustomAdapter;

function CustomAdapter(nsp) {
  Adapter.call(this, nsp);
};

CustomAdapter.prototype = Object.create(Adapter.prototype);
CustomAdapter.prototype.constructor = CustomAdapter;

CustomAdapter.prototype.broadcast = function(packet, opts){
  var rooms = opts.rooms || [];
  var except = opts.except || [];
  var flags = opts.flags || {};
  var packetOpts = {
    preEncoded: true,
    volatile: flags.volatile,
    compress: flags.compress
  };
  var ids = {};
  var self = this;
  var socket;

  packet.nsp = this.nsp.name;
  this.encoder.encode(packet, function(encodedPackets) {
    if (rooms.length) {
      for (var i = 0; i < rooms.length; i++) {
        var room = self.rooms[rooms[i]];
        if (!room) continue;
        for (var id in room) {
          if (room.hasOwnProperty(id)) {
            if (~except.indexOf(id)) continue;
            socket = self.nsp.connected[id];
            if (socket) {
              ids[id] = ids[id] || 0;
              if(++ids[id] === rooms.length){
                socket.packet(encodedPackets, packetOpts);
              }
            }
          }
        }
      }
    } else {
      for (var id in self.sids) {
        if (self.sids.hasOwnProperty(id)) {
          if (~except.indexOf(id)) continue;
          socket = self.nsp.connected[id];
          if (socket) socket.packet(encodedPackets, packetOpts);
        }
      }
    }
  });
};

And in your app file:

var io = require('socket.io')(server, {
  adapter: require('./CustomAdapter')
});
Sebastian Nette
  • 7,364
  • 2
  • 17
  • 17
  • This looks to be by far the most promising approach. I havent had time to test it, but I'll give you the bounty before it expires for your work and effort. – Firas Dib Jul 20 '15 at 12:26
  • Thanks! :) There are also other approaches you could try. E.g. iterating over all users in `A`, looking up if they are also in `B` (by using indexOf on their `room` property) and then encoding your message first and sending it via `socket.packet` instead of `socket.emit`. I didn't try out the code either, so let me know if something doens't work as expected. – Sebastian Nette Jul 20 '15 at 12:31
  • Will do! I'll have some free time at the end of this week. My main problem was adding my functions to the prototype of socket.io – Firas Dib Jul 20 '15 at 13:18
0
io.sockets.adapter.room["Room A"].forEach(function(user_a){
  io.sockets.adapter.room["Room B"].forEach(function(user_b){
    if(user_a.id == user_b.id){
      user_a.emit('your event', { your: 'data' });
    }
  });
});
Sebastián Espinosa
  • 2,123
  • 13
  • 23