Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
64 changes: 45 additions & 19 deletions core/src/commonMain/kotlin/com/powersync/sync/SyncOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,31 @@ package com.powersync.sync

import com.powersync.ExperimentalPowerSyncAPI
import com.powersync.PowerSyncDatabase
import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.rsocket.kotlin.keepalive.KeepAlive
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

/**
* Configuration options for the [PowerSyncDatabase.connect] method, allowing customization of
* the HTTP client used to connect to the PowerSync service.
*/
public sealed class SyncClientConfiguration {
/**
* Extends the default Ktor [HttpClient] configuration with the provided block.
*/
public class ExtendedConfig(public val block: HttpClientConfig<*>.() -> Unit) :
SyncClientConfiguration()

/**
* Provides an existing [HttpClient] instance to use for connecting to the PowerSync service.
* This client should be configured with the necessary plugins and settings to function correctly.
*/
public class ExistingClient(public val client: HttpClient) :
SyncClientConfiguration()
}

/**
* Experimental options that can be passed to [PowerSyncDatabase.connect] to specify an experimental
* connection mechanism.
Expand All @@ -15,28 +36,33 @@ import kotlin.time.Duration.Companion.seconds
* for the rest of the SDK though.
*/
public class SyncOptions
@ExperimentalPowerSyncAPI
constructor(
@property:ExperimentalPowerSyncAPI
public val newClientImplementation: Boolean = false,
@property:ExperimentalPowerSyncAPI
public val method: ConnectionMethod = ConnectionMethod.Http,
@ExperimentalPowerSyncAPI
constructor(
@property:ExperimentalPowerSyncAPI
public val newClientImplementation: Boolean = false,
@property:ExperimentalPowerSyncAPI
public val method: ConnectionMethod = ConnectionMethod.Http,
/**
* The user agent to use for requests made to the PowerSync service.
*/
public val userAgent: String = userAgent(),
@property:ExperimentalPowerSyncAPI
/**
* Allows configuring the [HttpClient] used for connecting to the PowerSync service.
*/
public val clientConfiguration: SyncClientConfiguration? = null,
) {
public companion object {
/**
* The user agent to use for requests made to the PowerSync service.
* The default sync options, which are safe and stable to use.
*
* Constructing non-standard sync options requires an opt-in to experimental PowerSync
* APIs, and those might change in the future.
*/
public val userAgent: String = userAgent(),
) {
public companion object {
/**
* The default sync options, which are safe and stable to use.
*
* Constructing non-standard sync options requires an opt-in to experimental PowerSync
* APIs, and those might change in the future.
*/
@OptIn(ExperimentalPowerSyncAPI::class)
public val defaults: SyncOptions = SyncOptions()
}
@OptIn(ExperimentalPowerSyncAPI::class)
public val defaults: SyncOptions = SyncOptions()
}
}

/**
* The connection method to use when the SDK connects to the sync service.
Expand Down
60 changes: 49 additions & 11 deletions core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,37 @@ internal class SyncStream(

private var clientId: String? = null

private val httpClient: HttpClient =
createClient {
install(HttpTimeout)
install(ContentNegotiation)
install(WebSockets)
private val httpClient: HttpClient = when (val config = options.clientConfiguration) {
is SyncClientConfiguration.ExtendedConfig -> {
createClient {
configureHttpClient()
// Apply additional configuration
config.block(this)
}
}

install(DefaultRequest) {
headers {
append("User-Agent", options.userAgent)
}
is SyncClientConfiguration.ExistingClient -> config.client

null -> {
// Default client configuration
createClient {
configureHttpClient()
}
}
}

private fun HttpClientConfig<*>.configureHttpClient() {
install(HttpTimeout)
install(ContentNegotiation)
install(WebSockets)

install(DefaultRequest) {
headers {
append("User-Agent", options.userAgent)
}
}
}


fun invalidateCredentials() {
connector.invalidateCredentials()
Expand Down Expand Up @@ -366,15 +385,18 @@ internal class SyncStream(
}
}
}

Instruction.CloseSyncStream -> {
logger.v { "Closing sync stream connection" }
fetchLinesJob!!.cancelAndJoin()
fetchLinesJob = null
logger.v { "Sync stream connection shut down" }
}

Instruction.FlushSileSystem -> {
// We have durable file systems, so flushing is not necessary
}

is Instruction.LogLine -> {
logger.log(
severity =
Expand All @@ -388,11 +410,13 @@ internal class SyncStream(
throwable = null,
)
}

is Instruction.UpdateSyncStatus -> {
status.update {
applyCoreChanges(instruction.status)
}
}

is Instruction.FetchCredentials -> {
if (instruction.didExpire) {
connector.invalidateCredentials()
Expand All @@ -414,9 +438,11 @@ internal class SyncStream(
}
}
}

Instruction.DidCompleteSync -> {
status.update { copy(downloadError = null) }
}

is Instruction.UnknownInstruction -> {
logger.w { "Unknown instruction received from core extension: ${instruction.raw}" }
}
Expand All @@ -429,6 +455,7 @@ internal class SyncStream(
connectViaHttp(start.request).collect {
controlInvocations.send(PowerSyncControlArguments.TextLine(it))
}

is ConnectionMethod.WebSocket ->
connectViaWebSocket(start.request, method).collect {
controlInvocations.send(PowerSyncControlArguments.BinaryLine(it))
Expand Down Expand Up @@ -463,7 +490,13 @@ internal class SyncStream(

val req =
StreamingSyncRequest(
buckets = initialBuckets.map { (bucket, after) -> BucketRequest(bucket, after) },
buckets =
initialBuckets.map { (bucket, after) ->
BucketRequest(
bucket,
after,
)
},
clientId = clientId!!,
parameters = params,
)
Expand Down Expand Up @@ -664,7 +697,12 @@ internal class SyncStream(
): SyncStreamState {
val batch = SyncDataBatch(listOf(data))
bucketStorage.saveSyncData(batch)
status.update { copy(downloading = true, downloadProgress = downloadProgress?.incrementDownloaded(batch)) }
status.update {
copy(
downloading = true,
downloadProgress = downloadProgress?.incrementDownloaded(batch),
)
}
return state
}

Expand Down
1 change: 1 addition & 0 deletions demos/hello-powersync/composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ kotlin {
implementation(compose.ui)
@OptIn(ExperimentalComposeLibrary::class)
implementation(compose.components.resources)
implementation(projectLibs.ktor.client.logging)
}

androidMain.dependencies {
Expand Down
2 changes: 1 addition & 1 deletion demos/hello-powersync/composeApp/composeApp.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Pod::Spec.new do |spec|
spec.vendored_frameworks = 'build/cocoapods/framework/composeApp.framework'
spec.libraries = 'c++'
spec.ios.deployment_target = '15.2'
spec.dependency 'powersync-sqlite-core', '0.3.12'
spec.dependency 'powersync-sqlite-core', '0.4.0'

if !Dir.exist?('build/cocoapods/framework/composeApp.framework') || Dir.empty?('build/cocoapods/framework/composeApp.framework')
raise "
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.powersync.demos

import com.powersync.DatabaseDriverFactory
import com.powersync.ExperimentalPowerSyncAPI
import com.powersync.PowerSyncDatabase
import com.powersync.connector.supabase.SupabaseConnector
import com.powersync.db.getString
import com.powersync.sync.SyncClientConfiguration
import com.powersync.sync.SyncOptions
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.plugins.logging.LogLevel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.runBlocking

Expand Down Expand Up @@ -62,15 +67,27 @@ class PowerSync(
id ?: database.getOptional("SELECT id FROM customers LIMIT 1", mapper = { cursor ->
cursor.getString(0)!!
})
?: return
?: return

database.writeTransaction { tx ->
tx.execute("DELETE FROM customers WHERE id = ?", listOf(targetId))
}
}

@OptIn(ExperimentalPowerSyncAPI::class)
suspend fun connect() {
database.connect(connector)
println("connecting to PowerSync...")
database.connect(
connector,
options =
SyncOptions(
clientConfiguration = SyncClientConfiguration.ExtendedConfig {
install(Logging) {
level = LogLevel.ALL
}
}
),
)
}

suspend fun disconnect() {
Expand Down
8 changes: 4 additions & 4 deletions demos/hello-powersync/iosApp/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PODS:
- composeApp (1.0.0):
- powersync-sqlite-core (= 0.3.12)
- powersync-sqlite-core (0.3.12)
- powersync-sqlite-core (= 0.4.0)
- powersync-sqlite-core (0.4.0)

DEPENDENCIES:
- composeApp (from `../composeApp`)
Expand All @@ -15,8 +15,8 @@ EXTERNAL SOURCES:
:path: "../composeApp"

SPEC CHECKSUMS:
composeApp: 904d95008148b122d963aa082a29624b99d0f4e1
powersync-sqlite-core: fcc32da5528fca9d50b185fcd777705c034e255b
composeApp: f3426c7c85040911848919eebf5573c0f1306733
powersync-sqlite-core: 3bfe9a3c210e130583496871b404f18d4cfbe366

PODFILE CHECKSUM: 4680f51fbb293d1385fb2467ada435cc1f16ab3d

Expand Down
35 changes: 0 additions & 35 deletions demos/supabase-todolist/iosApp/iosApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@
7555FF79242A565900829871 /* Resources */,
F85CB1118929364A9C6EFABC /* Frameworks */,
3C5ACF3A4AAFF294B2A5839B /* [CP] Embed Pods Frameworks */,
1015E800EC39A6B62654C306 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
Expand Down Expand Up @@ -192,23 +191,6 @@
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
};
1015E800EC39A6B62654C306 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n";
showEnvVarsInLog = 0;
};
3C5ACF3A4AAFF294B2A5839B /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
Expand Down Expand Up @@ -248,23 +230,6 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
F72245E8E98E97BEF8C32493 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.
uuid = { module = "com.benasher44:uuid", version.ref = "uuid" }

ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-contentnegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
Expand Down
Loading