muweb-socket
Version:
WebSocket communication for mudb
178 lines (148 loc) • 6.08 kB
text/typescript
import {
MuSessionId,
MuSocketState,
MuSocket,
MuSocketSpec,
} from 'mudb/socket';
const hasWindow = typeof window === 'object' && 'addEventListener' in window;
const WS:typeof WebSocket = typeof WebSocket !== 'undefined' ? WebSocket : require.call(null, 'uws');
export class MuWebSocket implements MuSocket {
public readonly sessionId:MuSessionId;
public state = MuSocketState.INIT;
private _url:string;
private _reliableSocket:WebSocket|null = null;
private _unreliableSockets:WebSocket[] = [];
private _maxSockets = 5;
private _nextSocketSend = 0;
constructor (spec:{
sessionId:MuSessionId,
url:string,
maxSockets?:number,
}) {
this.sessionId = spec.sessionId;
this._url = spec.url;
if (spec.maxSockets) {
this._maxSockets = Math.max(1, spec.maxSockets | 0);
}
}
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');
}
// used to reliably close sockets
const sockets:WebSocket[] = [];
function removeSocket (socket) {
for (let i = 0; i < sockets.length; ++i) {
if (sockets[i] === socket) {
sockets.splice(i, 1);
}
}
}
if (hasWindow) {
window.addEventListener('beforeunload', () => {
for (let i = 0; i < sockets.length; ++i) {
sockets[i].close();
}
});
}
const openSocket = () => {
const socket = new WS(this._url);
socket.binaryType = 'arraybuffer';
sockets.push(socket);
// when connection is ready
socket.onopen = () => {
socket.onmessage = (ev) => {
if (this.state === MuSocketState.CLOSED) {
socket.close();
return;
}
if (typeof ev.data === 'string') {
// on receiving the first message from server,
// determine whether this should be a reliable socket
if (JSON.parse(ev.data).reliable) {
this.state = MuSocketState.OPEN;
// reset message handler
socket.onmessage = ({ data }) => {
if (this.state !== MuSocketState.OPEN) {
return;
}
if (typeof data === 'string') {
spec.message(data, false);
} else {
spec.message(new Uint8Array(data), false);
}
};
socket.onclose = () => {
this.state = MuSocketState.CLOSED;
// remove the socket beforehand so that it will not be closed more than once
removeSocket(socket);
for (let i = 0; i < sockets.length; ++i) {
sockets[i].close();
}
spec.close();
};
this._reliableSocket = socket;
spec.ready();
} else {
// reset message handler
socket.onmessage = ({ data }) => {
if (this.state !== MuSocketState.OPEN) {
return;
}
if (typeof data === 'string') {
spec.message(data, true);
} else {
spec.message(new Uint8Array(data), true);
}
};
socket.onclose = () => {
// to avoid closing the socket more than once
removeSocket(socket);
for (let i = this._unreliableSockets.length - 1; i >= 0; --i) {
if (this._unreliableSockets[i] === socket) {
this._unreliableSockets.splice(i, 1);
}
}
};
this._unreliableSockets.push(socket);
}
}
};
socket.send(JSON.stringify({
sessionId: this.sessionId,
}));
};
};
for (let i = 0; i < this._maxSockets; ++i) {
openSocket();
}
}
public send (data:Uint8Array, unreliable?:boolean) {
if (this.state !== MuSocketState.OPEN) {
return;
}
if (unreliable) {
if (this._unreliableSockets.length > 0) {
this._unreliableSockets[this._nextSocketSend++ % this._unreliableSockets.length].send(data);
}
} else if (this._reliableSocket) {
this._reliableSocket.send(data);
}
}
public close () {
if (this.state === MuSocketState.CLOSED) {
return;
}
// necessary
this.state = MuSocketState.CLOSED;
if (this._reliableSocket) {
this._reliableSocket.close();
}
for (let i = 0; i < this._unreliableSockets.length; ++i) {
this._unreliableSockets[i].close();
}
}
}