From 0682ae7065a7a6b8dc30ea5da049c916b757a87e Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Wed, 25 Mar 2026 15:09:11 +0100
Subject: [PATCH] fix: parallel host probing then single connect to winner

---
 lib/services/mqtt_service.dart |   70 ++++++++++++++++------------------
 1 files changed, 33 insertions(+), 37 deletions(-)

diff --git a/lib/services/mqtt_service.dart b/lib/services/mqtt_service.dart
index 5df0b7b..4e34697 100644
--- a/lib/services/mqtt_service.dart
+++ b/lib/services/mqtt_service.dart
@@ -110,47 +110,43 @@
 
     final clientId = await _getClientId();
 
-    // Connection order: local → cached discovery → Bonjour/scan → VPN → remote
-    final attempts = <MapEntry<String, int>>[];  // host → timeout ms
-    if (config.localHost != null && config.localHost!.isNotEmpty) {
-      attempts.add(MapEntry(config.localHost!, 2500));
-    }
-    // Try cached discovered host before scanning again
-    if (_lastDiscoveredHost != null) {
-      attempts.add(MapEntry(_lastDiscoveredHost!, 3000));
-    }
-    if (config.vpnHost != null && config.vpnHost!.isNotEmpty) {
-      attempts.add(MapEntry(config.vpnHost!, 3000));
-    }
-    if (config.host.isNotEmpty) {
-      attempts.add(MapEntry(config.host, 5000));
-    }
-    _mqttLog('MQTT: attempts=${attempts.map((e) => e.key).join(", ")} port=${config.port}');
+    // Probe all hosts in parallel to find which one responds, then connect to the winner
+    final hosts = <String>{};
+    if (config.localHost != null && config.localHost!.isNotEmpty) hosts.add(config.localHost!);
+    if (_lastDiscoveredHost != null) hosts.add(_lastDiscoveredHost!);
+    if (config.vpnHost != null && config.vpnHost!.isNotEmpty) hosts.add(config.vpnHost!);
+    if (config.host.isNotEmpty) hosts.add(config.host);
+    _mqttLog('MQTT: probing ${hosts.length} hosts in parallel: ${hosts.join(", ")}');
 
-    for (final attempt in attempts) {
-      if (_intentionalClose) return;
-      _mqttLog('MQTT: trying ${attempt.key}:${config.port}');
-      try {
-        if (await _tryConnect(attempt.key, clientId, timeout: attempt.value)) return;
-      } catch (e) {
-        _mqttLog('MQTT: ${attempt.key} error=$e');
+    // Probe all configured hosts in parallel — first to respond wins
+    String? winner;
+    if (hosts.isNotEmpty) {
+      final probes = hosts.map((h) => _probeHost(h, config.port)).toList();
+      // Also start discovery in parallel
+      if (_lastDiscoveredHost == null) {
+        probes.add(() async {
+          final discovered = await _discoverViaMdns();
+          if (discovered != null) {
+            _lastDiscoveredHost = discovered;
+            return discovered;
+          }
+          return null;
+        }());
       }
+      final results = await Future.wait(probes);
+      winner = results.firstWhere((r) => r != null, orElse: () => null);
+    } else if (_lastDiscoveredHost == null) {
+      // No configured hosts — try discovery only
+      winner = await _discoverViaMdns();
+      if (winner != null) _lastDiscoveredHost = winner;
     }
 
-    // All configured hosts failed — try Bonjour/subnet scan (only once, not on retry)
-    if (_lastDiscoveredHost == null && !_intentionalClose) {
-      _mqttLog('MQTT: trying Bonjour/subnet discovery...');
-      final discovered = await _discoverViaMdns();
-      if (discovered != null && !_intentionalClose) {
-        _lastDiscoveredHost = discovered;
-        _mqttLog('MQTT: discovered $discovered, connecting...');
-        try {
-          if (await _tryConnect(discovered, clientId, timeout: 3000)) return;
-        } catch (e) {
-          _mqttLog('MQTT: discovered host $discovered error=$e');
-        }
-      } else {
-        _mqttLog('MQTT: discovery returned nothing');
+    if (winner != null && !_intentionalClose) {
+      _mqttLog('MQTT: probe winner: $winner, connecting...');
+      try {
+        if (await _tryConnect(winner, clientId, timeout: 5000)) return;
+      } catch (e) {
+        _mqttLog('MQTT: connect to $winner failed: $e');
       }
     }
 

--
Gitblit v1.3.1