diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index d00d49af..ef2f9b72 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -699,43 +699,44 @@ class MeshCoreConnector extends ChangeNotifier { _loadChannelOrder(); // Initialize retry service callbacks - _retryService?.initialize(RetryServiceConfig( - sendMessage: _sendMessageDirect, - addMessage: _addMessage, - updateMessage: _updateMessage, - clearContactPath: clearContactPath, - setContactPath: setContactPath, - calculateTimeout: - (pathLength, messageBytes, {String? contactKey}) => calculateTimeout( - pathLength: pathLength, - messageBytes: messageBytes, - contactKey: contactKey, - ), - getSelfPublicKey: () => _selfPublicKey, - prepareContactOutboundText: prepareContactOutboundText, - appSettingsService: appSettingsService, - debugLogService: _appDebugLogService, - recordPathResult: _recordPathResult, - selectRetryPath: - (contactKey, attemptIndex, maxRetries, recentSelections) => - _selectAutoPathForAttempt( - contactKey, - attemptIndex: attemptIndex, - maxRetries: maxRetries, - recentSelections: recentSelections, - ), - onDeliveryObserved: - (contactKey, pathLength, messageBytes, tripTimeMs) { - final secSinceRx = DateTime.now().difference(_lastRxTime).inSeconds; - _timeoutPredictionService?.recordObservation( - contactKey: contactKey, + _retryService?.initialize( + RetryServiceConfig( + sendMessage: _sendMessageDirect, + addMessage: _addMessage, + updateMessage: _updateMessage, + clearContactPath: clearContactPath, + setContactPath: setContactPath, + calculateTimeout: (pathLength, messageBytes, {String? contactKey}) => + calculateTimeout( pathLength: pathLength, messageBytes: messageBytes, - tripTimeMs: tripTimeMs, - secondsSinceLastRx: secSinceRx, - ); - }, - )); + contactKey: contactKey, + ), + getSelfPublicKey: () => _selfPublicKey, + prepareContactOutboundText: prepareContactOutboundText, + appSettingsService: appSettingsService, + debugLogService: _appDebugLogService, + recordPathResult: _recordPathResult, + selectRetryPath: + (contactKey, attemptIndex, maxRetries, recentSelections) => + _selectAutoPathForAttempt( + contactKey, + attemptIndex: attemptIndex, + maxRetries: maxRetries, + recentSelections: recentSelections, + ), + onDeliveryObserved: (contactKey, pathLength, messageBytes, tripTimeMs) { + final secSinceRx = DateTime.now().difference(_lastRxTime).inSeconds; + _timeoutPredictionService?.recordObservation( + contactKey: contactKey, + pathLength: pathLength, + messageBytes: messageBytes, + tripTimeMs: tripTimeMs, + secondsSinceLastRx: secSinceRx, + ); + }, + ), + ); final maxRetries = _appSettingsService?.settings.maxMessageRetries ?? 5; _retryService?.setMaxRetries(maxRetries); } @@ -908,7 +909,8 @@ class MeshCoreConnector extends ChangeNotifier { List recentSelections = const [], }) { final hasKnownPaths = - _pathHistoryService?.getRecentPaths(contactPubKeyHex).isNotEmpty ?? false; + _pathHistoryService?.getRecentPaths(contactPubKeyHex).isNotEmpty ?? + false; if (!hasKnownPaths) { return null; } @@ -3619,10 +3621,7 @@ class MeshCoreConnector extends ChangeNotifier { void _handleIncomingChannelMessage(Uint8List frame) { final parsed = ChannelMessage.fromFrame(frame); if (parsed != null && parsed.channelIndex != null) { - if (_shouldDropSelfChannelMessage( - parsed.senderName, - parsed.pathBytes, - )) { + if (_shouldDropSelfChannelMessage(parsed.senderName, parsed.pathBytes)) { return; } final contentHash = _computeContentHash( @@ -4246,7 +4245,9 @@ class MeshCoreConnector extends ChangeNotifier { final pathLenRaw = raw[index++]; final pathByteLen = _decodePathByteLen(pathLenRaw); if (raw.length < index + pathByteLen) return null; - final pathBytes = Uint8List.fromList(raw.sublist(index, index + pathByteLen)); + final pathBytes = Uint8List.fromList( + raw.sublist(index, index + pathByteLen), + ); index += pathByteLen; if (raw.length <= index) return null; final payload = Uint8List.fromList(raw.sublist(index)); @@ -4273,12 +4274,19 @@ class MeshCoreConnector extends ChangeNotifier { input[0] = payloadType; input.setRange(1, input.length, payload); final digest = crypto.sha256.convert(input).bytes; - return digest.sublist(0, 8).map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + return digest + .sublist(0, 8) + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(); } /// Content-based dedup hash for sync queue messages (no raw payload available). /// Prefixed with 'c:' to avoid collisions with packet hashes. - String _computeContentHash(int channelIdx, int timestampSecs, String fullText) { + String _computeContentHash( + int channelIdx, + int timestampSecs, + String fullText, + ) { final textBytes = utf8.encode(fullText); final input = Uint8List(5 + textBytes.length); input[0] = channelIdx; diff --git a/lib/services/message_retry_service.dart b/lib/services/message_retry_service.dart index 2f105112..b2844252 100644 --- a/lib/services/message_retry_service.dart +++ b/lib/services/message_retry_service.dart @@ -22,7 +22,11 @@ class _AckHistoryEntry { } /// (messageId, timestamp, attemptIndex) — stored per ACK hash for O(1) lookup. -typedef AckHashMapping = ({String messageId, DateTime timestamp, int attemptIndex}); +typedef AckHashMapping = ({ + String messageId, + DateTime timestamp, + int attemptIndex, +}); class RetryServiceConfig { final void Function(Contact, String, int, int) sendMessage; @@ -31,7 +35,7 @@ class RetryServiceConfig { final Function(Contact)? clearContactPath; final Function(Contact, Uint8List, int)? setContactPath; final int Function(int pathLength, int messageBytes, {String? contactKey})? - calculateTimeout; + calculateTimeout; final Uint8List? Function()? getSelfPublicKey; final String Function(Contact, String)? prepareContactOutboundText; final AppSettingsService? appSettingsService; @@ -43,7 +47,8 @@ class RetryServiceConfig { int attemptIndex, int maxRetries, List recentSelections, - )? selectRetryPath; + )? + selectRetryPath; const RetryServiceConfig({ required this.sendMessage, @@ -132,7 +137,8 @@ class MessageRetryService extends ChangeNotifier { }) async { final messageId = const Uuid().v4(); final resolved = resolvePathSelection(contact); - final messagePathBytes = pathBytes ?? Uint8List.fromList(resolved.pathBytes); + final messagePathBytes = + pathBytes ?? Uint8List.fromList(resolved.pathBytes); final messagePathLength = pathLength ?? (resolved.useFlood ? -1 : resolved.hopCount); final message = Message( @@ -262,7 +268,8 @@ class MessageRetryService extends ChangeNotifier { if (config.setContactPath != null && config.clearContactPath != null) { final bool useFlood = currentSelection != null ? currentSelection.useFlood - : (effectiveMessage.pathLength != null && effectiveMessage.pathLength! < 0); + : (effectiveMessage.pathLength != null && + effectiveMessage.pathLength! < 0); final List pathBytes = currentSelection != null ? currentSelection.pathBytes : effectiveMessage.pathBytes; diff --git a/lib/services/path_history_service.dart b/lib/services/path_history_service.dart index 809f867f..68a92453 100644 --- a/lib/services/path_history_service.dart +++ b/lib/services/path_history_service.dart @@ -469,17 +469,18 @@ class PathHistoryService extends ChangeNotifier { final highestRouteWeight = _getHighestKnownRouteWeight(ranked); ranked.sort((a, b) { - final scoreCompare = _scorePathRecord( - b, - fastestTripMs: fastestTripMs, - highestRouteWeight: highestRouteWeight, - ).compareTo( - _scorePathRecord( - a, - fastestTripMs: fastestTripMs, - highestRouteWeight: highestRouteWeight, - ), - ); + final scoreCompare = + _scorePathRecord( + b, + fastestTripMs: fastestTripMs, + highestRouteWeight: highestRouteWeight, + ).compareTo( + _scorePathRecord( + a, + fastestTripMs: fastestTripMs, + highestRouteWeight: highestRouteWeight, + ), + ); if (scoreCompare != 0) { return scoreCompare; } @@ -531,8 +532,7 @@ class PathHistoryService extends ChangeNotifier { (DateTime.now().difference(path.timestamp!).inMinutes / 60.0 / 24.0)); - final routeWeight = - (path.routeWeight / highestRouteWeight).clamp(0.0, 1.0); + final routeWeight = (path.routeWeight / highestRouteWeight).clamp(0.0, 1.0); return (reliability * 0.45) + (latency * 0.25) + diff --git a/test/models/model_changes_test.dart b/test/models/model_changes_test.dart index 165b91de..a80c7944 100644 --- a/test/models/model_changes_test.dart +++ b/test/models/model_changes_test.dart @@ -15,7 +15,9 @@ Uint8List _buildContactFrame({ }) { final writer = BytesBuilder(); writer.addByte(respCodeContact); // 3 - writer.add(pubKey ?? Uint8List.fromList(List.generate(32, (i) => i + 1))); // valid pubkey + writer.add( + pubKey ?? Uint8List.fromList(List.generate(32, (i) => i + 1)), + ); // valid pubkey writer.addByte(1); // type writer.addByte(0); // flags writer.addByte(pathLen); @@ -239,21 +241,23 @@ void main() { expect(record.routeWeight, equals(4.0)); }); - test('fromJson with missing route_weight defaults to 1.0 (backward compat)', - () { - final json = { - 'hop_count': 1, - 'trip_time_ms': 100, - 'timestamp': DateTime(2024).toIso8601String(), - 'was_flood': false, - 'path_bytes': [], - 'success_count': 0, - 'failure_count': 0, - // 'route_weight' intentionally omitted - }; - final record = PathRecord.fromJson(json); - expect(record.routeWeight, equals(1.0)); - }); + test( + 'fromJson with missing route_weight defaults to 1.0 (backward compat)', + () { + final json = { + 'hop_count': 1, + 'trip_time_ms': 100, + 'timestamp': DateTime(2024).toIso8601String(), + 'was_flood': false, + 'path_bytes': [], + 'success_count': 0, + 'failure_count': 0, + // 'route_weight' intentionally omitted + }; + final record = PathRecord.fromJson(json); + expect(record.routeWeight, equals(1.0)); + }, + ); }); group('AppSettings — new fields', () { diff --git a/test/services/path_history_service_test.dart b/test/services/path_history_service_test.dart index 561bad3f..87ae729d 100644 --- a/test/services/path_history_service_test.dart +++ b/test/services/path_history_service_test.dart @@ -140,7 +140,11 @@ void main() { attemptIndex: i, maxRetries: 5, ); - expect(selection.useFlood, isFalse, reason: 'attempt $i should be path'); + expect( + selection.useFlood, + isFalse, + reason: 'attempt $i should be path', + ); expect(selection.pathBytes, equals([0x01, 0x02])); } }); @@ -400,45 +404,49 @@ void main() { expect(first.pathBytes, equals([0x02])); }); - test('higher route weight wins when other factors are effectively tied', () async { - final pubKey = _hex('rank4'); - final sharedTimestamp = - DateTime.now().subtract(const Duration(minutes: 30)); - storage._store[pubKey] = ContactPathHistory( - contactPubKeyHex: pubKey, - recentPaths: [ - PathRecord( - hopCount: 1, - tripTimeMs: 750, - timestamp: sharedTimestamp, - wasFloodDiscovery: false, - pathBytes: const [0x01], - successCount: 1, - failureCount: 0, - routeWeight: 4.0, - ), - PathRecord( - hopCount: 1, - tripTimeMs: 750, - timestamp: sharedTimestamp, - wasFloodDiscovery: false, - pathBytes: const [0x02], - successCount: 1, - failureCount: 0, - routeWeight: 1.0, - ), - ], - ); - svc.getRecentPaths(pubKey); - await _flush(); + test( + 'higher route weight wins when other factors are effectively tied', + () async { + final pubKey = _hex('rank4'); + final sharedTimestamp = DateTime.now().subtract( + const Duration(minutes: 30), + ); + storage._store[pubKey] = ContactPathHistory( + contactPubKeyHex: pubKey, + recentPaths: [ + PathRecord( + hopCount: 1, + tripTimeMs: 750, + timestamp: sharedTimestamp, + wasFloodDiscovery: false, + pathBytes: const [0x01], + successCount: 1, + failureCount: 0, + routeWeight: 4.0, + ), + PathRecord( + hopCount: 1, + tripTimeMs: 750, + timestamp: sharedTimestamp, + wasFloodDiscovery: false, + pathBytes: const [0x02], + successCount: 1, + failureCount: 0, + routeWeight: 1.0, + ), + ], + ); + svc.getRecentPaths(pubKey); + await _flush(); - final first = svc.selectPathForAttempt( - pubKey, - attemptIndex: 0, - maxRetries: 5, - ); - expect(first.pathBytes, equals([0x01])); - }); + final first = svc.selectPathForAttempt( + pubKey, + attemptIndex: 0, + maxRetries: 5, + ); + expect(first.pathBytes, equals([0x01])); + }, + ); }); // -------------------------------------------------------------------------