dev
dart 296 lines 7.88 KB
Raw
1 abstract class SyncStatus {
2 const SyncStatus();
3 double progress();
4
5 String formattedProgress() {
6 return "${(progress() * 100).toStringAsFixed(2)}%";
7 }
8 }
9
10 class StartingScanSyncStatus extends SyncStatus {
11 StartingScanSyncStatus(this.beginHeight);
12
13 final int beginHeight;
14 @override
15 double progress() => 0.0;
16
17 @override
18 String toString() => 'Starting Scan $beginHeight';
19 }
20
21 class SyncingSyncStatus extends SyncStatus {
22 SyncingSyncStatus(this.blocksLeft, this.ptc) {
23 updateEtaHistory(blocksLeft);
24 _globalSyncStartTime ??= DateTime.now();
25 }
26
27 double ptc;
28 int blocksLeft;
29
30 @override
31 double progress() => ptc;
32
33 @override
34 String toString() => '$blocksLeft';
35
36 /// Returns true if we should show blocks remaining instead of percentage
37 /// Shows blocks remaining for the first 15 seconds of syncing
38 bool shouldShowBlocksRemaining() {
39 if (_globalSyncStartTime == null) return true;
40 final elapsed = DateTime.now().difference(_globalSyncStartTime!);
41 return elapsed.inSeconds < 15;
42 }
43
44 /// Reset the global sync start time (call when sync completes or fails)
45 static void resetSyncStartTime() {
46 _globalSyncStartTime = null;
47 }
48
49 factory SyncingSyncStatus.fromHeightValues(int chainTip, int initialSyncHeight, int syncHeight) {
50 final track = chainTip - initialSyncHeight;
51 final diff = track - (chainTip - syncHeight);
52 final ptc = diff <= 0 ? 0.0 : diff / track;
53 final left = chainTip - syncHeight;
54 updateEtaHistory(left + 1);
55
56 // sum 1 because if at the chain tip, will say "0 blocks left"
57 return SyncingSyncStatus(left + 1, ptc);
58 }
59
60 static void updateEtaHistory(int blocksLeft) {
61 blockHistory[DateTime.now()] = blocksLeft;
62 }
63
64 static Map<DateTime, int> blockHistory = {};
65 static Duration? lastEtaDuration;
66 static const int _minDataPoints = 3;
67 static DateTime? _globalSyncStartTime;
68
69 String? getFormattedEtaWithPlaceholder() {
70 // If we have enough data, show actual ETA
71 if (blockHistory.length >= _minDataPoints) {
72 final eta = getFormattedEta();
73 if (eta != null) return eta;
74 }
75
76 // Show the placeholder ETA while gathering data
77 return 'Syncing...';
78 }
79
80 String? getFormattedEta() {
81 Duration? duration = getEtaDuration();
82
83 // Don't show ETA for very long durations or very few blocks
84 if (duration.inDays > 0 || blocksLeft < 100) return null;
85
86 // Apply smoothing to prevent ETA jumping
87 duration = _applySmoothing(duration);
88 lastEtaDuration = duration;
89
90 return _formatDuration(duration);
91 }
92
93 Duration getEtaDuration() {
94 DateTime now = DateTime.now();
95 DateTime completionTime = calculateEta();
96 return completionTime.difference(now);
97 }
98
99 Duration _applySmoothing(Duration newDuration) {
100 if (lastEtaDuration == null) {
101 return newDuration;
102 }
103
104 final currentMs = lastEtaDuration!.inMilliseconds;
105 final newMs = newDuration.inMilliseconds;
106 final diff = ((newMs - currentMs) / 1000).abs();
107
108 // Apply different smoothing based on the magnitude of change
109 if (diff > 3600) {
110 // If it's more than 1 hour difference, it's a large change so we move by max 30 minutes
111 final direction = newMs > currentMs ? 1 : -1;
112 final maxChange = 30 * 60 * 1000;
113 final adjustedMs = currentMs + (direction * maxChange);
114 return Duration(milliseconds: adjustedMs);
115 } else if (diff > 300) {
116 // If it's more than 5 minutes difference, it's a medium change so we move by max 2 minutes
117 final direction = newMs > currentMs ? 1 : -1;
118 final maxChange = 2 * 60 * 1000;
119 final adjustedMs = currentMs + (direction * maxChange);
120 return Duration(milliseconds: adjustedMs);
121 } else if (diff > 60) {
122 // If it's more than 1 minute difference, it's a small change so we move by max 30 ms
123 final direction = newMs > currentMs ? 1 : -1;
124 final maxChange = 30 * 1000;
125 final adjustedMs = currentMs + (direction * maxChange);
126 return Duration(milliseconds: adjustedMs);
127 }
128
129 return newDuration;
130 }
131
132 String _formatDuration(Duration duration) {
133 final hours = duration.inHours;
134 final minutes = duration.inMinutes.remainder(60);
135 final seconds = duration.inSeconds.remainder(60);
136 if (minutes == 0 && hours == 0) {
137 return '${seconds}s';
138 }
139 if (hours == 0) {
140 return '${minutes}min ${seconds}s';
141 }
142 return '${hours}h ${minutes}min ${seconds}s';
143 }
144
145 DateTime calculateEta() {
146 double rate = _calculateBlockRate();
147 if (rate == 0) {
148 return DateTime.now().add(const Duration(days: 2));
149 }
150 int remainingBlocks = this.blocksLeft;
151 double timeRemainingMs = remainingBlocks / rate;
152 return DateTime.now().add(Duration(milliseconds: timeRemainingMs.round()));
153 }
154
155 // Enhanced block rate calculation with weighted averages
156 double _calculateBlockRate() {
157 List<DateTime> timestamps = blockHistory.keys.toList();
158 List<int> blockCounts = blockHistory.values.toList();
159
160 if (timestamps.length < 2) return 0;
161
162 // Sort by timestamp to ensure chronological order
163 final sortedData =
164 List.generate(timestamps.length, (i) => MapEntry(timestamps[i], blockCounts[i]))
165 ..sort((a, b) => a.key.compareTo(b.key));
166
167 double totalWeightedTime = 0;
168 double totalWeightedBlocks = 0;
169 double totalWeight = 0;
170
171 for (int i = 0; i < sortedData.length - 1; i++) {
172 final current = sortedData[i];
173 final next = sortedData[i + 1];
174
175 final blocksProcessed = current.value - next.value;
176
177 if (blocksProcessed <= 0) continue; // Skip invalid data
178
179 final timeDifference = next.key.difference(current.key);
180
181 if (timeDifference.inMilliseconds <= 0) continue; // Skip invalid time
182
183 // Weight recent data more heavily (exponential decay)
184 final weight = 1.0 / (1.0 + (sortedData.length - 1 - i) * 0.1);
185
186 totalWeightedTime += timeDifference.inMilliseconds * weight;
187 totalWeightedBlocks += blocksProcessed * weight;
188 totalWeight += weight;
189 }
190
191 if (totalWeight == 0 || totalWeightedTime == 0) return 0;
192
193 final weightedRate = totalWeightedBlocks / totalWeightedTime;
194
195 return weightedRate;
196 }
197 }
198
199 class ProcessingSyncStatus extends SyncStatus {
200 final String? message;
201
202 ProcessingSyncStatus({this.message});
203
204 @override
205 double progress() => 0.99;
206
207 @override
208 String toString() => 'Processing';
209 }
210
211 class SyncedSyncStatus extends SyncStatus {
212 @override
213 double progress() => 1.0;
214
215 @override
216 String toString() => 'Synced';
217 }
218
219 class SyncedTipSyncStatus extends SyncedSyncStatus {
220 SyncedTipSyncStatus(this.tip);
221
222 final int tip;
223
224 @override
225 String toString() => 'Synced Tip $tip';
226 }
227
228 class SyncronizingSyncStatus extends SyncStatus {
229 @override
230 double progress() => 0.0;
231
232 @override
233 String toString() => 'Synchronizing';
234 }
235
236 class NotConnectedSyncStatus extends SyncStatus {
237 const NotConnectedSyncStatus();
238
239 @override
240 double progress() => 0.0;
241
242 @override
243 String toString() => 'Not Connected';
244 }
245
246 class AttemptingSyncStatus extends SyncStatus {
247 @override
248 double progress() => 0.0;
249
250 @override
251 String toString() => 'Attempting';
252 }
253
254 class AttemptingScanSyncStatus extends SyncStatus {
255 @override
256 double progress() => 0.0;
257
258 @override
259 String toString() => 'Attempting Scan';
260 }
261
262 class FailedSyncStatus extends NotConnectedSyncStatus {
263 String? error;
264 FailedSyncStatus({this.error});
265
266 @override
267 String toString() => error ?? super.toString();
268 }
269
270 class ConnectingSyncStatus extends SyncStatus {
271 @override
272 double progress() => 0.0;
273
274 @override
275 String toString() => 'Connecting';
276 }
277
278 class ConnectedSyncStatus extends SyncStatus {
279 @override
280 double progress() => 0.0;
281
282 @override
283 String toString() => 'Connected';
284 }
285
286 class UnsupportedSyncStatus extends NotConnectedSyncStatus {}
287
288 class TimedOutSyncStatus extends NotConnectedSyncStatus {
289 @override
290 String toString() => 'Timed out';
291 }
292
293 class LostConnectionSyncStatus extends NotConnectedSyncStatus {
294 @override
295 String toString() => 'Reconnecting';
296 }