import {LogCategory, logWarn} from "tc-shared/log"; import {ConnectionStatistics} from "tc-shared/connection/ConnectionBase"; import { tr } from "tc-shared/i18n/localize"; const kPreventOpeningWebSocketClosing = false; export type WebSocketUrl = { secure: boolean; host: string, port: number, path?: string }; export class WrappedWebSocket { public readonly address: WebSocketUrl; public state: "unconnected" | "connecting" | "connected" | "errored"; private socket: WebSocket; /* callbacks for events after the socket has successfully connected! */ public callbackMessage: (message) => void; public callbackDisconnect: (code: number, reason?: string) => void; public callbackErrored: () => void; private errorQueue = []; private connectResultListener = []; private bytesReceived; private bytesSend; constructor(addr: WebSocketUrl) { this.address = addr; this.state = "unconnected"; } getControlStatistics() : ConnectionStatistics { return { bytesReceived: this.bytesReceived, bytesSend: this.bytesSend }; } socketUrl() : string { let result = ""; result += this.address.secure ? "wss://" : "ws://"; result += this.address.host + ":" + this.address.port; if(this.address.path) { result += (this.address.path.startsWith("/") ? "" : "/") + this.address.path; } return result } doConnect() { this.closeConnection(); this.state = "connecting"; try { this.socket = new WebSocket(this.socketUrl()); this.socket.onopen = () => { this.state = "connected"; this.fireConnectResult(); }; this.socket.onclose = event => { if(this.state === "connecting") { this.errorQueue.push(new Error(tr("Unexpected close with code ") + event.code + (event.reason ? " (" + event.reason + ")" : ""))); this.state = "errored"; this.fireConnectResult(); } else if(this.state === "connected") { if(this.callbackDisconnect) this.callbackDisconnect(event.code, event.reason); this.closeConnection(); } }; this.socket.onmessage = event => { if(typeof event.data === "string") { this.bytesReceived += event.data.length; } else if(event.data instanceof ArrayBuffer) { this.bytesReceived += event.data.byteLength; } if(this.callbackMessage) { this.callbackMessage(event.data); } }; this.socket.onerror = () => { if(this.state === "connected") { this.state = "errored"; if(this.callbackErrored) { this.callbackErrored(); } } else if(this.state === "connecting") { this.state = "errored"; this.fireConnectResult(); } } } catch (error) { this.state = "errored"; this.errorQueue.push(error); this.fireConnectResult(); } } async awaitConnectResult() { while (this.state === "connecting") { await new Promise(resolve => this.connectResultListener.push(resolve)); } } closeConnection() { this.state = "unconnected"; if(this.socket) { this.socket.onopen = undefined; this.socket.onclose = undefined; this.socket.onerror = undefined; this.socket.onmessage = undefined; try { if(this.socket.readyState === WebSocket.OPEN) { this.socket.close(3000); } else if(this.socket.readyState === WebSocket.CONNECTING) { if(kPreventOpeningWebSocketClosing) { /* to prevent the "WebSocket is closed before the connection is established." warning in the console */ const socket = this.socket; const cleanup = () => { if(socket.readyState === WebSocket.OPEN) socket.close(); socket.onopen = undefined; socket.onclose = undefined; socket.onerror = undefined; socket.onmessage = undefined; }; socket.onopen = cleanup; socket.onclose = cleanup; socket.onerror = cleanup; socket.onmessage = cleanup; } else { this.socket.close(); } } } catch (error) { logWarn(LogCategory.NETWORKING, tr("Failed to close the web socket to %s: %o"), this.socketUrl(), error); } this.socket = undefined; } this.bytesReceived = 0; this.bytesSend = 0; this.errorQueue = []; this.fireConnectResult(); } private fireConnectResult() { while(this.connectResultListener.length > 0) this.connectResultListener.pop()(); } hasError() { return this.errorQueue.length !== 0; } popError() { return this.errorQueue.pop_front(); } sendMessage(message: string | ArrayBufferLike | Blob | ArrayBufferView) { if(typeof message === "string") { this.bytesSend += message.length; } else if(message instanceof ArrayBuffer) { this.bytesSend += message.byteLength; } this.socket.send(message); } }