React Native one to one conversation using socket.io - node.js

i currently have a react native app with nodejs express Sequelize as my backend and postgres as my database.
So, on my posts screen next to each post, i have a text input and a button where the current user can send the user of the post an initial message. Once the button is pressed, a conversation between these 2 users about this post is created in my database and stored in my conversation table and an entry of the message sent is also stored in my messages table.
I have implemented bidirectional communication between these 2 users. But my problem is i need to refresh the app in order to show the user current user the sent message and to show the receiving user the received message.
I have been researching for a while now and trying to understand how to implement this feature using socket.io but could not get anywhere.
Client Side
Here is my Chat Screen
function ChatScreen({route,navigation}) {
const message = route.params.message;
const [messages, setMessages] = useState(message.Messages);
const [text, setText] = useState('');
const { user } = useAuth();
const [socket, setSocket] = useState(null);
useEffect(() => {
const newsocket =io.connect(socketurl)
setMessages(messages);
newsocket.on('connection', msg => {
console.log('i have joined')
setMessages(messages=>messages.concat(msg))
setSocket(newsocket)
})
return()=>newsocket.close;
}, []);
const updateText=(text)=>{
setText(text);
}
const onSend = (ConversationId,senderId,receiverId,message) => {
console.log("sent")
messagesApi.sendMessage({ConversationId,senderId,receiverId,message});
setText("")
socket.emit('message', { to: (user.id===route.params.message.user1 ?
route.params.message.user2 : route.params.message.user1), from:
user.id, message,ConversationId });
};
return(
<Text>{user.id === message.Recipient.id ?
message.Creator.name:message.Recipient.name}</Text>
<KeyboardAvoidingView
style={{
display: "flex",
flex: 1,
}}
behavior={Platform.OS === "ios" ? "padding" : null}
keyboardVerticalOffset={Platform.OS === "ios" ? 25 : 0}
>
<FlatList
inverted
data={message.Messages}
keyExtractor={(message) => message.id.toString()}
renderItem={({item,index})=>(
<MessageBubble
text={item.message}
mine={item.senderId !== user.id}
/>
)}/>
<View style={styles.messageBoxContainer}>
<TextInput
style={styles.messageBox}
placeholder="Message..."
multiline
clearButtonMode="while-editing"
onChangeText={updateText}
value={text}
autoCorrect={false}
/>
<TouchableOpacity onPress={onSend}>
<Text style={styles.send}>Send</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
)
Server Side
index.js
const express = require("express");
const app = express();
const http = require("http");
const socketio = require("socket.io")
const server=http.createServer(app);
const io =socketio(server)
io.on("connection", socket => {
socket.on('message', (data) => {
socket.join(data.ConversationId);
io.sockets.in(data.to).emit('send_message', { message: data.message,
to: data.to });
});
});
const port = process.env.PORT || config.get("port");
server.listen(port, function () {
console.log(`Server started on port ${port}...`);
});
Currently when i send a message, the message gets stored in my database but my chat does not update instantly (ie. not live), i need to refresh my app and the messages appear.
Can someone please help me and check if the implementation of socket i currently have is correct and if so, how do i render my flatlist instantly?
UPDATE
i think something is wrong in my useEffect, because when i open the chat i am not getting "i have joined" in the console:
useEffect(() => {
setMessages(messages);
socket.on('connect', msg => {
console.log('i have joined')
setMessages(messages=>messages.concat(msg))
})
}, []);

Your currently creating a new connection on every state change.. const socket =io.connect(socketurl)
You have a useEffect callback and that would be the logical place to put your connection logic, currently your only listening once for a connection, but creating multiple connections, so your 'connection' event is never called on these new connections. But you only want to connect once anyway, so we just need to put the connection logic also inside the useEffect, not just the connection event.
Because connecting to a socket is async, you will want to wait for the connection before rendering. So what we could do is store the socket in state, and when we get a connection set socket state, this will fire a re-render with a now valid socket.
eg.
const [socket, setSocket] = useState(null);
...
useEffect(() => {
const socket = io.connect(socketurl)
setMessages(messages);
newsocket.on('connect', msg => { //connect not connection
console.log('i have joined')
setMessages(messages=>messages.concat(msg));
setSocket(newSocket);
});
//might make sense to close the socket too,
//otherwise a memory leak.
return () => newSocket.close();
}, [route, navigation]);
if (!socket) {
//we don't have a socket yet,
return "loading..";
} else {
// we have a socket,
const onSend = (ConversationId,senderId,receiverId,message) => {...
....
// now render..
return (
<Text>{.........

Socket Index.js
/** Socket.io server listens to our app **/
var io = require('socket.io').listen(app);
io.on('connection', function (socket) {
/** On User Log In **/
socket.on('login', function (data) {
console.log('user joined >>', data)
userList.addUser(data, socket);
});
// Example Event //
socket.on('get_online_friends', userId => {
//Get List Data And Then //
let data = [UserList];
socket.emit('send_online_friend', data);
}
)
// On Logout //
socket.on('disconnect', function (reason) {
var offlineId = userList.removeUser(socket)
);
}
user_list.js
var userList = {};
module.exports = userList;
var userData = [];
var userSocData = {};
userList.user = userData;
userList.userSoc = userSocData;
userList.getUserList = function () {
return userSocData;
};
userList.addUser = function (user, client) {
userSocData[user] = {
socketId: client.id
}
};
userList.setReceiverId = function (user, client) {
var index = userData.findIndex(x => x.user_id == user['user_id']);
if (index !== -1) {
userData[index]['receiver_id'] = user['receiver_id'];
}
userSocData[user['user_id']] = {
socket: client.id
};
};
userList.removeUser = function (client) {
for (const property in userSocData) {
if (client.id === userSocData[property].socketId) {
var userID = property;
delete userSocData[property]
}
}
return userID;
};
Front End
***
socket.emit("get_request", userData.user_id);
socket.on("get_request_data", function (data) {
if (data.status) {
self.setState({ onlineFriends: data.data });
}
});
***

Related

App stops working when i keep sending multiple messages with socket.io

I have a react native expo CLI app add i am trying to create a in app built messaging app like whatsapp inside. All my api calls work in storing the messages and get all the conversations. I have an all messages screen is shows all the conversation the user has with the last message sent inside the chat and a chat screen that shows all the messages sent inside that conversation.
I am using socket.io-client and everything works accordingly except when I keep sending multiple messages my backend stops connecting to my app, then I need to reload my app again then the same happens. Why is that happening?
Also, something is wrong with my code, the messages are getting emitted multiple times.
Here is my code
NodeJS
const server = http.createServer(app);
const io = socket(server)
io.on('connection', (socket) => {
// connection
console.log('User ' + socket.id + ' connected')
// joining a room
socket.on('subscribe', (room) => {
console.log('user socket', socket.id, 'joining room', room);
socket.join(room);
});
// send message within room
socket.on('message', (data) => {
io.in(data.conversationId).emit('send_message', {
message: data.message,
receiverId: data.to,
senderId: data.from,
id: data.conversationId,
data: data.data
});
})
// seen message
socket.on('markSeen', (data) => {
// Emit 'markedSeen' event
console.log(socket.id, 'has seen your message')
io.emit('markedSeen', data);
});
// main socket listening
socket.on('listening', (data) => {
console.log(data)
console.log('user socket', socket.id, 'is listening', data.conversationId, data.message)
io.emit('socketListening', data)
});
// user typing within room
socket.on('typing', (data) => {
console.log('user socket', socket.id, 'in room', data.chatId, data.text)
io.in(data.chatId).emit('typingResponse', data.text)
});
// waiting to enter room
socket.on('waiting', (data) => {
console.log('user', socket.id, 'is waiting.');
});
// disconnect
socket.on('disconnect',() => {
console.log('USER DISCONNECTED', socket.id)
})
socket.on('unsubscribe', (data) => {
console.log(data)
socket.leave(data.id);
socket.to(data.id).emit('user left', socket.id);
console.log('user left', socket.id)
});
})
React Native
Messages Screen
const [listings, setListings] = useState([]);
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const loadListings = async() => {
setLoading(true);
const response = await messagesApi.getMessages();
setLoading(false);
if(refreshing) setRefreshing(false);
if (!response.ok) return setError(true);
setError(false);
setListings(response.data)
};
const { user } = useAuth();
const socketUrl = 'IP_ADDRESS';
let socket = useRef(null);
useEffect(()=>{
loadListings();
socket.current = io.connect(socketUrl)
socket.current.on('connect', msg => {
console.log(`user: ${user.id} is waiting.`)
console.log(`user: ${user.id} is waiting. socketID: ${socket.current.id}`)
socket.current.emit('waiting', user.id);
});
return(() => {
console.log(`user: ${user.id} --- socketid ${socket.current.id} deleted`)
socket.current.removeAllListeners('connect')
});
}, [socketUrl])
useEffect(() => {
socket.current.on("socketListening", (msg) => {
console.log('MSG',msg)
const result = listings.find(e => e.id === msg.conversationId)
if (result !== undefined) {
const resultMessages = result.Messages;
const newMessages = [msg,...resultMessages]
result.Messages = newMessages;
result.lastMessage = msg.message;
const arraywithoutrecord = listings.filter(e=>e.id!=msg.conversationId)
setListings([result,...arraywithoutrecord])
}
})
socket.current.on('markedSeen', (message) => {
const result = listings?.find(e => e.id === message)
if (result !== undefined) {
result.Messages[0].seenByUser = true;
const arraywithoutrecord = listings?.filter(e=>e.id!=message)
setListings([result,...arraywithoutrecord])
}
});
}, [listings]);
Chat Screen
const message = route.params.message;
const [messages, setMessages] = useState([]);
const [refreshing, setRefreshing] = useState(false);
const [text, setText] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const[isAdminTyping, setIsAdminTyping] = useState(false)
const { user } = useAuth();
const socketUrl = 'IP_ADDRESS';
let socket = useRef(null);
useEffect(() => {
setMessages(message.Messages)
socket.current = io.connect(socketUrl)
socket.current.on('connect', msg => {
console.log(`user: ${user.id} has joined conversation ${message.id}`,'connection socket id', socket.current.id)
socket.current.emit('subscribe', message.id);
socket.current.emit('markSeen', message.id);
});
socket.current.on("send_message", (msg) => {
setMessages(messages => [msg, ...messages]);
socket.current.emit('markSeen', message.id);
});
socket.current.on("typingResponse", (msg) => {
if (msg === 'Admin is typing...') {
setIsAdminTyping(true)
} else {
setIsAdminTyping(false)
}
})
return(() => {
console.log(`user: ${user.id} left the room ${message.id} --- socketid ${socket.current.id}`)
socket.current.close()
});
}, [socketUrl]);
const seenByUser = async () => {
const response = await messagesApi.seenByUser({conversationId:message.id});
if (response.ok) {
console.log('all messages seen')
}
}
useEffect(() => {
seenByUser();
},[messages])
const onSend = async(conversationId, senderId, receiverId, message) => {
const response = await messagesApi.sendMessage({ conversationId, senderId, receiverId, message });
if (!response.ok) return setError(true);
console.log("sent")
const to = (user.id === route.params.message.user ?route.params.message.admin : route.params.message.user)
socket.current.emit('message', { to: to, from: user.id, message, conversationId, data: response.data });
setText("")
socket.current.emit('listening',response.data);
};
const onFocus = () => {
socket.current.emit('typing', { chatId: message.id, text: 'User is typing...' });
}
const onBlur = () => {
socket.current.emit('typing', { chatId: message.id, text: 'User is NOT typing...' });
}
My entire Node index.js file
const express = require("express");
const users = require("./routes/users");
const auth = require("./routes/auth");
const listing = require("./routes/listing");
const helmet = require("helmet");
const compression = require("compression");
const config = require("config");
const app = express();
var cors = require('cors')
app.use(cors())
//Socket.io
const http = require("http");
const socket = require("socket.io")
const server=http.createServer(app);
const io =socket(server,
{
cors: {
origin:
[
"http://localhost:3000", /\.localhost\:3000$/,"IP_ADDRESS"
]
,
methods: ["GET", "POST"],
credentials: true
},
maxHttpBufferSize: 4e6 // 4Mb
}
)
io.on('connection',(socket)=>{
// connection
console.log('User '+socket.id+' connected')
// joining a room
socket.on('subscribe', (room)=> {
console.log('user socket',socket.id,'joining room', room);
socket.join(room);
});
// send message within room
socket.on('message', (data) => {
socket.to(data.conversationId).emit('send_message', {
message: data.message, receiverId: data.to,senderId:data.from,id:data.conversationId,data:data.data
});
})
// seen message
socket.on('markSeen', (data)=> {
// Emit 'markedSeen' event
console.log(socket.id,'has seen your message')
io.emit('markedSeen', data);
});
// main socket listening
socket.on('listening', (data) => {
console.log(data)
console.log('user socket',socket.id,'is listening',data.conversationId,data.message)
io.emit('socketListening',data)
}
);
// user typing within room
socket.on('typing', (data) => {
console.log('user socket',socket.id,'in room',data.chatId,data.text)
io.in(data.chatId).emit('typingResponse', data.text)
}
);
// waiting to enter room
socket.on('waiting', (data)=> {
console.log('user',socket.id, 'is waiting.');
});
// disconnect
socket.on('disconnect',(reason)=>{
socket.disconnect()
console.log('socket disconnected',socket.disconnect().disconnected)
console.log('USER DISCONNECTED',socket.id)
})
})
app.use(express.json({limit: '50mb'}));
app.use(express.urlencoded({limit: '50mb', extended: true, parameterLimit: 500000}));
app.use(express.static("public"));
app.use(helmet());
app.use(compression());
app.use("/api/users/", users);
app.use("/api/auth/", auth)
app.use("/api/listings/", listing);
const port = process.env.PORT || config.get("port");
server.listen(port, function() {
console.log(`Server started on port ${port}...`);
});
Cant speak to why it stops connecting, would really need more details on that one. For example, is it 'new' connections, or is your current connection dropping etc.
The whole point of WS is a persistent connection, so, once a user is connected, they should not need to reconnect again (unless they got disconnected)
Without knowing the whole flow of things in your app ( this is just going off what you have posted here )
It looks like you are having a user connect 'each time' they hit that page, if my assumption is correct here, then don't do that :D
This kind of leads into your other question about multiple messages being sent and then ties back into the whole websocket idea in the first place, its a persistent connection so you only need to do thing once.
For example, if you subscribe to a channel or room, you only need to do that one time, once you subscribe to a channel that socketID is tied to those emits, and a socket can subscribe to a channel multiple times. Knowing that, if, when a user hits your messages screen, and your code goes through and subscribes them to that channel each time they will in turn get multiple emits.
Again, without seeing the full flow of data it looks like every time a user hit your chat screen you are attempting to
connect to socket
server setting up the on events
Essentially telling them to connect again and listen for those calls, the more times they join, the more they see right.
What you want to do is make sure you define your socket globally AND only 'listen' for channels or emits 'once'
What I have done in the past is to have a main socket function when a user joins the event, this is done on login and or opening the app. This will perform the socket connect, run through all the connections etc and at the end I call a socketInit function. This function sets up all my .on statements in a conditional block, that ensures it will only run one time. So, something like if(socketInit = false) {run all my on code etc}
This ensures everything is only called one time. Then, inside each route, I do the 'emit' to the socket server telling it to 'join the room' or 'getchats' etc. Because I only have the .on called once, it means when I asked the socket server for the data via the emit what it returns on my .on only gets called once, because that .getChat on is only defined once and it knows what to do with that data and updates the screen accordingly.
Hope that helps!

Using Socket.io not working in React Native Mobile App

I have tried to connect socket with React Native mobile App.
Node server socket.io is working i have use with react web, But when i use React Native Mobile App there was not working
Client (React Native
import io from 'socket.io-client';
const ENDPOINT = "http://192.168.1.3:7000";
const socket = io(ENDPOINT, {transports: ['websocket']});
const getMessage = () => {
socket.on('getMessage', (data) => {
console.log('socket data', data);
})
};
const sendMessage = () => {
var userid = log.userid
var message = "msg from app"
socket.emit('send msg', { userid, message });
}
Server (Node Js)
const app = express();
const server = app.listen(7000);
const io = require('socket.io')(server, {
cors : {
origin:'*'
}
});
io.on("connection",(socket)=>{
socket.on('sendMessage',({userid, message})=>{
io.emit('getMessage',{userid, message});
})
})
dev! I solved this just not using socket.io. I got that Socket.io is not a good choice in mobile apps. Use WebSocket API instead.
REACT COMPONENT
import React from 'react';
import { View, Text } from 'react-native';
class WebSocketConnection extends React.Component {
constructor(props) {
super(props);
this.webSocket = this.webSocket.bind(this);
}
webSocket() {
const ws = new WebSocket('ws:172.16.20.201:8080');
ws.onopen = (e) => {
console.log('connected on wsServer');
}
ws.addEventListener('open', function (event) {
ws.send('Hello from React Native!');
});
ws.addEventListener('message', function (event) {
this.message = event.data;
console.log('Message from server ', event.data);
});
ws.onerror = (e) => {
console.log(e);
}
}
render() {
return (
<View>
<Text>
Socket.io YES
</Text>
</View>
)
}
componentDidMount() {
this.webSocket();
}
}
export default WebSocketConnection;
SERVER
/**
* Create WebSocket server.
*/
const WebSocket = require('ws');
const serverWs = new WebSocket.Server({
port: 8080
});
let sockets = [];
serverWs.on('connection', function(socket) {
sockets.push(socket);
console.log(`connectet client`);
// When you receive a message, send that message to every socket.
socket.on('message', function(msg) {
console.log(msg);
sockets.forEach(s => s.send(msg));
});
// When a socket closes, or disconnects, remove it from the array.
socket.on('close', function() {
sockets = sockets.filter(s => s !== socket);
});
});
While using Express, I bind this server inside /bin/www and work fine to me.
I was able to work when i changed the react-native to use the ws protocol. My code was as below
useEffect(()=>{
const socket = io("ws://localhost:3000");
socket.on("connect", () => {
console.log(socket.connected); // true
});
},[])

Unable to properly establish a connection between React Native client and Node.js server with redux-saga and socket.io

Quick context: I'm trying to build a react native prototype of a comment page where users can receive live updates (comments, users entering the comment screen, users leaving, etc.). To do this, I am using react-redux, redux-saga, socket.io, and node.js (server). I'm new to redux-saga so I might be missing something obvious here, so hang on, please... The culprit definitely lies in the watchCommentActions function/saga...
The problem: As soon as it is done mounting, the comment screen dispatches the following action { type: comment.room.join, value }, which is then correctly acknowledged by rootSaga, however, when trying to connect to the socket using a promise-resolve structure via const socket = yield call(connect); the promise never resolves, which blocks the generator (it does not proceed to the next yield). What's weird is that on the other side the server does log the connection to the socket, so the connection client --> server appears to be ok. Also, by hot reloading the app I can manage to resolve the promise (it's like the generator needs to run twice to resolve the socket connection), but then the socket.emit("join-room") never reaches the server and the generator gets stuck again.
Similarly, when I try to fire the write generator by posting a comment and thus dispatching {type: comment.post.start, value } the *socket.emit("comment", {text: value.text}) does not reach the server either.
To sum it up briefly nothing's really working and no error is getting thrown... GREAT.
Last words: Before moving my socket logic to saga the socket connection was working seamlessly. I've also tried to reuse the documentation's implementation with channels by using the same connect function instead of createWebSocketConection (https://redux-saga.js.org/docs/advanced/Channels.html) but the promise-resolve-socket situation still occurs. Also, I've noticed similar questions derived from the same git repo I've studied to understand the sagas logic (https://github.com/kuy/redux-saga-chat-example/blob/master/src/client/sagas.js), however, none of them allowed me to understand what's wrong with my implementation. Finally, if there is a better way to implement this logic with redux-saga, I am interested, all I want is a robust, centralized, and reusable implementation.
Sagas/index.js
import { all, takeEvery, takeLatest } from "redux-saga/effects";
import { comment } from "../Reducers/commentCacheReducer";
import { like } from "../Reducers/postsCacheReducer";
import { posts } from "../Reducers/postsReducer";
import flow from "./commentSagas";
import { likePost, unlikePosts } from "./likeSagas";
import { fetchPosts } from "./postsSagas";
function* watchLikeActions() {
yield takeLatest(like.add.start, likePost);
yield takeLatest(like.remove.start, unlikePost);
}
function* watchFetchActions() {
yield takeEvery(posts.fetch.start, fetchPosts);
}
function* watchCommentsActions() {
yield takeEvery(comment.room.join, flow);
}
export default function* rootSaga() {
yield all([watchLikeActions(), watchFetchActions(), watchCommentsActions()]);
}
Sagas/commentSaga.js
import { eventChannel } from "redux-saga";
import { call, cancel, fork, put, take } from "redux-saga/effects";
import io from "socket.io-client";
import { endpoint } from "../../API/ServerAPI";
import { addUser, fetchComment, leaveRoom, removeUser } from "../Actions/commentActions";
import { comment } from "../Reducers/commentCacheReducer";
function connect() {
const socket = io(endpoint);
return new Promise((resolve) => {
socket.on("connection", () => {
resolve(socket);
});
});
}
function subscribe(socket) {
return new eventChannel((emit) => {
socket.on("users.join-room", ({ userId }) => {
emit(addUser({ userId }));
});
socket.on("users.leave-room", ({ userId }) => {
emit(removeUser({ userId }));
});
socket.on("comments.new", ({ comments }) => {
emit(fetchComment({ comments }));
});
socket.on("users.join-room", ({ userId }) => {
emit(addUser({ userId }));
});
return () => {};
});
}
function* read(socket) {
const channel = yield call(subscribe, socket);
while (true) {
let action = yield take(channel);
yield put(action);
}
}
function* write(socket) {
while (true) {
const { value } = yield take(comment.post.start);
socket.emit("comment", { text: value.text });
}
}
function* handleIO(socket) {
yield fork(read, socket);
yield fork(write, socket);
}
export default function* flow() {
const socket = yield call(connect);
socket.emit("join-room", (res) => {
console.log(JSON.stringify(res));
});
const task = yield fork(handleIO, socket);
let action = yield take(leaveRoom);
yield cancel(task);
yield put(action);
socket.emit("leave-room");
}
server.js
const http = require("http");
const app = require("./app");
const socketIo = require("socket.io");
const mongoose = require("mongoose");
const normalizePort = (val) => {
const port = parseInt(val, 10);
if (isNaN(port)) {
return val;
}
if (port >= 0) {
return port;
}
return false;
};
const port = normalizePort(process.env.PORT || "3000");
app.set("port", port);
const errorHandler = (error) => {
if (error.syscall !== "listen") {
throw error;
}
const address = server.address();
const bind = typeof address === "string" ? "pipe " + address : "port: " + port;
switch (error.code) {
case "EACCES":
console.error(bind + " requires elevated privileges.");
process.exit(1);
break;
case "EADDRINUSE":
console.error(bind + " is already in use.");
process.exit(1);
break;
default:
throw error;
}
};
const server = http.createServer(app);
const io = socketIo(server);
server.on("error", errorHandler);
server.on("listening", () => {
const address = server.address();
const bind = typeof address === "string" ? "pipe " + address : "port " + port;
console.log("Listening on " + bind);
});
// comments room
// Storing in variable just for testing purposes, will
// connect to MongoDB once the socket problem gets solved.
let userIds = [];
io.on("connection", (socket) => {
console.log("[server] connect");
});
io.on("join-room", (socket, {userId}) => {
console.log(`[server] join-room: ${userId}`);
userIds.push(userId);
socket.socket.username = userId;
socket.broadcast.emit("users.join-room", { userId });
});
io.on("leave-room", (socket) => {
const { userId } = socket.socket;
if (userId) {
console.log(`[server] leaving-room: ${userId}`);
userIds = userIds.filter((u) => u !== userId);
delete socket.socket["userId"];
socket.broadcast("users.leave-room", { userId });
}
});
// Storing in variable just for testing purposes, will
// connect to MongoDB once the socket problem gets solved.
let messages = [];
io.on("comment", (socket, { text }) => {
console.log(`[server] message: ${text}`);
const message = {
id: messages.length,
text,
userId: socket.socket.userId
};
messages.push(message);
socket.broadcast("comments.new", { message });
});
EDIT 1
After quickly going through socket.io documentation I realised that my server quick implementation was faulty, I simply forgot to register event handlers inside the connecting protocol... However, the generator still requires to be triggered twice for the socket connection to start, allowing the promise to resolve and the user to join the socket room.
io.on("connect", (socket) => {
console.log("[server] connect");
socket.on("join-room", ({ userId }) => {
console.log(`[server] join-room: ${userId}`);
userIds.push(userId);
socket.username = userId;
socket.broadcast.emit("users.join-room", { userId });
});
socket.on("leave-room", ({ userId }) => {
if (userId) {
console.log(`[server] leaving-room: ${userId}`);
userIds = userIds.filter((u) => u !== userId);
delete socket["userId"];
socket.broadcast.emit("users.leave-room", { userId });
}
});
socket.on("comment", ({ text }) => {
console.log(`[server] message: ${text}`);
const message = {
id: messages.length,
text,
userId: socket.userId
};
messages.push(message);
socket.broadcast.emit("comments.new", { message });
});
});
It’s connect, not connection
https://github.com/socketio/socket.io-client
(commentSagas.js > connect())

Using MongoDB for real-time chat database

I am working on a personal project to increase my skill and experience with Nodejs, Express, Socket.io and MongoDB. I seem to have hit a wall in formulating how the database should work for this sort of app.I have been thinking about it and could use some help from anyone who can take some time. My application allows the user to enter a username and a choose a room title. After this they are loaded into the room and others can join that room and chat in real time. I want to persist the data, saving it for each room on disconnect and repopulating it on connect.
Each Room has a name associated with it and the messages themselves. Each message has a name of sender, the timestamp, and the text/content.
But when it comes to actually structuring the models and how to organize collections, I am getting confused. Can anyone help me out or set me on the right path for this kind of application?
server.js(backend)
require('dotenv').config();
const path = require('path');
const http = require('http');
const express = require('express');
const socketio = require('socket.io');
const mongoose = require('mongoose');
const formatMessage = require('./utils/messages');
const {
userJoin,
getCurrentUser,
userLeave,
getRoomUsers
} = require('./utils/users');
const app = express();
const server = http.createServer(app);
const io = socketio(server);
// Set static folder
app.use(express.static(path.join(__dirname, 'public')));
const botName = 'ChatCord Bot';
const messages = [];
//Database connection
const uri = process.env.ATLAS_URI;
mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true
});
const connection = mongoose.connection;
connection.once('open', () => {
console.log("MongoDB database connection established successfully");
})
// Run when client connects
io.on('connection', socket => {
socket.on('joinRoom', ({
username,
room
}) => {
const user = userJoin(socket.id, username, room);
socket.join(user.room);
// Welcome current user
socket.emit('message', formatMessage(botName, 'Welcome to ChatCord!'));
//Load messages for room from database
socket.broadcast.to(user.room).emit(
'message', formatMessage()
)
// Broadcast when a user connects
socket.broadcast
.to(user.room)
.emit(
'message',
formatMessage(botName, `${user.username} has joined the chat`)
);
// Send users and room info
io.to(user.room).emit('roomUsers', {
room: user.room,
users: getRoomUsers(user.room)
});
});
// Listen for chatMessage
socket.on('chatMessage', msg => {
const user = getCurrentUser(socket.id);
io.to(user.room).emit('message', formatMessage(user.username, msg));
});
// Runs when client disconnects
socket.on('disconnect', () => {
const user = userLeave(socket.id);
if (user) {
io.to(user.room).emit(
'message',
formatMessage(botName, `${user.username} has left the chat`)
);
// Send users and room info
io.to(user.room).emit('roomUsers', {
room: user.room,
users: getRoomUsers(user.room)
});
}
//Save messages for room to database
});
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => console.log(`Server running on port ${PORT}`));
main.js(frontend)
const chatForm = document.getElementById('chat-form');
const chatMessages = document.querySelector('.chat-messages');
const roomName = document.getElementById('room-name');
const userList = document.getElementById('users');
//Get username and room from URL
const {username, room } = Qs.parse(location.search, {
ignoreQueryPrefix: true
});
console.log(username, room);
const socket = io();
//Join chatroom
socket.emit('joinRoom', {username, room});
//Get room and users
socket.on('roomUsers', ({ room, users }) => {
outputRoomName(room);
outputUsers(users);
})
socket.on('message', message => {
outputMessage(message);
//Scroll down on new message
chatMessages.scrollTop = chatMessages.scrollHeight;
});
//Message submit
chatForm.addEventListener('submit', (e) => {
e.preventDefault();
//Get message text
const msg = e.target.elements.msg.value;
//Emit message to server
socket.emit('chatMessage',msg);
//Clear input
e.target.elements.msg.value = '';
e.target.elements.msg.focus();
});
//Output message to DOM
function outputMessage(message) {
const div = document.createElement('div');
div.classList.add('message');
div.innerHTML = `<p class="meta">${message.username} <span>${message.time}</span></p>
<p class="text">
${message.text}
</p>`;
document.querySelector('.chat-messages').appendChild(div);
}
//Add room name to DOM
function outputRoomName(room) {
roomName.innerText = room;
}
//Add users to DOM
function outputUsers(users) {
userList.innerHTML = `${users.map(user => `<li>${user.username}</li>`).join('')}`;
}
room.model.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const roomSchema = new Schema({
id: mongoose.ObjectId,
messages: [
{
id: mongoose.ObjectId,
authorUsername: String,
time: Date,
content: String
}
]
});
const Room = mongoose.model("Room", roomSchema);
module.exports = Room;
users.js
const users = [];
//Join user to chat
function userJoin(id, username, room) {
const user = {id, username, room};
users.push(user);
return user;
}
//Get current user
function getCurrentUser(id) {
return users.find(user => user.id === id);
}
//User leaves chat
function userLeave(id) {
const index = users.findIndex(user => user.id === id);
if(index !== -1) {
return users.splice(index, 1)[0];
}
}
//Get room users
function getRoomUsers(room) {
return users.filter(user => user.room === room);
}
module.exports = {
userJoin,
getCurrentUser,
userLeave,
getRoomUsers
}
A simple approach is structuring the db with only one schema, the Room schema.
When a user sends a message in a room, push to the messages array of that same room.
Then, in socket.io (node) you emit a socket (with the room id) and send the message, and in the client side, set the user to receive all sockets of that room.
Room schema:
{
id,
messages: [
{
id,
authorUsername,
content
}
]
}

NodeJS + WS access currently running WS server instance

I have implemented a simple REST API using NodeJS, ExpressJS and routing-controllers. I have also implemented a basic WebSocket server running alongside the REST API and using WS.
const app = express();
app.use(bodyParser.json({limit: "50mb"}));
app.use(bodyParser.urlencoded({limit: "50mb", extended: true}));
useExpressServer(app, {
controllers: [
UserController
]
});
const server = app.listen(21443, (err: Error) => {
console.log("listening on port 21443");
});
const wss = new WebSocket.Server({server});
wss.on("connection", (ws: WebSocket) => {
ws.on("message", (message: string) => {
console.log("received: %s", message);
ws.send(`Hello, you sent -> ${message}`);
});
ws.send("Hi there, I am a WebSocket server");
});
My question is how to I get access to the currently running WS instance so that I am able to send or broadcast from my controller methods. I have a number of POST methods that run long processes and so return a HTTP 200 to the client, I then would like to either send or broadcast to all connected WS clients.
What is the correct way to access the WebSocket.Server instance from within my controller classes?
You can create the websocket earlier and pass the instance around:
const notifier = new NotifierService();
notifier.connect(http.createServer(app));
app.get("/somethingHappened", () => {
notifier.broadcast("new notification!!");
});
app.use(routes(notifier))
Full code:
app.js
Pass the websocket to the other routes:
const express = require("express");
const http = require("http");
const NotifierService = require("../server/NotifierService.js");
const routes = require("./routes");
const app = express();
const server = http.createServer(app);
const notifier = new NotifierService();
notifier.connect(server);
app.get("/somethingHappened", () => {
notifier.broadcast("new notification!!");
});
// to demonstrate how the notifier instance can be
// passed around to different routes
app.use(routes(notifier));
server
.listen(4000)
.on("listening", () =>
console.log("info", `HTTP server listening on port 4000`)
);
NotifierService.js class that handles the websocket
const url = require("url");
const { Server } = require("ws");
class NotifierService {
constructor() {
this.connections = new Map();
}
connect(server) {
this.server = new Server({ noServer: true });
this.interval = setInterval(this.checkAll.bind(this), 10000);
this.server.on("close", this.close.bind(this));
this.server.on("connection", this.add.bind(this));
server.on("upgrade", (request, socket, head) => {
console.log("ws upgrade");
const id = url.parse(request.url, true).query.storeId;
if (id) {
this.server.handleUpgrade(request, socket, head, (ws) =>
this.server.emit("connection", id, ws)
);
} else {
socket.destroy();
}
});
}
add(id, socket) {
console.log("ws add");
socket.isAlive = true;
socket.on("pong", () => (socket.isAlive = true));
socket.on("close", this.remove.bind(this, id));
this.connections.set(id, socket);
}
send(id, message) {
console.log("ws sending message");
const connection = this.connections.get(id);
connection.send(JSON.stringify(message));
}
broadcast(message) {
console.log("ws broadcast");
this.connections.forEach((connection) =>
connection.send(JSON.stringify(message))
);
}
isAlive(id) {
return !!this.connections.get(id);
}
checkAll() {
this.connections.forEach((connection) => {
if (!connection.isAlive) {
return connection.terminate();
}
connection.isAlive = false;
connection.ping("");
});
}
remove(id) {
this.connections.delete(id);
}
close() {
clearInterval(this.interval);
}
}
module.exports = NotifierService;
routes.js
const express = require("express");
const router = express.Router();
module.exports = (webSocketNotifier) => {
router.post("/newPurchase/:id", (req, res, next) => {
webSocketNotifier.send(req.params.id, "purchase made");
res.status(200).send();
});
return router;
};
List of connected clients are stored inside wss object. You can receive and loop through them like this:
wss.clients.forEach((client) => {
if (client.userId === current_user_id && client.readyState === WebSocket.OPEN) {
// this is the socket of your current user
}
})
Now you need to somehow identify your client. You can do it by assigning some id to this client on connection:
wss.on('connection', async (ws, req) => {
// req.url is the url that user connected with
// use a query parameter on connection, or an authorization token by which you can identify the user
// so your connection url will look like
// http://example.com/socket?token=your_token
ws.userId = your_user_identifier
....
})
To broadcast use:
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
If your controller and socket will be in different files (and I am sure they will), you will have to export the wss object in your socket file and import it in controller.

Resources