Files
hash-wallet/cw_core/lib/sync_status.dart
T
David AdegokeandGitHub 9775f7a629 fix: Sync bar ETA (#2515)
* fix: Sync bar ETA

* fix: Smooth ETA - WIP

* fix: Show disconnected status when sync disconnects

* feat: Display blocks remaining for the first five seconds and then switch to percentage

* Update

* fix: Extend duration for showing blocks remaining during sync from 5 to 15 seconds; update connectivity check to handle multiple connectivity results, and handle disconnected state while syncing

* feat: Initially display block if the a reconnection is triggered while syncing

* fix: More improvements to syncing
2025-11-12 11:40:04 +01:00

297 lines
7.9 KiB
Dart

abstract class SyncStatus {
const SyncStatus();
double progress();
String formattedProgress() {
return "${(progress() * 100).toStringAsFixed(2)}%";
}
}
class StartingScanSyncStatus extends SyncStatus {
StartingScanSyncStatus(this.beginHeight);
final int beginHeight;
@override
double progress() => 0.0;
@override
String toString() => 'Starting Scan $beginHeight';
}
class SyncingSyncStatus extends SyncStatus {
SyncingSyncStatus(this.blocksLeft, this.ptc) {
updateEtaHistory(blocksLeft);
_globalSyncStartTime ??= DateTime.now();
}
double ptc;
int blocksLeft;
@override
double progress() => ptc;
@override
String toString() => '$blocksLeft';
/// Returns true if we should show blocks remaining instead of percentage
/// Shows blocks remaining for the first 15 seconds of syncing
bool shouldShowBlocksRemaining() {
if (_globalSyncStartTime == null) return true;
final elapsed = DateTime.now().difference(_globalSyncStartTime!);
return elapsed.inSeconds < 15;
}
/// Reset the global sync start time (call when sync completes or fails)
static void resetSyncStartTime() {
_globalSyncStartTime = null;
}
factory SyncingSyncStatus.fromHeightValues(int chainTip, int initialSyncHeight, int syncHeight) {
final track = chainTip - initialSyncHeight;
final diff = track - (chainTip - syncHeight);
final ptc = diff <= 0 ? 0.0 : diff / track;
final left = chainTip - syncHeight;
updateEtaHistory(left + 1);
// sum 1 because if at the chain tip, will say "0 blocks left"
return SyncingSyncStatus(left + 1, ptc);
}
static void updateEtaHistory(int blocksLeft) {
blockHistory[DateTime.now()] = blocksLeft;
}
static Map<DateTime, int> blockHistory = {};
static Duration? lastEtaDuration;
static const int _minDataPoints = 3;
static DateTime? _globalSyncStartTime;
String? getFormattedEtaWithPlaceholder() {
// If we have enough data, show actual ETA
if (blockHistory.length >= _minDataPoints) {
final eta = getFormattedEta();
if (eta != null) return eta;
}
// Show the placeholder ETA while gathering data
return '--:--';
}
String? getFormattedEta() {
Duration? duration = getEtaDuration();
// Don't show ETA for very long durations or very few blocks
if (duration.inDays > 0 || blocksLeft < 100) return null;
// Apply smoothing to prevent ETA jumping
duration = _applySmoothing(duration);
lastEtaDuration = duration;
return _formatDuration(duration);
}
Duration getEtaDuration() {
DateTime now = DateTime.now();
DateTime completionTime = calculateEta();
return completionTime.difference(now);
}
Duration _applySmoothing(Duration newDuration) {
if (lastEtaDuration == null) {
return newDuration;
}
final currentMs = lastEtaDuration!.inMilliseconds;
final newMs = newDuration.inMilliseconds;
final diff = ((newMs - currentMs) / 1000).abs();
// Apply different smoothing based on the magnitude of change
if (diff > 3600) {
// If it's more than 1 hour difference, it's a large change so we move by max 30 minutes
final direction = newMs > currentMs ? 1 : -1;
final maxChange = 30 * 60 * 1000;
final adjustedMs = currentMs + (direction * maxChange);
return Duration(milliseconds: adjustedMs);
} else if (diff > 300) {
// If it's more than 5 minutes difference, it's a medium change so we move by max 2 minutes
final direction = newMs > currentMs ? 1 : -1;
final maxChange = 2 * 60 * 1000;
final adjustedMs = currentMs + (direction * maxChange);
return Duration(milliseconds: adjustedMs);
} else if (diff > 60) {
// If it's more than 1 minute difference, it's a small change so we move by max 30 ms
final direction = newMs > currentMs ? 1 : -1;
final maxChange = 30 * 1000;
final adjustedMs = currentMs + (direction * maxChange);
return Duration(milliseconds: adjustedMs);
}
return newDuration;
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final hours = twoDigits(duration.inHours);
final minutes = twoDigits(duration.inMinutes.remainder(60));
final seconds = twoDigits(duration.inSeconds.remainder(60));
if (hours == '00') {
return '${minutes}m${seconds}s';
}
return '${hours}h${minutes}m${seconds}s';
}
DateTime calculateEta() {
double rate = _calculateBlockRate();
if (rate == 0) {
return DateTime.now().add(const Duration(days: 2));
}
int remainingBlocks = this.blocksLeft;
double timeRemainingMs = remainingBlocks / rate;
return DateTime.now().add(Duration(milliseconds: timeRemainingMs.round()));
}
// Enhanced block rate calculation with weighted averages
double _calculateBlockRate() {
List<DateTime> timestamps = blockHistory.keys.toList();
List<int> blockCounts = blockHistory.values.toList();
if (timestamps.length < 2) return 0;
// Sort by timestamp to ensure chronological order
final sortedData =
List.generate(timestamps.length, (i) => MapEntry(timestamps[i], blockCounts[i]))
..sort((a, b) => a.key.compareTo(b.key));
double totalWeightedTime = 0;
double totalWeightedBlocks = 0;
double totalWeight = 0;
for (int i = 0; i < sortedData.length - 1; i++) {
final current = sortedData[i];
final next = sortedData[i + 1];
final blocksProcessed = current.value - next.value;
if (blocksProcessed <= 0) continue; // Skip invalid data
final timeDifference = next.key.difference(current.key);
if (timeDifference.inMilliseconds <= 0) continue; // Skip invalid time
// Weight recent data more heavily (exponential decay)
final weight = 1.0 / (1.0 + (sortedData.length - 1 - i) * 0.1);
totalWeightedTime += timeDifference.inMilliseconds * weight;
totalWeightedBlocks += blocksProcessed * weight;
totalWeight += weight;
}
if (totalWeight == 0 || totalWeightedTime == 0) return 0;
final weightedRate = totalWeightedBlocks / totalWeightedTime;
return weightedRate;
}
}
class ProcessingSyncStatus extends SyncStatus {
final String? message;
ProcessingSyncStatus({this.message});
@override
double progress() => 0.99;
@override
String toString() => 'Processing';
}
class SyncedSyncStatus extends SyncStatus {
@override
double progress() => 1.0;
@override
String toString() => 'Synced';
}
class SyncedTipSyncStatus extends SyncedSyncStatus {
SyncedTipSyncStatus(this.tip);
final int tip;
@override
String toString() => 'Synced Tip $tip';
}
class SyncronizingSyncStatus extends SyncStatus {
@override
double progress() => 0.0;
@override
String toString() => 'Synchronizing';
}
class NotConnectedSyncStatus extends SyncStatus {
const NotConnectedSyncStatus();
@override
double progress() => 0.0;
@override
String toString() => 'Not Connected';
}
class AttemptingSyncStatus extends SyncStatus {
@override
double progress() => 0.0;
@override
String toString() => 'Attempting';
}
class AttemptingScanSyncStatus extends SyncStatus {
@override
double progress() => 0.0;
@override
String toString() => 'Attempting Scan';
}
class FailedSyncStatus extends NotConnectedSyncStatus {
String? error;
FailedSyncStatus({this.error});
@override
String toString() => error ?? super.toString();
}
class ConnectingSyncStatus extends SyncStatus {
@override
double progress() => 0.0;
@override
String toString() => 'Connecting';
}
class ConnectedSyncStatus extends SyncStatus {
@override
double progress() => 0.0;
@override
String toString() => 'Connected';
}
class UnsupportedSyncStatus extends NotConnectedSyncStatus {}
class TimedOutSyncStatus extends NotConnectedSyncStatus {
@override
String toString() => 'Timed out';
}
class LostConnectionSyncStatus extends NotConnectedSyncStatus {
@override
String toString() => 'Reconnecting';
}