UNPKG

muweb-socket

Version:

WebSocket communication for mudb

285 lines (234 loc) 9.47 kB
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); } } }