| .. | .. |
|---|
| 114 | 114 | |
|---|
| 115 | 115 | final clientId = await _getClientId(); |
|---|
| 116 | 116 | |
|---|
| 117 | | - // Probe all hosts in parallel to find which one responds, then connect to the winner |
|---|
| 118 | | - final hosts = <String>{}; |
|---|
| 117 | + // Phase 1: Race configured hosts (fast — just TLS probe, ~1s each) |
|---|
| 118 | + final hosts = <String>[]; |
|---|
| 119 | 119 | if (config.localHost != null && config.localHost!.isNotEmpty) hosts.add(config.localHost!); |
|---|
| 120 | | - if (_lastDiscoveredHost != null) hosts.add(_lastDiscoveredHost!); |
|---|
| 120 | + if (_lastDiscoveredHost != null && !hosts.contains(_lastDiscoveredHost)) hosts.add(_lastDiscoveredHost!); |
|---|
| 121 | 121 | if (config.vpnHost != null && config.vpnHost!.isNotEmpty) hosts.add(config.vpnHost!); |
|---|
| 122 | 122 | if (config.host.isNotEmpty) hosts.add(config.host); |
|---|
| 123 | | - _mqttLog('MQTT: probing ${hosts.length} hosts in parallel: ${hosts.join(", ")}'); |
|---|
| 124 | | - onStatusDetail?.call('Probing ${hosts.length} hosts...'); |
|---|
| 123 | + _mqttLog('MQTT: racing ${hosts.length} configured hosts: ${hosts.join(", ")}'); |
|---|
| 124 | + onStatusDetail?.call('Connecting...'); |
|---|
| 125 | 125 | |
|---|
| 126 | | - // Probe all configured hosts in parallel — first to respond wins |
|---|
| 126 | + // Race: first probe to succeed wins, don't wait for others |
|---|
| 127 | 127 | String? winner; |
|---|
| 128 | 128 | if (hosts.isNotEmpty) { |
|---|
| 129 | | - final probes = hosts.map((h) => _probeHost(h, config.port)).toList(); |
|---|
| 130 | | - // Also start discovery in parallel |
|---|
| 131 | | - if (_lastDiscoveredHost == null) { |
|---|
| 132 | | - probes.add(() async { |
|---|
| 133 | | - final discovered = await _discoverViaMdns(); |
|---|
| 134 | | - if (discovered != null) { |
|---|
| 135 | | - _lastDiscoveredHost = discovered; |
|---|
| 136 | | - return discovered; |
|---|
| 129 | + final completer = Completer<String?>(); |
|---|
| 130 | + int pending = hosts.length; |
|---|
| 131 | + for (final host in hosts) { |
|---|
| 132 | + _probeHost(host, config.port).then((result) { |
|---|
| 133 | + if (result != null && !completer.isCompleted) { |
|---|
| 134 | + completer.complete(result); |
|---|
| 135 | + } else { |
|---|
| 136 | + pending--; |
|---|
| 137 | + if (pending <= 0 && !completer.isCompleted) { |
|---|
| 138 | + completer.complete(null); // All failed |
|---|
| 139 | + } |
|---|
| 137 | 140 | } |
|---|
| 138 | | - return null; |
|---|
| 139 | | - }()); |
|---|
| 141 | + }); |
|---|
| 140 | 142 | } |
|---|
| 141 | | - final results = await Future.wait(probes); |
|---|
| 142 | | - winner = results.firstWhere((r) => r != null, orElse: () => null); |
|---|
| 143 | | - } else if (_lastDiscoveredHost == null) { |
|---|
| 144 | | - // No configured hosts — try discovery only |
|---|
| 145 | | - winner = await _discoverViaMdns(); |
|---|
| 146 | | - if (winner != null) _lastDiscoveredHost = winner; |
|---|
| 143 | + winner = await completer.future.timeout( |
|---|
| 144 | + const Duration(seconds: 3), |
|---|
| 145 | + onTimeout: () => null, |
|---|
| 146 | + ); |
|---|
| 147 | + } |
|---|
| 148 | + |
|---|
| 149 | + // Phase 2: If configured hosts failed, try Bonjour/subnet discovery |
|---|
| 150 | + if (winner == null && !_intentionalClose) { |
|---|
| 151 | + _mqttLog('MQTT: configured hosts failed, trying discovery...'); |
|---|
| 152 | + onStatusDetail?.call('Scanning network...'); |
|---|
| 153 | + final discovered = await _discoverViaMdns(); |
|---|
| 154 | + if (discovered != null) { |
|---|
| 155 | + _lastDiscoveredHost = discovered; |
|---|
| 156 | + winner = discovered; |
|---|
| 157 | + } |
|---|
| 147 | 158 | } |
|---|
| 148 | 159 | |
|---|
| 149 | 160 | if (winner != null && !_intentionalClose) { |
|---|
| 150 | | - _mqttLog('MQTT: probe winner: $winner, connecting...'); |
|---|
| 161 | + _mqttLog('MQTT: winner: $winner, connecting...'); |
|---|
| 151 | 162 | onStatusDetail?.call('Connecting to $winner...'); |
|---|
| 152 | 163 | try { |
|---|
| 153 | 164 | if (await _tryConnect(winner, clientId, timeout: 5000)) return; |
|---|
| .. | .. |
|---|
| 314 | 325 | final socket = await SecureSocket.connect( |
|---|
| 315 | 326 | host, |
|---|
| 316 | 327 | port, |
|---|
| 317 | | - timeout: const Duration(seconds: 1), |
|---|
| 328 | + timeout: const Duration(milliseconds: 500), |
|---|
| 318 | 329 | onBadCertificate: (_) => true, // Accept self-signed cert during scan |
|---|
| 319 | 330 | ); |
|---|
| 320 | 331 | await socket.close(); |
|---|