diff --git a/CHANGELOG.md b/CHANGELOG.md index 19d1b945..b5e0c485 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,30 @@ # Changelog +## 1.10.0 (unreleased) + +- Add `appMetadata` parameter to `PowerSyncDatabase.connect()` to include application metadata in + sync requests. This metadata is merged into sync requests and displayed in PowerSync service logs. + +Note: This requires a PowerSync service version `>=1.17.0` in order for logs to display metadata. + +```kotlin +database.connect( + connector = connector, + appMetadata = mapOf( + "appVersion" to "1.0.0", + "deviceId" to "device456" + ) +) +``` + ## 1.9.0 -- Updated user agent string formats to allow viewing version distributions in the new PowerSync dashboard. +- Updated user agent string formats to allow viewing version distributions in the new PowerSync + dashboard. - Sync options: `newClientImplementation` is now the default. - Make `androidx.sqlite:sqlite-bundled` an API dependency of `:core` to avoid toolchain warnings. -- On Apple platforms, use a websocket protocol as a workaround to clients not supporting backpressure in HTTP response +- On Apple platforms, use a websocket protocol as a workaround to clients not supporting + backpressure in HTTP response streams. ## 1.8.1 @@ -14,15 +33,19 @@ ## 1.8.0 -- Refactor SDK: `com.powersync:powersync-core` has an identical API, but now depends on +- Refactor SDK: `com.powersync:powersync-core` has an identical API, but now depends on `com.powersync:powersync-common` where most logic is implemented. - - __POTENTIALLY BREAKING CHANGE__: If you were injecting a `DatabaseDriverFactory` into Koin or Dagger, note that the - `PowerSyncDatabase()` factory method now takes a more generic `PersistentConnectionFactory`. - - If you're using `PowerSyncDatabase.inMemory`, you explicitly have to import `com.powersync.inMemory` now. + - __POTENTIALLY BREAKING CHANGE__: If you were injecting a `DatabaseDriverFactory` into Koin or + Dagger, note that the + `PowerSyncDatabase()` factory method now takes a more generic `PersistentConnectionFactory`. + - If you're using `PowerSyncDatabase.inMemory`, you explicitly have to import + `com.powersync.inMemory` now. - Update the PowerSync core extension to version 0.4.8. -- Add the `soft` flag to `disconnectAndClear()` which keeps an internal copy of synced data in the database, allowing +- Add the `soft` flag to `disconnectAndClear()` which keeps an internal copy of synced data in the + database, allowing faster re-sync if a compatible token is used in the next `connect()` call. -- Add the `clear` parameter to `RawTable` to run a statement helping the core extension clear raw tables. +- Add the `clear` parameter to `RawTable` to run a statement helping the core extension clear raw + tables. ## 1.7.0 diff --git a/common/src/commonIntegrationTest/kotlin/com/powersync/sync/SyncIntegrationTest.kt b/common/src/commonIntegrationTest/kotlin/com/powersync/sync/SyncIntegrationTest.kt index 2993a10c..c419fe52 100644 --- a/common/src/commonIntegrationTest/kotlin/com/powersync/sync/SyncIntegrationTest.kt +++ b/common/src/commonIntegrationTest/kotlin/com/powersync/sync/SyncIntegrationTest.kt @@ -95,6 +95,33 @@ abstract class BaseSyncIntegrationTest( } } + @Test + fun useAppMetadata() = + databaseTest { + database.connect( + connector, + options = getOptions(), + appMetadata = + mapOf( + "app_version" to "1.0.0", + ), + ) + turbineScope(timeout = 10.0.seconds) { + val turbine = database.currentStatus.asFlow().testIn(this) + turbine.waitFor { it.connected } + turbine.cancel() + } + + requestedSyncStreams shouldHaveSingleElement { + val meta = it.jsonObject["app_metadata"]!!.jsonObject + meta.keys shouldHaveSingleElement "app_version" + meta.values + .first() + .jsonPrimitive.content shouldBe "1.0.0" + true + } + } + @Test @OptIn(DelicateCoroutinesApi::class) fun closesResponseStreamOnDatabaseClose() = diff --git a/common/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt b/common/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt index 42271ebe..bc2de2a8 100644 --- a/common/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt +++ b/common/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt @@ -80,6 +80,7 @@ public interface PowerSyncDatabase : Queries { * 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. + * Use @param [appMetadata] to specify application metadata that will be displayed in PowerSync service logs. * * Example usage: * ``` @@ -91,11 +92,17 @@ public interface PowerSyncDatabase : Queries { * ) * ) * + * val appMetadata = mapOf( + * "appVersion" to "1.0.0", + * "deviceId" to "device456" + * ) + * * connect( * connector = connector, * crudThrottleMs = 2000L, * retryDelayMs = 10000L, - * params = params + * params = params, + * appMetadata = appMetadata * ) * ``` */ @@ -106,6 +113,7 @@ public interface PowerSyncDatabase : Queries { retryDelayMs: Long = 5000L, params: Map = emptyMap(), options: SyncOptions = SyncOptions(), + appMetadata: Map = emptyMap(), ) /** @@ -272,7 +280,8 @@ public interface PowerSyncDatabase : Queries { val logger = generateLogger(logger) // Since this returns a fresh in-memory database every time, use a fresh group to avoid warnings about the // same database being opened multiple times. - val collection = ActiveDatabaseGroup.GroupsCollection().referenceDatabase(logger, "test") + val collection = + ActiveDatabaseGroup.GroupsCollection().referenceDatabase(logger, "test") return openedWithGroup( SingleConnectionPool(factory.openInMemoryConnection()), diff --git a/common/src/commonMain/kotlin/com/powersync/bucket/BucketStorage.kt b/common/src/commonMain/kotlin/com/powersync/bucket/BucketStorage.kt index 8ea60a38..abca8525 100644 --- a/common/src/commonMain/kotlin/com/powersync/bucket/BucketStorage.kt +++ b/common/src/commonMain/kotlin/com/powersync/bucket/BucketStorage.kt @@ -71,6 +71,8 @@ internal sealed interface PowerSyncControlArguments { val includeDefaults: Boolean, @SerialName("active_streams") val activeStreams: List, + @SerialName("app_metadata") + val appMetadata: Map, ) : PowerSyncControlArguments { override val sqlArguments: Pair get() = "start" to JsonUtil.json.encodeToString(this) @@ -109,7 +111,8 @@ internal sealed interface PowerSyncControlArguments { class UpdateSubscriptions( activeStreams: List, ) : PowerSyncControlArguments { - override val sqlArguments: Pair = "update_subscriptions" to JsonUtil.json.encodeToString(activeStreams) + override val sqlArguments: Pair = + "update_subscriptions" to JsonUtil.json.encodeToString(activeStreams) } } diff --git a/common/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt b/common/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt index bf23a5af..2f2746e1 100644 --- a/common/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt +++ b/common/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt @@ -142,6 +142,7 @@ internal class PowerSyncDatabaseImpl( retryDelayMs: Long, params: Map, options: SyncOptions, + appMetadata: Map, ) { waitReady() mutex.withLock { @@ -159,6 +160,7 @@ internal class PowerSyncDatabaseImpl( options = options, schema = schema, activeSubscriptions = streams.currentlyReferencedStreams, + appMetadata = appMetadata, ) } } diff --git a/common/src/commonMain/kotlin/com/powersync/sync/StreamingSync.kt b/common/src/commonMain/kotlin/com/powersync/sync/StreamingSync.kt index 0052d4c9..29618b6a 100644 --- a/common/src/commonMain/kotlin/com/powersync/sync/StreamingSync.kt +++ b/common/src/commonMain/kotlin/com/powersync/sync/StreamingSync.kt @@ -59,6 +59,7 @@ import kotlinx.io.readByteArray import kotlinx.io.readIntLe import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.encodeToJsonElement import kotlin.time.Clock @@ -74,6 +75,7 @@ internal class StreamingSyncClient( private val options: SyncOptions, private val schema: Schema, private val activeSubscriptions: StateFlow>, + private val appMetadata: Map = emptyMap(), ) { private var isUploadingCrud = AtomicReference(null) private var completedCrudUploads = Channel(onBufferOverflow = BufferOverflow.DROP_OLDEST) @@ -92,6 +94,7 @@ internal class StreamingSyncClient( configureSyncHttpClient(options.userAgent) config.block(this) } + is SyncClientConfiguration.ExistingClient -> config.client } @@ -127,7 +130,13 @@ internal class StreamingSyncClient( status.update { copy(downloadError = e) } } finally { if (!result.hideDisconnectStateAndReconnectImmediately) { - status.update { copy(connected = false, connecting = true, downloading = false) } + status.update { + copy( + connected = false, + connecting = true, + downloading = false, + ) + } delay(retryDelayMs) } } @@ -297,7 +306,8 @@ internal class StreamingSyncClient( } else { // Use RSocket as a fallback to ensure we have backpressure on platforms that don't support it natively. flow { - val credentials = requireNotNull(connector.getCredentialsCached()) { "Not logged in" } + val credentials = + requireNotNull(connector.getCredentialsCached()) { "Not logged in" } emitAll( httpClient.rSocketSyncStream( @@ -367,6 +377,7 @@ internal class StreamingSyncClient( schema = schema.toSerializable(), includeDefaults = options.includeDefaultStreams, activeStreams = subscriptions.map { it.key }, + appMetadata = appMetadata, ), ) @@ -375,7 +386,9 @@ internal class StreamingSyncClient( activeSubscriptions.collect { if (subscriptions !== it) { subscriptions = it - controlInvocations.send(PowerSyncControlArguments.UpdateSubscriptions(activeSubscriptions.value.map { it.key })) + controlInvocations.send( + PowerSyncControlArguments.UpdateSubscriptions(activeSubscriptions.value.map { it.key }), + ) } } } @@ -536,6 +549,7 @@ internal class StreamingSyncClient( }, clientId = clientId!!, parameters = params, + appMetadata = appMetadata, ) lateinit var receiveLines: Job diff --git a/common/src/commonMain/kotlin/com/powersync/sync/StreamingSyncRequest.kt b/common/src/commonMain/kotlin/com/powersync/sync/StreamingSyncRequest.kt index a09c15f4..9ad273c0 100644 --- a/common/src/commonMain/kotlin/com/powersync/sync/StreamingSyncRequest.kt +++ b/common/src/commonMain/kotlin/com/powersync/sync/StreamingSyncRequest.kt @@ -12,6 +12,7 @@ internal data class StreamingSyncRequest( @SerialName("include_checksum") val includeChecksum: Boolean = true, @SerialName("client_id") val clientId: String, val parameters: JsonObject = JsonObject(mapOf()), + @SerialName("app_metadata") val appMetadata: Map = emptyMap(), ) { @SerialName("raw_data") private val rawData: Boolean = true diff --git a/demos/supabase-todolist/shared/build.gradle.kts b/demos/supabase-todolist/shared/build.gradle.kts index 2a6801ad..e461c952 100644 --- a/demos/supabase-todolist/shared/build.gradle.kts +++ b/demos/supabase-todolist/shared/build.gradle.kts @@ -123,5 +123,8 @@ buildkonfig { stringConfigField("POWERSYNC_URL") stringConfigField("SUPABASE_URL") stringConfigField("SUPABASE_ANON_KEY") + + // App version from Gradle project version + buildConfigField(STRING, "APP_VERSION", "\"${project.version}\"") } } diff --git a/demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/Auth.kt b/demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/Auth.kt index 465cec3b..c1894ce7 100644 --- a/demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/Auth.kt +++ b/demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/Auth.kt @@ -69,6 +69,9 @@ internal class AuthViewModel( } }, ), + appMetadata = mapOf( + "appVersion" to Config.APP_VERSION + ), ) }