TeaWeb/web/app/connection/ServerConnection.ts

542 lines
22 KiB
TypeScript
Raw Normal View History

2020-03-30 11:44:18 +00:00
import {
AbstractServerConnection,
CommandOptionDefaults,
CommandOptions, ConnectionPing,
ConnectionStateListener,
ConnectionStatistics,
2020-03-30 11:44:18 +00:00
} from "tc-shared/connection/ConnectionBase";
import {ConnectionHandler, ConnectionState, DisconnectReason} from "tc-shared/ConnectionHandler";
import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler";
import {ConnectionCommandHandler, ServerConnectionCommandBoss} from "tc-shared/connection/CommandHandler";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {settings, Settings} from "tc-shared/settings";
import * as log from "tc-shared/log";
import {LogCategory, logDebug, logError, logTrace} from "tc-shared/log";
2020-03-30 11:44:18 +00:00
import {Regex} from "tc-shared/ui/modal/ModalConnect";
import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler";
import {EventType} from "tc-shared/ui/frames/log/Definitions";
import {WrappedWebSocket} from "tc-backend/web/connection/WrappedWebSocket";
import {AbstractVoiceConnection} from "tc-shared/connection/VoiceConnection";
2020-09-03 10:14:15 +00:00
import {parseCommand} from "tc-backend/web/connection/CommandParser";
2020-09-16 19:50:21 +00:00
import {ServerAddress} from "tc-shared/tree/Server";
import {RtpVoiceConnection} from "tc-backend/web/voice/Connection";
import {VideoConnection} from "tc-shared/connection/VideoConnection";
import {VoiceConnection} from "tc-backend/web/legacy/voice/VoiceHandler";
import {LegacySupportVoiceBridge} from "tc-backend/web/connection/LegacySupportVoiceBridge";
import {ServerFeature} from "tc-shared/connection/ServerFeatures";
import {RTCConnection} from "tc-shared/connection/rtc/Connection";
import {RtpVideoConnection} from "tc-shared/connection/rtc/video/Connection";
import { tr } from "tc-shared/i18n/localize";
import {createErrorModal} from "tc-shared/ui/elements/Modal";
2020-03-30 11:44:18 +00:00
2019-02-23 13:15:22 +00:00
class ReturnListener<T> {
resolve: (value?: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
code: string;
timeout: number;
2019-02-23 13:15:22 +00:00
}
let globalReturnCodeIndex = 0;
2020-03-30 11:44:18 +00:00
export class ServerConnection extends AbstractServerConnection {
private remoteServerAddress: ServerAddress;
private handshakeHandler: HandshakeHandler;
private commandHandlerBoss: ServerConnectionCommandBoss;
private defaultCommandHandler: ConnectionCommandHandler;
private socket: WrappedWebSocket;
private connectCancelCallback: () => void;
private returnListeners: ReturnListener<CommandResult>[] = [];
2019-02-23 13:15:22 +00:00
private rtcConnection: RTCConnection;
private voiceConnection: RtpVoiceConnection;
private videoConnection: RtpVideoConnection;
2019-04-04 19:47:52 +00:00
/* legacy */
private oldVoiceConnection: VoiceConnection;
private legacyVoiceConnection: LegacySupportVoiceBridge;
private pingStatistics = {
2020-03-30 11:44:18 +00:00
thread_id: 0,
2019-08-21 08:00:01 +00:00
lastRequestTimestamp: 0,
lastResponseTimestamp: 0,
currentRequestId: 0,
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
interval: 5000,
timeout: 7500,
2019-08-21 08:00:01 +00:00
currentJsValue: 0,
currentNativeValue: 0 /* ping value for native (WS) */
2020-03-30 11:44:18 +00:00
};
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
constructor(client : ConnectionHandler) {
super(client);
2019-02-23 13:15:22 +00:00
this.commandHandlerBoss = new ServerConnectionCommandBoss(this);
this.defaultCommandHandler = new ConnectionCommandHandler(this);
2019-02-23 13:15:22 +00:00
this.commandHandlerBoss.register_handler(this.defaultCommandHandler);
2020-03-30 11:44:18 +00:00
this.command_helper.initialize();
2019-04-04 19:47:52 +00:00
this.rtcConnection = new RTCConnection(this, true);
this.voiceConnection = new RtpVoiceConnection(this, this.rtcConnection);
this.videoConnection = new RtpVideoConnection(this.rtcConnection);
this.oldVoiceConnection = new VoiceConnection(this);
this.legacyVoiceConnection = new LegacySupportVoiceBridge(this, this.oldVoiceConnection, this.voiceConnection);
2020-03-30 11:44:18 +00:00
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
destroy() {
this.disconnect("handle destroyed").catch(error => {
log.warn(LogCategory.NETWORKING, tr("Failed to disconnect on server connection destroy: %o"), error);
}).then(() => {
clearInterval(this.pingStatistics.thread_id);
2020-09-28 08:46:11 +00:00
if(this.connectCancelCallback) {
this.connectCancelCallback();
2020-09-28 08:46:11 +00:00
}
2019-08-21 08:00:01 +00:00
for(const listener of this.returnListeners) {
2020-03-30 11:44:18 +00:00
try {
listener.reject("handler destroyed");
} catch(error) {
log.warn(LogCategory.NETWORKING, tr("Failed to reject command promise: %o"), error);
2019-08-21 08:00:01 +00:00
}
2020-09-28 08:46:11 +00:00
clearTimeout(listener.timeout);
2020-03-30 11:44:18 +00:00
}
this.returnListeners = undefined;
2019-08-21 08:00:01 +00:00
this.rtcConnection.destroy();
2020-03-30 11:44:18 +00:00
this.command_helper.destroy();
2019-08-21 08:00:01 +00:00
this.defaultCommandHandler && this.commandHandlerBoss.unregister_handler(this.defaultCommandHandler);
this.defaultCommandHandler = undefined;
2019-08-21 08:00:01 +00:00
this.voiceConnection && this.voiceConnection.destroy();
this.voiceConnection = undefined;
2019-02-23 13:15:22 +00:00
this.oldVoiceConnection?.destroy();
this.oldVoiceConnection = undefined;
this.legacyVoiceConnection = undefined;
this.commandHandlerBoss && this.commandHandlerBoss.destroy();
this.commandHandlerBoss = undefined;
this.events.destroy();
2020-03-30 11:44:18 +00:00
});
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
async connect(address : ServerAddress, handshake: HandshakeHandler, timeout?: number) : Promise<void> {
const connectBeginTimestamp = Date.now();
timeout = typeof(timeout) === "number" ? timeout : 10_000;
2020-03-30 11:44:18 +00:00
try {
await this.disconnect();
2020-03-30 11:44:18 +00:00
} catch(error) {
log.error(LogCategory.NETWORKING, tr("Failed to close old connection properly. Error: %o"), error);
throw "failed to cleanup old connection";
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
this.updateConnectionState(ConnectionState.CONNECTING);
this.remoteServerAddress = address;
2019-02-23 13:15:22 +00:00
this.handshakeHandler = handshake;
this.handshakeHandler.setConnection(this);
2020-03-30 11:44:18 +00:00
/* The direct one connect directly to the target address. The other via the .con-gate.work */
let availableSockets: WrappedWebSocket[] = [];
proxySocket:
if(!settings.static_global(Settings.KEY_CONNECT_NO_DNSPROXY)) {
let host;
if(Regex.IP_V4.test(address.host)) {
host = address.host.replace(/\./g, "-") + ".con-gate.work";
} else if(Regex.IP_V6.test(address.host)) {
host = address.host.replace(/\[(.*)]/, "$1").replace(/:/g, "_") + ".con-gate.work";
} else {
break proxySocket;
2020-03-30 11:44:18 +00:00
}
2020-07-25 11:56:30 +00:00
availableSockets.push(new WrappedWebSocket({
host: host,
port: address.port,
secure: true
}))
}
2020-07-25 11:56:30 +00:00
availableSockets.push(new WrappedWebSocket({
host: address.host,
port: address.port,
secure: true
}));
let timeoutRaised = false;
let timeoutPromise = new Promise(resolve => setTimeout(() => {
timeoutRaised = true;
resolve();
}, timeout));
let cancelRaised = false;
let cancelPromise = new Promise(resolve => {
this.connectCancelCallback = () => {
this.connectCancelCallback = undefined;
cancelRaised = true;
resolve();
};
});
2019-08-21 08:00:01 +00:00
availableSockets.forEach(e => e.doConnect());
while (availableSockets.length > 0) {
await Promise.race([...availableSockets.map(e => e.awaitConnectResult()), timeoutPromise, cancelPromise]);
2019-08-21 08:00:01 +00:00
if(cancelRaised) {
log.debug(LogCategory.NETWORKING, tr("Aborting connect attempt due to a cancel request."));
availableSockets.forEach(e => e.closeConnection());
return
}
if(timeoutRaised) {
log.info(LogCategory.NETWORKING, tr("Connect timeout triggered. Aborting connect attempt!"));
availableSockets.forEach(e => e.closeConnection());
this.updateConnectionState(ConnectionState.UNCONNECTED); /* firstly update the state, that fire event */
this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE);
return
}
2019-02-23 13:15:22 +00:00
let finished = availableSockets.find(e => e.state !== "connecting");
if(!finished) continue; /* should not happen, but we want to ensure it */
availableSockets.remove(finished);
2020-03-30 11:44:18 +00:00
switch (finished.state) {
case "unconnected":
2020-07-25 11:56:30 +00:00
log.debug(LogCategory.NETWORKING, tr("Connection attempt to %s:%d via %s got aborted."), this.remoteServerAddress.host, this.remoteServerAddress.port, finished.socketUrl());
continue;
2020-03-30 11:44:18 +00:00
case "errored":
const error = finished.popError();
2020-07-25 11:56:30 +00:00
log.info(LogCategory.NETWORKING, tr("Connection attempt to %s:%d via %s failed:\n%o"), this.remoteServerAddress.host, this.remoteServerAddress.port, finished.socketUrl(), error);
continue;
2020-03-30 11:44:18 +00:00
case "connected":
break;
2020-03-30 11:44:18 +00:00
}
2019-02-23 13:15:22 +00:00
this.socket = finished;
/* abort any other ongoing connection attempts, we already succeeded */
availableSockets.forEach(e => e.closeConnection());
break;
}
2019-02-23 13:15:22 +00:00
if(!this.socket) {
log.info(LogCategory.NETWORKING, tr("Failed to connect to %s:%d. No connection attempt succeeded."), this.remoteServerAddress.host, this.remoteServerAddress.port);
this.updateConnectionState(ConnectionState.UNCONNECTED); /* firstly update the state, that fire event */
this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE);
return;
}
2019-02-23 13:15:22 +00:00
this.socket.callbackMessage = message => this.handleSocketMessage(message);
this.socket.callbackDisconnect = (code, reason) => {
try {
this.disconnect();
} catch (error) {
log.warn(LogCategory.NETWORKING, tr("Failed to disconnect with an already closed socket: %o"), error);
}
2019-02-23 13:15:22 +00:00
this.client.handleDisconnect(DisconnectReason.CONNECTION_CLOSED, {
code: code,
reason: reason
});
};
this.socket.callbackErrored = () => {
if(this.socket.hasError()) {
log.error(LogCategory.NETWORKING, tr("Server connection %s:%d has been terminated due to an unexpected error (%o)."),
this.remoteServerAddress.host,
this.remoteServerAddress.port,
this.socket.popError()
);
} else {
log.error(LogCategory.NETWORKING, tr("Server connection %s:%d has been terminated due to an unexpected error."), this.remoteServerAddress.host, this.remoteServerAddress.port);
}
try {
this.disconnect();
} catch (error) {
log.warn(LogCategory.NETWORKING, tr("Failed to disconnect with an already closed socket: %o"), error);
}
2019-02-23 13:15:22 +00:00
this.client.handleDisconnect(DisconnectReason.CONNECTION_CLOSED);
};
2019-02-23 13:15:22 +00:00
const connectEndTimestamp = Date.now();
log.info(LogCategory.NETWORKING, tr("Successfully initialized a connection to %s:%d via %s within %d milliseconds."),
this.remoteServerAddress.host,
this.remoteServerAddress.port,
2020-07-25 11:56:30 +00:00
this.socket.socketUrl(),
connectEndTimestamp - connectBeginTimestamp);
2019-08-21 08:00:01 +00:00
2020-09-03 10:14:15 +00:00
/* enabling raw commands, if the server supports it */
this.sendData(JSON.stringify({
type: "enable-raw-commands"
}))
this.startHandshake();
2020-03-30 11:44:18 +00:00
}
2019-02-23 13:15:22 +00:00
private startHandshake() {
2020-03-30 11:44:18 +00:00
this.updateConnectionState(ConnectionState.INITIALISING);
this.client.log.log(EventType.CONNECTION_LOGIN, {});
this.handshakeHandler.initialize();
this.handshakeHandler.startHandshake();
2020-03-30 11:44:18 +00:00
}
2020-03-30 11:44:18 +00:00
async disconnect(reason?: string) : Promise<void> {
if(this.connectCancelCallback)
this.connectCancelCallback();
if(this.connectionState === ConnectionState.UNCONNECTED)
2020-07-23 22:41:04 +00:00
return;
this.updateConnectionState(ConnectionState.DISCONNECTING);
try {
clearTimeout(this.pingStatistics.thread_id);
this.pingStatistics.thread_id = undefined;
2019-02-23 13:15:22 +00:00
if(typeof(reason) === "string") {
//TODO send disconnect reason
}
2019-08-21 08:00:01 +00:00
if(this.socket) {
this.socket.callbackMessage = undefined;
this.socket.callbackDisconnect = undefined;
this.socket.callbackErrored = undefined;
2019-02-23 13:15:22 +00:00
this.socket.closeConnection(); /* 3000 + 0xFF, tr("request disconnect") */
this.socket = undefined;
}
2019-02-23 13:15:22 +00:00
for(let future of this.returnListeners)
future.reject(tr("Connection closed"));
this.returnListeners = [];
} finally {
this.updateConnectionState(ConnectionState.UNCONNECTED);
}
2020-03-30 11:44:18 +00:00
}
2019-02-23 13:15:22 +00:00
private handleSocketMessage(data) {
2020-03-30 11:44:18 +00:00
if(typeof(data) === "string") {
let json;
try {
json = JSON.parse(data);
} catch(e) {
log.warn(LogCategory.NETWORKING, tr("Could not parse message json!"));
return;
}
if(json["type"] === undefined) {
log.warn(LogCategory.NETWORKING, tr("Missing data type in message!"));
return;
}
if(json["type"] === "command") {
2020-04-18 18:25:58 +00:00
/* devel-block(log-networking-commands) */
2020-03-30 11:44:18 +00:00
let group = log.group(log.LogType.DEBUG, LogCategory.NETWORKING, tr("Handling command '%s'"), json["command"]);
group.log(tr("Handling command '%s'"), json["command"]);
group.group(log.LogType.TRACE, tr("Json:")).collapsed(true).log("%o", json).end();
2020-04-18 18:25:58 +00:00
/* devel-block-end */
2020-03-30 11:44:18 +00:00
this.commandHandlerBoss.invoke_handle({
2020-03-30 11:44:18 +00:00
command: json["command"],
arguments: json["data"]
});
if(json["command"] === "initserver") {
this.handleServerInit();
2020-03-30 11:44:18 +00:00
}
2020-04-18 18:25:58 +00:00
/* devel-block(log-networking-commands) */
2020-03-30 11:44:18 +00:00
group.end();
2020-04-18 18:25:58 +00:00
/* devel-block-end */
2020-09-03 10:14:15 +00:00
} else if(json["type"] === "command-raw") {
const command = parseCommand(json["payload"]);
logTrace(LogCategory.NETWORKING, tr("Received command %s"), command.command);
this.commandHandlerBoss.invoke_handle({
command: command.command,
arguments: command.payload
});
if(command.command === "initserver") {
this.handleServerInit();
2020-09-03 10:14:15 +00:00
}
2020-03-30 11:44:18 +00:00
} else if(json["type"] === "ping") {
this.sendData(JSON.stringify({
type: 'pong',
payload: json["payload"]
}));
2020-03-30 11:44:18 +00:00
} else if(json["type"] === "pong") {
const id = parseInt(json["payload"]);
if(id != this.pingStatistics.currentRequestId) {
log.warn(LogCategory.NETWORKING, tr("Received pong which is older than the last request. Delay may over %oms? (Index: %o, Current index: %o)"), this.pingStatistics.timeout, id, this.pingStatistics.currentRequestId);
2019-08-21 08:00:01 +00:00
} else {
this.pingStatistics.lastResponseTimestamp = 'now' in performance ? performance.now() : Date.now();
this.pingStatistics.currentJsValue = this.pingStatistics.lastResponseTimestamp - this.pingStatistics.lastRequestTimestamp;
this.pingStatistics.currentNativeValue = parseInt(json["ping_native"]) / 1000; /* we're getting it in microseconds and not milliseconds */
this.events.fire("notify_ping_updated", { newPing: this.ping() });
//log.debug(LogCategory.NETWORKING, tr("Received new pong. Updating ping to: JS: %o Native: %o"), this._ping.value.toFixed(3), this._ping.value_native.toFixed(3));
2019-02-23 13:15:22 +00:00
}
} else if(json["type"] === "WebRTC") {
this.oldVoiceConnection?.handleControlPacket(json);
2019-02-23 13:15:22 +00:00
} else {
2020-03-30 11:44:18 +00:00
log.warn(LogCategory.NETWORKING, tr("Unknown command type %o"), json["type"]);
2019-02-23 13:15:22 +00:00
}
2020-03-30 11:44:18 +00:00
} else {
log.warn(LogCategory.NETWORKING, tr("Received unknown message of type %s. Dropping message"), typeof(data));
2019-02-23 13:15:22 +00:00
}
2020-03-30 11:44:18 +00:00
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
sendData(data: any) {
if(!this.socket || this.socket.state !== "connected") {
log.warn(LogCategory.NETWORKING, tr("Tried to send data via a non connected server socket."));
2020-03-30 11:44:18 +00:00
return;
2019-02-23 13:15:22 +00:00
}
this.socket.sendMessage(data);
2020-03-30 11:44:18 +00:00
}
2019-02-23 13:15:22 +00:00
private handleServerInit() {
this.pingStatistics.thread_id = setInterval(() => this.doNextPing(), this.pingStatistics.interval) as any;
this.doNextPing();
this.updateConnectionState(ConnectionState.CONNECTED);
this.client.serverFeatures.awaitFeatures().then(succeeded => {
if(!succeeded) {
/* something like a disconnect happened or so */
return;
}
if(this.client.serverFeatures.supportsFeature(ServerFeature.VIDEO, 1)) {
this.legacyVoiceConnection.setVoiceBridge("new").then(() => {
this.rtcConnection.doInitialSetup();
}).catch(error => {
logError(LogCategory.VOICE, tr("Failed to setup the voice bridge: %o"), error);
/* FIXME: Some kind of error modal? */
});
} else{
/* old voice connection */
logDebug(LogCategory.NETWORKING, tr("Using legacy voice connection for TeaSpeak server bellow 1.4.5"));
this.legacyVoiceConnection.setVoiceBridge("old").then(() => {
this.oldVoiceConnection.startVoiceBridge();
this.rtcConnection.setNotSupported();
}).catch(error => {
logError(LogCategory.VOICE, tr("Failed to setup the old voice bridge: %o"), error);
/* FIXME: Some kind of error modal? */
});
}
});
}
private static commandDataToJson(input: any) : string {
2020-03-30 11:44:18 +00:00
return JSON.stringify(input, (key, value) => {
switch (typeof value) {
case "boolean": return value == true ? "1" : "0";
case "function": return value();
default:
return value;
2019-02-23 13:15:22 +00:00
}
2020-03-30 11:44:18 +00:00
});
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
}
2020-03-30 11:44:18 +00:00
send_command(command: string, data?: any | any[], _options?: CommandOptions) : Promise<CommandResult> {
if(!this.socket || !this.connected()) {
2020-03-30 11:44:18 +00:00
log.warn(LogCategory.NETWORKING, tr("Tried to send a command without a valid connection."));
return Promise.reject(tr("not connected"));
2019-02-23 13:15:22 +00:00
}
2020-03-30 11:44:18 +00:00
const options: CommandOptions = {};
Object.assign(options, CommandOptionDefaults);
Object.assign(options, _options);
data = Array.isArray(data) ? data : [data || {}];
2020-03-30 11:44:18 +00:00
if(data.length == 0) /* we require min one arg to append return_code */
data.push({});
let result = new Promise<CommandResult>((resolve, failed) => {
let payload = Array.isArray(data) ? data : [data];
let returnCode = typeof payload[0]["return_code"] === "string" ? payload[0].return_code : ++globalReturnCodeIndex;
payload[0].return_code = returnCode;
2020-03-30 11:44:18 +00:00
let listener = new ReturnListener<CommandResult>();
listener.resolve = resolve;
listener.reject = failed;
listener.code = returnCode;
2020-03-30 11:44:18 +00:00
listener.timeout = setTimeout(() => {
this.returnListeners.remove(listener);
2020-03-30 11:44:18 +00:00
listener.reject("timeout");
}, options.timeout || 15_000);
this.returnListeners.push(listener);
2020-03-30 11:44:18 +00:00
this.sendData(ServerConnection.commandDataToJson({
2020-03-30 11:44:18 +00:00
"type": "command",
"command": command,
"data": payload,
2020-03-30 11:44:18 +00:00
"flags": options.flagset.filter(entry => entry.length != 0)
}))
2020-03-30 11:44:18 +00:00
});
return this.defaultCommandHandler.proxy_command_promise(result, options);
2020-03-30 11:44:18 +00:00
}
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
connected() : boolean {
return !!this.socket && this.socket.state === "connected";
2020-03-30 11:44:18 +00:00
}
2019-02-23 13:15:22 +00:00
getVoiceConnection(): AbstractVoiceConnection {
return this.legacyVoiceConnection;
}
getVideoConnection(): VideoConnection {
return this.videoConnection;
2020-03-30 11:44:18 +00:00
}
2019-04-04 19:47:52 +00:00
2020-03-30 11:44:18 +00:00
command_handler_boss(): AbstractCommandHandlerBoss {
return this.commandHandlerBoss;
2020-03-30 11:44:18 +00:00
}
2019-04-04 19:47:52 +00:00
2020-03-30 11:44:18 +00:00
handshake_handler(): HandshakeHandler {
return this.handshakeHandler;
2020-03-30 11:44:18 +00:00
}
2019-08-21 08:00:01 +00:00
2020-03-30 11:44:18 +00:00
remote_address(): ServerAddress {
return this.remoteServerAddress;
2020-03-30 11:44:18 +00:00
}
2019-08-21 08:00:01 +00:00
2020-07-25 11:56:30 +00:00
connectionProxyAddress(): ServerAddress | undefined {
return this.socket?.address;
}
private doNextPing() {
if(this.pingStatistics.lastRequestTimestamp + this.pingStatistics.timeout < Date.now()) {
this.pingStatistics.currentJsValue = this.pingStatistics.timeout;
this.pingStatistics.lastResponseTimestamp = this.pingStatistics.lastRequestTimestamp + 1;
2020-03-30 11:44:18 +00:00
}
if(this.pingStatistics.lastResponseTimestamp > this.pingStatistics.lastRequestTimestamp) {
this.pingStatistics.lastRequestTimestamp = 'now' in performance ? performance.now() : Date.now();
2020-03-30 11:44:18 +00:00
this.sendData(JSON.stringify({
type: 'ping',
payload: (++this.pingStatistics.currentRequestId).toString()
2020-03-30 11:44:18 +00:00
}));
2019-08-21 08:00:01 +00:00
}
2019-04-15 13:33:51 +00:00
}
ping(): ConnectionPing {
2020-03-30 11:44:18 +00:00
return {
javascript: this.pingStatistics.currentJsValue,
/* if the native value is zero that means we don't have any */
native: this.pingStatistics.currentNativeValue === 0 ? this.pingStatistics.currentJsValue : this.pingStatistics.currentNativeValue
2020-03-30 11:44:18 +00:00
};
2019-02-23 13:15:22 +00:00
}
getControlStatistics(): ConnectionStatistics {
return this.socket?.getControlStatistics() || { bytesSend: 0, bytesReceived: 0 };
}
2019-02-23 13:15:22 +00:00
}