Skip to content
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
30 changes: 27 additions & 3 deletions core/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.powersync.db.Queries
import com.powersync.db.crud.CrudBatch
import com.powersync.db.crud.CrudTransaction
import com.powersync.sync.SyncStatus
import com.powersync.utils.JsonParam

/**
* A PowerSync managed database.
Expand All @@ -27,14 +28,37 @@ public interface PowerSyncDatabase : Queries {
*
* The connection is automatically re-opened if it fails for any reason.
*
* Use @param [connector] to specify the [PowerSyncBackendConnector].
* Use @param [crudThrottleMs] to specify the time between CRUD operations. Defaults to 1000ms.
* Use @param [retryDelayMs] to specify the delay between retries after failure. Defaults to 5000ms.
* Use @param [params] to specify sync parameters from the client.
*
* Example usage:
* ```
* val params = JsonParam.Map(
* mapOf(
* "name" to JsonParam.String("John Doe"),
* "age" to JsonParam.Number(30),
* "isStudent" to JsonParam.Boolean(false)
* )
* )
*
* connect(
* connector = connector,
* crudThrottleMs = 2000L,
* retryDelayMs = 10000L,
* params = params
* )
* ```
* TODO: Internal Team - Status changes are reported on [statusStream].
*/

public suspend fun connect(connector: PowerSyncBackendConnector, crudThrottleMs: Long = 1000L,
retryDelayMs: Long = 5000L)
public suspend fun connect(
connector: PowerSyncBackendConnector,
crudThrottleMs: Long = 1000L,
retryDelayMs: Long = 5000L,
params: Map<String, JsonParam?> = emptyMap()
)


/**
Expand Down Expand Up @@ -94,4 +118,4 @@ public interface PowerSyncDatabase : Queries {
* To preserve data in local-only tables, set clearLocal to false.
*/
public suspend fun disconnectAndClear(clearLocal: Boolean = true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import com.powersync.db.internal.PowerSyncTransaction
import com.powersync.db.schema.Schema
import com.powersync.sync.SyncStatus
import com.powersync.sync.SyncStream
import com.powersync.utils.JsonParam
import com.powersync.utils.JsonUtil
import com.powersync.utils.Strings.quoteIdentifier
import com.powersync.utils.toJsonObject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
Expand Down Expand Up @@ -85,7 +86,12 @@ internal class PowerSyncDatabaseImpl(
}

@OptIn(FlowPreview::class)
override suspend fun connect(connector: PowerSyncBackendConnector, crudThrottleMs: Long, retryDelayMs: Long) {
override suspend fun connect(
connector: PowerSyncBackendConnector,
crudThrottleMs: Long,
retryDelayMs: Long,
params: Map<String, JsonParam?>)
{
// close connection if one is open
disconnect()

Expand All @@ -95,7 +101,8 @@ internal class PowerSyncDatabaseImpl(
connector = connector,
uploadCrud = suspend { connector.uploadData(this) },
retryDelayMs = retryDelayMs,
logger = logger
logger = logger,
params = params.toJsonObject()
)

syncJob = scope.launch {
Expand Down
8 changes: 6 additions & 2 deletions core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import com.powersync.bucket.Checkpoint
import com.powersync.bucket.WriteCheckpointResponse
import co.touchlab.stately.concurrency.AtomicBoolean
import com.powersync.connectors.PowerSyncBackendConnector
import com.powersync.utils.JsonParam
import com.powersync.utils.JsonUtil
import com.powersync.utils.toJsonObject
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.HttpTimeout
Expand Down Expand Up @@ -41,7 +43,8 @@ internal class SyncStream(
private val connector: PowerSyncBackendConnector,
private val uploadCrud: suspend () -> Unit,
private val retryDelayMs: Long = 5000L,
private val logger: Logger
private val logger: Logger,
private val params: JsonObject
) {
private var isUploadingCrud = AtomicBoolean(false)

Expand Down Expand Up @@ -245,7 +248,8 @@ internal class SyncStream(

val req = StreamingSyncRequest(
buckets = initialBuckets.map { (bucket, after) -> BucketRequest(bucket, after) },
clientId = clientId!!
clientId = clientId!!,
parameters = params
)

streamingSyncRequest(req).collect { value ->
Expand Down
30 changes: 30 additions & 0 deletions core/src/commonMain/kotlin/com/powersync/utils/Json.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package com.powersync.utils

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive

/**
* A global instance of a JSON serializer.
Expand All @@ -12,3 +17,28 @@ internal object JsonUtil {
}
}

public sealed class JsonParam {
public data class Number(val value: kotlin.Number) : JsonParam()
public data class String(val value: kotlin.String) : JsonParam()
public data class Boolean(val value: kotlin.Boolean) : JsonParam()
public data class Map(val value: kotlin.collections.Map<kotlin.String, JsonParam>) : JsonParam()
public data class Collection(val value: kotlin.collections.Collection<JsonParam>) : JsonParam()
public data class JsonElement(val value: kotlinx.serialization.json.JsonElement) : JsonParam()
public data object Null : JsonParam()

internal fun toJsonElement(): kotlinx.serialization.json.JsonElement = when (this) {
is Number -> JsonPrimitive(value)
is String -> JsonPrimitive(value)
is Boolean -> JsonPrimitive(value)
is Map -> JsonObject(value.mapValues { it.value.toJsonElement() })
is Collection -> JsonArray(value.map { it.toJsonElement() })
is JsonElement -> value
Null -> JsonNull
}
}

public fun Map<String, JsonParam?>.toJsonObject(): JsonObject {
return JsonObject(this.mapValues { (_, value) ->
value?.toJsonElement() ?: JsonNull
})
}
167 changes: 167 additions & 0 deletions core/src/commonTest/kotlin/com/powersync/utils/JsonTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package com.powersync.utils


import kotlinx.serialization.json.*
import kotlin.test.*

class JsonTest {
@Test
fun testNumberToJsonElement() {
val number = JsonParam.Number(42)
val jsonElement = number.toJsonElement()
assertTrue(jsonElement is JsonPrimitive)
assertEquals(42, jsonElement.int)
}

@Test
fun testStringToJsonElement() {
val string = JsonParam.String("test")
val jsonElement = string.toJsonElement()
assertTrue(jsonElement is JsonPrimitive)
assertEquals("test", jsonElement.content)
}

@Test
fun testBooleanToJsonElement() {
val boolean = JsonParam.Boolean(true)
val jsonElement = boolean.toJsonElement()
assertTrue(jsonElement is JsonPrimitive)
assertTrue(jsonElement.boolean)
}

@Test
fun testMapToJsonElement() {
val map = JsonParam.Map(mapOf(
"key1" to JsonParam.String("value1"),
"key2" to JsonParam.Number(42)
))
val jsonElement = map.toJsonElement()
assertTrue(jsonElement is JsonObject)
assertEquals("value1", jsonElement["key1"]?.jsonPrimitive?.content)
assertEquals(42, jsonElement["key2"]?.jsonPrimitive?.int)
}

@Test
fun testListToJsonElement() {
val list = JsonParam.Collection(listOf(
JsonParam.String("item1"),
JsonParam.Number(42)
))
val jsonElement = list.toJsonElement()
assertTrue(jsonElement is JsonArray)
assertEquals("item1", jsonElement[0].jsonPrimitive.content)
assertEquals(42, jsonElement[1].jsonPrimitive.int)
}

@Test
fun testJsonElementParamToJsonElement() {
val originalJson = buildJsonObject {
put("key", "value")
}
val jsonElementParam = JsonParam.JsonElement(originalJson)
val jsonElement = jsonElementParam.toJsonElement()
assertEquals(originalJson, jsonElement)
}

@Test
fun testNullToJsonElement() {
val nullParam = JsonParam.Null
val jsonElement = nullParam.toJsonElement()
assertTrue(jsonElement is JsonNull)
}

@Test
fun testMapToJsonObject() {
val params = mapOf(
"string" to JsonParam.String("value"),
"number" to JsonParam.Number(42),
"boolean" to JsonParam.Boolean(true),
"null" to JsonParam.Null
)
val jsonObject = params.toJsonObject()
assertEquals("value", jsonObject["string"]?.jsonPrimitive?.content)
assertEquals(42, jsonObject["number"]?.jsonPrimitive?.int)
assertEquals(true, jsonObject["boolean"]?.jsonPrimitive?.boolean)
assertTrue(jsonObject["null"] is JsonNull)
}

@Test
fun testComplexNestedMapToJsonObject() {
val complexNestedMap = mapOf(
"string" to JsonParam.String("value"),
"number" to JsonParam.Number(42),
"boolean" to JsonParam.Boolean(true),
"null" to JsonParam.Null,
"nestedMap" to JsonParam.Map(mapOf(
"list" to JsonParam.Collection(listOf(
JsonParam.Number(1),
JsonParam.String("two"),
JsonParam.Boolean(false)
)),
"deeplyNested" to JsonParam.Map(mapOf(
"jsonElement" to JsonParam.JsonElement(buildJsonObject {
put("key", "value")
put("array", buildJsonArray {
add(1)
add("string")
add(true)
})
}),
"mixedList" to JsonParam.Collection(arrayListOf(
JsonParam.Number(3.14),
JsonParam.Map(mapOf(
"key" to JsonParam.String("nestedValue")
)),
JsonParam.Null
)
)
))
))
)

val jsonObject = complexNestedMap.toJsonObject()

// Verify top-level elements
assertEquals("value", jsonObject["string"]?.jsonPrimitive?.content)
assertEquals(42, jsonObject["number"]?.jsonPrimitive?.int)
assertEquals(true, jsonObject["boolean"]?.jsonPrimitive?.boolean)
assertTrue(jsonObject["null"] is JsonNull)

// Verify nested map
val nestedMap = jsonObject["nestedMap"]?.jsonObject
assertNotNull(nestedMap)

// Verify nested list
val nestedList = nestedMap["list"]?.jsonArray
assertNotNull(nestedList)
assertEquals(3, nestedList.size)
assertEquals(1, nestedList[0].jsonPrimitive.int)
assertEquals("two", nestedList[1].jsonPrimitive.content)
assertEquals(false, nestedList[2].jsonPrimitive.boolean)

// Verify deeply nested map
val deeplyNested = nestedMap["deeplyNested"]?.jsonObject
assertNotNull(deeplyNested)

// Verify JsonElement
val jsonElement = deeplyNested["jsonElement"]?.jsonObject
assertNotNull(jsonElement)
assertEquals("value", jsonElement["key"]?.jsonPrimitive?.content)
val jsonElementArray = jsonElement["array"]?.jsonArray
assertNotNull(jsonElementArray)
assertEquals(3, jsonElementArray.size)
assertEquals(1, jsonElementArray[0].jsonPrimitive.int)
assertEquals("string", jsonElementArray[1].jsonPrimitive.content)
assertEquals(true, jsonElementArray[2].jsonPrimitive.boolean)

// Verify mixed list
val mixedList = deeplyNested["mixedList"]?.jsonArray
assertNotNull(mixedList)
assertEquals(3, mixedList.size)
assertEquals(3.14, mixedList[0].jsonPrimitive.double)
val nestedMapInList = mixedList[1].jsonObject
assertNotNull(nestedMapInList)
assertEquals("nestedValue", nestedMapInList["key"]?.jsonPrimitive?.content)
assertTrue(mixedList[2] is JsonNull)
}
}
Loading