lavva.exalushome
Version:
Library implementing communication and abstraction layers for ExalusHome system
909 lines • 44.3 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import * as signalR from "@microsoft/signalr";
import { DataFrame, Method, Status, } from "../DataFrame";
import { ConnectionResult, AuthorizationInfo, ConnectionState, StreamError, BrokerInfo, BrokerEnvironment, } from "./IExalusConnectionService";
import { TypedEvent } from "../TypedEvent";
import { Api } from "../Api";
import { LoggerService } from "./Logging/LoggerService";
import { Event } from "../Event";
import { ControllerConfigurationService } from "./Controller/ControllerConfigurationService";
import { WebApiCacheService } from "./WebApi/WebApiCacheService";
import { SessionService } from "./Session/SessionService";
import { LogLevel } from "./Logging/ILoggerService";
import { DependencyContainer } from "../DependencyContainer";
import { AppState } from "./IAppStateService";
import { AppStateService } from "./AppStateService";
class FastRetryPolicy {
nextRetryDelayInMilliseconds() {
return 1000; // constant back-off
}
}
const TRANSPORTS = [
signalR.HttpTransportType.WebSockets,
signalR.HttpTransportType.ServerSentEvents,
signalR.HttpTransportType.LongPolling,
];
export class SignalrLogger {
constructor() {
this._log = Api.Get(LoggerService.ServiceName);
}
log(level, message) {
if (level < ExalusConnectionService.SignalRLogLevel)
return;
message = `[SignalR] ${message}`;
switch (level) {
case signalR.LogLevel.Critical:
case signalR.LogLevel.Error:
this._log.Error(message);
break;
case signalR.LogLevel.Warning:
this._log.Warning(message);
break;
case signalR.LogLevel.Information:
this._log.Info(message);
break;
default:
this._log.Debug(message);
break;
}
}
}
export class ExalusConnectionService {
constructor() {
this._connectTask = null;
this._authorizeTask = null;
this._pingTimerId = null;
this._consecutivePingFailures = 0;
this._disconnectedOnPurpose = false;
this._address = "packets-broker1.tr7.pl";
this._serversBrokerAddress = "https://servers-broker.tr7.pl";
this._serversBrokerAddressList = [
"https://servers-broker.tr7.pl",
"https://broker.tr7.pl",
"https://dev-broker.tr7.pl",
];
this._allBrokersChecked = false;
this._packetsBrokerServers = [
"packets-broker1.tr7.pl",
"packets-broker2.tr7.pl"
];
this._connectedServerAddressResult = null;
this._connectedServerAddressWaiters = [];
this._isAuthorized = false;
this._lastReceivedPacket = Date.now();
this._everConnected = false;
this._pendingRequests = new Map();
this._log = Api.Get(LoggerService.ServiceName);
this._appState = Api.Get(AppStateService.ServiceName);
this._controllerConfiguration = null;
this._cache = null;
this._session = null;
this._dataReceivedEvent = new TypedEvent();
this._pongReceivedEvent = new Event();
this._authorizationReceivedEvent = new TypedEvent();
this._registrationReceivedEvent = new TypedEvent();
this._streamStartedEvent = new TypedEvent();
this._connectionStateChangedEvent = new TypedEvent();
this._errorOccuredEvent = new TypedEvent();
this._tryReconnect = () => {
if (this._connectTask)
return; // avoid piling reconnects
if (navigator.onLine) {
this.validateConnectionToController().then((isConnected) => {
if (!isConnected) {
this._log.Debug(ExalusConnectionService.ServiceName, "Connection lost → reconnecting...");
void this.serialisedConnect();
}
});
}
};
this._onVisibility = () => {
if (this._connectTask)
return;
if (document.visibilityState === "visible") {
this._log.Debug(ExalusConnectionService.ServiceName, "Page visible → connection check");
this.validateConnectionToController().then((isConnected) => {
if (!isConnected) {
this._log.Debug(ExalusConnectionService.ServiceName, "Connection lost → reconnecting...");
void this.serialisedConnect();
}
});
}
};
this._onHidden = () => { };
this._onVisible = () => {
if (this._connectTask)
return;
this.validateConnectionToController().then((isConnected) => {
if (!isConnected) {
this._log.Debug(ExalusConnectionService.ServiceName, "Connection lost → reconnecting...");
void this.serialisedConnect();
}
});
};
this._appState.OnAppStateChanged().Subscribe((newState) => __awaiter(this, void 0, void 0, function* () {
var _a;
this._log.Debug(ExalusConnectionService.ServiceName, `App state changed: ${newState}`);
switch (newState) {
case AppState.ExitedLowPowerMode:
case AppState.ReturnedFromSuspension:
this._log.Debug(ExalusConnectionService.ServiceName, `Handling app state change: ${newState}`);
if (yield this.RestoreConnectionAsync())
(_a = this._session) === null || _a === void 0 ? void 0 : _a.RestoreSessionAsync();
break;
case AppState.Connected:
this.resolveConnectedBrokerInfo(this._serversBrokerAddress, this._address);
}
}));
}
GetServiceName() {
return ExalusConnectionService.ServiceName;
}
GetAuthorizationInfo() {
return this._serialId && this._PIN ? new AuthorizationInfo(this._serialId, this._PIN) : null;
}
GetControllerSerialNumber() {
return this._serialId;
}
GetControllerPin() {
return this._PIN;
}
SetServersBrokerAddress(a) {
this._serversBrokerAddress = a;
}
SetDefaultPacketsBrokerAddress(a) {
this._address = a;
}
GetConnectedBrokerInfoAsync() {
return __awaiter(this, arguments, void 0, function* (timeoutMs = 8000) {
if (this._connectedServerAddressResult !== null) {
return this._connectedServerAddressResult;
}
return new Promise((resolve, reject) => {
let timeoutId;
const waiter = (brokerInfo) => {
if (timeoutId !== undefined)
window.clearTimeout(timeoutId);
resolve(brokerInfo);
};
this._connectedServerAddressWaiters.push(waiter);
if (timeoutMs === null)
return;
timeoutId = window.setTimeout(() => {
const idx = this._connectedServerAddressWaiters.indexOf(waiter);
if (idx >= 0)
this._connectedServerAddressWaiters.splice(idx, 1);
reject(new Error("GetConnectedBrokerInfoAsync timed out"));
}, timeoutMs);
});
});
}
EnablePacketsLogging() {
window["packets"] = true;
}
DisablePacketsLogging() {
window["packets"] = false;
}
SubscribeTo(resourceId, handler) {
const h = (f) => {
if (f.Resource === resourceId)
handler(f);
};
this._dataReceivedEvent.Subscribe(h);
return () => this._dataReceivedEvent.Unsubscribe(h);
}
reauthorizeIfPossible() {
return __awaiter(this, void 0, void 0, function* () {
if (!this._serialId || !this._PIN) {
this._isAuthorized = false;
return false;
}
const ok = yield this.AuthorizeAsync(new AuthorizationInfo(this._serialId, this._PIN));
this._isAuthorized = ok;
return ok;
});
}
GetServerAddressAsync() {
return __awaiter(this, void 0, void 0, function* () {
if (!this._serialId) {
this._log.Warning(ExalusConnectionService.ServiceName, "GetServerAddressAsync() – serialId not set");
return null;
}
const url = `${this._serversBrokerAddress}/api/connections/broker/whichserver/${this._serialId}`;
try {
const resp = yield fetch(url);
switch (resp.status) {
case 200: {
const addr = (yield resp.text()).trim();
if (addr) {
this._log.Debug(ExalusConnectionService.ServiceName, `Broker address resolved: ${addr}`);
return addr;
}
this._log.Warning(ExalusConnectionService.ServiceName, "Broker returned empty address");
return null;
}
case 204: {
if (!this._allBrokersChecked) {
this._log.Warning(ExalusConnectionService.ServiceName, `Servers-broker ${this._serversBrokerAddress} returned 204 (no content) – controller not found on this server`);
yield this.rotateServersBroker();
return this.GetServerAddressAsync();
}
else {
this._log.Error(ExalusConnectionService.ServiceName, "All servers-brokers checked, controller not found – likely offline");
return null;
}
}
default: {
this._log.Warning(ExalusConnectionService.ServiceName, `GetServerAddressAsync() – HTTP ${resp.status}`);
return null;
}
}
}
catch (err) {
this._log.Error(ExalusConnectionService.ServiceName, String(err));
return null;
}
});
}
rotateServersBroker() {
return __awaiter(this, void 0, void 0, function* () {
const idx = this._serversBrokerAddressList.indexOf(this._serversBrokerAddress);
const next = idx + 1 < this._serversBrokerAddressList.length ? idx + 1 : 0;
this._serversBrokerAddress = this._serversBrokerAddressList[next];
this._allBrokersChecked = next === 0;
this._log.Info(ExalusConnectionService.ServiceName, `Switching servers-broker to: ${this._serversBrokerAddress}`);
});
}
ConnectAsync(address) {
return __awaiter(this, void 0, void 0, function* () {
this._address = address;
return this.serialisedConnect();
});
}
ConnectAndAuthorizeAsync(info) {
return __awaiter(this, void 0, void 0, function* () {
this.resetConnectedServerAddressResolution();
this._serialId = info.serialNumber;
this._PIN = info.pin;
Api.WorksInContextOf = this._serialId;
// reset servers-broker rotation before each attempt
this._allBrokersChecked = false;
this._serversBrokerAddress = this._serversBrokerAddressList[0];
const packetsBroker = yield this.GetServerAddressAsync();
if (packetsBroker) {
this.SetDefaultPacketsBrokerAddress(packetsBroker);
if ((yield this.serialisedConnect()) === ConnectionResult.Connected && (yield this.AuthorizeAsync(info))) {
return ConnectionResult.Connected;
}
}
for (const host of this._packetsBrokerServers) {
this._log.Info(ExalusConnectionService.ServiceName, `Fallback broker: ${host}`);
this.SetDefaultPacketsBrokerAddress(host);
if ((yield this.serialisedConnect()) === ConnectionResult.Connected &&
(yield this.AuthorizeAsync(info))) {
this._disconnectedOnPurpose = false;
return ConnectionResult.Connected;
}
}
return ConnectionResult.FailedToConnect;
});
}
resetConnectedServerAddressResolution() {
this._connectedServerAddressResult = null;
}
resolveConnectedBrokerInfo(brokerAddress, controllerAddress) {
const isDev = this._serversBrokerAddress.includes('dev-broker.tr7.pl') && !this._packetsBrokerServers.any(s => { var _a; return s.includes((_a = this._currentAddress) !== null && _a !== void 0 ? _a : ""); });
this._connectedServerAddressResult = new BrokerInfo(brokerAddress, controllerAddress, isDev ? BrokerEnvironment.Development : BrokerEnvironment.Production);
while (this._connectedServerAddressWaiters.length > 0) {
const waiter = this._connectedServerAddressWaiters.shift();
waiter === null || waiter === void 0 ? void 0 : waiter(this._connectedServerAddressResult);
}
}
AuthorizeAsync(info) {
return __awaiter(this, void 0, void 0, function* () {
if (this._authorizeTask)
return this._authorizeTask;
this._authorizeTask = new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
var _a;
const timeoutId = window.setTimeout(() => {
this._authorizationReceivedEvent.Unsubscribe(onAuth);
this._isAuthorized = false;
this._log.Debug(ExalusConnectionService.ServiceName, `AuthorizeAsync timed out for id ${timeoutId}`);
resolve(false);
}, 2000);
this._log.Debug(ExalusConnectionService.ServiceName, `AuthorizeAsync - created timeoutId ${timeoutId}`);
const onAuth = (ok) => {
window.clearTimeout(timeoutId);
this._authorizationReceivedEvent.Unsubscribe(onAuth);
this._log.Debug(ExalusConnectionService.ServiceName, `Authorization → ${ok}`);
this._isAuthorized = ok;
if (ok)
this._connectionStateChangedEvent.Invoke(ConnectionState.ConnectedAndAuthorized);
this._log.Debug(ExalusConnectionService.ServiceName, `AuthorizeAsync isAuthorized: ${ok} cleared timeoutId ${timeoutId}`);
resolve(ok);
};
this._authorizationReceivedEvent.Subscribe(onAuth);
this._log.Debug(ExalusConnectionService.ServiceName, `Authorizing to ${info.SerialNumber}... for timoeoutId ${timeoutId}`);
yield ((_a = this._connection) === null || _a === void 0 ? void 0 : _a.send("AuthorizeTo", info.SerialNumber, info.PIN));
})).finally(() => { this._authorizeTask = null; });
return this._authorizeTask;
});
}
IsConnected() {
var _a;
return ((_a = this._connection) === null || _a === void 0 ? void 0 : _a.state) === signalR.HubConnectionState.Connected;
}
DisconnectAsync() {
return __awaiter(this, void 0, void 0, function* () {
var _a;
this._disconnectedOnPurpose = true;
this._log.Debug(ExalusConnectionService.ServiceName, "Disconnecting...");
yield ((_a = this._connection) === null || _a === void 0 ? void 0 : _a.stop());
this.cleanup();
});
}
SendAsync(dataFrame_1) {
return __awaiter(this, arguments, void 0, function* (dataFrame, logTx = false) {
var _a, _b;
if (!this.IsConnected())
throw new Error("Not connected");
const sendCore = () => __awaiter(this, void 0, void 0, function* () {
yield this._connection.invoke("SendTo", this._serialId, dataFrame);
});
const dump = window["packets"] === true;
if (dump || logTx) {
this._log.Debug(ExalusConnectionService.ServiceName, `⇢ ${dataFrame.Resource} ${dataFrame.Method} ${dataFrame.TransactionId}` +
(dump ? `\n${JSON.stringify(dataFrame, null, 2)}` : ""));
}
try {
yield sendCore();
return true;
}
catch (err) {
const msg = String((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err).toLowerCase();
if (msg.includes("unauthorized")) {
// 1) try to re-authorize on current connection
const reauthOk = yield this.reauthorizeIfPossible();
// 2) if still unauthorized, re-resolve broker and reconnect
if (!reauthOk) {
// reset broker rotation before resolving again
this._allBrokersChecked = false;
this._serversBrokerAddress = this._serversBrokerAddressList[0];
const addr = yield this.GetServerAddressAsync();
if (addr && addr !== this._address) {
this._log.Info(ExalusConnectionService.ServiceName, `Switching to resolved broker: ${addr}`);
try {
yield ((_b = this._connection) === null || _b === void 0 ? void 0 : _b.stop());
}
catch (_c) { }
this.cleanup();
yield this.ConnectAsync(addr);
yield this.reauthorizeIfPossible();
}
}
if (this.IsConnected() && this._isAuthorized) {
try {
yield sendCore();
return true;
}
catch (retryError) {
err = retryError;
}
}
}
this._log.Error(ExalusConnectionService.ServiceName, String(err));
return false;
}
});
}
RestoreConnectionAsync() {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
if (this._disconnectedOnPurpose)
return false;
const auth = this.GetAuthorizationInfo();
if (auth === null) {
(_a = DependencyContainer.Log) === null || _a === void 0 ? void 0 : _a.Error(ExalusConnectionService.ServiceName, "RestoreSessionAsync failed, no authorization info");
return false;
}
const start = Date.now();
const maxTime = 30 * 60 * 1000; // 30 minutes
let attempt = 0;
do {
if (this.IsConnected()) {
if (yield this.AuthorizeAsync(auth))
return true;
}
const result = yield this.ConnectAndAuthorizeAsync(auth);
if (result === ConnectionResult.Connected)
return true;
const delay = Math.min(30000, 5000 * Math.pow(2, attempt++));
yield new Promise(r => setTimeout(r, delay));
this._allBrokersChecked = false;
this._serversBrokerAddress = this._serversBrokerAddressList[0];
} while (Date.now() - start < maxTime);
(_b = DependencyContainer.Log) === null || _b === void 0 ? void 0 : _b.Error(ExalusConnectionService.ServiceName, "RestoreSessionAsync failed, authorization failed");
return false;
});
}
SendAndWaitForResponseAsync(dataFrame_1, timeout_1, useCache_1) {
return __awaiter(this, arguments, void 0, function* (dataFrame, timeout, useCache, logTransmission = true, retryOnUnauthorized = true) {
var _a, _b, _c;
if (dataFrame.Method === Method.Get && useCache) {
if (!(yield ((_a = this._controllerConfiguration) === null || _a === void 0 ? void 0 : _a.DidCofigurationChangeAsync()))) {
const cached = (_b = this._cache) === null || _b === void 0 ? void 0 : _b.GetCache(dataFrame);
if (cached)
return cached;
}
}
if (!this.IsConnected()) {
const restored = yield this.RestoreConnectionAsync();
if (!restored)
throw new Error("Failed to restore connection!");
}
if (dataFrame.Resource !== "/users/user/login")
yield ((_c = this._session) === null || _c === void 0 ? void 0 : _c.WaitForSessionCreationAsync());
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
const timeoutId = window.setTimeout(() => {
this._dataReceivedEvent.Unsubscribe(onRx);
this._pendingRequests.delete(dataFrame.TransactionId);
reject(new signalR.TimeoutError("Response timeout"));
}, timeout);
const onRx = (frame) => {
var _a;
if (frame.TransactionId !== dataFrame.TransactionId)
return;
window.clearTimeout(timeoutId);
this._dataReceivedEvent.Unsubscribe(onRx);
this._pendingRequests.delete(dataFrame.TransactionId);
if (dataFrame.Method === Method.Get && useCache && frame.Status !== Status.UserIsNotLoggedIn)
(_a = this._cache) === null || _a === void 0 ? void 0 : _a.Cache(frame);
if (retryOnUnauthorized && frame.Status === Status.UserIsNotLoggedIn) {
void (() => __awaiter(this, void 0, void 0, function* () {
var _a;
try {
yield ((_a = this._session) === null || _a === void 0 ? void 0 : _a.RestoreSessionAsync());
resolve(yield this.SendAndWaitForResponseAsync(dataFrame, timeout, useCache, logTransmission, false));
}
catch (error) {
reject(error);
}
}))();
return;
}
resolve(frame);
};
this._dataReceivedEvent.Subscribe(onRx);
this._pendingRequests.set(dataFrame.TransactionId, {
resolve: (v) => resolve(v),
reject,
timeoutId,
});
if (!(yield this.SendAsync(dataFrame, logTransmission))) {
window.clearTimeout(timeoutId);
this._dataReceivedEvent.Unsubscribe(onRx);
this._pendingRequests.delete(dataFrame.TransactionId);
reject(new Error("Failed to send request"));
}
}));
});
}
SendAndHandleResponseAsync(dataFrame_1, timeout_1, dataHandler_1) {
return __awaiter(this, arguments, void 0, function* (dataFrame, timeout, dataHandler, logTransmission = true) {
var _a;
if (!this.IsConnected())
throw new Error("Connection is not established");
if (dataFrame.Resource !== "/users/user/login")
yield ((_a = this._session) === null || _a === void 0 ? void 0 : _a.WaitForSessionCreationAsync());
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
const resetTimeout = () => window.setTimeout(() => {
cleanup();
reject(new signalR.TimeoutError("MultiDataResponse timeout"));
}, timeout);
let tid = resetTimeout();
const cleanup = () => {
window.clearTimeout(tid);
this._dataReceivedEvent.Unsubscribe(onRx);
this._pendingRequests.delete(dataFrame.TransactionId);
};
const onRx = (frame) => {
if (frame.TransactionId !== dataFrame.TransactionId)
return;
window.clearTimeout(tid);
switch (frame.Status) {
case Status.MultiDataResponseStart:
case Status.MultiDataResponse:
dataHandler(frame);
tid = resetTimeout();
break;
case Status.MultiDataResponseStop:
case Status.OK:
dataHandler(frame);
cleanup();
resolve();
break;
default:
cleanup();
reject(new Error(`Unexpected status ${frame.Status}`));
}
};
this._dataReceivedEvent.Subscribe(onRx);
this._pendingRequests.set(dataFrame.TransactionId, {
resolve: () => resolve(),
reject,
timeoutId: tid,
});
if (!(yield this.SendAsync(dataFrame, logTransmission))) {
cleanup();
reject(new Error("Failed to send request"));
}
}));
});
}
SendAndHandleStreamAsync(dataFrame_1, streamHandler_1) {
return __awaiter(this, arguments, void 0, function* (dataFrame, streamHandler, logTransmission = true) {
var _a;
if (!this.IsConnected())
throw new Error("Connection is not established");
if (dataFrame.Resource !== "/users/user/login")
yield ((_a = this._session) === null || _a === void 0 ? void 0 : _a.WaitForSessionCreationAsync());
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
const timeoutId = window.setTimeout(() => {
this._streamStartedEvent.Unsubscribe(onStreamStart);
reject(new signalR.TimeoutError("Stream registration timeout"));
}, 8000);
const onStreamStart = (streamId) => {
if (streamId !== dataFrame.TransactionId)
return;
window.clearTimeout(timeoutId);
this._streamStartedEvent.Unsubscribe(onStreamStart);
this._connection
.stream("WatchStream", streamId)
.subscribe({
next: (item) => streamHandler.Next(item),
complete: () => {
streamHandler.Complete();
resolve();
},
error: (err) => {
streamHandler.Error(err);
reject(new StreamError(String(err)));
},
});
};
this._streamStartedEvent.Subscribe(onStreamStart);
if (!(yield this.SendAsync(dataFrame, logTransmission))) {
window.clearTimeout(timeoutId);
this._streamStartedEvent.Unsubscribe(onStreamStart);
reject(new Error("Failed to send request"));
}
}));
});
}
PingControllerAsync() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.IsConnected() || !this._isAuthorized)
return false;
if (Date.now() - this._lastReceivedPacket < ExalusConnectionService.PING_INTERVAL_MS)
return false;
const frame = new DataFrame();
frame.Resource = "/system/ping";
frame.Method = Method.Get;
try {
yield this.SendAndWaitForResponseAsync(frame, 2000, false, false);
return true;
}
catch (_a) {
return false;
}
});
}
OnDataReceivedEvent() {
return this._dataReceivedEvent;
}
OnConnectionStateChangedEvent() {
return this._connectionStateChangedEvent;
}
OnErrorOccuredEvent() {
return this._errorOccuredEvent;
}
serialisedConnect() {
if (this._connectTask)
return this._connectTask;
this._connectTask = this.connectCore().finally(() => (this._connectTask = null));
return this._connectTask;
}
// Wait until connection reaches any of the target states (or timeout)
waitForState(targets_1) {
return __awaiter(this, arguments, void 0, function* (targets, timeoutMs = 10000) {
var _a;
const start = Date.now();
while (true) {
const st = (_a = this._connection) === null || _a === void 0 ? void 0 : _a.state;
if (st != null && targets.includes(st))
return;
if (Date.now() - start > timeoutMs)
throw new Error(`waitForState timeout; state=${st}`);
yield new Promise(r => setTimeout(r, 50));
}
});
}
connectCore() {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
if (!this._address)
return ConnectionResult.ControllerIsNotConnected;
// Rebuild connection when broker address changes
if (this._connection && this._currentAddress !== this._address) {
this._log.Info(ExalusConnectionService.ServiceName, "Broker changed → rebuilding connection");
try {
yield this._connection.stop();
}
catch (_c) { }
this.cleanup();
}
// If something is already happening – wait rather than fighting it
if (this._connection) {
const s = this._connection.state;
if (s === signalR.HubConnectionState.Connected) {
// Optionally re-auth if we somehow lost auth flag
if (!this._isAuthorized)
yield this.reauthorizeIfPossible();
return ConnectionResult.Connected;
}
if (s === signalR.HubConnectionState.Connecting || s === signalR.HubConnectionState.Reconnecting) {
this._log.Info(ExalusConnectionService.ServiceName, "Existing connection is starting/reconnecting – waiting.");
try {
yield this.waitForState([signalR.HubConnectionState.Connected, signalR.HubConnectionState.Disconnected]);
}
catch ( /* ignore */_d) { /* ignore */ }
if (((_a = this._connection) === null || _a === void 0 ? void 0 : _a.state) === signalR.HubConnectionState.Connected) {
if (!this._isAuthorized)
yield this.reauthorizeIfPossible();
return ConnectionResult.Connected;
}
}
if (s === signalR.HubConnectionState.Disconnecting) {
yield this.waitForState([signalR.HubConnectionState.Disconnected]).catch(() => { });
}
}
this.initializeServices();
this.cleanup(); // ensures no dangling timers/handlers and clears _connection
for (const t of TRANSPORTS) {
const builder = new signalR.HubConnectionBuilder()
.withAutomaticReconnect(new FastRetryPolicy())
.configureLogging(new SignalrLogger())
// ASP.NET Core 8 stateful reconnect (buffering)
.withStatefulReconnect({ bufferSize: 256000 })
.withServerTimeout(ExalusConnectionService.SERVER_TIMOUT_MS)
.withKeepAliveInterval(ExalusConnectionService.PING_INTERVAL_MS - 1000)
.withUrl(`https://${this._address}/broker`, {
// skipNegotiation only for WebSockets
skipNegotiation: t === signalR.HttpTransportType.WebSockets,
transport: t,
});
if (this._log.LogLevel === LogLevel.Debug) {
builder.configureLogging(signalR.LogLevel.Debug);
}
const conn = builder.build();
this._connection = conn;
this._currentAddress = this._address;
this.wireConnectionEvents();
try {
yield conn.start(); // ← single start
yield this.reauthorizeIfPossible(); // ← auth immediately after fresh start
this.startPingLoop();
this._connectionStateChangedEvent.Invoke(ConnectionState.Connected);
return ConnectionResult.Connected;
}
catch (err) {
this._log.Warning(ExalusConnectionService.ServiceName, `Transport ${signalR.HttpTransportType[t]} failed (${err === null || err === void 0 ? void 0 : err.message}).`);
// Use local 'conn' because onclose→cleanup may null out this._connection
try {
yield conn.stop();
}
catch (_e) { }
try {
(_b = conn === null || conn === void 0 ? void 0 : conn.off) === null || _b === void 0 ? void 0 : _b.call(conn);
}
catch (_f) { }
// try next transport
}
}
this._connectionStateChangedEvent.Invoke(ConnectionState.Failed);
return ConnectionResult.FailedToConnect;
});
}
waitForReconnection(timeoutMs = 0) {
return new Promise((resolve, reject) => {
if (!this._connection || this._connection.state !== signalR.HubConnectionState.Reconnecting) {
reject(new Error("Connection is not in reconnecting state"));
return;
}
let timeoutId = undefined;
if (timeoutMs > 0) {
timeoutId = window.setTimeout(() => {
this._connectionStateChangedEvent.Unsubscribe(handler);
reject(new Error("Reconnection timeout"));
}, timeoutMs);
}
const handler = (state) => {
if (state === ConnectionState.Connected) {
if (timeoutId)
window.clearTimeout(timeoutId);
this._connectionStateChangedEvent.Unsubscribe(handler);
resolve();
}
else if (state === ConnectionState.Disconnected || state === ConnectionState.Failed) {
if (timeoutId)
window.clearTimeout(timeoutId);
this._connectionStateChangedEvent.Unsubscribe(handler);
reject(new Error("Connection closed during reconnection"));
}
};
this._connectionStateChangedEvent.Subscribe(handler);
if (this._connection.state !== signalR.HubConnectionState.Reconnecting) {
if (timeoutId)
window.clearTimeout(timeoutId);
this._connectionStateChangedEvent.Unsubscribe(handler);
if (this._connection.state === signalR.HubConnectionState.Connected) {
resolve();
}
else {
reject(new Error(`Connection is in unexpected state: ${this._connection.state}`));
}
}
});
}
initializeServices() {
this._controllerConfiguration = Api.Get(ControllerConfigurationService.ServiceName);
this._cache = Api.Get(WebApiCacheService.ServiceName);
this._session = Api.Get(SessionService.ServiceName);
}
wireConnectionEvents() {
if (!this._connection)
return;
this._connection.on("Pong", () => {
this._lastReceivedPacket = Date.now();
this._pongReceivedEvent.Invoke();
});
// Server calls this; make it quiet and useful
this._connection.on("controllerdisconnected", () => {
this._isAuthorized = false;
this._connectionStateChangedEvent.Invoke(ConnectionState.Disconnected);
});
this._connection.on("Authorization", (ok) => this._authorizationReceivedEvent.Invoke(ok));
this._connection.on("Registration", (d) => this._registrationReceivedEvent.Invoke(d));
this._connection.on("SendError", (s, d) => {
if (s.startsWith("NotAuthorized:")) {
this.AuthorizeAsync(new AuthorizationInfo(this._serialId, this._PIN));
}
else {
this._errorOccuredEvent.Invoke([s, d]);
}
});
this._connection.on("Data", (_, json) => {
this._lastReceivedPacket = Date.now();
if (window["packets"] === true)
this._log.Debug(ExalusConnectionService.ServiceName, `Received: ${JSON.stringify(JSON.parse(json), null, 2)}`);
this._dataReceivedEvent.Invoke(JSON.parse(json));
});
this._connection.onclose(() => {
this._isAuthorized = false;
this.cleanup();
this._connectionStateChangedEvent.Invoke(ConnectionState.Disconnected);
});
this._connection.onreconnecting(() => this._connectionStateChangedEvent.Invoke(ConnectionState.Reconnecting));
this._connection.onreconnected(() => __awaiter(this, void 0, void 0, function* () {
var _a;
this._connectionStateChangedEvent.Invoke(ConnectionState.Connected);
if (yield this.RestoreConnectionAsync())
(_a = this._session) === null || _a === void 0 ? void 0 : _a.RestoreSessionAsync();
}));
}
startPingLoop() {
if (this._pingTimerId !== null)
clearInterval(this._pingTimerId);
this._pingTimerId = window.setInterval(() => void this.pingOnce(), ExalusConnectionService.PING_INTERVAL_MS);
window.addEventListener("online", this._tryReconnect);
window.addEventListener("offline", this._tryReconnect);
document.addEventListener("visibilitychange", this._onVisibility, true);
window.addEventListener("pagehide", this._onHidden, true);
window.addEventListener("pageshow", this._onVisible, true);
}
isBusy() {
var _a;
const s = (_a = this._connection) === null || _a === void 0 ? void 0 : _a.state;
return s === signalR.HubConnectionState.Connecting
|| s === signalR.HubConnectionState.Disconnecting
|| s === signalR.HubConnectionState.Reconnecting;
}
pingOnce() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.IsConnected() || this.isBusy())
return;
if (Date.now() - this._lastReceivedPacket < ExalusConnectionService.PING_INTERVAL_MS)
return;
const ok = yield this.PingControllerAsync();
if (ok) {
this._consecutivePingFailures = 0;
}
else if (++this._consecutivePingFailures >= ExalusConnectionService.MAX_CONSECUTIVE_PING_FAILURES) {
this._consecutivePingFailures = 0;
this._log.Warning(ExalusConnectionService.ServiceName, "Ping failed too many times → reconnecting...");
// Prefer a fresh connect; re-authorization will trigger after start()
void this.serialisedConnect();
}
});
}
validateConnectionToController() {
return __awaiter(this, void 0, void 0, function* () {
var _a;
if (this.IsConnected()) {
const frame = new DataFrame();
frame.Resource = "/system/ping";
frame.Method = Method.Get;
try {
const res = yield this.SendAndWaitForResponseAsync(frame, ExalusConnectionService.SERVER_TIMOUT_MS + 1000, false, false);
if ((res === null || res === void 0 ? void 0 : res.Status) !== Status.OK) {
this._log.Warning(ExalusConnectionService.ServiceName, `Connection check failed, ${res == null ? "result is null" : `status: ${res.Status}`}`);
return false;
}
else if (((_a = this._connection) === null || _a === void 0 ? void 0 : _a.state) === signalR.HubConnectionState.Reconnecting) {
this._log.Warning(ExalusConnectionService.ServiceName, "Connection check failed, connection is in reconnecting state");
return false;
}
else {
this._log.Info(ExalusConnectionService.ServiceName, "Connection still active");
return true;
}
}
catch (_b) {
return false;
}
}
else {
this._log.Warning(ExalusConnectionService.ServiceName, "Connection was lost");
return false;
}
});
}
cleanup() {
var _a, _b;
if (this._pingTimerId !== null) {
clearInterval(this._pingTimerId);
this._pingTimerId = null;
}
// reject all outstanding waiters
for (const [, p] of this._pendingRequests) {
window.clearTimeout(p.timeoutId);
p.reject(new Error("Connection lost"));
}
this._pendingRequests.clear();
window.removeEventListener("online", this._tryReconnect);
window.removeEventListener("offline", this._tryReconnect);
document.removeEventListener("visibilitychange", this._onVisibility, true);
window.removeEventListener("pagehide", this._onHidden, true);
window.removeEventListener("pageshow", this._onVisible, true);
try {
(_b = (_a = this._connection) === null || _a === void 0 ? void 0 : _a.off) === null || _b === void 0 ? void 0 : _b.call(_a);
}
catch (_c) { }
this._connection = undefined;
this._currentAddress = undefined;
}
}
ExalusConnectionService.PING_INTERVAL_MS = 5000;
ExalusConnectionService.MAX_CONSECUTIVE_PING_FAILURES = 6;
ExalusConnectionService.SERVER_TIMOUT_MS = 10000; // keep the original name
ExalusConnectionService.SignalRLogLevel = signalR.LogLevel.Warning;
ExalusConnectionService.ServiceName = "ExalusConnectionService";
//# sourceMappingURL=ExalusConnectionService.js.map