I'm using VueJS and socket.io to generate a message to display to all of the users of my application.
My message is emitted from my action of my store.
It's working well but when I test my code, I have an error: "cannot read property $socket of undefined" (comes from my action).
My main.js:
/*Socket*/
import io from 'socket.io-client'
if(window.location.hostname === 'localhost'){
//Dev
console.log('Dev')
Vue.prototype.$socket = io(`http://localhost:3000`)
}else{
//Prod
console.log('Prod')
Vue.prototype.$socket = io.connect(window.location.hostname)
}
Vue.use(Vuelidate)
Vue.use(BootstrapVue)
Vue.use(BootstrapVueIcons)
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
My action of my store:
const addPick = function(context) {
//console.log('actions - ADD PICK');
let request_body = {
game_id : context.state.selectedGame.id,
player_id : context.state.pickedPlayer.id,
user_id : context.state.user.id
}
axios.post(`api/add_picked_player`,request_body).then(response => {
console.log(response.data);
//Building of the message to display to the other players
let object_to_emit = {};
object_to_emit = {
message : context.state.pickedPlayer.name + ' has been selected by '+context.state.user.username,
game_id : context.state.selectedGame.id
}
this._vm.$socket.emit('player_added',object_to_emit);
this.dispatch('getDraftList'); //to update the player available in the draft
this.dispatch('getUserListInGame'); //to update the user information in the game in the UserLineUp component
context.commit('UpdateUserListInGame',context.state.userListInGame);
this.dispatch('getPickedTeamByUser'); //to update the team picked of the user
}).catch(error => console.log(error));
};
My test:
import io from 'socket.io-client'
/*Mocking of Socket io*/
let mockSocket = io('http://localhost:3000');
let message = "message to send";
jest.mock("socket.io-client", () => {
const socket = {
emit: jest.fn(() => {
return new Promise((resolve) => {
resolve(true)
})
})
}
return jest.fn(() => socket);
})
describe('addPick', () => {
test('Success: should add a player and update information (player available, user in the game, team picked by users) in the store', async () => {
const context = {
state: {
selectedGame: {
id:119
},
pickedPlayer: {
id:1
},
user: {
id:1
},
userListInGame: [
{
id:1
},{
id:19
}
]
},
commit: jest.fn()
}
const response = {
data: "the draft is not finished"
};
//const dispatch = jest.fn();
axios.post.mockResolvedValue(response) //OR axios.post.mockImplementationOnce(() => Promise.resolve(response));
await actions.addPick(context)
expect(axios.post).toHaveBeenCalledWith("api/add_picked_player",{
"game_id" : context.state.selectedGame.id,
"player_id" : context.state.pickedPlayer.id,
"user_id":context.state.user.id
});
expect(axios.post).toHaveBeenCalledTimes(2)
//expect(context.commit).toHaveBeenCalledWith("UpdateUserListInGame", context.state.userListInGame)
expect(io).toHaveBeenCalledWith(`http://localhost:3000`)
expect(io).toHaveBeenCalledTimes(1)
await mockSocket.emit("player_added",message)
expect(mockSocket.emit).toHaveBeenCalledTimes(1)
expect(mockSocket.emit).toHaveBeenCalledWith("player_added",message)
//expect(context.commit).toHaveBeenCalledWith("UpdateUserListInGame", context.state.userListInGame)
});
I mocked Axios and my axios request is called successfully.
Same for my emit function of io socket.
The test goes successfully for now but:
I have the error message in my console.log of my action: "cannot read property $socket of undefined"
When I try to test expect(context.commit).toHaveBeenCalledWith("UpdateUserListInGame", context.state.userListInGame), I have an error because of my socket io. This error is the context.commit is not called. (This test is successful without all the message emitted and my socket io part)
What is wrong?
The way I parameter my socket IO in my main.js ?
The way I call my emit() function ? Do I have another possibility of using "this._vm" instead?
Related
When I'm emitting message from client side.
I'm receiving lot of same duplicated message
on server side or in my message displaying container div here is the code
Global Socket Provider Context
A simple context which provide socket to all children or components
// fileName : SocketProvider.js
import { createContext } from "react";
import { io } from "socket.io-client";
const socket = io("http://localhost:8080/", { withCredentials: true });
const SocketContext = createContext();
export const SocketProvider = ({ children }) => {
return (
<SocketContext.Provider value={{ socket }}>
{children}
</SocketContext.Provider>
);
};
export default SocketContext;
And I've wrapped SocketProvider in App.js like this
<SocketProvider>
<Main/>
</SocketProvider>
In Dashboard Page or Main Page
Where I've div for displaying messages and input message box whenever user press enter or click send button which is img I'm emitting a message here is the code
// filename : Main.jsx
// all things imported correctly
import ...
const Main = ()=> {
const { socket } = useContext(SocketContext);
const [message, setMessage] = useState("");
const [conversation, setConversation] = useState([]);
// another global context
const { currentUserId } = useContext(UserContext);
const { selectedUserId, setSelectedUserId } = useContext(SelectedUserContext);
// code for changing selectedUserId
// ...
...
.... //
// on start sending userJoined
useEffect(() => {
socket.emit("userJoined", {
senderUserId: currentUserId,
receiverUserId: selectedUserId,
});
}, [socket, currentUserId, selectedUserId]);
// useEffect for listening messages
useEffect(() => {
const onMessageReceived = (msg) => {
setConversation((conversations) => [...conversations, msg]);
};
socket.on("message", onMessageReceived);
return () => {
socket.off("message", onMessageReceived);
};
}, [socket]);
// this function will send message
const sendMessage = useCallback(() => {
if (!message) return;
socket.emit("message", {
msg: message,
sentBy: "Unknown"
});
setMessage("");
}, [socket, message]);
// on enter btn pressed
useEffect(() => {
const handleEnterKeyPressed = (e) => {
if (e.keyCode === 13 && e.shiftKey === false && e.ctrlKey === false)
sendMessage();
};
element.addEventListener("keydown", handleEnterKeyPressed);
return () => {
element.removeEventListener("keydown", handleEnterKeyPressed);
};
}, [sendMessage]);
return (
<div className="container">
<div className="msgContainer">
{conversation.map((data)=>((
<h1 className="...">{data.msg}</h1>
<p className="...">{data.sentBy}</p>
)}
</div>
<input
value={message}
onChange={(e)=>setMessage(e.target.value)}
placeholder="Enter message here"/>
<img className="..." src={sendIcon} onClick={sendMessage}>
</div>
);
}
export default Main;
Code In Server Side Where I'm Listening for message
//filename: server.js
io.on("connection", (socket) => {
socket.on("userJoined", (userJoinedData) => {
const chatRoomId = generateChatRoomId(userJoinedData);
socket.on("message", async (msgData) => {
// this console log many times or more than 11
console.log(`DEBUG: New Message Arrived :`, msgData);
// saving msg to mongo db
const savedMessage = await saveMsg(msgData);
io.to(chatRoomId).emit("message", savedMessage);
});
});
});
and even I got following warning in terminal :
(node:14040) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 message listeners added
to [Socket]. Use emitter.setMaxListeners() to increase limit
Any help will be always appreciated :)
As #NickParsons mentioned -
Is that peice of code being executed multiple times?
Whenever currentUserId or selectedUserId changes you are emiting userJoined in server code. And userJoined listener on server side get called and your message listener listen again your data so that's why you are getting duplicated data.
To fix this you have to place message listener outside of userJoined listener!
I know you are getting some info of userJoinedData so that's why you have place message listener inside userJoined.
So you have to also pass that userJoinedData while emitting message in your client side and change code in your server side like this -
//filename: server.js
io.on("connection", (socket) => {
socket.on("userJoined", (userJoinedData) => {
const chatRoomId = generateChatRoomId(userJoinedData);
// .. your other codes
});
// but place message listener outside here
socket.on("message", async (msgData, userJoinedData) => {
// now here also you can generate chatRoomId
const chatRoomId = generateChatRoomId(userJoinedData);
// saving msg to mongo db
const savedMessage = await saveMsg(msgData);
io.to(chatRoomId).emit("message", savedMessage);
});
});
Working on creating a function that receives an object. This object contains key, value pairs of usernames.
I iterate through the key value pairs and for each pair, create a Twilio Conference Room topic, I assign key+value to this topic name
Inside the for loop, I add each pair as a participant in a Twilio Conference Room, create a conference room for each and presumably redirect each pair to their respective conference rooms.
However, the intended action of redirecting each pair to their respective Twilio Conference rooms is not occurring. I instead get the errors:
Uncaught TypeError: Cannot read properties of null (reading 'connect')
I initially start off by creating a device object in a previous component:
const setupTwilio = (nickname) => {
fetch(`http://127.0.0.1:8000/voice_chat/token/${nickname}`)
.then(response => console.log('setupTwilio returns: ',response.json()))
.then(data => {
const twilioToken = JSON.parse(data).token;
const device = new Device(twilioToken);
device.updateOptions(twilioToken, {
codecPreferences: ['opus', 'pcmu'],
fakeLocalDTMF: true,
maxAverageBitrate: 16000,
maxCallSignalingTimeoutMs: 30000
});
device.on('error', (device) => {
console.log("error: ", device)
});
setState({... state, device, twilioToken, nickname})
})
.catch((error) => {
console.log(error)
})
};
const handleSubmit = e => {
e.preventDefault();
const nickname = user.username;
setupTwilio(nickname);
const rooms = state.rooms;
updateWaitingRoomStatus()
setState((state) => {
return {...state, rooms }
});
sendWaitingRoomUsersToRedisCache()
history.push('/rooms');
}
From here the user then navigates to the next component/page. This is the waiting room. In this component, a useEffect creates said room and redirects each pair:
useEffect((() => {
handleRoomCreate()
}), [availableOnlineUsers])
const createRoomHandler = (curr,matched) => {
const userData = {'roomName': curr+matched, 'participantLabel': [curr, matched] }
axios.post('http://127.0.0.1:8000/voice_chat/rooms', userData )
.then(res => {
console.log('axios call has been hit', res.data)
})
}
const handleRoomCreate = () => {
for ( let [k, v] of new Map( Object.entries(randomizeAndMatch(availableOnlineUsers.current)))){
const createdRoomTopic = k+v
setState({ ...state, createdRoomTopic})
const selectedRoom = {
room_name: state.createdRoomTopic, participants: [k,v]
};
setState({ ...state, selectedRoom})
const rooms = state.rooms;
const roomId = rooms.push(selectedRoom)
createRoomHandler(k,v);
history.push(`/rooms/${roomId}`)
}
}
Here is a sample of the data I get from randomizeAndMatch(availableOnlineUsers.current), the function that returns an object with pairs of users:
{testingUser: 'emmTest', testing22: 'AnotherUser'}
This is the voice_chat/rooms API endpoint:
...
def post(self, request, *args, **kwargs):
room_name = request.POST.get("roomName", "default")
participant_label = request.POST.get("participantLabel", "default")
response = VoiceResponse()
dial = Dial()
dial.conference(
name=room_name,
participant_label=participant_label,
start_conference_on_enter=True,
)
print(dial)
response.append(dial)
return HttpResponse(response.to_xml(), content_type="text/xml")
I know the above view works when it comes to creating a conference call and presume I wouldn't have to make any changes to this view to create multiple conference calls since I would be iteratively posting to this endpoint
The error appears to point to this Rooms, specifically that there is no room_name in state? In Rooms I grab the selectedRoom room_name from state
const roomName = state.selectedRoom.room_name;
useEffect(() => {
const params = {
roomName: roomName, participantLabel: nickname
};
if (!call) {
const callPromise = device.connect({ params });
callPromise.then((call) => {
setCall(call);
});
}
I do this because I need params to actually start the conference call. Any ideas what I am doing wrong?
UPDATE
Realized I wasn't resolving the promise and creating a device object
Removing the console log I had in my setupTwilio function resolved this:
const setupTwilio = (nickname) => {
fetch(`http://127.0.0.1:8000/voice_chat/token/${nickname}`)
.then(response => response.json())
.then(data => {
const twilioToken = JSON.parse(data).token;
const device = new Device(twilioToken);
device.updateOptions(twilioToken, {
codecPreferences: ['opus', 'pcmu'],
fakeLocalDTMF: true,
maxAverageBitrate: 16000,
maxCallSignalingTimeoutMs: 30000
});
device.on('error', (device) => {
console.log("error: ", device)
});
setState({... state, device, twilioToken, nickname})
})
.catch((error) => {
console.log(error)
})
};
However, I am not getting the error:
error: ConnectionError: ConnectionError (53000): Raised whenever a signaling connection error occurs that is not covered by a more specific error code.
at ConnectionError.TwilioError [as constructor] (twilioError.ts:48:1)
at new ConnectionError (generated.ts:340:1)
at PStream._handleTransportMessage (pstream.js:176:1)
at WSTransport.emit (events.js:153:1)
at WSTransport._this._onSocketMessage (wstransport.ts:453:1)
I'm using websocket in react. This is the code for the component:
import React, { useState, useEffect } from "react";
export default function Component(props) {
const [socket, setSocket] = useState();
const parseMessage = (msg) => {
if (msg[0] !== "R") sendMessage("123"); // ignore the very first message from the socket.
};
const sendMessage = (msg) => socket.send(msg); // error at this line
useEffect(() => {
const socket = new WebSocket("wss://ws.ifelse.io/");
socket.addEventListener("message", ({ data }) => {
if (socket) parseMessage(data);
});
setSocket(socket);
}, []);
const sendMsg = () => {
socket.send("test");
};
return <button onClick={() => sendMsg("clicked")}>send msg</button>;
}
I'm getting this error: TypeError: Cannot read properties of undefined (reading 'send') at the marked line. The WebSocket is just an echo server, it sends back the same thing you send it.
If I wrap the socket.send in a try-catch block, I can still send and receive messages from the WebSocket using the button, but the error still occurs at that line in sendMessage.
It's clear that the socket variable is not undefined as I'm able to send and receive messages before and after the error occurs.
So my question is why is the socket variable undefined only for the brief period after receiving a message, and what is the fix for this issue.
Better solution would be to initialize the socket outside
import React, { useState, useEffect } from "react";
const socket = new WebSocket("wss://ws.ifelse.io/")
export default function Component(props) {
const ws = useRef(socket)
useEffect(() => {
ws.current?.addEventListener("message", ({ data }) => {
parseMessage(data);
});
return () => {
ws.current?.removeAllListeners()
}
}, [])
const parseMessage = (msg) => {
if (msg[0] !== "R") sendMessage("123"); // ignore the very first message from the socket.
};
const sendMessage = (msg) => ws.current?.send(msg);
const sendMsg = () => {
ws.current?.send("test");
};
return <button onClick={() => sendMsg("clicked")}>send msg</button>;
}
The useEffect runs just once and in that moment the socket is still undefined. Function sendMessage references undefined socket when the effect runs. When the socket is set using setSocket, component will rerender, but the new instance of sendMessage (now referencing the existing socket) will never be used, because the effect will not run again.
It is better to use ref in this case.
export default function Component(props) {
const socket = useRef();
const sendMessage = (msg) => socket.current.send(msg);
useEffect(() => {
socket.current = new WebSocket("wss://ws.ifelse.io/");
socket.current.addEventListener("message", ({ data }) => {
parseMessage(data);
});
return () => {
... release resources here
}
}, []);
...
}
When you use useState you create a state variable with an update state function. You also provide the initial value of the state variable in useState(initialState).
In your case you are not passing anything (i.e. you are telling React it's undefined, that's why at this line:
const sendMessage = (msg) => socket.send(msg); // error at this line
your code cannot use socket.send because socket is undefined.
Your useEffect runs after the return function and then the socket instance is created as well as set to socket state variable.
You need to make sure that you are only sending message over socket after socket is created.
For more on useEffect you can read this post:
https://jayendra.xyz/2019/06/17/how-useeffect-works/
You need to wait for the socket to be established first and then set the socket using setSocket in the open event. The way you are doing it now expects the first message to be sent by the server and then you setSocket, if the server does not send a message before you click on the button, the socket instance is not set in your state.
Do this instead:
socket.addEventListener('open', function (event) {
setSocket(socket);
});
Here is the full code
import React, { useState, useEffect } from "react";
export default function Component(props) {
const [socket, setSocket] = useState();
const [disabled, setButtonDisabled] = useState(true);
const parseMessage = (msg) => {
if (msg[0] !== "R") sendMessage("123"); // ignore the very first message from the socket.
};
const sendMessage = (msg) => socket.send(msg); // error at this line
useEffect(() => {
const socket = new WebSocket("wss://ws.ifelse.io/");
socket.addEventListener('open', function (event) {
setSocket(socket);
setButtonDisabled(false);
});
socket.addEventListener("message", ({ data }) => {
if (data) parseMessage(data);
});
return ()=>{
socket.close();
};
}, []);
const sendMsg = () => {
if(socket) socket.send("test");
};
return <button disabled={disabled} onClick={() => sendMsg("clicked")}>send msg</button>;
}
I am trying to test a WebSockets server. The onMessage handler gets called for the first time only - any further messages published to the server are not logged. Why is this happening?
`
const WebSocket = require('ws');
const ws = new WebSocket(WEBSOCKETS_ENDPOINT);
describe('When a speed test is triggered', () => {
test('Then AWS should send the result to the client', done => {
ws.on('open', async () => {
ws.send(JSON.stringify({
message: 'start-speed-test-rbsid',
data: RBSID,
platform: "android"
}));
ws.on('message', data => {
data = JSON.parse(data)
// this gets called only once - any further messages published from the server are not logged
console.log(data)
});
})
})
afterAll(() => ws.close())
});
Message data should be logged on every message that was published
I have no problem using you code against echo server.
const WebSocket = require('ws');
const ws = new WebSocket('wss://echo.websocket.org');
describe('When a speed test is triggered', () => {
test('Then AWS should send the result to the client', done => {
ws.on('open', async () => {
ws.send(JSON.stringify({
message: 'start-speed-test-rbsid',
data: 'RBSID',
platform: "android"
}));
ws.on('message', data => {
// this gets called only once - any further messages published from the server are not logged
console.log(data)
});
ws.send(JSON.stringify({
message: 'start-speed-test-rbsid',
data: 'RBSID',
platform: "android"
}));
})
})
afterAll(() => ws.close());
});
Another thing is, if you are writing unit test, and the test involves network IO, it is usually suggested to mock the network component.
const WebSocket = require('ws');
jest.mock('ws', () => {
class MockedWebSocket {}
return MockedWebSocket;
});
console.log(WebSocket); // mocked
I am create mean stack application using nestjs.In nest js i am using websockets.I don't know how to test websockets in postman. Normally i have test route url in postman and get o/p Like: "http://localhost:3000/{routeUrl}" but how to give using sockets i am confused
example :
#WebSocketGateway()
export class MessagesGateway implements OnGatewayDisconnect {
constructor(#InjectModel(Message) private readonly messagesModel:
ModelType<Message>,
#InjectModel(Room) private readonly roomsModel: ModelType<Room>,
#InjectModel(User) private readonly usersModel: ModelType<User>) { // <1> }
async handleDisconnect(client: Socket) { // <2>
const user = await this.usersModel.findOne({clientId: client.id});
if (user) {
client.server.emit('users-changed', {user: user.nickname, event: 'left'});
user.clientId = null;
await this.usersModel.findByIdAndUpdate(user._id, user);
} }
#SubscribeMessage('enter-chat-room') // <3> async
enterChatRoom(client: Socket, data: { nickname: string, roomId: string
}) {
let user = await this.usersModel.findOne({nickname: data.nickname});
if (!user) {
user = await this.usersModel.create({nickname: data.nickname, clientId: client.id});
} else {
user.clientId = client.id;
user = await this.usersModel.findByIdAndUpdate(user._id, user, {new: true});
}
client.join(data.roomId).broadcast.to(data.roomId)
.emit('users-changed', {user: user.nickname, event: 'joined'}); // <3> }
#SubscribeMessage('leave-chat-room') // <4> async
leaveChatRoom(client: Socket, data: { nickname: string, roomId: string
}) {
const user = await this.usersModel.findOne({nickname: data.nickname});
client.broadcast.to(data.roomId).emit('users-changed', {user: user.nickname, event: 'left'}); // <3>
client.leave(data.roomId); }
#SubscribeMessage('add-message') // <5> async addMessage(client:
Socket, message: Message) {
message.owner = await this.usersModel.findOne({clientId: client.id});
message.created = new Date();
message = await this.messagesModel.create(message);
client.server.in(message.room as string).emit('message', message); } }
In your case, when you do not specify the path of the WS, it is listening on the same port as the server listening and path does not matter at all.
WS is a different protocol and you have to make the mental switch to this way of thinking.
You will not be able to test WS server with Postman. To do that you have to create your WS client, however, I strongly encourage you to create E2E tests with Nest to test it with the code, not manually.
Here's my working example:
import * as WebSocket from 'ws'
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [
SocketModule,
],
})
.compile()
app = moduleFixture.createNestApplication()
app.useWebSocketAdapter(new WsAdapter(app))
await app.init()
})
it('should connect successfully', (done) => {
const address = app.getHttpServer().listen().address()
const baseAddress = `http://[${address.address}]:${address.port}`
const socket = new WebSocket(baseAddress)
socket.on('open', () => {
console.log('I am connected! YEAAAP')
done()
})
socket.on('close', (code, reason) => {
done({ code, reason })
})
socket.on ('error', (error) => {
done(error)
})
})
Detail about this answer is discussed here
The second args of cb from onConnection
incomingMessage
this.server.on('connection', (socket:WebSocket, incomingMessage) => {
console.log(incomingMessage)
console.log('connection')
})
incomingMessage.url
'/v2/ta-websocket/22950b69-7928-43b9-8c38-efc5c126208e'
screen shot here