///
declare var baseUrl: string;
declare var appInsights: Client;
import { debugSettings } from "../debugsettings";
import * as timeService from "../timeservice";
import { CancelToken } from "./cancelToken";
import { ConnectionService } from "./connectionservice";
import { connectionService, slowInternetService } from "./index";
export class ConnectionWrapper {
public terminated = false;
private refreshHandle: number = null;
constructor(public connection: SignalR.Hub.Connection) {
connection.connectionSlow(() => {
if (this.terminated) {
return;
}
this.logEvent("Connection slow");
connectionService.connectionSlow.dispatch();
});
connection.reconnecting(() => {
if (this.terminated) {
return;
}
connectionService.reconnecting.dispatch();
});
connection.reconnected(() => {
if (this.terminated) {
return;
}
connectionService.reconnected.dispatch();
});
connection.received(() => {
if (this.terminated) {
return;
}
connectionService.received.dispatch();
});
connection.disconnected(() => {
if (this.terminated) {
return;
}
connectionService.disconnected.dispatch();
});
connection.stateChanged((state) => {
if (this.terminated) {
return;
}
this.onConnectionStateChanged(state);
});
connection.error((error: any) => {
if (this.terminated) {
return;
}
this.logEvent("SignalR Error hapens", error);
if (error.source === "TimeoutException") {
this.logEvent("Timeout for connection.");
connectionService.recoverableError.dispatch();
return;
}
const source = error.source as CloseEvent;
if (source === null || source === undefined) {
// We don't know that this is means, so just fail
// so issue will be easiely identifiable.
this.logEvent("Unrecoverable SignalR error happens, please discover that this is means.");
return;
}
/* tslint:disable:no-string-literal */
if (error["message"] !== null
&& error["message"] !== undefined
&& error.message === "Error during negotiation request.") {
this.logEvent("Error during negotiation. Schedule reconnecting.");
connectionService.recoverableError.dispatch();
return;
}
if (source["code"] === null || source["code"] === undefined) {
this.logEvent(
"Unrecoverable SignalR error without code happens, please discover that this is means.",
source);
return;
}
/* tslint:enable:no-string-literal */
this.logEvent("Error code is: ", source.code);
if (source.code === 1006) {
// Attempt to reestablish connection.
this.logEvent("Attempt to reestablish connection due to recoverable error.");
connectionService.recoverableError.dispatch();
return;
}
console.warn(error);
});
}
public terminateConnection(forceDisconnect = false) {
const hubId = this.connection.id;
const connectionInfo = "HID:" + hubId;
this.logEvent("Terminating connection " + connectionInfo);
slowInternetService.manualDisconnect = true;
try {
this.connection.stop(false, false);
} catch (e) {
// Skip exception here. They occurs during bad internet connection
// when connection not even fully starts, so no disconnection correctly happens.
}
this.cancelRefereshConnection();
this.terminated = true;
}
public establishConnection(maxAttempts = 3) {
const attempts = connectionService.attempts++;
connectionService.lastAttempt = attempts;
const result = this.establishConnectionCore(maxAttempts);
return result;
}
public async establishConnectionAsync(maxAttempts = 3, cancellationToken?: CancelToken) {
const attempts = connectionService.attempts++;
connectionService.lastAttempt = attempts;
return await this.establishConnectionCoreAsync(maxAttempts, cancellationToken);
}
public buildStartConnection() {
let supportedTransports = null;
const androidVersion = this.getAndroidVersion();
if (androidVersion === false || (androidVersion as string).indexOf("4.4") === 0) {
supportedTransports = ["webSockets"];
this.logEvent("Select WebSockets as single protocol");
} else {
this.logEvent("Select default connection protocols.");
}
const startConnection = () => {
const buildMainPromise = () => {
let promise: JQueryPromise;
if (supportedTransports === null) {
promise = this.connection.start();
} else {
promise = this.connection.start({ transport: supportedTransports });
}
return promise;
};
const fixup = $.Deferred();
const x = (attempts) => {
if (attempts === 0) {
fixup.reject();
}
const promise = buildMainPromise();
promise.then(() => {
if (this.terminated) {
fixup.reject();
return;
}
if (this.connection.state === 1) {
if (this.terminated) {
const hubId = this.connection.id;
const connectionInfo = "HID:" + hubId;
console.warn(`Attempt connect to terminated connection ${connectionInfo}`);
}
fixup.resolve();
return;
}
if (this.connection.state === 4) {
fixup.reject();
return;
}
timeService.setTimeout(function() {
x(attempts - 1);
}, 100);
}, () => {
fixup.reject();
});
};
x(30);
return fixup;
};
return startConnection;
}
public async buildStartConnectionAsync() {
let supportedTransports = null;
const androidVersion = this.getAndroidVersion();
if (androidVersion === false || (androidVersion as string).indexOf("4.4") === 0) {
supportedTransports = ["webSockets"];
this.logEvent("Select WebSockets as single protocol");
} else {
this.logEvent("Select default connection protocols.");
}
const tryConnection = async (attemptsLeft: number) => {
this.logEvent(`Attempt to establish connection. Attempts left ${attemptsLeft}.`);
if (supportedTransports === null) {
await this.connection.start();
} else {
await this.connection.start({ transport: supportedTransports });
}
if (this.terminated) {
throw new Error("SignalR connection was terminated.");
}
this.logEvent(`Connection started with state ${this.connection.state}.`);
if (this.connection.state === 1) {
return;
}
if (this.connection.state === 4) {
throw new Error("SignalR connection has invalid state.");
}
return new Promise(function(resolve, reject) {
timeService.setTimeout(async function() {
if (attemptsLeft <= 0) {
reject(new Error("Last retry did not work. Stop attempts."));
} else {
await tryConnection(attemptsLeft - 1);
resolve();
}
}, 100);
});
};
return await tryConnection(30);
}
private onConnectionStateChanged(state: SignalR.StateChanged) {
this.logEvent("SignalR state changed from: " + ConnectionService.stateConversion[state.oldState]
+ " to: " + ConnectionService.stateConversion[state.newState]);
if (state.newState === 4) {
connectionService.isDisconnected = true;
} else {
connectionService.isDisconnected = false;
}
if (state.newState === 1) {
connectionService.isConnected = true;
} else {
connectionService.isConnected = false;
}
}
private establishConnectionCore(maxAttempts) {
const result = $.Deferred();
if (maxAttempts <= 0) {
this.logEvent("Stop connection attempts");
slowInternetService.onDisconnected();
result.reject("maxAttemptsReached");
return result;
}
const startConnection = this.buildStartConnection();
this.logEvent("Establishing connection. Attempts left: " + (maxAttempts - 1));
connectionService.isStopped = false;
startConnection().then(() => {
if (this.terminated) {
result.reject();
}
const hubId = this.connection.id;
const connectionInfo = "HID:" + hubId;
this.logEvent("Connected to server! Connection " + connectionInfo);
this.refreshHandle = setTimeout(() => {
// this.terminateConnection();
// connectionService.recoverableError.dispatch();
}, 1000 * 60 * 60);
result.resolve(this);
}).then(null, (message: string = "") => {
this.logEvent("Could not Connect!" + message);
timeService.setTimeout(() => {
if (this.terminated) {
this.logEvent("Reset terminate status for connection before attemt to try new connection");
this.terminated = false;
}
this.establishConnection(maxAttempts - 1).then(() => {
result.resolve(this);
}, () => {
result.reject();
});
}, 100);
});
return result;
}
private async establishConnectionCoreAsync(maxAttempts: number, cancellationToken?: CancelToken) {
if (maxAttempts <= 0) {
this.logEvent("Stop connection attempts");
slowInternetService.onDisconnected();
throw new Error("maxAttemptsReached");
}
if (cancellationToken) {
cancellationToken.throwIfRequested();
}
this.logEvent("Establishing connection. Attempts left: " + (maxAttempts - 1));
connectionService.isStopped = false;
try {
await this.buildStartConnectionAsync();
if (this.terminated) {
throw new Error("SignalR connection was terminated");
}
const hubId = this.connection.id;
const connectionInfo = "HID:" + hubId;
this.logEvent("Connected to server! Connection " + connectionInfo);
return this;
} catch (e) {
this.logEvent("Could not Connect!" + e.message);
return new Promise((resolve, reject) => {
timeService.setTimeout(async () => {
try {
await this.establishConnectionAsync(maxAttempts - 1);
resolve(this);
} catch (err) {
reject();
}
}, 100);
});
}
}
private cancelRefereshConnection() {
if (this.refreshHandle) {
clearTimeout(this.refreshHandle);
}
this.refreshHandle = null;
}
private getAndroidVersion(userAgent = null) {
const ua = userAgent || navigator.userAgent;
const match = ua.match(/Android\s([0-9\.]*)/);
return match ? match[1] : false;
}
private logEvent(message: string, ...params: any[]) {
if (debugSettings.connection.signalR) {
// tslint:disable-next-line:no-console
console.log(message, params);
}
appInsights.trackTrace(message);
}
}