Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7644f00
Add instruction class for core extension
simolus3 Apr 24, 2025
3afb537
Fix sync integration tests
simolus3 Apr 29, 2025
a472d50
Start with rsocket support
simolus3 Apr 29, 2025
e428df3
Add RSocket client
simolus3 May 6, 2025
6e127f1
Better logs for unknown requests
simolus3 May 6, 2025
15a7674
Test token expiry handling
simolus3 May 6, 2025
90b79b1
Dump sync stream to file
simolus3 May 7, 2025
5b513e5
Remove unused imports
simolus3 May 7, 2025
c8b01ee
Merge remote-tracking branch 'origin/main' into rust-sync-client
simolus3 May 7, 2025
803c1ed
Restore support for old implementation
simolus3 May 8, 2025
9b3fd05
Make public
simolus3 May 8, 2025
90714b5
Reformat
simolus3 May 8, 2025
e002c26
Merge remote-tracking branch 'origin/main' into rust-sync-client
simolus3 May 21, 2025
8830a8f
Merge remote-tracking branch 'origin/main' into rust-sync-client
simolus3 Jun 12, 2025
2c2332f
Fix sync progress regression
simolus3 Jun 12, 2025
3caf26d
Fix passing parameters
simolus3 Jun 12, 2025
ee121e7
Disable configuration cache
simolus3 Jun 12, 2025
eeade05
Revert extension loading tmp changes
simolus3 Jun 12, 2025
6618b23
Revert changes to demo for now
simolus3 Jun 12, 2025
1b03b8a
Merge branch 'main' into rust-sync-client
simolus3 Jun 19, 2025
834955a
Don't leak RSocket into API
simolus3 Jun 19, 2025
07e8616
Add changelog entry
simolus3 Jun 19, 2025
b67a485
Warn for unknown instructions instead of throwing
simolus3 Jun 19, 2025
217d03a
Add helper for Swift SDK
simolus3 Jun 19, 2025
0eed4e4
Make user agent configurable
simolus3 Jun 19, 2025
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
Prev Previous commit
Next Next commit
Start with rsocket support
  • Loading branch information
simolus3 committed Apr 29, 2025
commit a472d5041bb5d9b121a5875d45d6647a55509ecc
1 change: 1 addition & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ kotlin {
implementation(libs.ktor.client.contentnegotiation)
implementation(libs.ktor.serialization.json)
implementation(libs.kotlinx.io)
implementation(libs.rsocket.client)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(libs.stately.concurrency)
Expand Down
2 changes: 2 additions & 0 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.Queries
import com.powersync.db.crud.CrudBatch
import com.powersync.db.crud.CrudTransaction
import com.powersync.db.schema.Schema
import com.powersync.sync.SyncOptions
import com.powersync.sync.SyncStatus
import com.powersync.utils.JsonParam
import kotlin.coroutines.cancellation.CancellationException
Expand Down Expand Up @@ -94,6 +95,7 @@ public interface PowerSyncDatabase : Queries {
crudThrottleMs: Long = 1000L,
retryDelayMs: Long = 5000L,
params: Map<String, JsonParam?> = emptyMap(),
options: SyncOptions = SyncOptions()
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.powersync.db.internal.PowerSyncVersion
import com.powersync.db.schema.Schema
import com.powersync.db.schema.toSerializable
import com.powersync.sync.PriorityStatusEntry
import com.powersync.sync.SyncOptions
import com.powersync.sync.SyncStatus
import com.powersync.sync.SyncStatusData
import com.powersync.sync.SyncStream
Expand Down Expand Up @@ -153,6 +154,7 @@ internal class PowerSyncDatabaseImpl(
crudThrottleMs: Long,
retryDelayMs: Long,
params: Map<String, JsonParam?>,
options: SyncOptions
) {
waitReady()
mutex.withLock {
Expand All @@ -168,13 +170,13 @@ internal class PowerSyncDatabaseImpl(
params = params.toJsonObject(),
scope = scope,
createClient = createClient,
options = options,
),
crudThrottleMs,
)
}
}

@OptIn(FlowPreview::class)
internal fun connectInternal(
stream: SyncStream,
crudThrottleMs: Long,
Expand Down
37 changes: 37 additions & 0 deletions core/src/commonMain/kotlin/com/powersync/sync/SyncOptions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.powersync.sync

import io.rsocket.kotlin.keepalive.KeepAlive
import kotlin.time.Duration.Companion.seconds

public class SyncOptions(
public val method: ConnectionMethod = ConnectionMethod.WebSocket,
)

/**
* The connection method to use when the SDK connects to the sync service.
*/
public sealed interface ConnectionMethod {
/**
* Receive sync lines via an streamed HTTP response from the sync service.
*
* This mode is less efficient than [WebSocket] because it doesn't support backpressure
* properly and uses JSON instead of the more efficient BSON representation for sync lines.
*/
public data object Http: ConnectionMethod

/**
* Receive binary sync lines via RSocket over a WebSocket connection.
*
* This is the default mode, and recommended for most clients.
*/
public data class WebSocket(
val keepAlive: KeepAlive = DefaultKeepAlive
): ConnectionMethod {
private companion object {
val DefaultKeepAlive = KeepAlive(
interval = 20.0.seconds,
maxLifetime = 30.0.seconds,
)
}
}
}
69 changes: 67 additions & 2 deletions core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.powersync.sync

import BuildConfig
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import co.touchlab.stately.concurrency.AtomicBoolean
Expand All @@ -18,6 +19,7 @@ import io.ktor.client.call.body
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.timeout
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.request.get
import io.ktor.client.request.headers
import io.ktor.client.request.preparePost
Expand All @@ -26,9 +28,21 @@ import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.http.URLBuilder
import io.ktor.http.URLProtocol
import io.ktor.http.Url
import io.ktor.http.contentType
import io.ktor.http.takeFrom
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.readUTF8Line
import io.rsocket.kotlin.keepalive.KeepAlive
import io.rsocket.kotlin.ktor.client.RSocketSupport
import io.rsocket.kotlin.ktor.client.rSocket
import io.rsocket.kotlin.payload.Payload
import io.rsocket.kotlin.payload.PayloadMimeType
import io.rsocket.kotlin.payload.buildPayload
import io.rsocket.kotlin.payload.data
import io.rsocket.kotlin.payload.metadata
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
Expand All @@ -44,8 +58,11 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.JsonObject
import kotlin.time.Duration.Companion.seconds

internal class SyncStream(
private val bucketStorage: BucketStorage,
Expand All @@ -55,6 +72,7 @@ internal class SyncStream(
private val logger: Logger,
private val params: JsonObject,
private val scope: CoroutineScope,
private val options: SyncOptions,
createClient: (HttpClientConfig<*>.() -> Unit) -> HttpClient,
) {
private var isUploadingCrud = AtomicBoolean(false)
Expand All @@ -71,6 +89,37 @@ internal class SyncStream(
createClient {
install(HttpTimeout)
install(ContentNegotiation)

(options.method as? ConnectionMethod.WebSocket)?.let {
install(WebSockets)
install(RSocketSupport) {
connector {
connectionConfig {
payloadMimeType = PayloadMimeType(
metadata = "application/json",
data = "application/json"
)

setupPayload {
buildPayload {
@Serializable
class ConnectionSetupMetadata(
// Kind of annoying to specify this here, https://github.com/rsocket/rsocket-kotlin/issues/311
val token: String = "TODO: token",
@SerialName("user_agent")
val userAgent: String = "Kotlin SDK"
)

metadata(JsonUtil.json.encodeToString(ConnectionSetupMetadata()))
}
}

keepAlive = it.keepAlive
}
}
}
}

}

fun invalidateCredentials() {
Expand Down Expand Up @@ -184,7 +233,7 @@ internal class SyncStream(
return body.data.writeCheckpoint
}

private fun streamingSyncRequest(req: JsonObject): Flow<String> =
private fun connectViaHttp(req: JsonObject): Flow<String> =
flow {
val credentials = connector.getCredentialsCached()
require(credentials != null) { "Not logged in" }
Expand Down Expand Up @@ -225,6 +274,22 @@ internal class SyncStream(
}
}

private fun connectViaWebSocket(req: JsonObject): Flow<ByteArray> = flow {
val credentials = connector.getCredentialsCached()
require(credentials != null) { "Not logged in" }
val uri = URLBuilder(credentials.endpointUri("sync/stream")).apply {
protocol = when (protocolOrNull) {
URLProtocol.HTTP -> URLProtocol.WS
else -> URLProtocol.WSS
}
}

val rSocket = httpClient.rSocket { url.takeFrom(uri) }
rSocket.requestStream(buildPayload {
metadata(JsonUtil.json.encodeToString(""))
})
}

private suspend fun streamingSyncIteration() {
val iteration = ActiveIteration()

Expand Down Expand Up @@ -308,7 +373,7 @@ internal class SyncStream(
}

private suspend fun connect(start: Instruction.EstablishSyncStream) {
streamingSyncRequest(start.request).collect { rawLine ->
connectViaHttp(start.request).collect { rawLine ->
control("line_text", rawLine)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class SyncStreamTest {
logger = logger,
params = JsonObject(emptyMap()),
scope = this,
options = SyncOptions(),
)

syncStream.invalidateCredentials()
Expand Down Expand Up @@ -137,6 +138,7 @@ class SyncStreamTest {
logger = logger,
params = JsonObject(emptyMap()),
scope = this,
options = SyncOptions(),
)

syncStream.status.update { copy(connected = true) }
Expand Down Expand Up @@ -176,6 +178,7 @@ class SyncStreamTest {
logger = logger,
params = JsonObject(emptyMap()),
scope = this,
options = SyncOptions()
)

// Launch streaming sync in a coroutine that we'll cancel after verification
Expand Down
4 changes: 3 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ kotlin = "2.1.10"
coroutines = "1.8.1"
kotlinx-datetime = "0.6.2"
kotlinx-io = "0.5.4"
ktor = "3.0.1"
ktor = "3.1.0"
rsocket = "0.20.0"
uuid = "0.8.2"
powersync-core = "0.3.12"
sqlite-jdbc = "3.49.1.0"
Expand Down Expand Up @@ -85,6 +86,7 @@ ktor-client-contentnegotiation = { module = "io.ktor:ktor-client-content-negotia
ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" }
ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
rsocket-client = { module = "io.rsocket.kotlin:ktor-client-rsocket", version.ref = "rsocket" }

sqldelight-driver-native = { module = "app.cash.sqldelight:native-driver", version.ref = "sqlDelight" }
sqliter = { module = "co.touchlab:sqliter-driver", version.ref = "sqliter" }
Expand Down