diff --git a/Cargo.lock b/Cargo.lock index a12cea0f8..60445962b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1387,7 +1387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -2073,7 +2073,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2347,7 +2347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2865,7 +2865,7 @@ dependencies = [ "gobject-sys 0.21.5", "libc", "system-deps 7.0.8", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3488,7 +3488,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.5", + "socket2 0.5.10", "system-configuration", "tokio", "tower-service", @@ -4316,7 +4316,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.20", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4485,7 +4485,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4928,7 +4928,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -5602,7 +5602,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.5", + "socket2 0.5.10", "thiserror 2.0.20", "tokio", "tracing", @@ -5641,9 +5641,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.5", + "socket2 0.5.10", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6198,7 +6198,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6257,7 +6257,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6899,7 +6899,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7565,8 +7565,8 @@ dependencies = [ [[package]] name = "tauri-plugin-mihomo" -version = "0.5.6" -source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo?branch=main#8bf567b9e2230cdd02f9b8c9774fb8eb0d71af1e" +version = "0.6.0" +source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo?branch=main#17c7757389ca73d29882753ecf7a160de29aabbe" dependencies = [ "arc-swap", "clashmap", @@ -7863,7 +7863,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8582,7 +8582,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.20", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8685,7 +8685,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -9270,7 +9270,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -9874,7 +9874,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" dependencies = [ "cfg-if", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e990449d..791e94bc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -234,7 +234,7 @@ importers: version: 2.5.1(react@19.2.8) tauri-plugin-mihomo-api: specifier: github:clash-verge-rev/tauri-plugin-mihomo#main - version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/8bf567b9e2230cdd02f9b8c9774fb8eb0d71af1e + version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/17c7757389ca73d29882753ecf7a160de29aabbe types-pac: specifier: ^1.0.3 version: 1.0.3 @@ -4838,9 +4838,9 @@ packages: resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} engines: {node: '>=18'} - tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/8bf567b9e2230cdd02f9b8c9774fb8eb0d71af1e: - resolution: {gitHosted: true, integrity: sha512-ZUNzoqUI/328gbYuFUw9oKe0BVi/reurZZ2ut1+B8ZgEtZ6dtNgKMea7Kp8UvKTnF543WvI/8RwetH1Fzkflzw==, tarball: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/8bf567b9e2230cdd02f9b8c9774fb8eb0d71af1e} - version: 0.5.6 + tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/17c7757389ca73d29882753ecf7a160de29aabbe: + resolution: {gitHosted: true, integrity: sha512-DoJmecNZhwYzj8Gy0tpRoJDaWBLXIM6zwOBu2ITLKgFm+TUaMyqHmJ/tWzRfeqHX8izkgMi/TFNw9BO/JWkTyQ==, tarball: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/17c7757389ca73d29882753ecf7a160de29aabbe} + version: 0.6.0 teex@1.0.1: resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} @@ -10220,7 +10220,7 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/8bf567b9e2230cdd02f9b8c9774fb8eb0d71af1e: + tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/17c7757389ca73d29882753ecf7a160de29aabbe: dependencies: '@tauri-apps/api': 2.11.1 diff --git a/src/hooks/use-connection-data.ts b/src/hooks/use-connection-data.ts index 5ad13e254..510cbef26 100644 --- a/src/hooks/use-connection-data.ts +++ b/src/hooks/use-connection-data.ts @@ -24,6 +24,10 @@ interface ConnectionMonitorData { closedConnections: IConnectionsItem[] } +interface ConnectionSummaryPayload { + count?: number +} + interface ConnectionSummaryData { activeConnectionCount: number } @@ -34,9 +38,6 @@ const initConnSummaryData: ConnectionSummaryData = { let connectionData: ConnectionMonitorData = initConnData let connectionSummary: ConnectionSummaryData = initConnSummaryData -let connectionSocket: MihomoWebSocket | null = null -let connectionConnecting = false -let reconnectTimer: ReturnType | null = null let flushTimer: ReturnType | null = null let pendingMessageData: string | null = null let lastFlushAt = 0 @@ -52,9 +53,6 @@ const notifySummaryListeners = () => { summaryListeners.forEach((listener) => listener()) } -const hasConnectionSubscribers = () => - connectionListeners.size > 0 || summaryListeners.size > 0 - const sameMetadata = (left: ConnectionMetadata, right: ConnectionMetadata) => metadataValue(left.network) === metadataValue(right.network) && metadataValue(left.type) === metadataValue(right.type) && @@ -208,16 +206,16 @@ const mergeConnectionSnapshot = ( } const mergeConnectionSummary = ( - payload: IConnections, + payload: ConnectionSummaryPayload, ): ConnectionSummaryData => ({ - activeConnectionCount: payload.connections?.length ?? 0, + activeConnectionCount: payload.count ?? 0, }) const flushPendingMessage = () => { flushTimer = null const messageData = pendingMessageData pendingMessageData = null - if (!messageData || !hasConnectionSubscribers()) return + if (!messageData || connectionListeners.size === 0) return let payload: IConnections try { @@ -228,10 +226,6 @@ const flushPendingMessage = () => { } lastFlushAt = Date.now() - connectionSummary = mergeConnectionSummary(payload) - notifySummaryListeners() - - if (connectionListeners.size === 0) return connectionData = mergeConnectionSnapshot(payload, connectionData) notifyConnectionListeners() @@ -253,115 +247,171 @@ const enqueueConnectionMessage = (messageData: string) => { ) } -const clearReconnectTimer = () => { - if (!reconnectTimer) return - window.clearTimeout(reconnectTimer) - reconnectTimer = null -} - -const closeConnectionSocket = async () => { - const socket = connectionSocket - connectionSocket = null - if (!socket) return - - try { - await socket.close() - } catch (err) { - console.warn('Failed to close connection websocket', err) - } -} - -const scheduleReconnect = () => { - if (!hasConnectionSubscribers()) return - if (reconnectTimer) return - reconnectTimer = window.setTimeout(() => { - reconnectTimer = null - void connectConnectionSocket() - }, CONNECTION_RECONNECT_DELAY_MS) -} - -async function reconnectConnectionSocket() { - if (!hasConnectionSubscribers()) return - await closeConnectionSocket() - scheduleReconnect() -} - -async function connectConnectionSocket() { - if (connectionSocket || connectionConnecting) return - if (!hasConnectionSubscribers()) return - - clearReconnectTimer() - connectionConnecting = true - - try { - const socket = await MihomoWebSocket.connect_connections() - if (!hasConnectionSubscribers()) { - await socket.close() - return - } - connectionSocket = socket - socket.addListener((message) => { - if (connectionSocket !== socket) return - if (message.type !== 'Text') return - if (message.data.startsWith('Websocket error')) { - void reconnectConnectionSocket() - return - } - - enqueueConnectionMessage(message.data) - }) - } catch { - scheduleReconnect() - } finally { - connectionConnecting = false - } -} - -const startConnectionMonitor = () => { - void connectConnectionSocket() -} - -const stopConnectionMonitorIfIdle = () => { - if (hasConnectionSubscribers()) return - - clearReconnectTimer() +const clearPendingMessage = () => { pendingMessageData = null if (flushTimer) { window.clearTimeout(flushTimer) flushTimer = null } - void closeConnectionSocket() } +interface SocketSupervisor { + start: () => void + stopIfIdle: () => void + reconnect: () => Promise +} + +const createSocketSupervisor = (options: { + listeners: Set + connectSocket: () => Promise + onText: (data: string) => void + closeLogLabel: string + onIdle?: () => void +}): SocketSupervisor => { + const { listeners, connectSocket, onText, closeLogLabel, onIdle } = options + const hasSubscribers = () => listeners.size > 0 + let socket: MihomoWebSocket | null = null + let connecting = false + let reconnectTimer: ReturnType | null = null + + const clearReconnectTimer = () => { + if (!reconnectTimer) return + window.clearTimeout(reconnectTimer) + reconnectTimer = null + } + + const closeSocket = async () => { + const current = socket + socket = null + if (!current) return + + try { + await current.close() + } catch (err) { + console.warn(`Failed to close ${closeLogLabel} websocket`, err) + } + } + + const scheduleReconnect = () => { + if (!hasSubscribers()) return + if (reconnectTimer) return + reconnectTimer = window.setTimeout(() => { + reconnectTimer = null + void connect() + }, CONNECTION_RECONNECT_DELAY_MS) + } + + const reconnect = async () => { + if (!hasSubscribers()) return + await closeSocket() + scheduleReconnect() + } + + const connect = async () => { + if (socket || connecting) return + if (!hasSubscribers()) return + + clearReconnectTimer() + connecting = true + + try { + const connected = await connectSocket() + if (!hasSubscribers()) { + await connected.close() + return + } + socket = connected + connected.addListener((message) => { + if (socket !== connected) return + if (message.type !== 'Text') return + if (message.data.startsWith('Websocket error')) { + void reconnect() + return + } + + onText(message.data) + }) + } catch { + scheduleReconnect() + } finally { + connecting = false + } + } + + return { + start: () => { + void connect() + }, + stopIfIdle: () => { + if (hasSubscribers()) return + + onIdle?.() + clearReconnectTimer() + void closeSocket() + }, + reconnect, + } +} + +const handleSummaryText = (messageData: string) => { + let payload: ConnectionSummaryPayload + try { + payload = JSON.parse(messageData) as ConnectionSummaryPayload + } catch (err) { + console.error( + '[Connections] Failed to parse connections count payload', + err, + ) + return + } + + connectionSummary = mergeConnectionSummary(payload) + notifySummaryListeners() +} + +const connectionSupervisor = createSocketSupervisor({ + listeners: connectionListeners, + connectSocket: () => MihomoWebSocket.connect_connections(), + onText: enqueueConnectionMessage, + closeLogLabel: 'connection', + onIdle: clearPendingMessage, +}) + +const summarySupervisor = createSocketSupervisor({ + listeners: summaryListeners, + connectSocket: () => MihomoWebSocket.connect_connections_count(), + onText: handleSummaryText, + closeLogLabel: 'connections count', +}) + const getConnectionSnapshot = () => connectionData const getConnectionSummarySnapshot = () => connectionSummary const subscribeConnectionData = (listener: ConnectionListener) => { connectionListeners.add(listener) - startConnectionMonitor() + connectionSupervisor.start() return () => { connectionListeners.delete(listener) - stopConnectionMonitorIfIdle() + connectionSupervisor.stopIfIdle() } } const subscribeConnectionSummary = (listener: ConnectionListener) => { summaryListeners.add(listener) - startConnectionMonitor() + summarySupervisor.start() return () => { summaryListeners.delete(listener) - stopConnectionMonitorIfIdle() + summarySupervisor.stopIfIdle() } } const refreshConnectionData = () => { - pendingMessageData = null - if (flushTimer) { - window.clearTimeout(flushTimer) - flushTimer = null - } + clearPendingMessage() + void connectionSupervisor.reconnect() +} - void reconnectConnectionSocket() +const refreshConnectionSummary = () => { + void summarySupervisor.reconnect() } const clearClosedConnectionData = () => { @@ -414,7 +464,7 @@ export const useConnectionSummaryData = (options?: { enabled?: boolean }) => { ) const response = useMemo(() => ({ data }), [data]) const refreshGetClashConnectionSummary = useCallback(() => { - refreshConnectionData() + refreshConnectionSummary() }, []) return {