From d51ff90e4bdb08cb49fa81c5658ce642b4dc226e Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 5 May 2025 13:32:33 +0200 Subject: [PATCH 1/4] Add sync progress APIs --- CHANGELOG.md | 4 ++ .../xcshareddata/swiftpm/Package.resolved | 11 ++++- .../Components/ListView.swift | 38 +++++++++++--- .../Kotlin/sync/KotlinSyncStatusData.swift | 35 +++++++++++++ .../Protocol/sync/DownloadProgress.swift | 49 +++++++++++++++++++ .../Protocol/sync/SyncStatusData.swift | 6 +++ 6 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 Sources/PowerSync/Protocol/sync/DownloadProgress.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 79d31c7..084ea12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## unreleased + +- Add sync progress information through `SyncStatusData.downloadProgress`. + # 1.0.0 - Improved the stability of watched queries. Watched queries were previously susceptible to runtime crashes if an exception was thrown in the update stream. Errors are now gracefully handled. diff --git a/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4f14951..fea5149 100644 --- a/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "33297127250b66812faa920958a24bae46bf9e9d1c38ea6b84ca413efaf16afd", + "originHash" : "2d885a1b46f17f9239b7876e3889168a6de98024718f2d7af03aede290c8a86a", "pins" : [ { "identity" : "anycodable", @@ -10,6 +10,15 @@ "version" : "0.6.7" } }, + { + "identity" : "powersync-kotlin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/powersync-ja/powersync-kotlin.git", + "state" : { + "revision" : "ccd2e595195c59d570eb93a878ad6a5cfca72ada", + "version" : "1.0.1+SWIFT.0" + } + }, { "identity" : "powersync-sqlite-core-swift", "kind" : "remoteSourceControl", diff --git a/Demo/PowerSyncExample/Components/ListView.swift b/Demo/PowerSyncExample/Components/ListView.swift index 21c1028..484ee20 100644 --- a/Demo/PowerSyncExample/Components/ListView.swift +++ b/Demo/PowerSyncExample/Components/ListView.swift @@ -1,6 +1,7 @@ import SwiftUI import IdentifiedCollections import SwiftUINavigation +import PowerSync struct ListView: View { @Environment(SystemManager.self) private var system @@ -9,18 +10,32 @@ struct ListView: View { @State private var error: Error? @State private var newList: NewListContent? @State private var editing: Bool = false - @State private var didSync: Bool = false + @State private var status: SyncStatusData? = nil var body: some View { - if !didSync { - Text("Busy with sync!").task { - do { - try await system.db.waitForFirstSync(priority: 1) - didSync = true; - } catch {} + if status?.hasSynced != true { + VStack { + if let status = self.status { + if status.hasSynced != true { + Text("Busy with initial sync...") + + if let progress = status.downloadProgress { + ProgressView(value: progress.fraction) + + if progress.downloadedOperations == progress.totalOperations { + Text("Applying server-side changes...") + } else { + Text("Downloaded \(progress.downloadedOperations) out of \(progress.totalOperations)") + } + } + } + } else { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } } } - + List { if let error { ErrorText(error) @@ -79,6 +94,13 @@ struct ListView: View { } } } + .task { + self.status = system.db.currentStatus + + for await status in system.db.currentStatus.asFlow() { + self.status = status + } + } } func handleDelete(at offset: IndexSet) async { diff --git a/Sources/PowerSync/Kotlin/sync/KotlinSyncStatusData.swift b/Sources/PowerSync/Kotlin/sync/KotlinSyncStatusData.swift index a1dd744..0d2d759 100644 --- a/Sources/PowerSync/Kotlin/sync/KotlinSyncStatusData.swift +++ b/Sources/PowerSync/Kotlin/sync/KotlinSyncStatusData.swift @@ -37,6 +37,11 @@ extension KotlinSyncStatusDataProtocol { ) } + var downloadProgress: (any SyncDownloadProgress)? { + guard let kotlinProgress = base.downloadProgress else { return nil } + return KotlinSyncDownloadProgress(progress: kotlinProgress) + } + var hasSynced: Bool? { base.hasSynced?.boolValue } @@ -80,3 +85,33 @@ extension KotlinSyncStatusDataProtocol { ) } } + +protocol KotlinProgressWithOperationsProtocol: ProgressWithOperations { + var base: any PowerSyncKotlin.ProgressWithOperations { get } +} + +extension KotlinProgressWithOperationsProtocol { + var totalOperations: Int32 { + return base.totalOperations + } + + var downloadedOperations: Int32 { + return base.downloadedOperations + } +} + +struct KotlinProgressWithOperations: KotlinProgressWithOperationsProtocol { + let base: PowerSyncKotlin.ProgressWithOperations +} + +struct KotlinSyncDownloadProgress: KotlinProgressWithOperationsProtocol, SyncDownloadProgress { + let progress: PowerSyncKotlin.SyncDownloadProgress + + var base: any PowerSyncKotlin.ProgressWithOperations { + progress + } + + func untilPriority(priority: BucketPriority) -> any ProgressWithOperations { + return KotlinProgressWithOperations(base: progress.untilPriority(priority: priority.priorityCode)) + } +} diff --git a/Sources/PowerSync/Protocol/sync/DownloadProgress.swift b/Sources/PowerSync/Protocol/sync/DownloadProgress.swift new file mode 100644 index 0000000..d504fee --- /dev/null +++ b/Sources/PowerSync/Protocol/sync/DownloadProgress.swift @@ -0,0 +1,49 @@ +/// Information about a progressing download. +/// +/// This reports the ``totalOperations`` amount of operations to download, how many of them +/// have already been downloaded as ``downloadedOperations`` and finally a ``fraction`` indicating +/// relative progress. +/// +/// To obtain a ``ProgressWithOperations`` instance, either use ``SyncStatusData/downloadProgress`` +/// for global progress or ``SyncDownloadProgress/untilPriority``. +public protocol ProgressWithOperations { + /// How many operations need to be downloaded in total for the current download + /// to complete. + var totalOperations: Int32 { get } + + /// How many operations, out of ``totalOperations``, have already been downloaded. + var downloadedOperations: Int32 { get } +} + +public extension ProgressWithOperations { + var fraction: Float { + if (self.totalOperations == 0) { + return 0.0 + } + + return Float.init(self.downloadedOperations) / Float.init(self.totalOperations) + } +} + +/// Provides realtime progress on how PowerSync is downloading rows. +/// +/// This type reports progress by extending ``ProgressWithOperations``, meaning that the +/// ``totalOperations``, ``downloadedOperations`` and ``fraction`` properties are available +/// on this instance. +/// Additionally, it's possible to obtain progress towards a specific priority only (instead +/// of tracking progress for the entire download) by using ``untilPriority``. +/// +/// The reported progress always reflects the status towards the end of a sync iteration (after +/// which a consistent snapshot of all buckets is available locally). +/// +/// In rare cases (in particular, when a [compacting](https://docs.powersync.com/usage/lifecycle-maintenance/compacting-buckets) +/// operation takes place between syncs), it's possible for the returned numbers to be slightly +/// inaccurate. For this reason, ``SyncDownloadProgress`` should be seen as an approximation of progress. +/// The information returned is good enough to build progress bars, but not exaxt enough to track +/// individual download counts. +/// +/// Also note that data is downloaded in bulk, which means that individual counters are unlikely +/// to be updated one-by-one. +public protocol SyncDownloadProgress: ProgressWithOperations { + func untilPriority(priority: BucketPriority) -> ProgressWithOperations +} diff --git a/Sources/PowerSync/Protocol/sync/SyncStatusData.swift b/Sources/PowerSync/Protocol/sync/SyncStatusData.swift index d4aa035..66b836a 100644 --- a/Sources/PowerSync/Protocol/sync/SyncStatusData.swift +++ b/Sources/PowerSync/Protocol/sync/SyncStatusData.swift @@ -11,6 +11,12 @@ public protocol SyncStatusData { /// Indicates whether the system is actively downloading changes. var downloading: Bool { get } + /// Realtime progress information about downloaded operations during an active sync. + /// + /// For more information on what progress is reported, see ``SyncDownloadProgress``. + /// This value will be non-null only if ``downloading`` is `true`. + var downloadProgress: SyncDownloadProgress? { get } + /// Indicates whether the system is actively uploading changes. var uploading: Bool { get } From c9dbc2e10368af0f5e1bf15f7cc019e0364dd535 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 5 May 2025 13:39:21 +0200 Subject: [PATCH 2/4] Format --- Demo/PowerSyncExample/Components/ListView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Demo/PowerSyncExample/Components/ListView.swift b/Demo/PowerSyncExample/Components/ListView.swift index 484ee20..6e3ebc1 100644 --- a/Demo/PowerSyncExample/Components/ListView.swift +++ b/Demo/PowerSyncExample/Components/ListView.swift @@ -35,7 +35,7 @@ struct ListView: View { } } } - + List { if let error { ErrorText(error) From 391f7effaca0f188b0e1deff5324c552e4c0b632 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 5 May 2025 13:40:59 +0200 Subject: [PATCH 3/4] Build docs for all branches --- .github/workflows/docs.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 5ba4276..3d3434d 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -2,8 +2,6 @@ name: Deploy Docs on: push: - branches: - - main permissions: contents: read @@ -47,6 +45,7 @@ jobs: # Deployment job deploy: + if: ${{ github.ref == 'refs/heads/main' }} environment: name: github-pages url: ${{ needs.build.outputs.page_url }} From 798752e45f3d9d0f636fb881185f76cf2e9f70a1 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 5 May 2025 13:58:47 +0200 Subject: [PATCH 4/4] Improve docs --- .../Protocol/sync/DownloadProgress.swift | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/Sources/PowerSync/Protocol/sync/DownloadProgress.swift b/Sources/PowerSync/Protocol/sync/DownloadProgress.swift index d504fee..5b114ab 100644 --- a/Sources/PowerSync/Protocol/sync/DownloadProgress.swift +++ b/Sources/PowerSync/Protocol/sync/DownloadProgress.swift @@ -5,7 +5,7 @@ /// relative progress. /// /// To obtain a ``ProgressWithOperations`` instance, either use ``SyncStatusData/downloadProgress`` -/// for global progress or ``SyncDownloadProgress/untilPriority``. +/// for global progress or ``SyncDownloadProgress/untilPriority(priority:)``. public protocol ProgressWithOperations { /// How many operations need to be downloaded in total for the current download /// to complete. @@ -16,6 +16,12 @@ public protocol ProgressWithOperations { } public extension ProgressWithOperations { + /// The relative amount of ``totalOperations`` to items in ``downloadedOperations``, as a + /// number between `0.0` and `1.0` (inclusive). + /// + /// When this number reaches `1.0`, all changes have been received from the sync service. + /// Actually applying these changes happens before the ``SyncStatusData/downloadProgress`` + /// field is cleared though, so progress can stay at `1.0` for a short while before completing. var fraction: Float { if (self.totalOperations == 0) { return 0.0 @@ -28,10 +34,10 @@ public extension ProgressWithOperations { /// Provides realtime progress on how PowerSync is downloading rows. /// /// This type reports progress by extending ``ProgressWithOperations``, meaning that the -/// ``totalOperations``, ``downloadedOperations`` and ``fraction`` properties are available -/// on this instance. +/// ``ProgressWithOperations/totalOperations``, ``ProgressWithOperations/downloadedOperations`` +/// and ``ProgressWithOperations/fraction`` properties are available on this instance. /// Additionally, it's possible to obtain progress towards a specific priority only (instead -/// of tracking progress for the entire download) by using ``untilPriority``. +/// of tracking progress for the entire download) by using ``untilPriority(priority:)``. /// /// The reported progress always reflects the status towards the end of a sync iteration (after /// which a consistent snapshot of all buckets is available locally). @@ -45,5 +51,10 @@ public extension ProgressWithOperations { /// Also note that data is downloaded in bulk, which means that individual counters are unlikely /// to be updated one-by-one. public protocol SyncDownloadProgress: ProgressWithOperations { + /// Returns download progress towardss all data up until the specified `priority` + /// being received. + /// + /// The returned ``ProgressWithOperations`` instance tracks the target amount of operations that + /// need to be downloaded in total and how many of them have already been received. func untilPriority(priority: BucketPriority) -> ProgressWithOperations }