1
Fork 0

Simplify the initial connection protocol.

The ServerConnection.connect method is no longer async,
we rely on the onconnected callback only.  The onconnected
callback is now only called after the initial handshake
completes.  There is a new onerror callback.
This commit is contained in:
Juliusz Chroboczek 2024-06-10 19:41:23 +02:00
parent 58934a1a46
commit 7151fad149
3 changed files with 205 additions and 172 deletions

View File

@ -21,12 +21,12 @@ async function start(url) {
// connect to the server // connect to the server
if(token) { if(token) {
await serverConnect(status, token); serverConnect(status, token);
} else if(status.authPortal) { } else if(status.authPortal) {
window.location.href = groupStatus.authPortal window.location.href = groupStatus.authPortal
return; return;
} else { } else {
await serverConnect(status, null); serverConnect(status, null);
} }
} }
@ -46,7 +46,7 @@ function displayStatus(status) {
* @parm {Object} status * @parm {Object} status
* @parm {string} token * @parm {string} token
*/ */
async function serverConnect(status, token) { function serverConnect(status, token) {
// create the connection to the server // create the connection to the server
let conn = new ServerConnection(); let conn = new ServerConnection();
conn.onconnected = async function() { conn.onconnected = async function() {
@ -66,7 +66,7 @@ async function serverConnect(status, token) {
conn.onjoined = onJoined; conn.onjoined = onJoined;
// connect and wait for the onconnected callback // connect and wait for the onconnected callback
await conn.connect(status.endpoint); conn.connect(status.endpoint);
} }
/** /**

View File

@ -420,6 +420,10 @@ function gotClose(code, reason) {
if(code != 1000) { if(code != 1000) {
console.warn('Socket close', code, reason); console.warn('Socket close', code, reason);
} }
let form = document.getElementById('userform');
if(!(form instanceof HTMLFormElement))
throw new Error('Bad type for userform');
form.active = true;
} }
/** /**
@ -433,7 +437,7 @@ function gotDownStream(c) {
}; };
c.onerror = function(e) { c.onerror = function(e) {
console.error(e); console.error(e);
displayError(e); displayError(e.toString());
}; };
c.ondowntrack = function(track, transceiver, stream) { c.ondowntrack = function(track, transceiver, stream) {
setMedia(c); setMedia(c);
@ -3903,12 +3907,12 @@ function displayMessage(message) {
return displayError(message, "info"); return displayError(message, "info");
} }
let connecting = false;
document.getElementById('userform').onsubmit = async function(e) { document.getElementById('userform').onsubmit = async function(e) {
e.preventDefault(); e.preventDefault();
if(connecting)
return; let form = this;
if(!(form instanceof HTMLFormElement))
throw new Error('Bad type for userform');
setVisibility('passwordform', true); setVisibility('passwordform', true);
@ -3921,12 +3925,8 @@ document.getElementById('userform').onsubmit = async function(e) {
getInputElement('presentoff').checked = true; getInputElement('presentoff').checked = true;
// Connect to the server, gotConnected will join. // Connect to the server, gotConnected will join.
connecting = true; form.active = false;
try { serverConnect();
await serverConnect();
} finally {
connecting = false;
}
}; };
document.getElementById('disconnectbutton').onclick = function(e) { document.getElementById('disconnectbutton').onclick = function(e) {
@ -3997,6 +3997,10 @@ async function serverConnect() {
serverConnection.close(); serverConnection.close();
serverConnection = new ServerConnection(); serverConnection = new ServerConnection();
serverConnection.onconnected = gotConnected; serverConnection.onconnected = gotConnected;
serverConnection.onerror = function(e) {
console.error(e);
displayError(e.toString());
};
serverConnection.onpeerconnection = onPeerConnection; serverConnection.onpeerconnection = onPeerConnection;
serverConnection.onclose = gotClose; serverConnection.onclose = gotClose;
serverConnection.ondownstream = gotDownStream; serverConnection.ondownstream = gotDownStream;

View File

@ -152,6 +152,13 @@ function ServerConnection() {
* @type{(this: ServerConnection) => void} * @type{(this: ServerConnection) => void}
*/ */
this.onconnected = null; this.onconnected = null;
/**
* onerror is called whenever a fatal error occurs. The stream will
* then be closed, and onclose called normally.
*
* @type{(this: ServerConnection, error: unknown) => void}
*/
this.onerror = null;
/** /**
* onclose is called when the connection is closed * onclose is called when the connection is closed
* *
@ -263,6 +270,19 @@ ServerConnection.prototype.close = function() {
this.socket = null; this.socket = null;
}; };
/**
* error forcibly closes a server connection and invokes the onerror
* callback. The onclose callback will be invoked when the connection
* is effectively closed.
*
* @param {any} e
*/
ServerConnection.prototype.error = function(e) {
if(this.onerror)
this.onerror.call(this, e);
this.close();
};
/** /**
* send sends a message to the server. * send sends a message to the server.
* @param {message} m - the message to send. * @param {message} m - the message to send.
@ -279,191 +299,200 @@ ServerConnection.prototype.send = function(m) {
* connect connects to the server. * connect connects to the server.
* *
* @param {string} url - The URL to connect to. * @param {string} url - The URL to connect to.
* @returns {Promise<ServerConnection>}
* @function * @function
*/ */
ServerConnection.prototype.connect = async function(url) { ServerConnection.prototype.connect = function(url) {
let sc = this; let sc = this;
if(sc.socket) { if(sc.socket)
sc.socket.close(1000, 'Reconnecting'); throw new Error("Attempting to connect stale connection");
sc.socket = null;
}
sc.socket = new WebSocket(url); sc.socket = new WebSocket(url);
return await new Promise((resolve, reject) => { this.socket.onerror = function(e) {
this.socket.onerror = function(e) { if(sc.onerror)
reject(e); sc.onerror.call(sc, new Error('Socket error: ' + e));
}; };
this.socket.onopen = function(e) { this.socket.onopen = function(e) {
try {
sc.send({ sc.send({
type: 'handshake', type: 'handshake',
version: ['2'], version: ['2'],
id: sc.id, id: sc.id,
}); });
} catch(e) {
sc.error(e);
return;
}
};
this.socket.onclose = function(e) {
sc.permissions = [];
for(let id in sc.up) {
let c = sc.up[id];
c.close();
}
for(let id in sc.down) {
let c = sc.down[id];
c.close();
}
for(let id in sc.users) {
delete(sc.users[id]);
if(sc.onuser)
sc.onuser.call(sc, id, 'delete');
}
if(sc.group && sc.onjoined)
sc.onjoined.call(sc, 'leave', sc.group, [], {}, {}, '', '');
sc.group = null;
sc.username = null;
if(sc.onclose)
sc.onclose.call(sc, e.code, e.reason);
};
this.socket.onmessage = function(e) {
let m;
try {
m = JSON.parse(e.data);
} catch(e) {
sc.error(e);
return;
}
if(m.type !== 'handshake' && !sc.version) {
sc.error(new Error("Server didn't send handshake"));
return;
}
switch(m.type) {
case 'handshake': {
if((m.version instanceof Array) && m.version.includes('2')) {
sc.version = '2';
} else {
sc.version = null;
sc.error(new Error(`Unknown protocol version ${m.version}`));
return;
}
if(sc.onconnected) if(sc.onconnected)
sc.onconnected.call(sc); sc.onconnected.call(sc);
resolve(sc); break;
}; }
this.socket.onclose = function(e) { case 'offer':
sc.permissions = []; sc.gotOffer(m.id, m.label, m.source, m.username,
for(let id in sc.up) { m.sdp, m.replace);
let c = sc.up[id]; break;
c.close(); case 'answer':
} sc.gotAnswer(m.id, m.sdp);
for(let id in sc.down) { break;
let c = sc.down[id]; case 'renegotiate':
c.close(); sc.gotRenegotiate(m.id);
} break;
for(let id in sc.users) { case 'close':
delete(sc.users[id]); sc.gotClose(m.id);
if(sc.onuser) break;
sc.onuser.call(sc, id, 'delete'); case 'abort':
} sc.gotAbort(m.id);
if(sc.group && sc.onjoined) break;
sc.onjoined.call(sc, 'leave', sc.group, [], {}, {}, '', ''); case 'ice':
sc.group = null; sc.gotRemoteIce(m.id, m.candidate);
sc.username = null; break;
if(sc.onclose) case 'joined':
sc.onclose.call(sc, e.code, e.reason); if(m.kind === 'leave' || m.kind === 'fail') {
reject(new Error('websocket close ' + e.code + ' ' + e.reason)); for(let id in sc.users) {
}; delete(sc.users[id]);
this.socket.onmessage = function(e) { if(sc.onuser)
let m = JSON.parse(e.data); sc.onuser.call(sc, id, 'delete');
switch(m.type) {
case 'handshake': {
if((m.version instanceof Array) && m.version.includes('2')) {
sc.version = '2';
} else {
sc.version = null;
console.error(`Unknown protocol version ${m.version}`);
throw new Error(`Unknown protocol version ${m.version}`);
} }
break; sc.username = null;
} sc.permissions = [];
case 'offer': sc.rtcConfiguration = null;
sc.gotOffer(m.id, m.label, m.source, m.username, } else if(m.kind === 'join' || m.kind == 'change') {
m.sdp, m.replace); if(m.kind === 'join' && sc.group) {
break; throw new Error('Joined multiple groups');
case 'answer': } else if(m.kind === 'change' && m.group != sc.group) {
sc.gotAnswer(m.id, m.sdp); console.warn('join(change) for inconsistent group');
break; break;
case 'renegotiate':
sc.gotRenegotiate(m.id);
break;
case 'close':
sc.gotClose(m.id);
break;
case 'abort':
sc.gotAbort(m.id);
break;
case 'ice':
sc.gotRemoteIce(m.id, m.candidate);
break;
case 'joined':
if(m.kind === 'leave' || m.kind === 'fail') {
for(let id in sc.users) {
delete(sc.users[id]);
if(sc.onuser)
sc.onuser.call(sc, id, 'delete');
}
sc.username = null;
sc.permissions = [];
sc.rtcConfiguration = null;
} else if(m.kind === 'join' || m.kind == 'change') {
if(m.kind === 'join' && sc.group) {
throw new Error('Joined multiple groups');
} else if(m.kind === 'change' && m.group != sc.group) {
console.warn('join(change) for inconsistent group');
break;
}
sc.group = m.group;
sc.username = m.username;
sc.permissions = m.permissions || [];
sc.rtcConfiguration = m.rtcConfiguration || null;
} }
if(sc.onjoined) sc.group = m.group;
sc.onjoined.call(sc, m.kind, m.group, sc.username = m.username;
m.permissions || [], sc.permissions = m.permissions || [];
m.status, m.data, sc.rtcConfiguration = m.rtcConfiguration || null;
m.error || null, m.value || null); }
if(sc.onjoined)
sc.onjoined.call(sc, m.kind, m.group,
m.permissions || [],
m.status, m.data,
m.error || null, m.value || null);
break;
case 'user':
switch(m.kind) {
case 'add':
if(m.id in sc.users)
console.warn(`Duplicate user ${m.id} ${m.username}`);
sc.users[m.id] = {
username: m.username,
permissions: m.permissions || [],
data: m.data || {},
streams: {},
};
break; break;
case 'user': case 'change':
switch(m.kind) { if(!(m.id in sc.users)) {
case 'add': console.warn(`Unknown user ${m.id} ${m.username}`);
if(m.id in sc.users)
console.warn(`Duplicate user ${m.id} ${m.username}`);
sc.users[m.id] = { sc.users[m.id] = {
username: m.username, username: m.username,
permissions: m.permissions || [], permissions: m.permissions || [],
data: m.data || {}, data: m.data || {},
streams: {}, streams: {},
}; };
break; } else {
case 'change': sc.users[m.id].username = m.username;
if(!(m.id in sc.users)) { sc.users[m.id].permissions = m.permissions || [];
console.warn(`Unknown user ${m.id} ${m.username}`); sc.users[m.id].data = m.data || {};
sc.users[m.id] = {
username: m.username,
permissions: m.permissions || [],
data: m.data || {},
streams: {},
};
} else {
sc.users[m.id].username = m.username;
sc.users[m.id].permissions = m.permissions || [];
sc.users[m.id].data = m.data || {};
}
break;
case 'delete':
if(!(m.id in sc.users))
console.warn(`Unknown user ${m.id} ${m.username}`);
for(let t in sc.transferredFiles) {
let f = sc.transferredFiles[t];
if(f.userid === m.id)
f.fail('user has gone away');
}
delete(sc.users[m.id]);
break;
default:
console.warn(`Unknown user action ${m.kind}`);
return;
} }
if(sc.onuser)
sc.onuser.call(sc, m.id, m.kind);
break; break;
case 'chat': case 'delete':
case 'chathistory': if(!(m.id in sc.users))
if(sc.onchat) console.warn(`Unknown user ${m.id} ${m.username}`);
sc.onchat.call( for(let t in sc.transferredFiles) {
sc, m.source, m.dest, m.username, parseTime(m.time), let f = sc.transferredFiles[t];
m.privileged, m.type === 'chathistory', m.kind, if(f.userid === m.id)
'' + m.value, f.fail('user has gone away');
); }
break; delete(sc.users[m.id]);
case 'usermessage':
if(m.kind === 'filetransfer')
sc.fileTransfer(m.source, m.username, m.value);
else if(sc.onusermessage)
sc.onusermessage.call(
sc, m.source, m.dest, m.username, parseTime(m.time),
m.privileged, m.kind, m.error, m.value,
);
break;
case 'ping':
sc.send({
type: 'pong',
});
break;
case 'pong':
/* nothing */
break; break;
default: default:
console.warn('Unexpected server message', m.type); console.warn(`Unknown user action ${m.kind}`);
return; return;
} }
}; if(sc.onuser)
}); sc.onuser.call(sc, m.id, m.kind);
break;
case 'chat':
case 'chathistory':
if(sc.onchat)
sc.onchat.call(
sc, m.source, m.dest, m.username, parseTime(m.time),
m.privileged, m.type === 'chathistory', m.kind,
'' + m.value,
);
break;
case 'usermessage':
if(m.kind === 'filetransfer')
sc.fileTransfer(m.source, m.username, m.value);
else if(sc.onusermessage)
sc.onusermessage.call(
sc, m.source, m.dest, m.username, parseTime(m.time),
m.privileged, m.kind, m.error, m.value,
);
break;
case 'ping':
sc.send({
type: 'pong',
});
break;
case 'pong':
/* nothing */
break;
default:
console.warn('Unexpected server message', m.type);
return;
}
};
}; };
/** /**