TeaWeb/web/app/connection/WrappedWebSocket.ts

187 lines
5.8 KiB
TypeScript

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<void>(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);
}
}