muweb-socket
Version:
WebSocket communication for mudb
285 lines (234 loc) • 9.47 kB
text/typescript
import ws = require('uws');
import {
MuSessionId,
MuSocketState,
MuSocketServerState,
MuSocket,
MuSocketSpec,
MuSocketServer,
MuSocketServerSpec,
} from 'mudb/socket';
export interface UWSSocketInterface {
onmessage:(message:{ data:Uint8Array|string }) => void;
onclose:() => void;
send:(data:Uint8Array|string) => void;
close:() => void;
}
function noop () {}
export class MuWebSocketConnection {
public readonly sessionId:string;
public started = false;
public closed = false;
// every client communicates through one reliable and several unreliable sockets
public reliableSocket:UWSSocketInterface;
public unreliableSockets:UWSSocketInterface[] = [];
private _nextSocketSend = 0;
public pendingMessages:(Uint8Array|string)[] = [];
// for onmessage handler
public onMessage:(data:Uint8Array|string, unreliable:boolean) => void = noop;
// both for onclose handler
public onClose:() => void = noop;
public serverClose:() => void;
constructor (sessionId:string, reliableSocket:UWSSocketInterface, serverClose:() => void) {
this.sessionId = sessionId;
this.reliableSocket = reliableSocket;
this.serverClose = serverClose;
this.reliableSocket.onmessage = ({ data }) => {
if (this.started) {
if (typeof data === 'string') {
this.onMessage(data, false);
} else {
this.onMessage(new Uint8Array(data), false);
}
} else {
if (typeof data === 'string') {
this.pendingMessages.push(data);
} else {
this.pendingMessages.push(new Uint8Array(data).slice(0));
}
}
};
this.reliableSocket.onclose = () => {
this.closed = true;
for (let i = 0; i < this.unreliableSockets.length; ++i) {
this.unreliableSockets[i].close();
}
this.onClose();
// remove connection from server
this.serverClose();
};
}
public addUnreliableSocket (socket:UWSSocketInterface) {
if (this.closed) {
return;
}
this.unreliableSockets.push(socket);
socket.onmessage = ({ data }) => {
if (typeof data === 'string') {
this.onMessage(data, true);
} else {
this.onMessage(new Uint8Array(data), true);
}
};
socket.onclose = () => {
this.unreliableSockets.splice(this.unreliableSockets.indexOf(socket), 1);
};
}
public send (data:Uint8Array, unreliable:boolean) {
if (this.closed) {
return;
}
if (unreliable) {
if (this.unreliableSockets.length > 0) {
this.unreliableSockets[this._nextSocketSend++ % this.unreliableSockets.length].send(data);
}
} else {
this.reliableSocket.send(data);
}
}
public close () {
this.reliableSocket.close();
}
}
export class MuWebSocketClient implements MuSocket {
public readonly sessionId:MuSessionId;
private _connection:MuWebSocketConnection;
public state = MuSocketState.INIT;
constructor (connection:MuWebSocketConnection) {
this.sessionId = connection.sessionId;
this._connection = connection;
}
public open (spec:MuSocketSpec) {
if (this.state === MuSocketState.OPEN) {
throw new Error('mudb/web-socket: socket already open');
}
if (this.state === MuSocketState.CLOSED) {
throw new Error('mudb/web-socket: cannot reopen closed socket');
}
setTimeout(
() => {
this._connection.started = true;
// hook handlers on socket
this._connection.onMessage = spec.message;
this._connection.onClose = () => {
this.state = MuSocketState.CLOSED;
spec.close();
};
this.state = MuSocketState.OPEN;
spec.ready();
// process pending messages
for (let i = 0; i < this._connection.pendingMessages.length; ++i) {
spec.message(this._connection.pendingMessages[i], false);
}
this._connection.pendingMessages.length = 0;
// if socket already closed, then fire close event immediately
if (this._connection.closed) {
this.state = MuSocketState.CLOSED;
spec.close();
}
},
0);
}
public send (data:Uint8Array, unreliable?:boolean) {
this._connection.send(data, !!unreliable);
}
public close () {
this._connection.close();
}
}
export class MuWebSocketServer implements MuSocketServer {
private _connections:MuWebSocketConnection[] = [];
public clients:MuWebSocketClient[] = [];
public state = MuSocketServerState.INIT;
private _httpServer;
private _websocketServer:ws.Server;
private _onClose;
constructor (spec:{
server:object,
}) {
this._httpServer = spec.server;
}
private _findConnection (sessionId:string) : MuWebSocketConnection | null {
for (let i = 0; i < this._connections.length; ++i) {
if (this._connections[i].sessionId === sessionId) {
return this._connections[i];
}
}
return null;
}
public start (spec:MuSocketServerSpec) {
if (this.state === MuSocketServerState.RUNNING) {
throw new Error('mudb/web-socket: server already running');
}
if (this.state === MuSocketServerState.SHUTDOWN) {
throw new Error('mudb/web-socket: server already shut down, cannot restart');
}
setTimeout(
() => {
this._websocketServer = new ws.Server({
server: this._httpServer,
})
// called when connection is ready
.on('connection', (socket) => {
socket.onmessage = ({ data }) => {
try {
const sessionId = JSON.parse(data).sessionId;
if (typeof sessionId !== 'string') {
throw new Error('bad session ID');
}
let connection = this._findConnection(sessionId);
if (connection) {
// tell client to use this socket as an unreliable one
socket.send(JSON.stringify({
reliable: false,
}));
// all sockets except the first one opened are used as unreliable ones
// reset socket message handler
connection.addUnreliableSocket(socket);
return;
} else {
// this is client's first connection since no related connection object is found
// tell client to use this socket as a reliable one
socket.send(JSON.stringify({
reliable: true,
}));
// one connection object per client
// reset socket message handler
connection = new MuWebSocketConnection(sessionId, socket, () => {
if (connection) {
this._connections.splice(this._connections.indexOf(connection), 1);
for (let i = this.clients.length - 1; i >= 0; --i) {
if (this.clients[i].sessionId === connection.sessionId) {
this.clients.splice(i, 1);
}
}
}
});
this._connections.push(connection);
const client = new MuWebSocketClient(connection);
this.clients.push(client);
spec.connection(client);
return;
}
} catch (e) {
console.error(`mudb/web-socket: terminating socket due to ${e}`);
socket.terminate();
}
};
});
this._onClose = spec.close;
this.state = MuSocketServerState.RUNNING;
spec.ready();
},
0);
}
public close () {
if (this.state === MuSocketServerState.SHUTDOWN) {
return;
}
this.state = MuSocketServerState.SHUTDOWN;
if (this._websocketServer) {
this._websocketServer.close(this._onClose);
}
}
}