Handle websocket send during handshake without message loss - javascript

I'd like to achieve the following behavior. Suppose there is a button on the client side which triggers a function that sends a message via a websocket. The first time this function is called, a WebSocket instance is created and then used in the future calls.
Creating a WebSocket instance is non-blocking. The constructor immediately returns a WebSocket object, starting the handshake in the background. A successful connection triggers the onopen callback.
A problem shows up when the second call comes in and the websocket is still performing the handshake (e.g. user makes double click on the button). All the messages need to be queued and sent when the websocket completes the handshake.
The following piece of code uses jQuery and a custom event to gather all messages received during the handshake.
var ws = null;
function sendMessage(message) {
if (!ws) {
ws = new WebSocket("ws://example.com");
ws.onopen = function() {
$(document).trigger('handshakeComplete');
}
}
if (ws.readyState == WebSocket.CONNECTING) { // *
$(document).bind('handshakeComplete', message, function(event) {
ws.send(event.data);
});
} else if (ws.readyState == WebSocket.OPEN) {
// use the persistent connection
ws.send(message);
}
}
It is possible the condition at the starred line to be evaluated as true and in that moment the websocket passes in the OPEN state, the onopen callback is executed and only after that the current message is added in the queue waiting for the handshakeComplete event. This would result in the loss of messages.
I would like to avoid this and I would appreciate any ideas, comments or suggestions.

I think you're worried that handshakecomplete could fire between the test (== CONNECTING) and when the bind() happens, and the message for the handshakecomplete would never be sent.
In theory if all that existed was JS, this would be wrong, since the event wouldn't fire until the current code finished. However, in reality the event may be generated on another thread in the background, and merely be run in the foreground after we reach a stable state.
The answer is to not embed the message in the handshake event handler. I suggest adding the message to a queue/array, and in the onopen handler process the array. The onopen event can fire (get queued) while we're executing, but it won't run until we hit a stable state (where all the messages are in the queue/array).

Related

How to avoid race condition between `addEventListener` and `window.open`

As part of the OAuth flow for my site, I'm using window.open to allow the user to sign in to the 3rd party provider, and postMessage to send the auth token back to my webpage. The basic flow looks something like this, though I've removed the extra code related to timeouts, cleanups, error handling, and so on:
const getToken = () => new Promise((resolve, reject) => {
const childWindow = window.open(buildUrl({
url: "https://third-party-provider.com/oauth/authorize",
query: {
redirect_uri: "my-site.com/oauth-callback"
}
})
window.addEventListener('message', event => {
if(
event.origin === window.location.origin &&
event.source === childWindow
) {
if(event.data.error) reject(event.data.error)
else resolve(event.data)
}
})
// Plus some extra code to remove the event listener and close
// the child window.
}
The basic idea is that, when the user clicks authorize, they are redircted to my-site.com/oauth-callback. That page completes the OAuth flow on the server side, and then loads a page containing a small amount of javascript which simply calls window.opener.postMessage(...)
Now, the crux of my question is that there's actually a race condition here, at least in theory. The issue is that the childWindow could hypothetically call postMessage before addEventListener is called. Conversely, if I call addEventListener first, it could receive messages before childWindow is set, which I think will throw and exception? The key issue seems to be that window.open isn't asynchronous, so I can't atomically spawn a window and set up a message handler.
Currently, my solution is to set up the event handler first, and assume that messages don't come in while that function is being executed. Is that a reasonable assumption? If not, is there a better way to ensure this flow is correct?
There shouldn't be a race condition here, because the window.open and window.addEventListener occur together in the same tick, and Javascript is both single-threaded and non-reentrant.
If the child window does manage to posts a message before the call to window.open completes, the message will simply go into the event queue for the current Javascript context. Your code is still running, so the addEventListener call happens next. Finally, at some future point that we don't see here, your code returns control to the browser, which ends the current tick.
Once the current tick is over, the browser can check out the event queue and dispatch the next piece of work (presumably the message). Your subscriber is already in place, so everything is fine!

Firebase synchronisation of locally-modified data: handling errors & global status

I have two related questions regarding the Firebase web platform's
synchronisation of locally-modified data to the server:
Every client sharing a Firebase database maintains its own internal version of any active data.
When data is updated or saved, it is written to this local version of the database.
The Firebase client then synchronizes that data with the Firebase servers and with other clients on a 'best-effort' basis.
1. Handling sync errors
The data-modification methods
(set(),
remove(), etc)
can take an onComplete callback parameter:
A callback function that will be called when synchronization to the Firebase servers
has completed. The callback will be passed an Error object on failure; else null.
var onComplete = function(error) {
if (error) {
console.log('Synchronization failed');
} else {
console.log('Synchronization succeeded');
}
};
fredRef.remove(onComplete);
In the example above, what kind of errors should the fredRef.remove() callback expect to receive?
Temporary errors?
Client is offline (network connection lost) ?
Firebase server is temporarily overloaded or down for maintenance, but will be available again soon?
Permanent errors?
Permission denied (due to security rules) ?
Database location does not exist?
Is there a way to distinguish between temporary and permanent errors?
How should we handle / recover from these errors?
For temporary errors, do we need to call fredRef.remove() again after a short period of time, to retry the operation?
2. Global sync status
I realise that each call to set() and remove() will receive an individual sync success/failure
result in the onComplete callback.  But I'm looking for a way to determine the
global sync status of the whole Firebase client.
I'd like to use a beforeunload event listener
to warn the user when they attempt to leave the page before all modified data has been synced to the server,
and I'm looking for some function like firebase.isAllModifiedDataSynced().  Something like this:
window.addEventListener('beforeunload', function (event) {
if (!firebase.isAllModifiedDataSynced()) {
event.returnValue = 'Some changes have not yet been saved. If you ' +
'leave this page, your changes will be lost.';
}
});
Here's an example of the same functionality in Google Drive:
I'm aware of the special /.info/connected location:
it is useful for a client to know when it is online or offline.
Firebase clients provide a special location at /.info/connected which is updated every time the client's connection state changes.
Here is an example:
var connectedRef = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com/.info/connected");
connectedRef.on("value", function(snap) {
if (snap.val() === true) {
alert("connected");
} else {
alert("not connected");
}
});
The special /.info/connected location can be connected to a beforeunload event listener like this:
var connectedRef = new Firebase('https://myapp.firebaseio.com/.info/connected');
var isConnected = true;
connectedRef.on('value', function (snap) {
isConnected = snap.val();
});
window.addEventListener('beforeunload', function (event) {
if (!isConnected) {
event.returnValue = 'Some changes have not yet been saved. If you ' +
'leave this page, your changes will be lost.';
}
});
My question is:
If isConnected is true, does this also mean that all modified data has been synced to the server?
i.e. Does "connected" also mean "synced"?
If not, how can the app determine the global sync status of the whole Firebase client?
Is there a special /.info/synchronized location?
Does the app need to manually keep track of the sync success/failure result of every onComplete callback?
In the example above, what kind of errors should the fredRef.remove() callback expect to receive?
Client is offline (network connection lost) ?
No, this will not cause an error to be passed to the completion listener. It will simply cause the completion listener to not be called (yet).
Firebase server is temporarily overloaded or down for maintenance, but will be available again soon?
No. This is essentially the same as being without a network connection.
Permission denied (due to security rules) ?
Yes, this is will indeed cause an error to be passed to the completion handler.
Database location does not exist?
No, this will not cause an error to be caused to the completion listener.
If isConnected is true, does this also mean that all modified data has been synced to the server? i.e. Does "connected" also mean "synced"?
No it does not. .info/connected will fire with true when a connection is made to the database.
If not, how can the app determine the global sync status of the whole Firebase client?
There is currently no way to determine whether your local data is up to date with the server.
Is there a special /.info/synchronized location?
No, such a location doesn't exist.
Does the app need to manually keep track of the sync success/failure result of every onComplete callback?
That depends on the use-case. But if you want to simply know when all your writes are executed, push a dummy value and wait for that to complete. Since Firebase executes the writes in order, you can be certain at that stage that you've gotten the other events.

Read from socket until receiving message, removing event listener, then returning synchronously

I am having a bit of trouble with creating one function. I have two sockets. I want to write to one, which should trigger the other to begin output. I want to wait until receiving a specific reserved token to return, synchronously, and unbind the event listener.
execute: function (command) {
check(command, String);
// wait for the "okay" message on the messages socket
this._messages.on('data', Meteor.bindEnvironment(function (data) {
// if nothing comes back in 10 seconds, time out.
Meteor.setTimeout(function () {
var parse = data.toString('ascii').split(':');
if (parse[0] === 'End') {
// Receiving 'End' means the command was successful unbind this event handler and return
return 0;
}
}, 10000);
return 1;
}));
// send the command to the command socket
this._commands.write(`XQ#${command}\r`);
}
The problem I am having is forcing it to block until it receives this data, then unbinding the callback when the appropriate data is received or time out occurs.
I might be doing this wrong. It does kind of seem like unregistering a callback from... within the callback is basically like lifting a chair that you're already sitting on... but what is the appropriate way to handle this use case?

resending peerconnection offer

I'm currently trying to rebroadcast my local stream to all my peer connections. options I tried:
1) Loop trough all my peer connection and recreate them with the new local stream. Problem that I encounter here is the fact that createOffer is asynchronous.
2) create 1 sdp and send it to all peers. Problem: no video
Would anyone have a way to resend an offer to a list of peers?
Each PC needs to recreate an offer (as bwrent said).
as you obviously are using a p2p multiparty (multiple peer connections) you might want to pass on the peerID to the createOffer success callback every time, then you don't have to worry about it being asynchronous. You need to make the full handshake (offer, answer, candidate) peerID dependent.
(Simplified) Example from our SDK
Skyway.prototype._doCall = function (targetMid) {
var pc = this._peerConnections[targetMid]; // this is thread / asynchronous safe
pc.createOffer(
function (offer) {
self._setLocalAndSendMessage(targetMid, offer); // pass the targetID down the callback chain
},
function (error) {this._onOfferOrAnswerError(targetMid, error);},
constraints
);
};
Skyway.prototype._setLocalAndSendMessage = function (targetMid, sessionDescription) {
var pc = this._peerConnections[targetMid]; // this is thread / asynchronous safe
pc.setLocalDescription(
sessionDescription,
self._sendMessage({ target: targetMid, ... }), // success callback
function () {} // error callback
);
};
If you mean async in a way that when a callback fires it has the wrong variable of who to send it to as the loop has ended and the variable contains the last 'person'? You could scope it to solve the asynchronous problem:
For(var i=0;i<peerConnections.length;i++){
(function(id){
//inside here you have the right id. Even if the loop ended and the i variable has changed to something else, the I'd variable still is the same.
})(i);
}
This is a bit like Alex' answer, as his anwer also describes an example of scoping the variable inside the function executing the .createOffer
Another way to handle this correctly is to use renegotiation. Whenever you change a stream, the on onnegotiation event handler is automatically fired. Inside this function you create a new offer and send that to the other person. As you mentioned you have multiple peer connect ions listening to the stream, you need to know whom to send the sdp to. If you would add the persons id to the rtc object, you can then get it back inside the onnegotioation event by calling this.id.

Javascript websockets closing immediately after opening

connection = new WebSocket("ws://localhost:1050/join?username=test")
connection.onopen = function(){
alert('Connection open!');
}
connection.onmessage = function(e){
var server_message = e.data;
alert(server_message);
}
connection.onclose = function() {
alert("websocket closing")
}
The connection to the server is established and an alert is displayed for Connection open! However immediately afterwards the connection closes. The server does not call close and there seem to be no other errors in the console. This is happening in both chrome and firefox.
I looked at a bunch of different similar examples on the web but to no avail.
to Keep Websocket Opened prevent handler from returning by return false; in connection.onmessage
like this :
connection.onmessage = function(e){
var server_message = e.data;
alert(server_message);
return false;
}
I believe I've stumbled across the solution that OP found but failed miserably to explain. I don't have enough reputation to comment, otherwise I'd be responding to all of the confused comments begging for clarification on OP's response.
The short version is that I think OP was referring to his server-side connection handler when he said "All I had to do was block the handler from returning before the websocket connection closes".
It turns out my server was closing the webSocket automatically because I didn't understand how a certain webSocket function worked. Specifically, I was using a Python server script with asyncio/websockets and the following code:
async def receiveCommandsLoop(player):
while True:
msg = await player.websocket.recv()
print(command)
async def handleClient(websocket, path):
username = await websocket.recv()
player = players[username]
...
#Start task to listen for commands from player
asyncio.get_event_loop().create_task(receiveCommandsLoop(player))
start_server = websockets.serve(handleClient, '', 8765)
The idea was that websockets.serve would use handleClient to begin the connection and do some setup, then create a new task with receiveCommandsLoop that would take over the job of communication.
But it turns out: when you call websockets.serve, Python expects that when your handler (in this case, handleClient) returns, you must be done with the socket, and it closes it automatically.
Thus, by the time receiveCommandsLoop was run, handleClient had returned, and the webSocket had been automatically closed.
I was able to fix this by simply modifying my handleClient function to directly run the loop originally contained in receiveCommandsLoop. Hope this helps someone out there.
This also could be the case when you're trying to send binary data over a websocket connection, but some side (client or server) is trying to interpret it as a text - many libraries and frameworks do it unless you explicitly specify you do want binary data.
It could also be a login problem. The websocket will automatically close the website required authentication but no authentication information was provided.
Piecing together hints from this post and others, I found a solution that works when using the python websocket server example found everywhere that includes something like:
async def handler(websocket, path):
data = await websocket.recv()
reply = f"Data recieved as: {data}!"
await websocket.send(reply)
To those of us new to websocket, I think the assumption is that the handler function will be called each time the client sends a message, which turns out not to be the case. As others mention, the connection closes as soon as the handler function returns once. The solution I found is to change it to:
async def handler(websocket, path):
async for data in websocket:
reply = f"Data recieved as: {data}!"
print(data)
await websocket.send(reply)
My client-side javascript code is equivalent to the OP's and I didn't have to change anything for this to work.
Unfortunately I can't explain why async for data in websocket: makes it actually wait forever and spontaneously run the inner code block each time a message is received, but it does for me and I get all the expected log messages both on the python server side and the client javascript console.
If anyone more knowledgeable on this topic can comment on whether this is a good-for-general-use solution or if there's a gotcha to look out for here, it would be much appreciated.
Fixed it!
All I had to do was block the handler from returning before the websocket connection closes

Categories

Resources