Skip to content
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

feat: improve errors in swift #116

Merged
merged 5 commits into from
Feb 10, 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
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Changelog

## 1.0.0-BETA21

* Improve error handling for Swift by adding @Throws annotation so errors can be handled in Swift
* Throw PowerSync exceptions for all public facing methods

## 1.0.0-BETA20

* Add cursor optional functions: `getStringOptional`, `getLongOptional`, `getDoubleOptional`, `getBooleanOptional` and `getBytesOptional` when using the column name which allow for optional return types
* Throw errors for invalid column on all cursor functions
* `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
Expand All @@ -22,8 +28,6 @@
import com.powersync.db.SqlCursor
```



## 1.0.0-BETA18

* 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.
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ let package = Package(
)
,
]
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.powersync.connectors.PowerSyncBackendConnector
import com.powersync.connectors.PowerSyncCredentials
import com.powersync.db.crud.CrudEntry
import com.powersync.db.crud.UpdateType
import com.powersync.db.runWrappedSuspending
import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.annotations.SupabaseInternal
import io.github.jan.supabase.auth.Auth
Expand Down Expand Up @@ -94,52 +95,61 @@ public class SupabaseConnector(
email: String,
password: String,
) {
supabaseClient.auth.signInWith(Email) {
this.email = email
this.password = password
runWrappedSuspending {
supabaseClient.auth.signInWith(Email) {
this.email = email
this.password = password
}
}
}

public suspend fun signUp(
email: String,
password: String,
) {
supabaseClient.auth.signUpWith(Email) {
this.email = email
this.password = password
runWrappedSuspending {
supabaseClient.auth.signUpWith(Email) {
this.email = email
this.password = password
}
}
}

public suspend fun signOut() {
supabaseClient.auth.signOut()
runWrappedSuspending {
supabaseClient.auth.signOut()
}
}

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

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

public suspend fun loginAnonymously() {
supabaseClient.auth.signInAnonymously()
runWrappedSuspending {
supabaseClient.auth.signInAnonymously()
}
}

/**
* Get credentials for PowerSync.
*/
override suspend fun fetchCredentials(): PowerSyncCredentials {
check(supabaseClient.auth.sessionStatus.value is SessionStatus.Authenticated) { "Supabase client is not authenticated" }
override suspend fun fetchCredentials(): PowerSyncCredentials =
runWrappedSuspending {
check(supabaseClient.auth.sessionStatus.value is SessionStatus.Authenticated) { "Supabase client is not authenticated" }

// Use Supabase token for PowerSync
val session = supabaseClient.auth.currentSessionOrNull() ?: error("Could not fetch Supabase credentials")
// Use Supabase token for PowerSync
val session = supabaseClient.auth.currentSessionOrNull() ?: error("Could not fetch Supabase credentials")

check(session.user != null) { "No user data" }
check(session.user != null) { "No user data" }

// userId is for debugging purposes only
return PowerSyncCredentials(
endpoint = powerSyncEndpoint,
token = session.accessToken, // Use the access token to authenticate against PowerSync
userId = session.user!!.id,
)
}
// userId is for debugging purposes only
PowerSyncCredentials(
endpoint = powerSyncEndpoint,
token = session.accessToken, // Use the access token to authenticate against PowerSync
userId = session.user!!.id,
)
}

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

var lastEntry: CrudEntry? = null
try {
for (entry in transaction.crud) {
lastEntry = entry
var lastEntry: CrudEntry? = null
try {
for (entry in transaction.crud) {
lastEntry = entry

val table = supabaseClient.from(entry.table)
val table = supabaseClient.from(entry.table)

when (entry.op) {
UpdateType.PUT -> {
val data = entry.opData?.toMutableMap() ?: mutableMapOf()
data["id"] = entry.id
table.upsert(data)
}
when (entry.op) {
UpdateType.PUT -> {
val data = entry.opData?.toMutableMap() ?: mutableMapOf()
data["id"] = entry.id
table.upsert(data)
}

UpdateType.PATCH -> {
table.update(entry.opData!!) {
filter {
eq("id", entry.id)
UpdateType.PATCH -> {
table.update(entry.opData!!) {
filter {
eq("id", entry.id)
}
}
}
}

UpdateType.DELETE -> {
table.delete {
filter {
eq("id", entry.id)
UpdateType.DELETE -> {
table.delete {
filter {
eq("id", entry.id)
}
}
}
}
}
}

transaction.complete(null)
} catch (e: Exception) {
if (errorCode != null && PostgresFatalCodes.isFatalError(errorCode.toString())) {
/**
* Instead of blocking the queue with these errors,
* discard the (rest of the) transaction.
*
* Note that these errors typically indicate a bug in the application.
* If protecting against data loss is important, save the failing records
* elsewhere instead of discarding, and/or notify the user.
*/
Logger.e("Data upload error: ${e.message}")
Logger.e("Discarding entry: $lastEntry")
transaction.complete(null)
return
}
} catch (e: Exception) {
if (errorCode != null && PostgresFatalCodes.isFatalError(errorCode.toString())) {
/**
* Instead of blocking the queue with these errors,
* discard the (rest of the) transaction.
*
* Note that these errors typically indicate a bug in the application.
* If protecting against data loss is important, save the failing records
* elsewhere instead of discarding, and/or notify the user.
*/
Logger.e("Data upload error: ${e.message}")
Logger.e("Discarding entry: $lastEntry")
transaction.complete(null)
return@runWrappedSuspending
}

Logger.e("Data upload error - retrying last entry: $lastEntry, $e")
throw e
Logger.e("Data upload error - retrying last entry: $lastEntry, $e")
throw e
}
}
}
}
6 changes: 6 additions & 0 deletions core/src/commonMain/kotlin/com/powersync/Exceptions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.powersync

public class PowerSyncException(
message: String,
cause: Throwable,
) : Exception(message, cause)
11 changes: 9 additions & 2 deletions core/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.powersync.db.crud.CrudBatch
import com.powersync.db.crud.CrudTransaction
import com.powersync.sync.SyncStatus
import com.powersync.utils.JsonParam
import kotlin.coroutines.cancellation.CancellationException

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

/**
Expand Down Expand Up @@ -56,7 +58,7 @@ public interface PowerSyncDatabase : Queries {
* ```
* TODO: Internal Team - Status changes are reported on [statusStream].
*/

@Throws(PowerSyncException::class, CancellationException::class)
public suspend fun connect(
connector: PowerSyncBackendConnector,
crudThrottleMs: Long = 1000L,
Expand All @@ -81,6 +83,7 @@ public interface PowerSyncDatabase : Queries {
* data by transaction. One batch may contain data from multiple transactions,
* and a single transaction may be split over multiple batches.
*/
@Throws(PowerSyncException::class, CancellationException::class)
public suspend fun getCrudBatch(limit: Int = 100): CrudBatch?

/**
Expand All @@ -96,19 +99,21 @@ public interface PowerSyncDatabase : Queries {
* Unlike [getCrudBatch], this only returns data from a single transaction at a time.
* All data for the transaction is loaded into memory.
*/

@Throws(PowerSyncException::class, CancellationException::class)
public suspend fun getNextCrudTransaction(): CrudTransaction?

/**
* Convenience method to get the current version of PowerSync.
*/
@Throws(PowerSyncException::class, CancellationException::class)
public suspend fun getPowerSyncVersion(): String

/**
* Close the sync connection.
*
* Use [connect] to connect again.
*/
@Throws(PowerSyncException::class, CancellationException::class)
public suspend fun disconnect()

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

/**
Expand All @@ -127,5 +133,6 @@ public interface PowerSyncDatabase : Queries {
*
* Once close is called, this database cannot be used again - a new one must be constructed.
*/
@Throws(PowerSyncException::class, CancellationException::class)
public suspend fun close()
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ internal class BucketStorageImpl(
return id ?: throw IllegalStateException("Client ID not found")
}

override suspend fun nextCrudItem(): CrudEntry? =
db.getOptional(sql = nextCrudQuery, mapper = nextCrudMapper)
override suspend fun nextCrudItem(): CrudEntry? = db.getOptional(sql = nextCrudQuery, mapper = nextCrudMapper)

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

private val hasCrudQuery = "SELECT 1 FROM ps_crud LIMIT 1"
private val hasCrudMapper:(SqlCursor) -> Long = {
private val hasCrudMapper: (SqlCursor) -> Long = {
it.getLong(0)!!
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.powersync.connectors

import com.powersync.PowerSyncDatabase
import com.powersync.PowerSyncException
import com.powersync.db.runWrappedSuspending
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlin.coroutines.cancellation.CancellationException

/**
* Implement this to connect an app backend.
Expand All @@ -26,10 +29,13 @@ public abstract class PowerSyncBackendConnector {
*
* These credentials may have expired already.
*/
@Throws(PowerSyncException::class, CancellationException::class)
public open suspend fun getCredentialsCached(): PowerSyncCredentials? {
cachedCredentials?.let { return it }
prefetchCredentials()?.join()
return cachedCredentials
return runWrappedSuspending {
cachedCredentials?.let { return@runWrappedSuspending it }
prefetchCredentials()?.join()
cachedCredentials
}
}

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

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

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