Skip to content

Commit 6afa299

Browse files
Swift Strict Concurrency
1 parent 1c1f2bb commit 6afa299

File tree

9 files changed

+124
-50
lines changed

9 files changed

+124
-50
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import PowerSyncKotlin
2+
3+
// Since AllLeaseCallback is a protocol from PowerSyncKotlin, we need to use a wrapper class
4+
// to make it Sendable since we can't extend the protocol directly with Sendable.
5+
final class SendableAllLeaseCallback: @unchecked Sendable {
6+
private let wrapped: any PowerSyncKotlin.AllLeaseCallback
7+
8+
init(_ callback: any PowerSyncKotlin.AllLeaseCallback) {
9+
wrapped = callback
10+
}
11+
12+
func execute(
13+
writeLease: PowerSyncKotlin.SwiftLeaseAdapter,
14+
readLeases: [PowerSyncKotlin.SwiftLeaseAdapter]
15+
) throws {
16+
try wrapped.execute(writeLease: writeLease, readLeases: readLeases)
17+
}
18+
}

Sources/PowerSync/Kotlin/KotlinSQLiteConnectionPool.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ final class SwiftSQLiteConnectionPoolAdapter: PowerSyncKotlin.SwiftPoolAdapter {
2121
}
2222

2323
func linkExternalUpdates(callback: any KotlinSuspendFunction1) {
24-
updateTrackingTask = Task {
24+
let sendableCallback = SendableSuspendFunction1(callback)
25+
updateTrackingTask = Task { [pool] in
2526
do {
2627
for try await updates in pool.tableUpdates {
27-
_ = try await callback.invoke(p1: updates)
28+
_ = try await sendableCallback.invoke(p1: updates)
2829
}
2930
} catch {
3031
// none of these calls should actually throw
@@ -41,8 +42,9 @@ final class SwiftSQLiteConnectionPoolAdapter: PowerSyncKotlin.SwiftPoolAdapter {
4142

4243
func __leaseRead(callback: any LeaseCallback) async throws {
4344
return try await wrapExceptions {
45+
let sendableCallback = SendableLeaseCallback(callback)
4446
try await pool.read { lease in
45-
try callback.execute(
47+
try sendableCallback.execute(
4648
lease: KotlinLeaseAdapter(
4749
lease: lease
4850
)
@@ -53,8 +55,9 @@ final class SwiftSQLiteConnectionPoolAdapter: PowerSyncKotlin.SwiftPoolAdapter {
5355

5456
func __leaseWrite(callback: any LeaseCallback) async throws {
5557
return try await wrapExceptions {
58+
let sendableCallback = SendableLeaseCallback(callback)
5659
try await pool.write { lease in
57-
try callback.execute(
60+
try sendableCallback.execute(
5861
lease: KotlinLeaseAdapter(
5962
lease: lease
6063
)
@@ -67,8 +70,9 @@ final class SwiftSQLiteConnectionPoolAdapter: PowerSyncKotlin.SwiftPoolAdapter {
6770
// FIXME, actually use all connections
6871
// We currently only use this for schema updates
6972
return try await wrapExceptions {
73+
let sendableCallback = SendableAllLeaseCallback(callback)
7074
try await pool.write { lease in
71-
try callback.execute(
75+
try sendableCallback.execute(
7276
writeLease: KotlinLeaseAdapter(
7377
lease: lease
7478
),
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import PowerSyncKotlin
2+
3+
// Since SendableSuspendFunction1 is a protocol from PowerSyncKotlin, we need to use a wrapper class
4+
// to make it Sendable since we can't extend the protocol directly with Sendable.
5+
final class SendableSuspendFunction1: @unchecked Sendable {
6+
private let wrapped: any PowerSyncKotlin.KotlinSuspendFunction1
7+
8+
init(_ function: any PowerSyncKotlin.KotlinSuspendFunction1) {
9+
wrapped = function
10+
}
11+
12+
func invoke(p1: Any?) async throws -> Any? {
13+
return try await wrapped.invoke(p1: p1)
14+
}
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import PowerSyncKotlin
2+
3+
// Since LeaseCallback is a protocol from PowerSyncKotlin, we need to use a wrapper class
4+
// to make it Sendable since we can't extend the protocol directly with Sendable.
5+
final class SendableLeaseCallback: @unchecked Sendable {
6+
private let wrapped: any PowerSyncKotlin.LeaseCallback
7+
8+
init(_ callback: any PowerSyncKotlin.LeaseCallback) {
9+
self.wrapped = callback
10+
}
11+
12+
func execute(lease: PowerSyncKotlin.SwiftLeaseAdapter) throws {
13+
try wrapped.execute(lease: lease)
14+
}
15+
}

Sources/PowerSync/Kotlin/kotlinWithSession.swift

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,9 @@ import PowerSyncKotlin
33
func kotlinWithSession<ReturnType>(
44
db: OpaquePointer,
55
action: @escaping () throws -> ReturnType,
6-
onComplete: @escaping (Result<ReturnType, Error>, Set<String>) -> Void,
7-
) throws {
8-
try withSession(
6+
) throws -> WithSessionResult<ReturnType> {
7+
let baseResult = try withSession(
98
db: UnsafeMutableRawPointer(db),
10-
onComplete: { powerSyncResult, updates in
11-
let result: Result<ReturnType, Error>
12-
switch powerSyncResult {
13-
case let success as PowerSyncResult.Success:
14-
do {
15-
let casted = try safeCast(success.value, to: ReturnType.self)
16-
result = .success(casted)
17-
} catch {
18-
result = .failure(error)
19-
}
20-
21-
case let failure as PowerSyncResult.Failure:
22-
result = .failure(failure.exception.asError())
23-
24-
default:
25-
result = .failure(PowerSyncError.operationFailed(message: "Unknown error encountered when processing session"))
26-
}
27-
onComplete(result, updates)
28-
},
299
block: {
3010
do {
3111
return try PowerSyncResult.Success(value: action())
@@ -34,4 +14,26 @@ func kotlinWithSession<ReturnType>(
3414
}
3515
}
3616
)
17+
18+
var outputResult: Result<ReturnType, Error>
19+
switch baseResult.blockResult {
20+
case let success as PowerSyncResult.Success:
21+
do {
22+
let casted = try safeCast(success.value, to: ReturnType.self)
23+
outputResult = .success(casted)
24+
} catch {
25+
outputResult = .failure(error)
26+
}
27+
28+
case let failure as PowerSyncResult.Failure:
29+
outputResult = .failure(failure.exception.asError())
30+
31+
default:
32+
outputResult = .failure(PowerSyncError.operationFailed(message: "Unknown error encountered when processing session"))
33+
}
34+
35+
return WithSessionResult(
36+
blockResult: outputResult,
37+
affectedTables: baseResult.affectedTables
38+
)
3739
}

Sources/PowerSync/Protocol/SQLiteConnectionPool.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public protocol SQLiteConnectionLease {
88

99
/// An implementation of a connection pool providing asynchronous access to a single writer and multiple readers.
1010
/// This is the underlying pool implementation on which the higher-level PowerSync Swift SDK is built on.
11-
public protocol SQLiteConnectionPoolProtocol {
11+
public protocol SQLiteConnectionPoolProtocol: Sendable {
1212
var tableUpdates: AsyncStream<Set<String>> { get }
1313

1414
/// Calls the callback with a read-only connection temporarily leased from the pool.
@@ -30,5 +30,5 @@ public protocol SQLiteConnectionPoolProtocol {
3030
) async throws
3131

3232
/// Closes the connection pool and associated resources.
33-
func close() throws
33+
func close() async throws
3434
}

Sources/PowerSync/Utils/withSession.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import Foundation
22

3+
public struct WithSessionResult<ResultType: Sendable>: Sendable {
4+
public let blockResult: Result<ResultType, Error>
5+
public let affectedTables: Set<String>
6+
}
7+
38
/// Executes an action within a SQLite database connection session and handles its result.
49
///
510
/// The Raw SQLite connection is only available in some niche scenarios.
@@ -31,11 +36,9 @@ import Foundation
3136
public func withSession<ReturnType>(
3237
db: OpaquePointer,
3338
action: @escaping () throws -> ReturnType,
34-
onComplete: @escaping (Result<ReturnType, Error>, Set<String>) -> Void,
35-
) throws {
39+
) throws -> WithSessionResult<ReturnType> {
3640
return try kotlinWithSession(
3741
db: db,
3842
action: action,
39-
onComplete: onComplete,
4043
)
4144
}

Sources/PowerSyncGRDB/Connections/GRDBConnectionPool.swift

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ import SQLite3
1212
/// - Provides async streams of table updates for replication.
1313
/// - Bridges GRDB's managed connections to PowerSync's lease abstraction.
1414
/// - Allows both read and write access to raw SQLite connections.
15-
final class GRDBConnectionPool: SQLiteConnectionPoolProtocol {
15+
actor GRDBConnectionPool: SQLiteConnectionPoolProtocol {
1616
let pool: DatabasePool
1717

18-
public private(set) var tableUpdates: AsyncStream<Set<String>>
18+
let tableUpdates: AsyncStream<Set<String>>
1919
private var tableUpdatesContinuation: AsyncStream<Set<String>>.Continuation?
2020

21-
public init(
21+
init(
2222
pool: DatabasePool
2323
) {
2424
self.pool = pool
@@ -37,7 +37,7 @@ final class GRDBConnectionPool: SQLiteConnectionPoolProtocol {
3737
tableUpdatesContinuation = tempContinuation
3838
}
3939

40-
public func processPowerSyncUpdates(_ updates: Set<String>) async throws {
40+
func processPowerSyncUpdates(_ updates: Set<String>) async throws {
4141
try await pool.write { database in
4242
for table in updates {
4343
try database.notifyChanges(in: Table(table))
@@ -47,7 +47,7 @@ final class GRDBConnectionPool: SQLiteConnectionPoolProtocol {
4747
tableUpdatesContinuation?.yield(updates)
4848
}
4949

50-
public func read(
50+
func read(
5151
onConnection: @Sendable @escaping (SQLiteConnectionLease) throws -> Void
5252
) async throws {
5353
try await pool.read { database in
@@ -57,28 +57,27 @@ final class GRDBConnectionPool: SQLiteConnectionPoolProtocol {
5757
}
5858
}
5959

60-
public func write(
60+
func write(
6161
onConnection: @Sendable @escaping (SQLiteConnectionLease) throws -> Void
6262
) async throws {
6363
// Don't start an explicit transaction, we do this internally
64-
try await pool.writeWithoutTransaction { database in
64+
let result = try await pool.writeWithoutTransaction { database in
6565
guard let pointer = database.sqliteConnection else {
6666
throw PowerSyncGRDBError.connectionUnavailable
6767
}
6868

69-
try withSession(
69+
return try withSession(
7070
db: pointer,
7171
) {
7272
try onConnection(
7373
GRDBConnectionLease(database: database)
7474
)
75-
} onComplete: { _, changes in
76-
self.tableUpdatesContinuation?.yield(changes)
7775
}
7876
}
77+
tableUpdatesContinuation?.yield(result.affectedTables)
7978
}
8079

81-
public func withAllConnections(
80+
func withAllConnections(
8281
onConnection: @Sendable @escaping (SQLiteConnectionLease, [SQLiteConnectionLease]) throws -> Void
8382
) async throws {
8483
// FIXME, we currently don't support updating the schema
@@ -89,7 +88,7 @@ final class GRDBConnectionPool: SQLiteConnectionPoolProtocol {
8988
pool.invalidateReadOnlyConnections()
9089
}
9190

92-
public func close() throws {
91+
func close() async throws {
9392
try pool.close()
9493
}
9594
}

Tests/PowerSyncGRDBTests/BasicTest.swift

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ struct User: Codable, Identifiable, FetchableRecord, PersistableRecord {
88
var id: String
99
var name: String
1010

11-
static var databaseTableName = "users"
11+
static let databaseTableName = "users"
1212

1313
enum Columns {
1414
static let id = Column(CodingKeys.id)
@@ -21,7 +21,7 @@ struct Pet: Codable, Identifiable, FetchableRecord, PersistableRecord {
2121
var name: String
2222
var ownerId: String
2323

24-
static var databaseTableName = "pets"
24+
static let databaseTableName = "pets"
2525

2626
enum CodingKeys: String, CodingKey {
2727
case id
@@ -185,7 +185,12 @@ final class GRDBTests: XCTestCase {
185185

186186
let resultsStore = ResultsStore()
187187

188-
let watchTask = Task {
188+
let watchTask = Task { [database] in
189+
guard let database = database else {
190+
XCTFail("Database is nil")
191+
return
192+
}
193+
189194
let stream = try database.watch(
190195
options: WatchOptions(
191196
sql: "SELECT name FROM users ORDER BY id",
@@ -236,7 +241,11 @@ final class GRDBTests: XCTestCase {
236241

237242
let resultsStore = ResultsStore()
238243

239-
let watchTask = Task {
244+
let watchTask = Task { [database] in
245+
guard let database = database else {
246+
XCTFail("Database is nil")
247+
return
248+
}
240249
let stream = try database.watch(
241250
options: WatchOptions(
242251
sql: "SELECT name FROM users ORDER BY id",
@@ -292,7 +301,11 @@ final class GRDBTests: XCTestCase {
292301

293302
let resultsStore = ResultsStore()
294303

295-
let watchTask = Task {
304+
let watchTask = Task { [pool] in
305+
guard let pool = pool else {
306+
XCTFail("Database pool is nil")
307+
return
308+
}
296309
let observation = ValueObservation.tracking {
297310
try User.order(User.Columns.name.asc).fetchAll($0)
298311
}
@@ -353,7 +366,12 @@ final class GRDBTests: XCTestCase {
353366

354367
let resultsStore = ResultsStore()
355368

356-
let watchTask = Task {
369+
let watchTask = Task { [pool] in
370+
guard let pool = pool else {
371+
XCTFail("Database pool is nil")
372+
return
373+
}
374+
357375
let observation = ValueObservation.tracking {
358376
try User.order(User.Columns.name.asc).fetchAll($0)
359377
}

0 commit comments

Comments
 (0)