From a63c25cc8479b8477ddd350a6b0a14da7d2d55ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Sat, 25 Jul 2026 11:25:15 -0300 Subject: [PATCH] frontend: libs: MAVLink2Rest: Fix orphan WebSocket reconnects on close Disable reconnect on intentional close and close the previous send socket in setBaseUrl so probe/url changes cannot leave zombie sockets. --- .../src/libs/MAVLink2Rest/Endpoint.ts | 32 ++++++++++++-- core/frontend/src/libs/MAVLink2Rest/index.ts | 44 +++++++++++++++---- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/core/frontend/src/libs/MAVLink2Rest/Endpoint.ts b/core/frontend/src/libs/MAVLink2Rest/Endpoint.ts index 8e3d45529b..543411ee45 100644 --- a/core/frontend/src/libs/MAVLink2Rest/Endpoint.ts +++ b/core/frontend/src/libs/MAVLink2Rest/Endpoint.ts @@ -10,7 +10,14 @@ export default class Endpoint { latestData: any = null + private intentionalClose = false + + private reconnectTimeout: number | null = null + + private url: string + constructor(url: string) { + this.url = url this.socket = this.createSocket(url) } @@ -19,7 +26,8 @@ export default class Endpoint { * @param {string} url */ updateUrl(url: string): void { - this.socket.close() + this.url = url + this.closeIntentionally() this.socket = this.createSocket(url) } @@ -29,6 +37,7 @@ export default class Endpoint { * @returns WebSocket */ createSocket(url: string): WebSocket { + this.intentionalClose = false const socket = new WebSocket(url) socket.onmessage = (message: MessageEvent): void => { this.latestData = JSON.parse(message.data) @@ -37,8 +46,12 @@ export default class Endpoint { } } socket.onclose = () => { - setTimeout(() => { - this.socket = this.createSocket(url) + if (this.intentionalClose) { + return + } + this.reconnectTimeout = window.setTimeout(() => { + this.reconnectTimeout = null + this.socket = this.createSocket(this.url) }, 5000) } return socket @@ -60,4 +73,17 @@ export default class Endpoint { removeListener(listener: Listener): void { this.listeners = this.listeners.filter((item) => item !== listener) } + + private closeIntentionally(): void { + this.intentionalClose = true + if (this.reconnectTimeout !== null) { + clearTimeout(this.reconnectTimeout) + this.reconnectTimeout = null + } + this.socket.onclose = null + this.socket.onmessage = null + if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) { + this.socket.close() + } + } } diff --git a/core/frontend/src/libs/MAVLink2Rest/index.ts b/core/frontend/src/libs/MAVLink2Rest/index.ts index 63bed84630..683b27beff 100644 --- a/core/frontend/src/libs/MAVLink2Rest/index.ts +++ b/core/frontend/src/libs/MAVLink2Rest/index.ts @@ -3,13 +3,13 @@ import axios from 'axios' +import autopilot_data from '@/store/autopilot' import { Dictionary } from '@/types/common' import Endpoint from './Endpoint' import Listener from './Listener' +import { MavCmd, MAVLinkType } from './mavlink2rest-ts/messages/mavlink2rest-enum' import messageId from './MessageID' -import autopilot_data from '@/store/autopilot' -import { MAVLinkType, MavCmd } from './mavlink2rest-ts/messages/mavlink2rest-enum' class Mavlink2RestManager { baseUrl: string @@ -21,6 +21,10 @@ class Mavlink2RestManager { private socket: WebSocket | undefined = undefined + private intentionalClose = false + + private reconnectTimeout: number | null = null + private static instance: Mavlink2RestManager private constructor() { @@ -76,6 +80,7 @@ class Mavlink2RestManager { endpoint.updateUrl(`${url}?filter=${name}`) }) + this.closeSendSocket() this.socket = this.createSocket(`${url}?filter=THIS_SHOULD_ONLY_SEND`) } @@ -85,9 +90,14 @@ class Mavlink2RestManager { * @returns WebSocket */ createSocket(url: string): WebSocket { + this.intentionalClose = false const socket = new WebSocket(url) socket.onclose = () => { - setTimeout(() => { + if (this.intentionalClose) { + return + } + this.reconnectTimeout = window.setTimeout(() => { + this.reconnectTimeout = null this.socket = this.createSocket(url) }, 5000) } @@ -97,9 +107,26 @@ class Mavlink2RestManager { return socket } + private closeSendSocket(): void { + if (!this.socket) { + return + } + this.intentionalClose = true + if (this.reconnectTimeout !== null) { + clearTimeout(this.reconnectTimeout) + this.reconnectTimeout = null + } + this.socket.onclose = null + this.socket.onerror = null + if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) { + this.socket.close() + } + this.socket = undefined + } + /** * Helper for COMMAND_LONG messages - * @param command + * @param command */ sendCommandLong( @@ -110,7 +137,7 @@ class Mavlink2RestManager { param4: number | undefined = undefined, param5: number | undefined = undefined, param6: number | undefined = undefined, - param7: number | undefined = undefined + param7: number | undefined = undefined, ) { mavlink2rest.sendMessage({ header: { @@ -137,8 +164,7 @@ class Mavlink2RestManager { }) } - - waitForAck(command: MavCmd, timeout_seconds: number = 3): Promise { + waitForAck(command: MavCmd, timeout_seconds = 3): Promise { return new Promise((resolve, reject) => { const listener = this.startListening(MAVLinkType.COMMAND_ACK).setCallback( (content) => { @@ -149,12 +175,12 @@ class Mavlink2RestManager { if (message.command.type === command) { resolve(message) } - } + }, ).setFrequency(0) setTimeout(() => { listener.discard() reject(new Error(`timed out waiting for answer for ${command}`)) - }, timeout_seconds*1000) + }, timeout_seconds * 1000) }) }