Synchronous closing websocket in browser - javascript

I have a web app using WebSocket to connect the server and performing an action. After the action finished, the connection is going to be closed automatically.
But the user can restart the action by pressing a button, which close connection then create a new connection.
Example code when user restart the action:
if (this.connection) {
this.connection.close()
// this.connection = null
}
if (!this.connection) {
this.connection = new WebSocket(serverSocketURL)
// Other logic codes here
this.connection.onclose = () => {
this.connection = null
}
}
The problem is close() method is async so the second block code run before the connection is closed.
How to synchronous closing WebSocket connection?
Should I use setTimeout to wait a small time after call close() method?

Perhaps this will do what you want
When the connection is "re-connected" by the user, a second close listener is added to make a new connection - as this listener is added after the one that sets this.connection = null, it will be called after that is run, so there's no chance of a race condition
const makeConnection = () => {
this.connection = new WebSocket(serverSocketURL);
// Other logic codes here
this.connection.addEventListener('close', () => {
this.connection = null
});
};
if (this.connection) {
this.connection.addEventListener('close', makeConnection);
this.connection.close();
} else {
makeConnection();
}
or - using onclose instead of addEventListener('close',
const makeConnection = () => {
this.connection = new WebSocket(serverSocketURL);
// Other logic codes here
this.connection.onclose = () => {
this.connection = null
};
};
if (this.connection) {
this.connection.onclose = makeConnection;
this.connection.close();
} else {
makeConnection();
}

Related

Request video during PeerJs ongoing live connection (stream)

I am new to PeerJs and recently starting developing an app for my school during this Covid pandemic.
I have been able to deploy code to NodeJs server with express and was able to establish connection between 2 users.
But the problem arises when video is turned off from the beginning of stream for both users and a user wants to initiate a video call.
What I need is, to send some kind of notification to user 2 that user 1 is requesting for video. So that user 2 will turn on video.
My existing code is:
var url = new URL(window.location.href);
var disableStreamInBeginning = url.searchParams.get("disableStreamInBeginning"); // To disable video in the beginning
var passwordProtectedRoom = url.searchParams.get("passwordProtectedRoom");
var muteAllInBeginning = url.searchParams.get("muteAllInBeginning");
const socket = io('/')
const localVideoDiv = document.getElementById('local-video-div')
const oneOnOneSelf = document.getElementById('local-video')
const oneOnOneRemote = document.getElementById('remote-video')
if(typeof disableStreamInBeginning !== 'undefined' && disableStreamInBeginning == 'true'){
var disbaleSelfStream = true
} else {
var disbaleSelfStream = false
}
if(typeof passwordProtectedRoom !== 'undefined' && passwordProtectedRoom == 'true'){
var passwordProtected = true
} else {
var passwordProtected = false
}
if(typeof muteAllInBeginning !== 'undefined' && muteAllInBeginning == 'true'){
var muteAll = true
} else {
var muteAll = false
}
var systemStream
oneOnOneSelf.style.opacity = 0
oneOnOneRemote.style.opacity = 0
const myPeer = new Peer(undefined, {
host: '/',
port: '443',
path: '/myapp',
secure: true
})
const ownVideoView = document.createElement('video')
const peers = {}
navigator.mediaDevices.getUserMedia({
video: true,
audio: true
}).then(ownStream => {
systemStream = ownStream
addVideoStream(ownStream, oneOnOneSelf)
myPeer.on('call', call => {
call.answer(ownStream)
call.on('stream', remoteStream => {
addVideoStream(remoteStream, oneOnOneRemote)
})
})
socket.on('user-connected', userId => {
//connectToNewUser(userId, stream)
setTimeout(connectToNewUser, 1000, userId, ownStream)
})
})
socket.on('user-disconnected', userId => {
if (peers[userId]) peers[userId].close()
})
myPeer.on('open', id => {
//Android.onPeerConnected();
socket.emit('join-room', ROOM_ID, id)
})
function connectToNewUser(userId, stream) {
const call = myPeer.call(userId, stream)
call.on('stream', remoteStream => {
//console.log('Testing')
addVideoStream(remoteStream, oneOnOneRemote)
})
call.on('close', () => {
oneOnOneRemote.remove()
})
peers[userId] = call
}
function addVideoStream(stream, videoView) {
videoView.srcObject = stream
videoView.addEventListener('loadedmetadata', () => {
if(disbaleSelfStream){
audioVideo(true)
} else {
localVideoDiv.style.opacity = 0
videoView.style.opacity = 1
videoView.play()
}
})
}
function audioVideo(bool) {
if(bool == true){
localVideoDiv.style.opacity = 1
oneOnOneSelf.style.opacity = 0
systemStream.getVideoTracks()[0].enabled = false
} else {
if(disbaleSelfStream){
console.log('Waiting For Another User To Accept') // Here is need to inform user 2 to tun on video call
} else {
localVideoDiv.style.opacity = 0
oneOnOneSelf.style.opacity = 1
systemStream.getVideoTracks()[0].enabled = true
}
}
}
function muteUnmute(bool) {
if(bool == true){
systemStream.getAudioTracks()[0].enabled = true
} else {
systemStream.getAudioTracks()[0].enabled = false
}
}
function remoteVideoClick(){
alert('Hi');
}
Please help.
You can send messages back and forth directly using peer itself
const dataConnection = peer.connect(id) will connect you to the remote peer, it returns a dataConnection class instance that you can later use with the send method of that class.
Just remember that you also want to setup listener on the other side to listen for this events, like "open" to know when the data channel is open:
dataConnection.on('open', and dataConnection.on('data...
You have a bug in your code above, I know you didn't ask about it, it is hard to see and not always will manifest. The problem will occur when your originator sends a call before the destination has had time to receive the promise back with its local video/audio stream. The solution is to invert the order of the calls and to start by setting up the event handler for peer.on("call", ... rather than by starting by waiting for a promise to return when we ask for the video stream. The failure mode will depend on how long does it take for your destination client to signal it wants and call to the originator plus how long it takes for the originator to respond versus how long it takes for the stream promise to return on the destination client. You can see a complete working example, where messages are also sent back and forth here.
// Function to obtain stream and then await until after it is obtained to go into video chat call and answer code. Critical to start the event listener ahead of everything to ensure not to miss an incoming call.
peer.on("call", async (call) => {
let stream = null;
console.log('*** "call" event received, calling call.answer(strem)');
// Obtain the stream object
try {
stream = await navigator.mediaDevices.getUserMedia(
{
audio: true,
video: true,
});
// Set up event listener for a peer media call -- peer.call, returns a mediaConnection that I name call
// Answer the call by sending this clients video stream --myVideo-- to calling remote user
call.answer(stream);
// Create new DOM element to place the remote user video when it comes
const video = document.createElement('video');
// Set up event listener for a stream coming from the remote user in response to this client answering its call
call.on("stream", (userVideoStream) => {
console.log('***"stream" event received, calling addVideoStream(UserVideoStream)');
// Add remote user video stream to this client's active videos in the DOM
addVideoStream(video, userVideoStream);
});
} catch (err) {
/* handle the error */
console.log('*** ERROR returning the stream: ' + err);
};
});

Failed to execute 'send' on 'WebSocket': Still in CONNECTING state

I am new in react js development and try to integrate WebSocket un my app.
but got an error when I send messages during connection.
my code is
const url = `${wsApi}/ws/chat/${localStorage.getItem("sID")}/${id}/`;
const ws = new WebSocket(url);
ws.onopen = (e) => {
console.log("connect");
};
ws.onmessage = (e) => {
const msgRes = JSON.parse(e.data);
setTextMessage(msgRes.type);
// if (msgRes.success === true) {
// setApiMessagesResponse(msgRes);
// }
console.log(msgRes);
};
// apiMessagesList.push(apiMessagesResponse);
// console.log("message response", apiMessagesResponse);
ws.onclose = (e) => {
console.log("disconnect");
};
ws.onerror = (e) => {
console.log("error");
};
const handleSend = () => {
console.log(message);
ws.send(message);
};
and got this error
Failed to execute 'send' on 'WebSocket': Still in CONNECTING state
Sounds like you're calling ws.send before the socket has completed the connection process. You need to wait for the open event/callback, or check the readyState per docs and queue the send after the readyState changes i.e after the open callback has fired.
Not suggesting you do this, but it might help:
const handleSend = () => {
if (ws.readyState === WebSocket.OPEN) {
ws.send()
} else {
// Queue a retry
setTimeout(() => { handleSend() }, 1000)
}
};
As Logan has mentioned my first example is lazy. I just wanted to get OP unblocked and I trusted readers were intelligent enough to understand how to take it from there. So, make sure to handle the available states appropriately, e.g if readyState is WebSocket.CONNECTING then register a listener:
const handleSend = () => {
if (ws.readyState === WebSocket.OPEN) {
ws.send()
} else if (ws.readyState == WebSocket.CONNECTING) {
// Wait for the open event, maybe do something with promises
// depending on your use case. I believe in you developer!
ws.addEventListener('open', () => handleSend())
} else {
// etc.
}
};
I guess you can only send data with ws only if it's already open, and you do not check when it's open or not.
Basically you ask for an openning but you send a message before the server said it was open (it's not instant and you do not know how many time it can take ;) )
I think you should add a variable somithing like let open = false;
and rewrite the onopen
ws.onopen = (e) => {
open = true;
console.log("connect");
};
and then in your logic you can only send a message if open is equal to true
don't forget the error handling ;)

How to detect which message was sent from the Websocket server

I have a small web application listening for incoming messages from a Websocket server. I receive them like so
const webSocket = new WebSocket("wss://echo.websocket.org");
webSocket.onopen = event => webSocket.send("test");
webSocket.onmessage = event => console.log(event.data);
but the sending server is more complex. There are multiple types of messages that could come e.g. "UserConnected", "TaskDeleted", "ChannelMoved"
How to detect which type of message was sent? For now I modified the code to
const webSocket = new WebSocket("wss://echo.websocket.org");
webSocket.onopen = event => {
const objectToSend = JSON.stringify({
message: "test-message",
data: "test"
});
webSocket.send(objectToSend);
};
webSocket.onmessage = event => {
const objectToRead = JSON.parse(event.data);
if (objectToRead.message === "test-message") {
console.log(objectToRead.data);
}
};
So do I have to send an object from the server containing the "method name" / "message type" e.g. "TaskDeleted" to identify the correct method to execute at the client? That would result in a big switch case statement, no?
Are there any better ways?
You can avoid the big switch-case statement by mapping the methods directly:
// List of white-listed methods to avoid any funny business
let allowedMethods = ["test", "taskDeleted"];
function methodHandlers(){
this.test = function(data)
{
console.log('test was called', data);
}
this.taskDeleted = function(data)
{
console.log('taskDeleted was called', data);
}
}
webSocket.onmessage = event => {
const objectToRead = JSON.parse(event.data);
let methodName = objectToRead.message;
if (allowerMethods.indexOf(methodName)>=0)
{
let handler = new methodHandlers();
handler[methodName](data);
}
else
{
console.error("Method not allowed: ", methodName)
}
};
As you have requested in one of your comments to have a fluent interface for the websockets like socket.io.
You can make it fluent by using a simple PubSub (Publish Subscribe) design pattern so you can subscribe to specific message types. Node offers the EventEmitter class so you can inherit the on and emit events, however, in this example is a quick mockup using a similar API.
In a production environment I would suggest using the native EventEmitter in a node.js environment, and a browser compatible npm package in the front end.
Check the comments for a description of each piece.
The subscribers are saved in a simple object with a Set of callbacks, you can add unsubscribe if you need it.
note: if you are using node.js you can just extend EventEmitter
// This uses a similar API to node's EventEmitter, you could get it from a node or a number of browser compatible npm packages.
class EventEmitter {
// { [event: string]: Set<(data: any) => void> }
__subscribers = {}
// subscribe to specific message types
on(type, cb) {
if (!this.__subscribers[type]) {
this.__subscribers[type] = new Set
}
this.__subscribers[type].add(cb)
}
// emit a subscribed callback
emit(type, data) {
if (typeof this.__subscribers[type] !== 'undefined') {
const callbacks = [...this.__subscribers[type]]
callbacks.forEach(cb => cb(data))
}
}
}
class SocketYO extends EventEmitter {
constructor({ host }) {
super()
// initialize the socket
this.webSocket = new WebSocket(host);
this.webSocket.onopen = () => {
this.connected = true
this.emit('connect', this)
}
this.webSocket.onerror = console.error.bind(console, 'SockyError')
this.webSocket.onmessage = this.__onmessage
}
// send a json message to the socket
send(type, data) {
this.webSocket.send(JSON.stringify({
type,
data
}))
}
on(type, cb) {
// if the socket is already connected immediately call the callback
if (type === 'connect' && this.connected) {
return cb(this)
}
// proxy EventEmitters `on` method
return super.on(type, cb)
}
// catch any message from the socket and call the appropriate callback
__onmessage = e => {
const { type, data } = JSON.parse(e.data)
this.emit(type, data)
}
}
// create your SocketYO instance
const socket = new SocketYO({
host: 'wss://echo.websocket.org'
})
socket.on('connect', (socket) => {
// you can only send messages once the socket has been connected
socket.send('myEvent', {
message: 'hello'
})
})
// you can subscribe without the socket being connected
socket.on('myEvent', (data) => {
console.log('myEvent', data)
})

Node.js amqplib when to close connection

I am using amqplib to transfer messages in my node.js server. I saw an example from RabbitMQ official website:
var amqp = require('amqplib/callback_api');
amqp.connect('amqp://localhost', function(err, conn) {
conn.createChannel(function(err, ch) {
var q = 'hello';
var msg = 'Hello World!';
ch.assertQueue(q, {durable: false});
// Note: on Node 6 Buffer.from(msg) should be used
ch.sendToQueue(q, new Buffer(msg));
console.log(" [x] Sent %s", msg);
});
setTimeout(function() { conn.close(); process.exit(0) }, 500);
});
In this case, the connection is closed in an timeout function. I don't think this is a sustainable way to do it. However, ch.sendToQueue doesn't have a callback function allowing me to close connection after message is sent. What's a good point to close connection?
I'm using the promise API, but the process is the same. First you need to call channel.close() and then connection.close().
channel.sendToQueue() returns a boolean.
True when it's ready to accept more messages
False when you need to wait for the 'drain' event on channel before sending more messages.
This is my code using async/await:
async sendMsg(msg) {
const channel = await this.initChannel();
const sendResult = channel.sendToQueue(this.queue, Buffer.from(msg), {
persistent: true,
});
if (!sendResult) {
await new Promise((resolve) => channel.once('drain', () => resolve));
}
}
async close() {
if (this.channel) await this.channel.close();
await this.conn.close();
}

How can I ensure ZeroMQ doesn't block waiting for messages after sockets are closed?

This little test script shows my problem. It will send messages, close all sockets, and then just wait, never exiting. Supposedly setting ZMQ_LINGER to 0 is supposed to make it discard all queued messages immediately, so why isn't this allowing my Node.js process to exit?
const zmq = require('zmq')
const bindUrl = 'tcp://127.0.0.1:4000'
let timer
let publisher = zmq.socket('pub')
publisher.monitor(500, 0)
publisher.setsockopt(zmq.ZMQ_LINGER, 0)
publisher.bind(bindUrl)
let subscriber = zmq.socket('sub')
subscriber.monitor(500, 0)
subscriber.setsockopt(zmq.ZMQ_LINGER, 0)
subscriber.connect(bindUrl)
subscriber.on('connect_error', () => {
console.log('connect error')
})
subscriber.on('connect', () => {
subscriber.subscribe('some topic')
})
publisher.on('bind', function () {
console.log('bound')
timer = setInterval(() => publisher.send(['some topic', 'blah']), 1000)
})
publisher.on('bind_error', function () {
console.log('bind error')
})
subscriber.on('disconnect', function () {
console.log('subscriber disconnected')
subscriber.close()
})
subscriber.on('close', function () {
console.log('subscriber closed')
subscriber.removeAllListeners()
subscriber = null
})
publisher.on('unbind', function () {
console.log('publisher unbound')
publisher.close()
})
publisher.on('close', function () {
console.log('publisher closed')
publisher.removeAllListeners()
publisher = null
subscriber.disconnect(bindUrl)
})
subscriber.on('message', function (topic, message) {
console.log(topic.toString(), message.toString())
clearInterval(timer)
subscriber.unsubscribe('some topic')
publisher.unbind(bindUrl)
})
Output is the following, and the process never exits.
erin#titania:~/$ node test-disconnect.js
bound
some topic blah
publisher unbound
publisher closed
subscriber disconnected
subscriber closed
The fact that I am explicitly monitoring the sockets is what caused this behavior. I have to explicitly call socket.unmonitor when I'm ready for the process to exit.

Categories

Resources