11import 'package:collection/collection.dart' ;
2+ import 'package:meta/meta.dart' ;
23
34final class SyncStatus {
45 /// true if currently connected.
@@ -18,6 +19,12 @@ final class SyncStatus {
1819 /// This is only true when [connected] is also true.
1920 final bool downloading;
2021
22+ /// A realtime progress report on how many operations have been downloaded and
23+ /// how many are necessary in total to complete the next sync iteration.
24+ ///
25+ /// This field is only set when [downloading] is also true.
26+ final SyncDownloadProgress ? downloadProgress;
27+
2128 /// true if uploading changes
2229 final bool uploading;
2330
@@ -47,6 +54,7 @@ final class SyncStatus {
4754 this .connecting = false ,
4855 this .lastSyncedAt,
4956 this .hasSynced,
57+ this .downloadProgress,
5058 this .downloading = false ,
5159 this .uploading = false ,
5260 this .downloadError,
@@ -202,3 +210,83 @@ class UploadQueueStats {
202210 }
203211 }
204212}
213+
214+ @internal
215+ typedef OperationCounter = ({BucketPriority priority, int opCount});
216+
217+ @internal
218+ final class InternalSyncDownloadProgress {
219+ final List <OperationCounter > downloaded;
220+ final List <OperationCounter > target;
221+
222+ final int _totalDownloaded;
223+ final int _totalTarget;
224+
225+ InternalSyncDownloadProgress (this .downloaded, this .target)
226+ : _totalDownloaded = downloaded.map ((e) => e.opCount).sum,
227+ _totalTarget = target.map ((e) => e.opCount).sum;
228+
229+ static int sumInPriority (
230+ List <OperationCounter > counters, BucketPriority priority) {
231+ return counters
232+ .where ((e) => e.priority >= priority)
233+ .map ((e) => e.opCount)
234+ .sum;
235+ }
236+ }
237+
238+ /// Provides realtime progress about how PowerSync is downloading rows.
239+ ///
240+ /// The reported progress always reflects the status towards the end of a
241+ /// sync iteration (after which a consistent snapshot of all buckets is
242+ /// available locally). Note that [downloaded] starts at `0` every time an
243+ /// iteration begins.
244+ /// This has an effect when iterations are interrupted. Consider this flow
245+ /// as an example:
246+ ///
247+ /// 1. The client comes online for the first time and has to synchronize a
248+ /// large amount of rows (say 100k). Here, [downloaded] starts at `0` and
249+ /// [total] would be the `100,000` rows.
250+ /// 2. The client makes some progress, so that [downloaded] is perhaps
251+ /// `60,000`.
252+ /// 3. The client briefly looses connectivity.
253+ /// 4. Back online, a new sync iteration starts. This means that [downloaded]
254+ /// is reset to `0`. However, since half of the target has already been
255+ /// downloaded in the earlier iteration, [total] is now set to `40,000` to
256+ /// reflect the remaining rows to download in the new iteration.
257+ extension type SyncDownloadProgress ._(InternalSyncDownloadProgress _internal) {
258+ /// The amount of operations that have been downloaded in the current sync
259+ /// iteration.
260+ ///
261+ /// This number always starts at zero as [SyncStatus.downloading] changes
262+ /// from `false` to `true` .
263+ int get downloaded => _internal._totalDownloaded;
264+
265+ /// The total amount of operations expected for this sync operation.
266+ int get total => _internal._totalTarget;
267+
268+ /// The fraction of [total] operations that have already been [downloaded] , as
269+ /// a number between 0 and 1.
270+ double get progress => _internal._totalDownloaded / _internal._totalTarget;
271+
272+ int downloadedFor (BucketPriority priority) {
273+ return InternalSyncDownloadProgress .sumInPriority (
274+ _internal.downloaded, priority);
275+ }
276+
277+ int totalFor (BucketPriority priority) {
278+ return InternalSyncDownloadProgress .sumInPriority (
279+ _internal.target, priority);
280+ }
281+
282+ double progressFor (BucketPriority priority) {
283+ final downloaded = downloadedFor (priority);
284+ final total = totalFor (priority);
285+
286+ if (total == 0 ) {
287+ return 0 ;
288+ }
289+
290+ return downloaded / total;
291+ }
292+ }
0 commit comments