Skip to content

Commit 0c38635

Browse files
feat: improve errors in swift (#116)
* Thrown errors * Backend and Supabase connector exceptions thrown * feat: improve error handling for swift * fix: failing test * fix: remove throws from connectors --------- Co-authored-by: Kevin Galligan <[email protected]>
1 parent 1026961 commit 0c38635

File tree

18 files changed

+208
-103
lines changed

18 files changed

+208
-103
lines changed

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# Changelog
22

3+
## 1.0.0-BETA21
4+
5+
* Improve error handling for Swift by adding @Throws annotation so errors can be handled in Swift
6+
* Throw PowerSync exceptions for all public facing methods
7+
38
## 1.0.0-BETA20
9+
410
* Add cursor optional functions: `getStringOptional`, `getLongOptional`, `getDoubleOptional`, `getBooleanOptional` and `getBytesOptional` when using the column name which allow for optional return types
511
* Throw errors for invalid column on all cursor functions
612
* `getString`, `getLong`, `getBytes`, `getDouble` and `getBoolean` used with the column name will now throw an error for non-null values and expect a non optional return type
@@ -22,8 +28,6 @@
2228
import com.powersync.db.SqlCursor
2329
```
2430

25-
26-
2731
## 1.0.0-BETA18
2832

2933
* BREAKING CHANGE: Move from async sqldelight calls to synchronous calls. This will only affect `readTransaction` and `writeTransaction`where the callback function is no longer asynchronous.

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ let package = Package(
2121
)
2222
,
2323
]
24-
)
24+
)

connectors/supabase/src/commonMain/kotlin/com/powersync/connector/supabase/SupabaseConnector.kt

Lines changed: 71 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.powersync.connectors.PowerSyncBackendConnector
66
import com.powersync.connectors.PowerSyncCredentials
77
import com.powersync.db.crud.CrudEntry
88
import com.powersync.db.crud.UpdateType
9+
import com.powersync.db.runWrappedSuspending
910
import io.github.jan.supabase.SupabaseClient
1011
import io.github.jan.supabase.annotations.SupabaseInternal
1112
import io.github.jan.supabase.auth.Auth
@@ -94,52 +95,61 @@ public class SupabaseConnector(
9495
email: String,
9596
password: String,
9697
) {
97-
supabaseClient.auth.signInWith(Email) {
98-
this.email = email
99-
this.password = password
98+
runWrappedSuspending {
99+
supabaseClient.auth.signInWith(Email) {
100+
this.email = email
101+
this.password = password
102+
}
100103
}
101104
}
102105

103106
public suspend fun signUp(
104107
email: String,
105108
password: String,
106109
) {
107-
supabaseClient.auth.signUpWith(Email) {
108-
this.email = email
109-
this.password = password
110+
runWrappedSuspending {
111+
supabaseClient.auth.signUpWith(Email) {
112+
this.email = email
113+
this.password = password
114+
}
110115
}
111116
}
112117

113118
public suspend fun signOut() {
114-
supabaseClient.auth.signOut()
119+
runWrappedSuspending {
120+
supabaseClient.auth.signOut()
121+
}
115122
}
116123

117124
public fun session(): UserSession? = supabaseClient.auth.currentSessionOrNull()
118125

119126
public val sessionStatus: StateFlow<SessionStatus> = supabaseClient.auth.sessionStatus
120127

121128
public suspend fun loginAnonymously() {
122-
supabaseClient.auth.signInAnonymously()
129+
runWrappedSuspending {
130+
supabaseClient.auth.signInAnonymously()
131+
}
123132
}
124133

125134
/**
126135
* Get credentials for PowerSync.
127136
*/
128-
override suspend fun fetchCredentials(): PowerSyncCredentials {
129-
check(supabaseClient.auth.sessionStatus.value is SessionStatus.Authenticated) { "Supabase client is not authenticated" }
137+
override suspend fun fetchCredentials(): PowerSyncCredentials =
138+
runWrappedSuspending {
139+
check(supabaseClient.auth.sessionStatus.value is SessionStatus.Authenticated) { "Supabase client is not authenticated" }
130140

131-
// Use Supabase token for PowerSync
132-
val session = supabaseClient.auth.currentSessionOrNull() ?: error("Could not fetch Supabase credentials")
141+
// Use Supabase token for PowerSync
142+
val session = supabaseClient.auth.currentSessionOrNull() ?: error("Could not fetch Supabase credentials")
133143

134-
check(session.user != null) { "No user data" }
144+
check(session.user != null) { "No user data" }
135145

136-
// userId is for debugging purposes only
137-
return PowerSyncCredentials(
138-
endpoint = powerSyncEndpoint,
139-
token = session.accessToken, // Use the access token to authenticate against PowerSync
140-
userId = session.user!!.id,
141-
)
142-
}
146+
// userId is for debugging purposes only
147+
PowerSyncCredentials(
148+
endpoint = powerSyncEndpoint,
149+
token = session.accessToken, // Use the access token to authenticate against PowerSync
150+
userId = session.user!!.id,
151+
)
152+
}
143153

144154
/**
145155
* Upload local changes to the app backend (in this case Supabase).
@@ -148,59 +158,61 @@ public class SupabaseConnector(
148158
* If this call throws an error, it is retried periodically.
149159
*/
150160
override suspend fun uploadData(database: PowerSyncDatabase) {
151-
val transaction = database.getNextCrudTransaction() ?: return
161+
return runWrappedSuspending {
162+
val transaction = database.getNextCrudTransaction() ?: return@runWrappedSuspending
152163

153-
var lastEntry: CrudEntry? = null
154-
try {
155-
for (entry in transaction.crud) {
156-
lastEntry = entry
164+
var lastEntry: CrudEntry? = null
165+
try {
166+
for (entry in transaction.crud) {
167+
lastEntry = entry
157168

158-
val table = supabaseClient.from(entry.table)
169+
val table = supabaseClient.from(entry.table)
159170

160-
when (entry.op) {
161-
UpdateType.PUT -> {
162-
val data = entry.opData?.toMutableMap() ?: mutableMapOf()
163-
data["id"] = entry.id
164-
table.upsert(data)
165-
}
171+
when (entry.op) {
172+
UpdateType.PUT -> {
173+
val data = entry.opData?.toMutableMap() ?: mutableMapOf()
174+
data["id"] = entry.id
175+
table.upsert(data)
176+
}
166177

167-
UpdateType.PATCH -> {
168-
table.update(entry.opData!!) {
169-
filter {
170-
eq("id", entry.id)
178+
UpdateType.PATCH -> {
179+
table.update(entry.opData!!) {
180+
filter {
181+
eq("id", entry.id)
182+
}
171183
}
172184
}
173-
}
174185

175-
UpdateType.DELETE -> {
176-
table.delete {
177-
filter {
178-
eq("id", entry.id)
186+
UpdateType.DELETE -> {
187+
table.delete {
188+
filter {
189+
eq("id", entry.id)
190+
}
179191
}
180192
}
181193
}
182194
}
183-
}
184195

185-
transaction.complete(null)
186-
} catch (e: Exception) {
187-
if (errorCode != null && PostgresFatalCodes.isFatalError(errorCode.toString())) {
188-
/**
189-
* Instead of blocking the queue with these errors,
190-
* discard the (rest of the) transaction.
191-
*
192-
* Note that these errors typically indicate a bug in the application.
193-
* If protecting against data loss is important, save the failing records
194-
* elsewhere instead of discarding, and/or notify the user.
195-
*/
196-
Logger.e("Data upload error: ${e.message}")
197-
Logger.e("Discarding entry: $lastEntry")
198196
transaction.complete(null)
199-
return
200-
}
197+
} catch (e: Exception) {
198+
if (errorCode != null && PostgresFatalCodes.isFatalError(errorCode.toString())) {
199+
/**
200+
* Instead of blocking the queue with these errors,
201+
* discard the (rest of the) transaction.
202+
*
203+
* Note that these errors typically indicate a bug in the application.
204+
* If protecting against data loss is important, save the failing records
205+
* elsewhere instead of discarding, and/or notify the user.
206+
*/
207+
Logger.e("Data upload error: ${e.message}")
208+
Logger.e("Discarding entry: $lastEntry")
209+
transaction.complete(null)
210+
return@runWrappedSuspending
211+
}
201212

202-
Logger.e("Data upload error - retrying last entry: $lastEntry, $e")
203-
throw e
213+
Logger.e("Data upload error - retrying last entry: $lastEntry, $e")
214+
throw e
215+
}
204216
}
205217
}
206218
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.powersync
2+
3+
public class PowerSyncException(
4+
message: String,
5+
cause: Throwable,
6+
) : Exception(message, cause)

core/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.powersync.db.crud.CrudBatch
66
import com.powersync.db.crud.CrudTransaction
77
import com.powersync.sync.SyncStatus
88
import com.powersync.utils.JsonParam
9+
import kotlin.coroutines.cancellation.CancellationException
910

1011
/**
1112
* A PowerSync managed database.
@@ -25,6 +26,7 @@ public interface PowerSyncDatabase : Queries {
2526
/**
2627
* Suspend function that resolves when the first sync has occurred
2728
*/
29+
@Throws(PowerSyncException::class, CancellationException::class)
2830
public suspend fun waitForFirstSync()
2931

3032
/**
@@ -56,7 +58,7 @@ public interface PowerSyncDatabase : Queries {
5658
* ```
5759
* TODO: Internal Team - Status changes are reported on [statusStream].
5860
*/
59-
61+
@Throws(PowerSyncException::class, CancellationException::class)
6062
public suspend fun connect(
6163
connector: PowerSyncBackendConnector,
6264
crudThrottleMs: Long = 1000L,
@@ -81,6 +83,7 @@ public interface PowerSyncDatabase : Queries {
8183
* data by transaction. One batch may contain data from multiple transactions,
8284
* and a single transaction may be split over multiple batches.
8385
*/
86+
@Throws(PowerSyncException::class, CancellationException::class)
8487
public suspend fun getCrudBatch(limit: Int = 100): CrudBatch?
8588

8689
/**
@@ -96,19 +99,21 @@ public interface PowerSyncDatabase : Queries {
9699
* Unlike [getCrudBatch], this only returns data from a single transaction at a time.
97100
* All data for the transaction is loaded into memory.
98101
*/
99-
102+
@Throws(PowerSyncException::class, CancellationException::class)
100103
public suspend fun getNextCrudTransaction(): CrudTransaction?
101104

102105
/**
103106
* Convenience method to get the current version of PowerSync.
104107
*/
108+
@Throws(PowerSyncException::class, CancellationException::class)
105109
public suspend fun getPowerSyncVersion(): String
106110

107111
/**
108112
* Close the sync connection.
109113
*
110114
* Use [connect] to connect again.
111115
*/
116+
@Throws(PowerSyncException::class, CancellationException::class)
112117
public suspend fun disconnect()
113118

114119
/**
@@ -119,6 +124,7 @@ public interface PowerSyncDatabase : Queries {
119124
*
120125
* To preserve data in local-only tables, set clearLocal to false.
121126
*/
127+
@Throws(PowerSyncException::class, CancellationException::class)
122128
public suspend fun disconnectAndClear(clearLocal: Boolean = true)
123129

124130
/**
@@ -127,5 +133,6 @@ public interface PowerSyncDatabase : Queries {
127133
*
128134
* Once close is called, this database cannot be used again - a new one must be constructed.
129135
*/
136+
@Throws(PowerSyncException::class, CancellationException::class)
130137
public suspend fun close()
131138
}

core/src/commonMain/kotlin/com/powersync/bucket/BucketStorageImpl.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,7 @@ internal class BucketStorageImpl(
5353
return id ?: throw IllegalStateException("Client ID not found")
5454
}
5555

56-
override suspend fun nextCrudItem(): CrudEntry? =
57-
db.getOptional(sql = nextCrudQuery, mapper = nextCrudMapper)
56+
override suspend fun nextCrudItem(): CrudEntry? = db.getOptional(sql = nextCrudQuery, mapper = nextCrudMapper)
5857

5958
override fun nextCrudItem(transaction: PowerSyncTransaction): CrudEntry? =
6059
transaction.getOptional(sql = nextCrudQuery, mapper = nextCrudMapper)
@@ -81,7 +80,7 @@ internal class BucketStorageImpl(
8180
}
8281

8382
private val hasCrudQuery = "SELECT 1 FROM ps_crud LIMIT 1"
84-
private val hasCrudMapper:(SqlCursor) -> Long = {
83+
private val hasCrudMapper: (SqlCursor) -> Long = {
8584
it.getLong(0)!!
8685
}
8786

core/src/commonMain/kotlin/com/powersync/connectors/PowerSyncBackendConnector.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package com.powersync.connectors
22

33
import com.powersync.PowerSyncDatabase
4+
import com.powersync.PowerSyncException
5+
import com.powersync.db.runWrappedSuspending
46
import kotlinx.coroutines.CoroutineScope
57
import kotlinx.coroutines.Dispatchers
68
import kotlinx.coroutines.Job
79
import kotlinx.coroutines.SupervisorJob
810
import kotlinx.coroutines.launch
11+
import kotlin.coroutines.cancellation.CancellationException
912

1013
/**
1114
* Implement this to connect an app backend.
@@ -26,10 +29,13 @@ public abstract class PowerSyncBackendConnector {
2629
*
2730
* These credentials may have expired already.
2831
*/
32+
@Throws(PowerSyncException::class, CancellationException::class)
2933
public open suspend fun getCredentialsCached(): PowerSyncCredentials? {
30-
cachedCredentials?.let { return it }
31-
prefetchCredentials()?.join()
32-
return cachedCredentials
34+
return runWrappedSuspending {
35+
cachedCredentials?.let { return@runWrappedSuspending it }
36+
prefetchCredentials()?.join()
37+
cachedCredentials
38+
}
3339
}
3440

3541
/**
@@ -49,6 +55,7 @@ public abstract class PowerSyncBackendConnector {
4955
*
5056
* This may be called before the current credentials have expired.
5157
*/
58+
@Throws(PowerSyncException::class, CancellationException::class)
5259
public open suspend fun prefetchCredentials(): Job? {
5360
fetchRequest?.takeIf { it.isActive }?.let { return it }
5461

@@ -74,6 +81,7 @@ public abstract class PowerSyncBackendConnector {
7481
*
7582
* This token is kept for the duration of a sync connection.
7683
*/
84+
@Throws(PowerSyncException::class, CancellationException::class)
7785
public abstract suspend fun fetchCredentials(): PowerSyncCredentials?
7886

7987
/**
@@ -83,5 +91,6 @@ public abstract class PowerSyncBackendConnector {
8391
*
8492
* Any thrown errors will result in a retry after the configured wait period (default: 5 seconds).
8593
*/
94+
@Throws(PowerSyncException::class, CancellationException::class)
8695
public abstract suspend fun uploadData(database: PowerSyncDatabase)
8796
}

0 commit comments

Comments
 (0)