express-ws icon indicating copy to clipboard operation
express-ws copied to clipboard

Feature request: Broadcast on a single route

Open NewMountain opened this issue 8 years ago • 10 comments

Hi HenningM and thank you for putting this library together. I played around with it a bit and read your documentation about broadcasting for all clients on the server and not just a route.

Is there any way this (broadcasting to only clients on a route) could be done? If there are possibilities to consider, please let me know.

If not feasible, please let me know if there are other options (I can't use socketIO) I might consider.

Thank you!

NewMountain avatar Jun 28 '16 05:06 NewMountain

Hi. I know this is a hacky proof of concept, but I was able to add filter functions to create channels.

I can polish this up with nicer code and submit a pull request if you like. I looked through all of the client objects as they were being passed around and I discovered a few places where you could find the routes.


[client.upgradeReq.url,
 client.upgradeReq.originalUrl,
 client.upgradeReq._parsedUrl,
 client.upgradeReq.route
]

For now, this is a rough experiment on how I got it working (I am using this for an elm project):


app.get('/', function(req, res){
  res.sendFile(__dirname + '/elm/index.html');
});

app.ws('/', function(ws, req) {
  ws.on('message', function(msg) {
    expressWs.getWss().clients.forEach(function each(client) {
        if(client.upgradeReq.url.replace('.websocket', '') == '/'){
          client.send(msg);
        }
    });
  });
  console.log('socket', req.testing);
});


app.get('/test', function(req, res){
  res.sendFile(__dirname + '/elm/indexprime.html');
});

app.ws('/test', function(ws, req) {
  ws.on('message', function(msg) {
    expressWs.getWss().clients.forEach(function each(client) {
        if(client.upgradeReq.url.replace('.websocket', '') === '/test/'){
          client.send(msg);
        }
    });
  });
  console.log('socket', req.testing);
});

Please let me know if you are interested.

Thank you again for creating this wonderful project!

NewMountain avatar Jun 28 '16 07:06 NewMountain

Hey @NewMountain 10x for posting this it helped me a lot. I believe this should be build in the library itself but its one line and I can live without it.

dimitarkolev avatar Jul 01 '16 16:07 dimitarkolev

Hi @NewMountain,

Thanks a lot for your interest in this library and your suggestion. I like the idea. If you'd like to submit a pull request for this I'd definitely be interested in that. I would prefer a solution that didn't loop through all the connected clients though, but since you already mentioned that you consider this a hacky PoC I guess you're already aware that there are more efficient ways to do this.

HenningM avatar Jul 07 '16 13:07 HenningM

This feature would be very useful.

charly37 avatar Nov 06 '16 14:11 charly37

@NewMountain when you say

there are more efficient ways to do this.,

could you tell me what you have in mind ?

Thanks for your great work by the way !

cdouine39 avatar Nov 17 '16 13:11 cdouine39

app.ws('/', function(ws, req) { ws.on('message', function(msg) { expressWs.getWss().clients.forEach(function(client) { if (client.upgradeReq.url === req.url){client.send(msg)}; }); }); });

That's what I'm using right now. Seems to work OK.

harrychiling avatar Jan 18 '17 21:01 harrychiling

I've provided an explanation in this comment on how to build project-specific socket management abstractions, which addresses this issue as well :)

joepie91 avatar Feb 23 '17 13:02 joepie91

Hi all,

Sorry for disappearing from the face of the earth. I will start playing around with this but @cdouine39 the approach I am thinking is just have two objects: (object 1) k(channels) : v (users in channel) and (object 2) k (users) : v(channels user subscribes to). Performance wise, you just take a message on a channel, look up the channel users in the object, and map the send across the list.

As @joepie91 brilliantly pointed out, there are three significant events: add, remove and broadcast. The only other other thing I can think of is dynamically creating and killing channels, this use case is the reason I think the two object structure makes sense.

As an example, I want to create a sort-of slack-ish clone: I need the ability to:

  1. Create a new channel a. On creation of a new channel, add it to the channels object with a list of the user that created it (object 1) b. Add the channel to the list of channels the user is listening to (object 2)
  2. Delete a channel a. On deletion of a channel, get the list of users of the channel key (object 1), then delete the key b. For each user, remove the channel from their list
  3. Add users to the channel a. Add the user to the channel list (object 1) b. Add the channel to the user list (object 2)
  4. Remove users from the channel a. Remove the user from the channel list (object 1) b. Remove the channel from the user list (object 2)
  5. Broadcast to all members of the channel without mapping across every active user. a. The socket is aware of its route, so it just looks up the active listeners in object 1 and sends to each user (rather than mapping across the list of all active users as POC'd above)
  6. User "hangs up" a. Full disclosure the mechanics of a dead user is still something that is a little tricky for me, so I would appreciate some help or guidance here b. Once a user is identified as hung up, get the channel list of the user, delete the user key (object 2) c. For each channel, remove the user (object 1). d. If the user was the only person on the channel, delete the channel as well.

There may be some additional optimizations, but I think this is already a much nicer solution than mapping across all users attached to the server on every socket event. If the upkeep of scanning and maintaining the objects is too much CPU-bound work for the server, it can be farmed out to a child process.

I will actually need to implement this functionality at work in the next few weeks, so I will start hacking on this again.

One other thing, I come from a pretty hardcore FP school of thought, so I wanted to implement this functionality using a handful of composed functions with ramda-laden JS. If that is cool with @HenningM, I'd be happy to commit it.

Looking through the dependencies, it seems like this library is very clean of dependencies. If ramda is a total non-starter, let me know. I can write it without the sugar, it just might take me a little longer (and maybe improve performance at the margins).

NewMountain avatar Feb 26 '17 20:02 NewMountain

Hmm. I feel like this kind of socket management would be outside of the scope of a library like express-ws, although in the end, it'll be @HenningM's call. I think that the kind of implementation you describe is better solved on an application level, since different applications have different requirements, and adding it to express-ws looks like scope creep to me.

An example of the consequences of this can be seen in Socket.IO, which tries to implement multiple responsibilities including room management. The result is that a lot of people try to shoehorn their non-fitting usecase into Socket.IO's room abstraction, with predictably messy and poorly maintainable results.

Specifically for your usecase: the abstraction I suggested in #50 (which is meant to be used on an application level, rather than implemented in express-ws itself) could be relatively easily adapted to track channels instead of a global set of users. Then instead of:

let users = {
    tom: <socket>,
    matt: <socket>,
    jane: <socket>
}

... you might end up with something like:

let users = {
    channelOne: {
        tom: <socket>,
        matt: <socket>
    },
    channelTwo: {
        jane: <socket>
    }
}

... or a more complex abstraction along those lines. It'd really just involve some additional objects/arrays and levels of nesting. You could even add a 'room' abstraction for easier tracking/broadcasting, and just drop the user in a different channel depending on the route.

joepie91 avatar Feb 26 '17 22:02 joepie91

Hi again @NewMountain,

I think the functionality discussed in this issue would be a very interesting addition to the current library. I imagine a lot of express-ws users are already doing something quite similar to what you've outlined, so I definitely see the potential. I do, however, also agree with @joepie91 when he says that this doesn't have to be a part of express-ws. There's nothing stopping us from implementing this "on top of" what already exists in this library as far as I can see.

I do want to see this feature implemented though. It would be a very nice addition to the current functionality, and I'd be happy to help out in implementing this feature. As previously mentioned I would prefer an implementation based on this library and not built into it, but I'm open for discussion.

HenningM avatar Feb 27 '17 11:02 HenningM