socket.io and ui-router change state - node.js

I found some question about this, but no proper solution.
I want to accomplish something looks theoretically simple:
I have 2 views (ui-router states):
"list" that shows a list of documents with "edit" button. Clicking on the button you go to the editor.
"editor" that shows a form for edits a single document.
If an user visits the 'editor' I want to send a message to the "list" view and disable the edit button.
So what I'm trying to do is a simple websocket way to disable the access in write to documents that are in editing (just in frontend).
One of the problems come when I change route. The socket stop working. The idea is open a socket connection when I join the "list" view and disconnect when I leave it to any other route (including "editor"); the same with "editor" view. Below the code, but it doesn't work.
Following this idea, when the user clicks on the edit button (leave the 'list' view to the join the 'editor' view) I have to deal with a rapid disconnection (list view)-connection(editor view) in immediate sequence (I guess I cannot be sure about the sequence of the socket events..)
server-side
var lockedList = [];
io.on('connection', function (socket) {
console.log('a user connected:'+socket.id);
socket.emit('user:get',socket.id);
socket.emit('doc:list', lockedList);
socket.on('disconnect', function (s) {
console.error('a user disconnected:'+socket.id);
lockedList = lockedList.filter(function(el) {
return el.client !== socket.id;
});
socket.broadcast.emit('doc:list',lockedList);
socket.removeAllListeners('doc:list'); //?
socket.removeAllListeners('disconnect');
});
socket.on('doc:lock', function (obj) {
console.error('doc:lock:');
console.error(obj);
lockedList.push(obj);
socket.broadcast.emit('doc:list',lockedList);
});
socket.on('doc:unlock', function (obj) {
console.error('doc:unlock:');
console.error(obj);
lockedList = lockedList.filter(function(el) {
return el.client !== socket.id;
});
socket.broadcast.emit('doc:list',lockedList);
socket.disconnect();
});
socket.on('doc:list', function () {
console.log('doc:list')
socket.broadcast.emit('doc:list',lockedList);
});
socket.on('doc:askList', function () {
console.log('doc:askList')
socket.broadcast.emit('doc:list',lockedList);
});
socket.on('user:disconnect', function () {
console.log('user:disconnect')
socket.broadcast.emit('doc:list',lockedList);
socket.disconnect();
});
the "live" array of locked documents I want to keep server-side is in the form:
lockedList = [
{"client":"TourJX6zem1MpCx8AAAH","document":"580e899ca9c8621c48c0042d"}
]
keep the client socketId is necessary because I can't figure it out a better way to unlock a document an user stop editing (the user save the edits to the document or just close the browser/tab without save).
"List" angular controller
socket.on('connect', function () {
socket.emit('doc:askList', function () {});
});
socket.on('user:get', function (clientid) {
console.log("(list)here your socked id: " + clientid);
});
socket.on('doc:list', function (list) {
$scope.lockedList = list;
});
$scope.$on('$stateChangeStart', function (event, next) {
socket.disconnect();
});
$scope.$on('$destroy', function (event) {
socket.disconnect();
});
"Editor" angular controller
socket.on('connect', function () {
console.log("editor_connect");
});
socket.on('user:get', function (clientid) {
console.log("here your socked id: " + clientid);
$scope.clientid = clientid;
$scope.message = { client: clientid, document: $scope.model._id }; //$scope.model._id is the document id: once I have the client connection Id, I a emit doc:lock with the clientId and the documentId
socket.emit('doc:lock', $scope.message);
});
$scope.$on('$stateChangeStart', function (event, next) {
socket.disconnect();
});
$scope.$on('$destroy', function (event) {
socket.disconnect();
});
Last note: to solve the $apply issue, I use the socketio angular Service posted here https://stackoverflow.com/a/17092745/3671995

Related

How to set socket attribute globally using socket.io in nodejs from within a socket event handler?

Currently working on a portfolio project, and running into a problem where I am trying to set an attribute for the socket variable globally from within a local socket event handler so that it can be accessed when handling other socket events.
These are the events I'm handling: a login event and a disconnect event.
io.on("connect", socket => {
console.log(`User joined: `, socket.id)
socket.on("login", (data) => handleUserLogin(socket, data))
socket.on("disconnect", () => handleDisconnect(socket))
})
When the user logs on, I emit a login event from the client and the login event handler takes in data of a JSON object with user details, and a company ID, both sent from the client. I'm trying to save this companyId gloablly. This companyId attribute is supposed to help determine which list to append/collect etc.
const handleUserLogin = async (socket, data) => {
const { companyId, user } = data;
socket.join([`${socket.id}`, `${companyId}`]);
socket.companyId = companyId;
try {
const newOnlineUser = await redisClient.hset(`${companyId}:users:online`, `${socket.id}`, JSON.stringify(user))
if (newOnlineUser) {
const onlineUsers = await redisClient.hgetall(`${companyId}:users:online`)
socket.to(companyId).emit("user_status_change", { onlineUsers })
}
} catch (error) {
socket.to(`${socket.id}`).emit("error", { error })
}
};
When the socket disconnects, I want to remove the user from my redis list, which means I'll need this companyId attribute. But a value of null appears when I try to access: socket.companyId.
const handleDisconnect = async (socket) => {
console.log(`Disconnect: ${socket.companyId}`)
if (socket?.companyId) {
console.log('Disconnect event for user', socket.id, 'of company', socket.companyId, 'occurred.' )
try {
const offlineUser = await redisClient.hdel(`${socket.companyId}:users:online`, `${socket.id}`)
if (offlineUser) {
const onlineUsers = await redisClient.hgetall(`${companyId}:users:online`)
socket.to(companyId).emit("user_status_change", { onlineUsers })
}
} catch (error) {
console.log(error)
}
}
}
Would love to know how to deal with this, or at least find a way to set an attribute to the socket instance from within event handling, for which can be accessed also when handling other events.

Socket.io - React : How to retrieve saved socket.io data (MongoDB) in React?

I followed few tutorials on how to display messages sent in the chatroom before joining in, but I don't know how to display them in React and I have few questions below in the server side.
Client side, in constructor :
this.state = {
msg: "",
messages: []
};
Client side, I have a form which clicked button will send the message to the server by this function :
sendMessage(e) {
e.preventDefault();
let msg = this.state.msg;
this.socket.emit("sendMessage", msg);
this.setState({ msg: "" });
}
Server side, I have a mongoose Schema for the message, named Message and the collection in the database is messages.
const Message = new mongoose.Schema({
sender: {
type: "string"
},
message: {
type: "string"
}
});
var messages = [];
io.on("connection", (socket, user) => {
var user = socket.request.session.user;
Message.find({}).exec((err, messages) => {
if (err) throw err;
console.log(messages);
io.emit("showingPastMessages", messages);
});
console.log(messages) shows in PowerShell all the messages (entries) saved in Mongo in an array of javascript objects ?
[{id_ : 4qxxx, sender : 'user123', message : 'hello!'}, {id_ : 5exxx, sender : 'user456', message : 'hi!'}]
I would like to know if it is possible to access only to sender and message properties to send it to the client ? Something like messages.sender + messages.message because when I console.log(messages.message) it shows undefined
Here is where the server receives the message sent then saves it in Mongo.
socket.on("sendMessage", function(msg) {
var newMsg = new Message({ message: msg, sender: user });
newMsg.save(function(err) {
if (err) {
console.log(err);
} else {
messages.push(newMsg);
console.log(newMsg);
console.log(messages);
}
});
});
console.log(newMsg) shows the latest msg sent, but the console.log(messages) doesn't show the previous messages but only the latest one, why ?
then in React, I should have something like this in constructor, in ComponentDidMount() ? If should it be with prevState
this.socket.on("showingPastMessages", function(messages){
this.setState({ ...this.state.messages, messages})
});
Could you could give me some advices ?
Here my client side code to retrieve the data:
class Chat extends Component {
constructor(props) {
super(props);
this.state = {
msg: "",
messages: []
};
var socket = io('http://localhost:3000');
this.socket.on("history", function(messages) {
console.log(messages);
});
}
That's a good start. What I would suggest doing is making this a little more event driven. To do that, you'll want to add the sockets to a room when they connect. How many rooms and how to split up the rooms will depend on your app, but I'll demonstrate the basic idea.
First, when a socket connects to your server, add that socket to a room and emit your chat history immediately.
io.on('connection', async (socket) => {
socket.user = socket.request.session.user;
socket.join('chat');
try {
const messages = await Message.find({});
socket.emit('history', messages);
} catch (err) {
// Handle this error properly.
console.error(err);
}
});
Then, later on, when you receive a message, you'll want to save that message and emit it to all of the sockets in your chat room.
socket.on("sendMessage", (msg, callback) => {
const message = new Message({ message: msg, sender: socket.user });
message.save(function(err) {
if (err) {
return callback(err);
}
io.to('chat').emit('message', message);
});
});
Finally, on the client side, you'll want to listen for the history event. When you receive it, you'll want to clear the chat history you currently have and replace it with what the server is telling you. Maybe this would look something like
socket.on('history', (messages) => {
this.setState({ messages });
});
You'll also want to listen for this message event, but with this event, you'll only append the message to your history. This might look something like
socket.on('message', (message) => {
this.setState({ messages: [ ...messages, message ] });
});
A word of warning, if when you tell the server about a new message, do not add it to your messages state array until you receive the message event. If you do so, you will notice double messages. For example, this might look something like
onSendMessage(evnt) {
evnt.preventDefault();
socket.emit("sendMessage", msg);
this.setState({ msg: "" });
}
Note: After receiving some feedback from the OP, I wanted to add a section on where to put the event handlers attached to the socket (i.e. all the socket.ons). My suggestion would be to add this code in the file that defines the Message schema at the bottom of the file in the io.on('connection') callback. For example,
const Message = new mongoose.Schema({/*...*/});
io.on('connection', (socket) => {
// Everything else I wrote above...
socket.on('sendMessage', (msg, callback) => {
// ...
});
});
On the client side, the event handlers would probably be registered when the chat component is mounted. For example,
class ChatComponent extends React.Component {
componentDidMount() {
this.socket = io('https://your-server-or-localhost/');
this.socket.on('history', (messages) => {
// See above...
});
}
}

Make a new prop in socket io object and get undefined using react.js as client

I was trying to store data into a socket prop that I can reference it later. However, on others socket.on it comes out as undefined
Here is the code:
var userlist = ['Dang Huy','Alice','Bui Sam', 'Hai Hai'];
var onlineUser = [];
io.on('connection', socket => {
socket.on('userOnline', (data) => { //User Alice go in
if(userlist.includes(data)){
onlineUser.push(data);
io.sockets.emit('onlineUser', onlineUser);
}
socket.user_name = data; // store data into user_name
console.log(socket.user_name + ' is online'); //logout Alice is online
})
socket.on('userSendMessage', (data) => {
console.log(socket.user_name) // socket.user_name is undefined here!!
console.log(data) // still log out the message from the client side
io.sockets.emit('serverSendMessage', 'Hello')
})
socket.on('disconnect', function(){
onlineUser.splice(onlineUser.indexOf(socket.user_name), 1)
io.sockets.emit('onlineUser', onlineUser)
})
})
What do you mean by next event (could you provides an example?)Oh I notice something very wrong , you won't never go to some event and that is purely normal, your socket scope variable is into {} & you ask it out of them, just move the listeners up:
var userlist = ['Dang Huy','Alice','Bui Sam', 'Hai Hai'];
var onlineUser = [];
io.on('connection', socket => {
socket.on('userOnline', (data) => { //User Alice go in
if(userlist.includes(data)){
onlineUser.push(data);
io.sockets.emit('onlineUser', onlineUser);
}
socket.user_name = data; // store data into user_name
console.log(socket.user_name + ' is online'); //logout Alice is online
})
socket.on('userSendMessage', (data) => {
console.log(socket.user_name) // socket.user_name is undefined here!!
console.log(data) // still log out the message from the client side
io.sockets.emit('serverSendMessage', 'Hello')
})
socket.on('disconnect', function(){
onlineUser.splice(onlineUser.indexOf(socket.user_name), 1)
io.sockets.emit('onlineUser', onlineUser)
})
})
I found it out myself. The Problem was: I was using Reactjs as front-end framework, so each component has different socket session, therefore socket.username is defined if I call socket.emit('userSendMessage') in that same component that I connected to. If I socket.emit('userSendMessage') in other component, It will return undefined because of different session of each component.

React and Socket.io: Able to get initial data - but view doesn't update when a new post comes in

Not sure if the issue is how I have my sockets setup - or if I am incorrectly trying to render the data with React.
I can successfully pull in data with my socket - yet it doesn't live update state when new data is posted to the server. My intention is for the state in React to automatically render new data, which is always live because of the socket connection.
Here is my client app that gets messages from the server and renders them:
var Chat = React.createClass({
getInitialState: function() {
return {
messages: null
}
},
componentWillMount: function(){
var self = this;
socket.emit('getMessages');
socket.on('serverMessages', function (data) {
self.setState({messages: data})
});
},
render: function() {
var messages = this.state.messages ? <MessageList messages={this.state.messages}/> : null
return (
<div className="jumbotron">
{ messages }
<MessageForm submitMessage={this.submitMessage}/>
</div>
);
}
});
Just in case here is my server code that emits data:
io.on('connection', function (socket) {
socket.on('getMessages', function (data) {
Message.find(function(err, messages){
socket.emit('serverMessages', messages);
})
});
});
As of right now, you're "just" grabbing data from the server once the component has been loaded. To get something a bit more "real time" you'll want to either ping the server with the same emit statement you specified regularly (which defeats the point of using websockets, really, you could use long-polling) or have the server regularly send new data to all clients.
You can do EITHER:
A) Client side: "Polling" for information [Not Ideal]
Note: I initially put this in my answer because I saw the OP was "polling" when the controller was loaded. I didn't click on that this might be because the controller may not be loaded with the websocket so sending data on connect might not work here. My bad.
Replace socket.emit('getMessages') with something that will "poll" the websocket regularly for data:
setInterval(function () {
socket.emit('getMessages')
}, 10000); /* Request data from the socket every 10 seconds */
OR
B) Server side: Send new data as it becomes available. [Best Way]
Track all clients via a clients array and delete them from it when their session ends.
var clients = [];
io.on('connection', function (socket) {
clients.push(socket);
socket.on('end', function () {
// Could also splice the array below, but it still works.
delete clients[clients.indexOf(socket)];
});
/* Previous logic for server goes here */
});
Run this code when you need to push new messages from the database/data storage:
for (var i in clients) {
clients[i].emit('serverMessages', /* messages object */);
}
Your server code is only firing upon initial socket connection.
Server:
socket.on('getMessages', function (data) {
Message.find(function(err, messages){
socket.emit('serverMessages', messages);
})
});
Client:
var Chat = React.createClass({
getInitialState: function() {
return {
messages: null
}
},
componentWillMount: function(){
var self = this;
socket.emit('getMessages');
socket.on('serverMessages', function (data) {
self.setState({messages: data})
});
},
render: function() {
var messages = this.state.messages ? <MessageList messages={this.state.messages}/> : null
return (
<div className="jumbotron">
{ messages }
</div>
);
}
});
Based on naming convention, it also appears that your Message.find() is pulling a single message. I would recommend clarifying the labeling to match cardinality.
Try this:
var Chat = React.createClass({
getInitialState: function() {
return {
messages: null
}
},
componentWillMount: function(){
var self = this;
socket.emit('getMessages');
socket.on('serverMessages', function (data) {
self.setState({messages: data})
});
},
render: function() {
var messages = this.state.messages ? <MessageList messages={this.state.messages}/> : null
return (
<div className="jumbotron">
{ messages }
<MessageForm submitMessage={this.submitMessage}/>
</div>
);
}
});
Could it be possible its due to the componentWillMount lifecycle method? Could you try the componentDidMount instead.
It looks like render will see the state update but only gets executed once despite the state change according to facebook.

render dynamic value from passport.socketio to jade view

I'm relatively new to node and socketio. What I'm trying to achieve is authenticate users using passport and send total number logged user count to the view. Such that if a logged in user can see the total number of current-logged-in users and if a user logs that the count decreases or increases when someone else log-ins respectively.
Using passport.socketio to access authenticated passport user information from a socket.io connection.
In the callback, I'm storing the username in a mongoose collection, and accordingly on the logout removing the user from the collection. I get a count of the number of users in the model which I need to pass and bind to the view. Jade being the template engine. Below is how my onAuthorizeSuccess callback looks like where I try to pass the count to home.jade.
function onAuthorizeSuccess(data, accept) {
var username = data.user.username;
var User = mongoose.model('loggedusers', userSchema);
var user = new User({
username: username
});
user.save(function (err, data) {
if (err) console.log(err);
else {
console.log('Saved : ', data);
}
User.count({}, function (err, c) {
console.log('Count is ' + c);
app.get('/', function (req, res) {
res.render('home', {
count: {
countData: c
}
});
});
});
});
console.log('successful connection to socket.io ');
accept(); //Let the user through
}
And in the jade view I try to set it using
li Logged Users ---> #{countData.c}
But, countData is undefined in the view.
How should I go about rendering a dynamic value from the server to the view in jade?
Any assistance much appreciated.
Thanks,
Arnab
Your variable is wrong you should use instead #{count} with:
res.render('home', {count: c});
Figured this out.
Made a function to server content over a socket, that the control helps updating on the front-end
module.exports = function (socket) {
setInterval(function () {
var UserSchema = require('mongoose').model('loggedusers');
UserSchema.count({}, function(err, c){
console.log('Count is ' + c);
socket.emit('send:count', {
count: c
});
});
}, 1000);
};
And the angular controller
angular.module('myApp.controllers', []).
controller('AppCtrl', function ($scope, socket) {
socket.on('send:count', function (data) {
$scope.count = data.count;
});
});
and in jade {{count}} and add div.container(ng-controller='AppCtrl') should give the updated count on the front-end.
Arnab

Resources