enum PathRecordSource { learned, observed } class PathRecord { final List pathBytes; final int hopCount; final int hashSize; final PathRecordSource source; final int successCount; final int failureCount; final int lastRoundTripTimeMs; final DateTime lastUsedAt; const PathRecord({ required this.pathBytes, required this.hopCount, required this.hashSize, required this.source, required this.successCount, required this.failureCount, required this.lastRoundTripTimeMs, required this.lastUsedAt, }); String get signature => pathBytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); double get successRate => (successCount + 1) / (successCount + failureCount + 2); PathRecord copyWith({ List? pathBytes, int? hopCount, int? hashSize, PathRecordSource? source, int? successCount, int? failureCount, int? lastRoundTripTimeMs, DateTime? lastUsedAt, }) { return PathRecord( pathBytes: pathBytes ?? this.pathBytes, hopCount: hopCount ?? this.hopCount, hashSize: hashSize ?? this.hashSize, source: source ?? this.source, successCount: successCount ?? this.successCount, failureCount: failureCount ?? this.failureCount, lastRoundTripTimeMs: lastRoundTripTimeMs ?? this.lastRoundTripTimeMs, lastUsedAt: lastUsedAt ?? this.lastUsedAt, ); } Map toJson() { return { 'path_bytes': pathBytes, 'hop_count': hopCount, 'hash_size': hashSize, 'source': source.name, 'success_count': successCount, 'failure_count': failureCount, 'last_round_trip_time_ms': lastRoundTripTimeMs, 'last_used_at': lastUsedAt.toIso8601String(), }; } factory PathRecord.fromJson(Map json) { return PathRecord( pathBytes: (json['path_bytes'] as List? ?? const []) .map((value) => value as int) .toList(), hopCount: json['hop_count'] as int? ?? 0, hashSize: json['hash_size'] as int? ?? 1, source: PathRecordSource.values.firstWhere( (value) => value.name == (json['source'] as String? ?? 'learned'), orElse: () => PathRecordSource.learned, ), successCount: json['success_count'] as int? ?? 0, failureCount: json['failure_count'] as int? ?? 0, lastRoundTripTimeMs: json['last_round_trip_time_ms'] as int? ?? 0, lastUsedAt: DateTime.tryParse(json['last_used_at'] as String? ?? '') ?? DateTime.fromMillisecondsSinceEpoch(0), ); } } class FloodPathStats { final int successCount; final int failureCount; final int lastRoundTripTimeMs; final DateTime? lastUsedAt; const FloodPathStats({ required this.successCount, required this.failureCount, required this.lastRoundTripTimeMs, required this.lastUsedAt, }); const FloodPathStats.empty() : successCount = 0, failureCount = 0, lastRoundTripTimeMs = 0, lastUsedAt = null; FloodPathStats copyWith({ int? successCount, int? failureCount, int? lastRoundTripTimeMs, DateTime? lastUsedAt, }) { return FloodPathStats( successCount: successCount ?? this.successCount, failureCount: failureCount ?? this.failureCount, lastRoundTripTimeMs: lastRoundTripTimeMs ?? this.lastRoundTripTimeMs, lastUsedAt: lastUsedAt ?? this.lastUsedAt, ); } Map toJson() { return { 'success_count': successCount, 'failure_count': failureCount, 'last_round_trip_time_ms': lastRoundTripTimeMs, 'last_used_at': lastUsedAt?.toIso8601String(), }; } factory FloodPathStats.fromJson(Map json) { return FloodPathStats( successCount: json['success_count'] as int? ?? 0, failureCount: json['failure_count'] as int? ?? 0, lastRoundTripTimeMs: json['last_round_trip_time_ms'] as int? ?? 0, lastUsedAt: DateTime.tryParse(json['last_used_at'] as String? ?? ''), ); } } class ContactPathHistory { final String contactPublicKeyHex; final List directPaths; final FloodPathStats floodStats; final int rotationIndex; const ContactPathHistory({ required this.contactPublicKeyHex, required this.directPaths, required this.floodStats, required this.rotationIndex, }); const ContactPathHistory.empty(this.contactPublicKeyHex) : directPaths = const [], floodStats = const FloodPathStats.empty(), rotationIndex = 0; ContactPathHistory copyWith({ List? directPaths, FloodPathStats? floodStats, int? rotationIndex, }) { return ContactPathHistory( contactPublicKeyHex: contactPublicKeyHex, directPaths: directPaths ?? this.directPaths, floodStats: floodStats ?? this.floodStats, rotationIndex: rotationIndex ?? this.rotationIndex, ); } Map toJson() { return { 'direct_paths': directPaths.map((record) => record.toJson()).toList(), 'flood_stats': floodStats.toJson(), 'rotation_index': rotationIndex, }; } List get observedPaths => directPaths .where((record) => record.source == PathRecordSource.observed) .toList(); factory ContactPathHistory.fromJson( String contactPublicKeyHex, Map json, ) { return ContactPathHistory( contactPublicKeyHex: contactPublicKeyHex, directPaths: (json['direct_paths'] as List? ?? const []) .whereType>() .map(PathRecord.fromJson) .toList(), floodStats: json['flood_stats'] is Map ? FloodPathStats.fromJson(json['flood_stats'] as Map) : const FloodPathStats.empty(), rotationIndex: json['rotation_index'] as int? ?? 0, ); } }