Context: I have an angular application with a backend in nodejs. I have a feed that will update when I recieve a message from the server. When new data is inserted the server is notified, but my other component does not recieve anything. I have implemented the socket in a service that is injected into both components.
My server is build like this:
const port = 3000;
const server = require('http').Server(app);
const io = require('socket.io')(server);
io.on('connection', (socket) => {
console.log('New Connection..')
socket.on('action', (data) => {
switch(data) {
case 'new_odds':
socket.emit('refresh_odds', 'UPDATE FEED! (FROM SERVER)')
break;
case 'new_results':
break;
}
});
});
//listen on port omitted
My service in angular:
const SERVER_URL = 'http://localhost:3000';
#Injectable()
export class SocketService {
constructor() {
}
private socket;
public initSocket(): void {
this.socket = socketIo(SERVER_URL);
}
public disconnectSocket(): void {
this.socket.disconnect();
}
public send(action: Action): void {
this.socket.emit('action', action);
}
public onOddsMessage(): Observable<string> {
return new Observable<string>(observer => {
this.socket.on('refresh_odds', (data:string) => {
observer.next(data)
});
});
}
public onEvent(event: Event): Observable<any> {
return new Observable<Event>(observer => {
this.socket.on(event, () => observer.next());
});
}
}
My feed component uses the socket service to listen for emits:
constructor(private _socket : SocketService) {
}
ngOnInit() {
this.initIoConnection();
}
private initIoConnection(): void {
this._socket.initSocket();
this.ioConnection = this._socket.onOddsMessage()
.subscribe((data: string) => {
console.log('Recieved data from oddsMessage')
//this.loadBetFeed();
});
}
In a different component also using the service I'm trying to emit to the socket on the server. It does recieve the message on the server and emit a new message but my feed component does NOT pick up on this
testSocket() {
//NOTIFY SERVER THAT IT SHOULD TELL CLIENTS TO REFRESH
console.log('Test Socket Clicked')
this._socket.initSocket();
this._socket.send(Action.ODDS);
}
I don't understand what I'm doing wrong - I am using a shared service. Even if the components use different socket connections it shouldn't matter since they're listening for the same emits? I've tested in 2 browser tabs and also in incognito. Any help is appreciated!
The issue was I was emitting to the socket instead of the server which means only the current connection could see it.
New server:
io.on('connect', (socket) => {
console.log('Connected client on port %s.', port);
socket.on('action', (data) => {
io.emit('refresh_odds', 'UPDATE FEED! (FROM SERVER)') <-- changed socket to io
});
}
Related
I have a file called socket_io.js where I created a single instance of a socket io client in my react app as shown below:
socket_io.js
import EndPoints from './http/endpoints';
import io from "socket.io-client";
const socketUrl = EndPoints.SOCKET_BASE;
let socketOptions = { transports: ["websocket"] }
let socket;
if (!socket) {
socket = io(socketUrl, socketOptions);
socket.on('connect', () => {
console.log(`Connected to Server`);
})
socket.on('disconnect', () => {
console.log(`Disconnected from Server`);
})
}
export default socket;
Then I imported the above singleton in many react components as shown below.
MessagePage.js
import socket from '../socket_io.js';
let messageHandler=(data)=>{
}
useEffect(()=>{
socket.on('message',messageHandler); //This event no longer fires When the singleton socket io instance is reconnected
return ()=>{
socket.off('message');
}
},[]);
which works well but the issue I'm facing now is that when the singleton instance reconnects, the components referencing it are no longer receiving events from their respective handlers.
Possible causes of reconnection are when I manually restart the server
How can this be resolved?
I just solved this after working on it for a project of my own. My method involves two parts: creating the socket in a useEffect hook and then managing it using useRef for reconnection situations.
In Summary:
I think there are two issues. One is that the socket is being initialized as a singleton and not using a hook/context. I've read other reports of strangeness in this case, so I suggest switching to using context and creating your socket in a hook. Secondly, we have to manually store reconnection logic (although by generating the socket properly, it seems as though the actual event listeners are kept through reconnect).
export const SocketContext = createContext();
export const SocketContextProvider = ({ children }) => {
const [socket, setSocket] = useState();
const reconnectEmits = useRef([]);
// Here's your basic socket init.
useEffect(()=>{
const newSocket = io(url);
setSocket(newSocket);
return () => {
newSocket.close();
}
}, []);
// Code used to rejoin rooms, etc., on reconnect.
newSocket.io.on('reconnect', e => {
console.log("it took " + e + " tries to reconnect.");
for (let action of reconnectEmits.current) {
newSocket.emit(action.event, action.data);
}
})
// Here I also define a setListener and removeListener function, which determine which listeners a socket listens to. I don't have the code in front of me now, but it's pretty simple:
const addListener = (event, function) => {
// I use socket.off(event) right here to make sure I only have one listener per event, but you may not want this. If you don't use it you will need to make sure you use hooks to remove the event listeners that your components add to your socket when they are removed from the DOM.
socket.on(event, function);
}
// I implement an emit function here that's a wrapper, but I'm not sure if it's necessary. You could just expose the socket itself in the context. I just choose not to.
return (
<SocketContext.Provider value={{ emit, setListener, removeListener, addReconnectEmit, removeReconnectEmit }}>
{children}
</SocketContext.Provider>
)
}
And then in my components, in addition to having the emits to join rooms or conduct actions, I also provide the add and remove ReconnectEmit functions:
const addReconnectEmit = (event, data) => {
reconnectEmits.current = ([...reconnectEmits.current, { event, data }]);
console.log(reconnectEmits.current);
}
const removeReconnectEmit = (event, data) => {
console.log('removing reconnect event');
reconnectEmits.current = reconnectEmits.current.filter(e =>
{ return e.event !== event && e.data !== data }
);
console.log(reconnectEmits.current);
};
With these, I can set it so that, after a reconnect, my socket knows to reconnect to a certain room, etc. See here:
const Chatroom = ({ convoId }) => {
console.log("RENDERED: Chatroom");
const { emit, addReconnectEmit, removeReconnectEmit } = useContext(SocketContext);
useEffect(() => {
emit('joinConvo', convoId);
console.log("Emitting joinConvo message.");
addReconnectEmit('joinConvo', convoId);
return () => {
emit('leaveConvo', convoId);
removeReconnectEmit('leaveConvo', convoId);
}
}, [convoId, emit, addReconnectEmit, removeReconnectEmit]);
return (
<div id="chatroom">
<ChatroomOutput />
<ChatroomStatus />
<ChatroomControls convoId={convoId} />
</div>
);
}
I hope that helps! Between useEffect and manual reconnection logic, I just fixed similar issues to the ones you were having, where I was losing data on reconnection.
Saw you just answered yourself but my approach might still be valuable for others or if you continue to build a socket-client.
You need to abstract the listening components away from the socket object. The socket object upon onMessage needs to retrieve the subscribers and publish the new message to them. You can of course add filtering based on id, type or other properties. Also each component can drop its subscription when un-mounting or based on another need.
In order to show case I used timers but would be easily converted to messages.
socket_io.js
let socket;
const subscribers = []
if (!socket) {
// socket initial connect
socket = true
setInterval(() => {
console.log('interval runs', { socket })
if (socket) {
subscribers.forEach((sub) => {
sub.onMessage()
})
}
}, 1000)
setTimeout(() => {
// socket disconnects
socket = false
setTimeout(() => {
// socket reconnects
socket = true
}, 4000)
}, 4000)
}
export default subscribers;
MessagePage.js
import React, { useEffect, useState } from 'react'
import subscribers from './socket_io.js'
const MessagePage = () => {
const [messageCount, setMessageCount] = useState(0)
let messageHandler = (data) => {
setMessageCount((current) => current + 1)
}
useEffect(() => {
subscribers.push({
id: '1',
onMessage: (data) => messageHandler(data)
})
return () => {
const subToRemove = subscribers.findIndex((sub) => sub.id === '1')
subscribers.splice(subToRemove, 1)
}
}, []);
return (
<div>
Messages received: {messageCount}
</div>
)
}
export default MessagePage
Hope I could help.
export default expects a Hoistable Declarative , i.e function,express
socket_oi.js
import EndPoints from './http/endpoints';
import io from "socket.io-client";
const socketUrl = EndPoints.SOCKET_BASE;
let socketOptions = { transports: ["websocket"] }
let socket;
class Socket {
constructor (){
if (!socket) {
socket = io(socketUrl, socketOptions);
socket.on('connect', () => {
console.log(`Connected to Server`);
})
socket.on('disconnect', () => {
console.log(`Disconnected from Server`);
})
}
socket = this
}
}
//Freeze the object , to avoid modification by other functions/modules
let newSocketInstance = Object.freeze(new Socket)
module.exports = newSocketInstance;
MessagePage.js
import socket from '../socket_io.js';
const MessagePage = (props){
const messageHandler=(data)=>{
}
useEffect(()=>{
socket.on('message',messageHandler); //This event no longer fires When the
singleton socket io instance is reconnected
return ()=>{
socket.off('message');
}
},[]);
}
I'm trying to make this app based on node+socket.io+Vue.js.
Referring to the back end I set the server connected to socket io and all referring to the different emitters :
Class Server
const express = require("express");
const cors = require("cors");
const corsObject = {
origin: ["http://localhost:8100"],
methods: ["GET", "POST", "DELETE", "PUT", "PATCH"],
allowedHeaders: ["Content-Type", "Authorization","token-response"],
credentials: true,
};
const { conectToMongo } = require("../database/database");
const {
socketController,
} = require("../sockets-controllers/socket-controller");
class Server {
constructor() {
this.allowedOrigins = "http://localhost:8100";
this.app = express();
this.port = process.env.PORT; arranque
this.connectingdatabase();
this.server = require("http").createServer(this.app);
this.io = require("socket.io")(this.server, {
cors: corsObject,
});
this.connectionSocketClient();
this.middlewares();
this.routes();
}
middlewares() {
this.app.use(express.json()); //lectura y parsoin del body a fomato json
this.app.use(express.static("public")); //directorio fornt
this.app.use(cors({ credentials: true, origin: "http://localhost:8100" })); //cors
this.app.get("/", (request, response, next) => {
response.status(200).json({
ok: true,
message: "request correct",
});
});
}
routes() {
this.app.use("/user", require("../routes/user-routes"));
this.app.use("/auth", require("../routes/user-login"));
// this.app.use(this.usuariosPath,require('../routes/user-routes'))
}
async connectingdatabase() {
await conectToMongo();
}
connectionSocketClient() {
this.io.on("connection",(socket)=> socketController(socket,this.io));
}
portListener() {
this.server.listen(this.port, () => {
console.log(`server running on port : ${this.port}`);
});
}
}
module.exports = Server; //Exportando la clase interface creada para su uso en los demas modulos
In order to initialize the controllers i first set a class which might be used on controllers in a easier way importing its instance
class Message {
constructor(userId, userMessage, message) {
this.userId = userId;
this.userMessage = userMessage;
this.message = message;
}
} //Class and its constructor Message
class ChatMessage {
constructor() {
this.messages = [];
this.usersOnConnection = {};
}
//constructor of this class with two components , first a array of message
//and second a object that would be stacking the connected users
get usersConnected() {
return Object.values(this.usersOnConnection);
}
//This getter would retrieve the object of users connected converting it to
//an array
addUserToChat(user) {
this.usersOnConnection[user.id] = user
}
//this method would add a user to a chat i this case adding the new consumer
//to the object of users connected usersOnConnection(HERE problem too)
}
//Class and its constructor Message
module.exports = { Message, ChatMessage };
Then the socket controller on charge of set the logic would be like this
const { Socket } = require("socket.io");
const { ChatMessage, Message } = require("../models/chat-model");//Interfaces imported
const socketController = async (socket = new Socket(), io) => {
const user = await jwtValidatorRenew(
socket.handshake.headers["token-response"]
);
if (!user || user == undefined) {
console.log("user disconnected");
return socket.disconnect();
}
//if for some reason the user requested thorugh a token i retrieve is null
//or some like , the controller ends here with a disconnection to the socket
//else
const chatMessage =await new ChatMessage(); //creating new isntance of class ChatMessage
//first i do create a new instance of the class Chatmessage in order to access from here
chatMessage.addUserToChat(user); //adding user to the chat (HERE )
//then any time the browser is recharged or user inits session , this user
//would be added, using the method addUserToChat from the class instance
io.emit("active-users", chatMessage.usersConnected); //sending users connected
//once the user is added i do proceed to emit through io , the state of all connected
//consumers aiming to the flag "active-users", accesing the getter usersConnected
//from the instance
socket.on("disconnect", () => {
console.log("Client DisconectedConected", socket.id); // desconexion
chatMessage.disconnectUserFromChat(user.id);
//diconnecting user
io.emit("active-users", chatMessage.usersConnected);
});
//On disconnection the instance of chatmessage class is called in order to disconnect
//user according to its id. Also accession io from socket I do emit for all consumers
//a new state under the flag "active-users", passing ass method the getter usersConnected
//from the instance chatMessage(but only receive one user)
};
module.exports = { socketController };
Then on my front after setting socket io client i set this on my Vue state manangement(vuex ) for the socket connection, being this action dispatched any time i need.
ACTIONS VUEX
conectingSocket() {
const socket= io("localhost:3006", {
extraHeaders: {
"token-response": localStorage.getItem("token"),
},
});
socket.on("connect",()=>{
console.log("socket online");
})
//flag on connection
socket.on("disconnect",()=>{
console.log("socket offline");
})
//flag on discconnection
socket.on("active-users",(usersPayload)=>{
console.log(usersPayload);
})
//Here i log the users connected updated once the back emit on this flag a action . But always
//brings me the user of the browser that I recharge not updating the other ones
},
Then any time i initialize my app this method on front is triggered in order to retrieve the users
connected, thus for that simply call the method and set it also in my mounted Vue life cycle:
export default {
name: "AllUsersComponent",
components: { IonCard, IonContent, IonItem, IonInput, IonButton, IonLabel },
// components: { IonLabel, IonInput, IonItem },
data() {
return {
allUsersFinal: [],
message: "",
state: false,
socket: io(process.env.VUE_APP_BACK_URL),
//client socket and its connection
};
},
methods: {
...mapActions(["getAllUsers", "validateToken", "conectingSocket"]),
socketConection() {
this.$store.dispatch("conectingSocket");
},
...some methods
},
},
computed: {
...mapGetters(["getterGetAllUsers"]),
...some computed methods
},
mounted() {
this.socketConection()
},
created() {
...some methods
},
};
</script>
But keeps showing me only the user of the browser I do recharge. For this purpose i initialize on the app two users from different browsers(Firefox and Chrome)
Any help on this would be amazing!!!!!
I have front-end in ReactJS where I am integrating my backend in NodeJS through websocket(also using sharedb).
https://github.com/share/sharedb
My setup for this is below:
import sharedb from 'sharedb/lib/client';
// Using a Reconnecting Web Socket https://github.com/share/sharedb/pull/138/files
import { default as WebSocket } from 'reconnecting-websocket';
export default class ShareService {
constructor(location) {
this.socket = new WebSocket(location);
// Open WebSocket connection to ShareDB server
const connection = new sharedb.Connection(this.socket);
this.connection = connection;
this.location = location;
}
close() {
this.connection.close();
}
reconnect() {
this.socket = new WebSocket(this.location);
this.connection.bindToSocket(this.socket);
}
doc(collection, id) {
const doc = this.connection.get(collection, id);
return doc;
}
sendCommand(data) {
this.socket.send(JSON.stringify({
'a': 'command',
'deviceType': 'web',
...data
}));
}
}
And when I need to call from React I do:
this.shareService.sendCommand({
command: 'I_AM_COMMAND',
payload: {
activeAgendaId: 123,
}
});
I want to know if I can also make normal API calls to my Node from React even after WebSocket connection is there?
I have a socket.io server where I have defined a simple class named SocketObs to wrap a socket with an Observable.
The code is the following
class SocketObs {
constructor(private socket: any) {}
onEvent(event): Observable<any> {
return new Observable<any>((observer: Observer<any>) => {
this.socket.on(event, data => observer.next(data));
});
}
}
The server itself is implemented by the class MyServer, whose code is this
export class MyServer {
public static readonly PORT = 8081;
private app: express.Application;
private server: Server;
private io: socketIo.Server;
private port: string | number;
private observables = new Array<any>();
constructor() {
this.app = express();
this.port = process.env.PORT || MobileObjectServer.PORT;
this.server = createServer(this.app);
this.io = socketIo(this.server);
this.listen();
for(let i = 0; i < 20; i++) {
this.observables.push(from([1, 2, 3]))
}
}
private listen() {
this.server.listen(this.port, () => {
console.log('Running server STRESS on port %s', this.port);
});
this.io.on('connect', socket => {
console.log('Connected client on port %s.', this.port);
const socketObs = new SocketObs(socket);
socketObs.onEvent('monitor')
.subscribe(
() => this.monitoSubscribeToObservables(socketObs)
)
});
}
private monitoSubscribeToObservables(socket: SocketObs) {
this.observables.forEach(obs => {
obs
.pipe(takeUntil(socket.onEvent('disconnect')))
.subscribe(null, null, () => console.log('observable completed'));
})
}
public getApp(): express.Application {
return this.app;
}
}
The logic is very simple
when the server is created it fills an Array with 20 Observables
it sets to listening mode and waits until a 'monitor' message arrives
when the 'monitor' message arrives, it subscribes to the Observables
created at start time
the Observable subscriptions are completed when the socket is
disconnected (yes, I know that this is not really the case, but I
need to have the takeUntil operator for my case to happen)
If I run the server in node, I get the logic executed but then I get the following warning
(node:76408) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 disconnect listeners added. Use emitter.setMaxListeners() to increase limit
If I do the exact same thing but without using the , i.e. with this implementation
private listen() {
this.server.listen(this.port, () => {
console.log('Running server STRESS on port %s', this.port);
});
this.io.on('connect', socket => {
console.log('Connected client on port %s.', this.port);
socket.on('monitor', () => {
this.monitoSubscribeToObservables(socket);
});
});
}
private monitoSubscribeToObservables(socket: socketIo.Socket) {
const monitorDisconnected = new Subject<any>();
this.observables.forEach(obs => {
obs
.pipe(takeUntil(monitorDisconnected))
.subscribe(null, null, () => console.log('observable completed'));
})
socket.on('disconnect', () => {
console.log('Monitor disconnected');
monitorDisconnected.next();
});
}
the warning is not raised any more.
I am struggling to understand the reason of this different behavior.
I am working on a project that will use React as my client and Nodejs as my server. My design is that the Nodejs server will listen to some external data streams, process the data, save the data in MongoDB and then emit some events to React. The server-side code is like
const EventEmitter = require('events');
const WebSocket = require('ws');
const myEmitter = new EventEmitter();
const ws = new WebSocket('wss://someurl');
ws.on('message', (data) => {
........
/*
preprocess and do the mongodb stuff
*/
myEmitter.emit('someevent', data)});
});
My question is, how can I listen for such an event in my React client? If I stick with this approach, do I need to pass in myEmitter to my React components?
I am new to React so please let me know if there is any better way to solve the problem.
do I need to pass in myEmitter to my React components?
no... your client side and serverside code should be separate. You can use a client-side SocketIO app like socket.io.
if you're going to be listening for a bunch of different events in different components, consider using an enhancer style wrapper
function withSocket (event?, onEvent?) { // note: this is TS
return (Component) => {
class WithSocketEvent extends Component {
constructor (props) {
super(props)
this.socket = io.connect(SOCKET_ENDPOINT)
}
componentDidMount () {
if (event && onEvent) {
this.socket.on(event, onEvent)
}
}
componentWillUnmount () {
this.socket && this.socket.close()
}
render () {
return (
<Component
{ ...this.props }
socket={ this.socket }
/>
)
}
}
return WithSocketEvent
}
}
// usage
class HasSocketEvent extends Component {
componentDidMount () {
// handle the event in the component
this.props.socket.on("someEvent", this.onSocketEvent)
}
onSocketEvent = (event) => {
}
render () {
}
}
// handle the event outside the component
export default withSocket("someEvent", function () {
// so something
})(HasSocketEvent)
// or
export default withSocket()(HasSocketEvent)