Skip to content

Commit bbb2de3

Browse files
author
DominicGBauer
committed
feat: add sync changes
1 parent 549f04e commit bbb2de3

File tree

8 files changed

+472
-8
lines changed

8 files changed

+472
-8
lines changed
Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,56 @@
11
public protocol PowerSyncTransactionProtocol {
22
/// Execute a write query and return the number of affected rows
33
func execute(
4-
sql: String,
5-
parameters: [Any]?
4+
_ sql: String,
5+
_ parameters: [Any]?
66
) async throws -> Int64
77

88
/// Execute a read-only query and return a single optional result
99
func getOptional<RowType>(
10-
sql: String,
11-
parameters: [Any]?,
10+
_ sql: String,
11+
_ parameters: [Any]?,
1212
mapper: @escaping (SqlCursor) -> RowType
1313
) async throws -> RowType?
1414

1515
/// Execute a read-only query and return all results
1616
func getAll<RowType>(
17-
sql: String,
18-
parameters: [Any]?,
17+
_ sql: String,
18+
_ parameters: [Any]?,
1919
mapper: @escaping (SqlCursor) -> RowType
2020
) async throws -> [RowType]
2121

2222
/// Execute a read-only query and return a single result
2323
/// Throws if no result is found
2424
func get<RowType>(
25-
sql: String,
26-
parameters: [Any]?,
25+
_ sql: String,
26+
_ parameters: [Any]?,
2727
mapper: @escaping (SqlCursor) -> RowType
2828
) async throws -> RowType
2929
}
30+
31+
extension PowerSyncTransactionProtocol {
32+
public func execute(_ sql: String) async throws -> Int64 {
33+
return try await execute(sql, [])
34+
}
35+
36+
public func get<RowType>(
37+
_ sql: String,
38+
mapper: @escaping (SqlCursor) -> RowType
39+
) async throws -> RowType {
40+
return try await get(sql, [], mapper: mapper)
41+
}
42+
43+
public func getAll<RowType>(
44+
_ sql: String,
45+
mapper: @escaping (SqlCursor) -> RowType
46+
) async throws -> [RowType] {
47+
return try await getAll(sql, [], mapper: mapper)
48+
}
49+
50+
public func getOptional<RowType>(
51+
_ sql: String,
52+
mapper: @escaping (SqlCursor) -> RowType
53+
) async throws -> RowType? {
54+
return try await getOptional(sql, [], mapper: mapper)
55+
}
56+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import Foundation
2+
3+
struct StreamingSyncCheckpointDiff: Codable {
4+
let lastOpId: String
5+
let updatedBuckets: [BucketChecksum]
6+
let removedBuckets: [String]
7+
let writeCheckpoint: String?
8+
9+
enum CodingKeys: String, CodingKey {
10+
case lastOpId = "last_op_id"
11+
case updatedBuckets = "updated_buckets"
12+
case removedBuckets = "removed_buckets"
13+
case writeCheckpoint = "write_checkpoint"
14+
}
15+
16+
init(lastOpId: String, updatedBuckets: [BucketChecksum], removedBuckets: [String], writeCheckpoint: String? = nil) {
17+
self.lastOpId = lastOpId
18+
self.updatedBuckets = updatedBuckets
19+
self.removedBuckets = removedBuckets
20+
self.writeCheckpoint = writeCheckpoint
21+
}
22+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import Foundation
2+
import AnyCodable
3+
4+
struct StreamingSyncRequest: Codable {
5+
let buckets: [BucketRequest]
6+
let includeChecksum: Bool
7+
let clientId: String
8+
let parameters: [String: AnyCodable]
9+
private let rawData: Bool = true
10+
11+
enum CodingKeys: String, CodingKey {
12+
case buckets
13+
case includeChecksum = "include_checksum"
14+
case clientId = "client_id"
15+
case parameters
16+
case rawData = "raw_data"
17+
}
18+
19+
init(from decoder: Decoder) throws {
20+
let container = try decoder.container(keyedBy: CodingKeys.self)
21+
22+
self.buckets = try container.decode([BucketRequest].self, forKey: .buckets)
23+
self.includeChecksum = try container.decode(Bool.self, forKey: .includeChecksum)
24+
self.clientId = try container.decode(String.self, forKey: .clientId)
25+
self.parameters = try container.decode([String: AnyCodable].self, forKey: .parameters)
26+
}
27+
28+
func encode(to encoder: Encoder) throws {
29+
var container = encoder.container(keyedBy: CodingKeys.self)
30+
31+
try container.encode(buckets, forKey: .buckets)
32+
try container.encode(includeChecksum, forKey: .includeChecksum)
33+
try container.encode(clientId, forKey: .clientId)
34+
try container.encode(parameters, forKey: .parameters)
35+
try container.encode(rawData, forKey: .rawData)
36+
}
37+
38+
init(buckets: [BucketRequest],
39+
includeChecksum: Bool = true,
40+
clientId: String,
41+
parameters: [String: AnyCodable] = [:]
42+
) {
43+
self.buckets = buckets
44+
self.includeChecksum = includeChecksum
45+
self.clientId = clientId
46+
self.parameters = parameters
47+
}
48+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Foundation
2+
3+
struct SyncDataBatch: Codable {
4+
let buckets: [SyncDataBucket]
5+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Foundation
2+
3+
struct SyncDataBucket: Codable {
4+
let bucket: String
5+
let data: [OplogEntry]
6+
let hasMore: Bool
7+
let after: String?
8+
let nextAfter: String?
9+
10+
enum CodingKeys: String, CodingKey {
11+
case bucket
12+
case data
13+
case hasMore = "has_more"
14+
case after
15+
case nextAfter = "next_after"
16+
}
17+
18+
init(bucket: String, data: [OplogEntry], hasMore: Bool = false, after: String? = nil, nextAfter: String? = nil) {
19+
self.bucket = bucket
20+
self.data = data
21+
self.hasMore = hasMore
22+
self.after = after
23+
self.nextAfter = nextAfter
24+
}
25+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import Foundation
2+
3+
struct SyncLocalDatabaseResult: Codable {
4+
var ready: Bool = true
5+
var checkpointValid: Bool = true
6+
var checkpointFailures: [String]? = nil
7+
8+
enum CodingKeys: String, CodingKey {
9+
case ready
10+
case checkpointValid = "valid"
11+
case checkpointFailures = "failed_buckets"
12+
}
13+
14+
init(ready: Bool = true, checkpointValid: Bool = true, checkpointFailures: [String]? = nil) {
15+
self.ready = ready
16+
self.checkpointValid = checkpointValid
17+
self.checkpointFailures = checkpointFailures
18+
}
19+
20+
init(from decoder: Decoder) throws {
21+
let container = try decoder.container(keyedBy: CodingKeys.self)
22+
ready = try container.decodeIfPresent(Bool.self, forKey: .ready) ?? true
23+
checkpointValid = try container.decode(Bool.self, forKey: .checkpointValid)
24+
checkpointFailures = try container.decodeIfPresent([String].self, forKey: .checkpointFailures)
25+
}
26+
}
27+
28+
extension SyncLocalDatabaseResult: CustomStringConvertible {
29+
var description: String {
30+
return "SyncLocalDatabaseResult<ready=\(ready), checkpointValid=\(checkpointValid), failures=\(checkpointFailures ?? [])>"
31+
}
32+
}
33+
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import Foundation
2+
import Combine
3+
4+
/// Protocol defining the sync status interface
5+
public protocol SyncStatusData {
6+
/// true if currently connected.
7+
///
8+
/// This means the PowerSync connection is ready to download, and PowerSyncBackendConnector.uploadData may be called for any local changes.
9+
var connected: Bool { get }
10+
11+
/// true if the PowerSync connection is busy connecting.
12+
///
13+
/// During this stage, PowerSyncBackendConnector.uploadData may already be called, and uploading may be true.
14+
var connecting: Bool { get }
15+
16+
/// true if actively downloading changes.
17+
///
18+
/// This is only true when connected is also true.
19+
var downloading: Bool { get }
20+
21+
/// true if uploading changes
22+
var uploading: Bool { get }
23+
24+
/// Time that a last sync has fully completed, if any.
25+
///
26+
/// Currently this is reset to null after a restart.
27+
var lastSyncedAt: Date? { get }
28+
29+
/// Indicates whether there has been at least one full sync, if any.
30+
///
31+
/// Is nil when unknown, for example when state is still being loaded from the database.
32+
var hasSynced: Bool? { get }
33+
34+
/// Error during uploading.
35+
///
36+
/// Cleared on the next successful upload.
37+
var uploadError: Any? { get }
38+
39+
/// Error during downloading (including connecting).
40+
///
41+
/// Cleared on the next successful data download.
42+
var downloadError: Any? { get }
43+
44+
/// Convenience getter for either the value of downloadError or uploadError
45+
var anyError: Any? { get }
46+
}
47+
48+
/// Internal container for sync status data
49+
struct SyncStatusDataContainer: SyncStatusData {
50+
let connected: Bool
51+
let connecting: Bool
52+
let downloading: Bool
53+
let uploading: Bool
54+
let lastSyncedAt: Date?
55+
let hasSynced: Bool?
56+
let uploadError: Any?
57+
let downloadError: Any?
58+
59+
var anyError: Any? {
60+
return downloadError ?? uploadError
61+
}
62+
63+
init(
64+
connected: Bool = false,
65+
connecting: Bool = false,
66+
downloading: Bool = false,
67+
uploading: Bool = false,
68+
lastSyncedAt: Date? = nil,
69+
hasSynced: Bool? = nil,
70+
uploadError: Any? = nil,
71+
downloadError: Any? = nil
72+
) {
73+
self.connected = connected
74+
self.connecting = connecting
75+
self.downloading = downloading
76+
self.uploading = uploading
77+
self.lastSyncedAt = lastSyncedAt
78+
self.hasSynced = hasSynced
79+
self.uploadError = uploadError
80+
self.downloadError = downloadError
81+
}
82+
}
83+
84+
/// Public sync status class
85+
public class SyncStatus: SyncStatusData {
86+
private var data: SyncStatusDataContainer
87+
private let stateSubject: CurrentValueSubject<SyncStatusDataContainer, Never>
88+
89+
init(data: SyncStatusDataContainer = SyncStatusDataContainer()) {
90+
self.data = data
91+
self.stateSubject = CurrentValueSubject(data)
92+
}
93+
94+
/// Returns a publisher which emits whenever the sync status has changed
95+
public func asPublisher() -> AnyPublisher<SyncStatusData, Never> {
96+
return stateSubject
97+
.map { $0 as SyncStatusData }
98+
.eraseToAnyPublisher()
99+
}
100+
101+
/// Updates the internal sync status indicators and emits publisher updates
102+
internal func update(
103+
connected: Bool? = nil,
104+
connecting: Bool? = nil,
105+
downloading: Bool? = nil,
106+
uploading: Bool? = nil,
107+
hasSynced: Bool? = nil,
108+
lastSyncedAt: Date? = nil,
109+
uploadError: Any? = nil,
110+
downloadError: Any? = nil,
111+
clearUploadError: Bool = false,
112+
clearDownloadError: Bool = false
113+
) {
114+
data = SyncStatusDataContainer(
115+
connected: connected ?? data.connected,
116+
connecting: connecting ?? data.connecting,
117+
downloading: downloading ?? data.downloading,
118+
uploading: uploading ?? data.uploading,
119+
lastSyncedAt: lastSyncedAt ?? data.lastSyncedAt,
120+
hasSynced: hasSynced ?? data.hasSynced,
121+
uploadError: clearUploadError ? nil : (uploadError ?? data.uploadError),
122+
downloadError: clearDownloadError ? nil : (downloadError ?? data.downloadError)
123+
)
124+
stateSubject.send(data)
125+
}
126+
127+
public var connected: Bool { data.connected }
128+
public var connecting: Bool { data.connecting }
129+
public var downloading: Bool { data.downloading }
130+
public var uploading: Bool { data.uploading }
131+
public var lastSyncedAt: Date? { data.lastSyncedAt }
132+
public var hasSynced: Bool? { data.hasSynced }
133+
public var uploadError: Any? { data.uploadError }
134+
public var downloadError: Any? { data.downloadError }
135+
public var anyError: Any? { data.anyError }
136+
137+
public var description: String {
138+
return "SyncStatus(connected=\(connected), connecting=\(connecting), downloading=\(downloading), uploading=\(uploading), lastSyncedAt=\(String(describing: lastSyncedAt)), hasSynced=\(String(describing: hasSynced)), error=\(String(describing: anyError)))"
139+
}
140+
141+
/// Creates an empty sync status instance
142+
public static func empty() -> SyncStatus {
143+
return SyncStatus()
144+
}
145+
}

0 commit comments

Comments
 (0)