UNPKG

lavva.exalushome

Version:

Library implementing communication and abstraction layers for ExalusHome system

909 lines 44.3 kB
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