Storing global data with socket.io - javascript

I'm trying to store all user's data in an array with socket.io, but when i put a new information, the last one is deleted.
var userInformations = <{}[]>[];
socketInfo.on('putUserInformations', (userData:{}) => {
userInformations.push(userData)
console.log(userInformations)
})

Is it that you want to store something about what the user is doing in a particular room? There are two ways to do this, either store it on the socket ID, or store it in the room.
The way I've done this before is as follows. For storing data about the room, I attach it to the room ID by adding new keys to the room object. While the room remains active, this data will remain actively attached to the room, so you can easily access it through your code
io.sockets.adapter.rooms[ROOM_ID_GOES_HERE].lockedInput = { "some_data" : 1 }
I'm not sure how you've done this but you can test out this stuff by console.log(io.sockets.adapter.rooms) to see what you have there. For storing information about the socket user, you can just attach this directly to the socket, i.e.
io.on('connection', function(socket) {
socket.user = 'User 1';
});
One is private to that particular socket, and one is private to a particular room. Both of these allow you to gather information on the room/user and it will persist while the socket is active. When the socket/room are both closed, the data will not persist, but you can store it in a database and pull it in when the user reconnects if you need to.

Related

Passing new DOM Elements via Socketio

How can i pass DOM Elements server-side via socketio to display new message content.
// Client
// Pass DOM element as string
function elemToString(elem){
return elem.outerHTML
}
window.addEventListener("keyup", sendMessage, false);
function sendMessage(key) {
if (key.keyCode === 13) {
if (chatbox.value !== "") {
const child = document.createElement("div");
child.textContent = `${chatbox.value}`
child.className = "messages"
chat.appendChild(child);
socket.emit('chatMsg', elemToString(child))
chatbox.value = "";
chatbox.style.display = "none";
setTimeout(()=>{
child.remove()
},4000)
}
else chatbox.style.display = "none";
}
}
// User Class
class User {
constructor(name, x, y) {
this.alive = false
this.name = name
this.cells = []
this.room = 'none'
this.message = []
}
update(cells) {
this.cells = cells
}
}
module.exports = User
// Server
socket.on('chatMsg', elem => {
user.message.push(elem)
})
I have a hotkey which sends new divs which display message content. I want to pass the data server-side via socket.io so all users can see new message content instead of just the individual client.
The first question I would ask is whether you really need to pass around HTML or DOM objects? Based on the above snippet, it appears that sending the messages as strings (without any HTML markup, just the message body) is by far the easiest way for both the server and the chat client(s). As to sending the message to all users in the chat room, this feature is already built into socket.io (see https://socket.io/docs/rooms-and-namespaces/) and unless you need to implement custom logging or filtering, you could just use it out of the box
There are several ways you could go about building this chat server:
Simple / POC approach
If you just want to have a quick prototype and see if it does what you want it to do, and you are not too concerned with auditing, logging, filtering etc., then:
When the client connects, it "joins" a socket.io "room" (the name of the room could match the room on the client or you may choose to format/hash it etc.)
All chat room clients send events to that same room and listen to the events emitted by that room. The socket.io backend takes care of re-broadcasting the events, so you would not need to do anything
To send message to the room, the "sender" client would broadcast a well known event, something like { message: '<<message text>>' }. All clients listen to events in the room, and once a new event arrives they create a new HTML element (say, a <div>) with that message as content and append it to the chat window
This should cover the basic chat room scenario
A bit more involved chat server
In the real-world implementations, we typically want to have a bit more control over what events can and cannot be broadcasted (for example, we want to have the ability to silence a chatter or filter profanity/abusive language etc.) In this case, we cannot simply use blind broadcast, so typically the client would use two socket.io channels - a "room" for incoming messages and a direct channel for outgoing.
In this scenario, the server acts as a bridge - it listens to the incoming messages, does all the necessary filtering, preprocessing, sanitizes the messages to prevent injections etc., and then broadcasts the resulting message in the room. In this scenario:
All chat clients subscribe to the same "incoming message" room, just like in the previous scenario
When the chatter needs to send a message (say, they hit Enter), the client socket emits an event to the server and tells it which room this message is intended for (e.g. { incoming_message: 'xyz', user: 'username', room: 'room_21' }
The server receives the message and processes it (for example, checks the username against the list of banned users), then broadcasts the processed message in the room specified in the original message. The other chat participants receive the message and render it
In this scenario, you can keep the history of every message sent by a particular user (much like you do in your code snippet)
This link (https://socket.io/docs/#Sending-and-getting-data-acknowledgements) has a few examples on sending and broadcasting arbitrary JSON messages and also shows working with channels etc.
Hope that helps and good luck with your chat server!

Socket.io join another client to a room

My goal is when A user click on B user, they'll join in a room. I'm holding socket.id in array for all clients.
//CLIENT SIDE FOR A//
$('.chat-start').on('click', function(event) {
event.preventDefault();
var room_id = '72xaz132s'
var target_id = $('.target').data('id');
socket.emit('force join room', room_id, target_id);
});
//SERVER SIDE FOR A AS AN EXAMPLE//
var clients {'customIDA' : socket.id for a, 'customIDB' : socket.id for b}
socket.on('force join room', function (roomid, customIDB) {
socket.join(chat_id); //CLIENT A JOINS ROOM//
})
Client A joins room without problem, but how can i add also client B on same room ?
In io.on("connection") you can store socket object with user id as a object
{"socket": socket, "id": user_id}
to array. Then find index from array:
let user_index = users.findIndex(user => user.id == user_id);
And finally join to room.
let socketB = users[user_index].socket;
socketB.join('room');
You can call join to subscribe the socket to a given channel:
// On connection join room
io.on('connection', function(socket){
socket.join('ROOM ID');
});
And then simply use to or in (they are the same) when broadcasting or emitting:
io.to('some room').emit('some event');
To leave a channel you call leave in the same fashion as join.
Documentation: https://socket.io/docs/rooms-and-namespaces/
function join() {
// Run's once clicked
console.log('Join function here');
}
Join
Maybe handle it server side? Have a dictionary that stores User Ids and the Socket Id, you can store this information io.on("connection"). Then when User A does the process of creating the room, send to the server side the room id and something that identifies User B. Server side figures out the socket id of User B and joins them to the room.
This method is a bit of a hack, but it does allow you to force any specified socket into a room entirely server-side, once you have their socket ID, without maintaining another object array. SocketIO (v2.x) stores room allocations in two places to make things fun.
// $targetSocket is the socket ID of the connection you want to force into a room
$roomArray = $io->sockets->connected[$targetSocket]->rooms; //socket's rooms stored here
$roomArray[$newRoom] = $newRoom; // add the new room to this array
$io->sockets->connected[$targetSocket]->rooms = $roomArray; // save the modified array
$io->sockets->adapter->add($targetSocket, $newRoom); // update the adapter too
This is obviously PHP but it will translate easily into js.
You can do the reverse to kick users out of rooms too:
unset($roomArray[$oldRoom]); //delete room key and value from this socket's array
// save the array here, see above
$io->sockets->adapter->del($targetSocket, $oldRoom); // delete from adapter
Use both at once for a leave and join system.
This may seem pointless when a socket can add or remove itself so easily with socket->join('ROOM'); but there are circumstances in which you may need to do this from outside the connection's socket, such a game function.
For anyone facing a similar issue.
#Kouvolan Arnold's solution will work but having to store the socket object for each user is too much of a resource, especially when socket.io already stores them.
A simple solution here is for you to get the socket object already stored by locating it. The good news is that socket.io already provides that, see below.
//v2.x or less
io.sockets.sockets[USER_SOCKET_ID_HERE]
//v3.x and 4.x
io.sockets.sockets.get(USER_SOCKET_ID_HERE)
The code below would have worked well in your case.
//v2.x or less
io.sockets.sockets[clientSocketId].join(roomid);
//v3.x and 4.x
io.sockets.sockets.get(clientSocketId).join(roomid);
Thanks

Using global variable as database cache?

Site is working with nodejs+socketio+mysql.
Is it normal to create a global object just before starting my app to store everything I have in the database? Something like user's password hashes for a very quick authentication process, compare the given token + userid.
var GS= {
users: {
user1: {
token: "Djaskdjaklsdjklasjd"
}
,
user555: {
token: "zxczxczxczxc"
}
,
user1239: {
token: "ertertertertertret"
}
}
};
On connect, node check user with gived user_id.
if (GS.hasOwnPropery("user"+user_id)) {
//compare gived token GS["user"+user_id].token
} else {
//go to database to get unknown id and then store it in GS
GS["user"+user_id] = { token: database_result };
}
And with everything else the same thing, using object property instead of querying the database. So if someone go to url /gameinfo/id/1, I just look in variable GS["game"+url_param] = GS["game"+1] = GS.game1
And of course, we don't talk about millions of rows in the database. 50-70k max.Don't really want to use something like Redis or Tarantool.
You can have a global object to store these info, but there are something to consider:
If you app are running by more than one machine (instance), this object won't be shared between these them.
This leads to some functional downsides, like:
you would need sticky session to make sure request from one particular client always directed to one particular instance
you can not check status of an user having data stored in another instance ...
Basically, anything that requires you to access user session data, will be hard, if not impossible, to do
In case your server goes down, all session data will be lost
Having a big, deep nested object is dangerously easy to mess up
If you are confident that you can handle these downsides, or you will not encounter them in your application, then go ahead. Otherwise, you should consider using a real cache library, framework.

Chat application how to generate an id for a particular chat?

Let's say I have two users, "Matt" & "Kevin". Matt wants to message Kevin, by clicking a chat button to send Kevin a direct message a chat box boots up, he sends a message and Kevin receives it.
I generate the chat id by taking the person who sent it (Matt) and the person who received the message (Kevin) and concatenating it into an id.
var me = "Matt";
var user = "Kevin";
var uniqueChatID = me+user;
As I save the message server side (with mongoDB) the message object has a chatID of MattKevin. So now when I want to get back to that chat I can pull in all messages with the chatID of MattKevin.
This works fine, until Kevin wants to boot up a chat with Matt, then the id becomes KevinMatt. Now I am referencing a different chat, it's backwards. So If I want to pass uniqueChatID to get the messages it will pull a different set.
var me = "Kevin";
var user = "Matt";
var uniqueChatID = me+user;
So I am curious how can I set this up a bit better so that my program knows, ok Matt and Kevin have a chat, so if Matt messages Kevin it pulls in their chat or visa versa, Kevin messages Matt and it gets the same messages?
Sort them alphabetically:
var me = "Kevin";
var user = "Matt";
var uniqueChatID = [me, user].sort().join('');
That said, while this technically works, I'd recommend you do a little housekeeping - ensure they're always lowercase, and ensure on your db that you enforce unique usernames. Or, I'd even suggest giving the user a unique identifier (like a UUID) and use that instead to create the UCID:
var me = CurrentUser.uuid(); // 8cb3ebb8-30f9-11e5-a151-feff819cdc9f
var targetUser = Chat.targetUser(); // Matt: 9bc1ef9c-6719-4041-afd3-c5b87c90690d
var uniqueChatID = [me, targetUser].sort().join(',');
// 8cb3ebb8-30f9-11e5-a151-feff819cdc9f,9bc1ef9c-6719-4041-afd3-c5b87c90690d
And lastly, if your db supports relationships or connections, your best option is to separate chat table/collection for each chat and "connect" (or create a relationship) between both users and the chat. Then the next time you go and load it up, the connection will lead you to a unique chat that's connected to both users.
I think you approach is too complex. Furthermore, it looks like you want to embed the individual chat messages into the document bearing the created _id. The problem here is that there is a 16 MB size limit on BSON documents at the time of this writing. Upon reaching this limit, your users simply could not communicate any more. Increasing the size of documents may also lead to frequent document relocations, which is a very costly operation unless you use the new WiredTiger storage engine introduced in version 3.0 of MongoDB.
So we need a more scalable approach.
Here is how I would do it:
User:
{
_id: "Kevin",
email: "kevin#example.com"
/* Put further user details as you see fit*/
}
Message:
{
_id: new ObjectId(),
from: "Kevin",
/* You might want to have multi-person chats, hence the array */
to: ["Matt"],
ts: new ISODate(),
message: "Hi, Matt!"
}
Index:
db.messages.ensureIndex({from:1,to:1,ts:1})
Query for reconstructing all messages a user received:
var user = "Matt"
db.messages.find({"to": user}).sort({ts:1})
Now you can iterate over the result set and open a chat window for each "from" you find.
Query for reconstructing a defined chat
var user = "Matt"
var sender = "Kevin"
db.messages.find({"from": sender, "to":user}).sort({ts:1})
will give you all messages sent to Matt by Kevin, ordered by time. Since both queries should utilize the index, they should be pretty fast. You can use .limit(x) to query only the last x messages sent to user.
With this approach, you don't need an artificial _id, the index created allows you to do every query related to the participants efficiently and the messages can be sorted in order. Because each message is saved individually and does not change any more, you can store an almost indefinite number of messages and bypass the document relocation problem.

How to undo "Meteor.publish", and undo "new Meteor.Collection"

I see that when publishing, the collection._connection.publish_handlers is populated, and so does the collection._connection.method_handlers, and probably other areas.
I want to basically cleanup the memory by removing the references to that collection and it's publication entirely.
Basically each user of the app has a list of collections for that user. There is a publish function that looks like this for the user to get their list of collections:
Meteor.publish('users_collections', function() {
var self = this;
var handle = UsersCollections.find({ownerId: self.userId}).observeChanges({
added: function(id, collectionInfo) {
UsersCollectionManager.addUsersCollection(self.userId, collectionInfo.name);
}
});
});
That publishes that user's list of collections (and any user that connects gets their list).
Once the user gets their list, each of those collections is made reactive with new Meteor.Collection and then published.
UsersCollectionManager.addUsersCollection = function(userId, collectionName) {
if (self.collections[userId].collections[collectionName] === undefined) {
self.collections[userId].collections[collectionName] = new Meteor.Collection(collectionName);
Meteor.publish(collectionName, function() {
return self.collections[userId].collections[collectionName].find();
});
}
};
Once the user disconnects I have a function that gets run.
if that user doesn't have any connections open (ex: if they had multiple windows open and all the connections are closed "all windows closed") then it starts a 30s timeout to:
cleanup all these publish calls and new Meteor.Collection calls" to save memory
As the other user's of the app won't need this user's collections.
I'm not sure how to actually cleanup those from memory.
I don't see a "unpublish" or "Collection.stop" type of methods in the Meteor API.
How would I perform cleanup?
You need to do this in two steps. First, stop and delete the publications, then delete the collection from Meteor.
The first step is fairly easy. It requires you to store the handles of each subscription:
var handles = [];
Meteor.publish('some data', function() {
//Do stuff, send cursors, this.ready(), ...
handles.push(this);
});
And later in time, stop them:
var handle;
while(handle = handles.shift()) {
handle.stop();
}
All your publications are stopped. Now to delete the publication handler. It's less standard :
delete Meteor.default_server.publish_handlers['some data'];
delete Meteor.server.publish_handlers['some data'];
You basically have to burn down the reference to the handler. That's why it's less standard.
For the collection you first need to remove all documents then you have to delete the reference. Fortunately deleting all documents is very easy:
someCollection.remove({}, {multi : true}); //kaboom
Deleting the reference to the collection is trickier. If it is a collection declared without a name (new Mongo.Collection(null);), then you can just delete the variable (delete someCollection), it is not stored anywhere else as far as I know.
If it is a named collection, then it exists in the Mongo database, which means you have to tell Mongo to drop it. I have no idea how to do that at the moment. Maybe the Mongo driver can do it, ot it will need some takeover on the DDP connection.
You can call subscriptionHandle.stop(); on the client. If the user has disconnected, the publications will have stopped anyway.

Categories

Resources