Skip to content

Add sync progress APIs #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ name: Deploy Docs

on:
push:
branches:
- main

permissions:
contents: read
Expand Down Expand Up @@ -47,6 +45,7 @@ jobs:

# Deployment job
deploy:
if: ${{ github.ref == 'refs/heads/main' }}
environment:
name: github-pages
url: ${{ needs.build.outputs.page_url }}
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "33297127250b66812faa920958a24bae46bf9e9d1c38ea6b84ca413efaf16afd",
"originHash" : "2d885a1b46f17f9239b7876e3889168a6de98024718f2d7af03aede290c8a86a",
"pins" : [
{
"identity" : "anycodable",
Expand All @@ -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",
Expand Down
38 changes: 30 additions & 8 deletions Demo/PowerSyncExample/Components/ListView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import SwiftUI
import IdentifiedCollections
import SwiftUINavigation
import PowerSync

struct ListView: View {
@Environment(SystemManager.self) private var system
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
35 changes: 35 additions & 0 deletions Sources/PowerSync/Kotlin/sync/KotlinSyncStatusData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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))
}
}
60 changes: 60 additions & 0 deletions Sources/PowerSync/Protocol/sync/DownloadProgress.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/// 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(priority:)``.
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 {
/// 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
}

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
/// ``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(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).
///
/// 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 {
/// 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
}
6 changes: 6 additions & 0 deletions Sources/PowerSync/Protocol/sync/SyncStatusData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down