HenningM / express-ws

WebSocket endpoints for express applications
BSD 2-Clause "Simplified" License
876 stars 142 forks source link

Maintaining connection state? #50

Open JC3 opened 7 years ago

JC3 commented 7 years ago

So, if I need to maintain per-client state, should I just... add things to the websocket? Like:

app.ws("/endpoint", function (ws, req) {
    ws.on("message", function (message) {
       ws.messageCounter = (ws.messageCounter ? ws.messageCounter : 0) + 1;
       console.log("%d messages received on this connection", ws.messageCounter);
    });
});

Or is there some "real" way to do this that I am missing?

joepie91 commented 7 years ago

express-ws is a fairly low-level implementation - it really doesn't do anything beyond glueing together Express and ws, so you'd be responsible for managing connection-related metadata yourself.

The exact way to implement this depends on your requirements, really. For example, let's say that you're building a chat application - the following would apply:

Given those requirements, you might build a user-store.js that looks something like this:

'use strict';

module.exports = function createUserStore() {
    let users = {};

    return {
        addConnection: function(connection, user) {
            if (users[user.id] == null) {
                users[user.id] = {
                    username: user.username,
                    sockets: []
                }
            }

            users[user.id].sockets.push(connection);
        },
        removeConnection: function(connection, user) {
            if (users[user.id] != null) {
                let userSocketList = users[user.id].sockets;
                let socketIndex = userSocketList.indexOf(connection);

                if (socketIndex !== -1) {
                    userSocketList.splice(socketIndex, 1);
                }
            }
        },
        broadcastMessage: function(connection, user, message) {
            if (users[user.id] != null) {
                function sendMessageToSocket(socket) {
                    socket.send(JSON.stringify({
                        type: "message",
                        message: message, // We can access this because of lexical scope
                        source: user.id
                    }));
                }

                /* Broadcast to other users first. */
                Object.keys(users).forEach((userId) => {
                    if (userId !== user.id) {
                        let user = users[userId];

                        user.sockets.forEach((socket) => {
                            sendMessageToSocket(socket);
                        });
                    }
                });

                /* Then broadcast to other connections of the sending user - but
                * not the connection that the message originally came from! */
                users[user.id].sockets.forEach((socket) => {
                    sendMessageToSocket(socket);
                });
            } else {
                throw new Error("Tried to send a message from a non-existent user");
            }
        }
    }
}

... and then use it from your app something like this:

/* .... */

const createUserStore = require("./user-store");
let userStore = createUserStore();

/* ... */

app.ws("/endpoint", (ws, req) => {
    userStore.addConnection(ws, req.user); // Let's assume `req.user` was set by middleware or such

    ws.on("message", (message) => {
        let parsed = JSON.parse(message);
        userStore.broadcastMessage(ws, req.user, parsed.message);
    });

    ws.on("close", (code, reason) => {
        userStore.removeConnection(ws, req.user);
    });
});

(Note: This code is untested, so there may be some small bugs... it's just for illustrative purposes, to demonstrate building a project-specific abstraction.)

Of course your requirements will probably be different and you might need an entirely different kind of abstraction, but you'll generally want to take the approach of "build a separate abstraction and make it keep track of sockets in the appropriate manner".