From 4fbd1da48391ab37e80cd42fc0d0353d846297f4 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Wed, 19 Apr 2023 15:41:46 +0200 Subject: [PATCH 1/9] Add support for authentication flows --- .../kotlin/io/realm/kotlin/mongodb/App.kt | 10 ++ .../kotlin/mongodb/AuthenticationChange.kt | 40 +++++ .../realm/kotlin/mongodb/internal/AppImpl.kt | 21 ++- .../realm/kotlin/mongodb/internal/UserImpl.kt | 18 +- .../kotlin/test/mongodb/shared/AppTests.kt | 156 +++++++++++------- 5 files changed, 184 insertions(+), 61 deletions(-) create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt index b284e50f40..879b7961d3 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt @@ -23,6 +23,8 @@ import io.realm.kotlin.mongodb.exceptions.AuthException import io.realm.kotlin.mongodb.exceptions.InvalidCredentialsException import io.realm.kotlin.mongodb.internal.AppConfigurationImpl import io.realm.kotlin.mongodb.internal.AppImpl +import io.realm.kotlin.mongodb.sync.ConnectionStateChange +import kotlinx.coroutines.flow.Flow /** * An **App** is the main client-side entry point for interacting with an **Atlas App Services @@ -101,6 +103,14 @@ public interface App { */ public suspend fun login(credentials: Credentials): User + /** + * Create a [Flow] of [AuthenticationChange]-events to receive notifications of updates to all + * app user auth states like login and logout. + * + * @return a [Flow] of authentication events for users associated with this app. + */ + public fun authenticationChangeAsFlow(): Flow; + /** * Close the app instance and release all underlying resources. * diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt new file mode 100644 index 0000000000..ae3fd3c7e6 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt @@ -0,0 +1,40 @@ +package io.realm.kotlin.mongodb + +/** + * TODO + */ +public data class AuthenticationChange( + /** + * TODO + */ + public val type: Type, + /** + * TODO + */ + public val user: User +) { + + /** + * TODO + */ + public enum class Type { + /** + * TODO + */ + LOGGED_IN, + /** + * TODO + */ + LOGGED_OUT + } + + /** + * TODO + */ + public fun didLogIn(): Boolean = (type == Type.LOGGED_IN) + + /** + * TODO + */ + public fun didLogOut(): Boolean = (type == Type.LOGGED_OUT) +} \ No newline at end of file diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt index 02c999d992..b21972df35 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt @@ -25,10 +25,14 @@ import io.realm.kotlin.internal.interop.sync.NetworkTransport import io.realm.kotlin.internal.util.Validation import io.realm.kotlin.internal.util.use import io.realm.kotlin.mongodb.App +import io.realm.kotlin.mongodb.AuthenticationChange import io.realm.kotlin.mongodb.Credentials import io.realm.kotlin.mongodb.User import io.realm.kotlin.mongodb.auth.EmailPasswordAuth import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.cancel // TODO Public due to being a transitive dependency to UserImpl public class AppImpl( @@ -37,6 +41,7 @@ public class AppImpl( internal val nativePointer: RealmAppPointer private val networkTransport: NetworkTransport + private val authenticationChangeFlow = MutableSharedFlow() init { val appResources: Pair> = configuration.createNativeApp() @@ -73,10 +78,24 @@ public class AppImpl( } ) return channel.receive() - .getOrThrow() + .getOrThrow().also { user: User -> + reportUserLoggedIn(user) + } } } + private suspend fun reportUserLoggedIn(user: User) { + authenticationChangeFlow.emit(AuthenticationChange(AuthenticationChange.Type.LOGGED_IN, user)) + } + + internal suspend fun reportUserLoggedOut(user: User) { + authenticationChangeFlow.emit(AuthenticationChange(AuthenticationChange.Type.LOGGED_OUT, user)) + } + + override fun authenticationChangeAsFlow(): Flow { + return authenticationChangeFlow + } + override fun close() { // The native App instance is what keeps the underlying SyncClient thread alive. So closing // it will close the Sync thread and close any network dispatchers. diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/UserImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/UserImpl.kt index 4096e441f2..e3a1737e88 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/UserImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/UserImpl.kt @@ -94,6 +94,7 @@ public class UserImpl( } override suspend fun logOut() { + val reportLoggedOut = loggedIn Channel>(1).use { channel -> RealmInterop.realm_app_log_out( app.nativePointer, @@ -103,11 +104,16 @@ public class UserImpl( } ) return@use channel.receive() - .getOrThrow() + .getOrThrow().also { + if (reportLoggedOut) { + app.reportUserLoggedOut(this) + } + } } } override suspend fun remove(): User { + val reportLogOut = loggedIn Channel>(1).use { channel -> RealmInterop.realm_app_remove_user( app.nativePointer, @@ -117,7 +123,11 @@ public class UserImpl( } ) return@use channel.receive() - .getOrThrow() + .getOrThrow().also { + if (reportLogOut) { + app.reportUserLoggedOut(this) + } + } } return this } @@ -135,7 +145,9 @@ public class UserImpl( } ) return@use channel.receive() - .getOrThrow() + .getOrThrow().also { + app.reportUserLoggedOut(this) + } } } diff --git a/packages/test-sync/src/androidAndroidTest/kotlin/io/realm/kotlin/test/mongodb/shared/AppTests.kt b/packages/test-sync/src/androidAndroidTest/kotlin/io/realm/kotlin/test/mongodb/shared/AppTests.kt index 6522db2aa9..af04dcac85 100644 --- a/packages/test-sync/src/androidAndroidTest/kotlin/io/realm/kotlin/test/mongodb/shared/AppTests.kt +++ b/packages/test-sync/src/androidAndroidTest/kotlin/io/realm/kotlin/test/mongodb/shared/AppTests.kt @@ -24,6 +24,7 @@ import io.realm.kotlin.internal.platform.appFilesDirectory import io.realm.kotlin.internal.platform.fileExists import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.AppConfiguration +import io.realm.kotlin.mongodb.AuthenticationChange import io.realm.kotlin.mongodb.AuthenticationProvider import io.realm.kotlin.mongodb.Credentials import io.realm.kotlin.mongodb.User @@ -36,15 +37,20 @@ import io.realm.kotlin.test.mongodb.asTestApp import io.realm.kotlin.test.mongodb.createUserAndLogIn import io.realm.kotlin.test.util.TestHelper import io.realm.kotlin.test.util.TestHelper.randomEmail +import io.realm.kotlin.test.util.receiveOrFail +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.runBlocking import kotlin.test.AfterTest import kotlin.test.BeforeTest +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertNotEquals import kotlin.test.assertNull +import kotlin.test.assertSame import kotlin.test.assertTrue import kotlin.test.fail @@ -234,6 +240,16 @@ class AppTests { assertNull(app.currentUser) } + @Test + @Ignore // Waiting for https://github.com/realm/realm-core/issues/6514 + fun currentUser_clearedAfterUserIsRemoved() = runBlocking { + assertNull(app.currentUser) + val user1 = app.login(Credentials.anonymous()) + assertEquals(user1, app.currentUser) + user1.remove() + assertNull(app.currentUser) + } + // @Test // fun switchUser_nullThrows() { // try { @@ -249,63 +265,89 @@ class AppTests { // TODO("FIXME") // } // -// @Test -// fun authListener() { -// val userRef = AtomicReference(null) -// looperThread.runBlocking { -// val authenticationListener = object : AuthenticationListener { -// override fun loggedIn(user: User) { -// userRef.set(user) -// user.logOutAsync { /* Ignore */ } -// } -// -// override fun loggedOut(user: User) { -// assertEquals(userRef.get(), user) -// looperThread.testComplete() -// } -// } -// app.addAuthenticationListener(authenticationListener) -// app.login(Credentials.anonymous()) -// } -// } -// -// @Test -// fun authListener_nullThrows() { -// assertFailsWith { app.addAuthenticationListener(TestHelper.getNull()) } -// } -// -// @Test -// fun authListener_remove() = looperThread.runBlocking { -// val failListener = object : AuthenticationListener { -// override fun loggedIn(user: User) { fail() } -// override fun loggedOut(user: User) { fail() } -// } -// val successListener = object : AuthenticationListener { -// override fun loggedOut(user: User) { fail() } -// override fun loggedIn(user: User) { looperThread.testComplete() } -// } -// // This test depends on listeners being executed in order which is an -// // implementation detail, but there isn't a sure fire way to do this -// // without depending on implementation details or assume a specific timing. -// app.addAuthenticationListener(failListener) -// app.addAuthenticationListener(successListener) -// app.removeAuthenticationListener(failListener) -// app.login(Credentials.anonymous()) -// } -// -// @Test -// fun functions_defaultCodecRegistry() { -// var user = app.login(Credentials.anonymous()) -// assertEquals(app.configuration.defaultCodecRegistry, app.getFunctions(user).defaultCodecRegistry) -// } -// -// @Test -// fun functions_customCodecRegistry() { -// var user = app.login(Credentials.anonymous()) -// val registry = CodecRegistries.fromCodecs(StringCodec()) -// assertEquals(registry, app.getFunctions(user, registry).defaultCodecRegistry) -// } -// + @Test + fun authenticationChangeAsFlow() = runBlocking { + val c = Channel(1) + val job = async { + app.authenticationChangeAsFlow().collect { + c.send(it) + } + } + + val user1 = app.login(Credentials.anonymous()) + val loggedInEvent = c.receiveOrFail() + assertEquals(AuthenticationChange.Type.LOGGED_IN, loggedInEvent.type) + assertSame(user1, loggedInEvent.user) + assertTrue(loggedInEvent.didLogIn()) + assertFalse(loggedInEvent.didLogOut()) + + user1.logOut() + val loggedOutEvent = c.receiveOrFail() + assertEquals(AuthenticationChange.Type.LOGGED_OUT, loggedOutEvent.type) + assertSame(user1, loggedOutEvent.user) + assertTrue(loggedOutEvent.didLogOut()) + assertFalse(loggedOutEvent.didLogIn()) + + // Repeating logout does not trigger a new event + user1.logOut() + val user2 = app.login(Credentials.anonymous()) + val reloginEvent = c.receiveOrFail() + assertEquals(user2, reloginEvent.user) + assertEquals(AuthenticationChange.Type.LOGGED_IN, reloginEvent.type) + + job.cancel() + c.close() + } + + @Test + fun authenticationChangeAsFlow_removeUser() = runBlocking { + val c = Channel(1) + val job = async { + app.authenticationChangeAsFlow().collect { + c.send(it) + } + } + val user1 = app.login(Credentials.anonymous(reuseExisting = true)) + val loggedInEvent = c.receiveOrFail() + assertEquals(AuthenticationChange.Type.LOGGED_IN, loggedInEvent.type) + + user1.remove() + val loggedOutEvent = c.receiveOrFail() + assertEquals(AuthenticationChange.Type.LOGGED_OUT, loggedOutEvent.type) + assertSame(user1, loggedOutEvent.user) + assertTrue(loggedOutEvent.didLogOut()) + assertFalse(loggedOutEvent.didLogIn()) + + job.cancel() + c.close() + + // Work-around for https://github.com/realm/realm-core/issues/6514 + // By logging the user back in, the TestApp teardown can correctly remove it. + app.login(Credentials.anonymous(reuseExisting = true)).logOut() + } + + @Test + fun authenticationChangeAsFlow_deleteUser() = runBlocking { + val c = Channel(1) + val job = async { + app.authenticationChangeAsFlow().collect { + c.send(it) + } + } + val user = app.login(Credentials.anonymous(reuseExisting = true)) + val loggedInEvent = c.receiveOrFail() + assertEquals(AuthenticationChange.Type.LOGGED_IN, loggedInEvent.type) + + user.delete() + val loggedOutEvent = c.receiveOrFail() + assertEquals(AuthenticationChange.Type.LOGGED_OUT, loggedOutEvent.type) + assertSame(user, loggedOutEvent.user) + assertTrue(loggedOutEvent.didLogOut()) + assertFalse(loggedOutEvent.didLogIn()) + + job.cancel() + c.close() + } @Test fun encryptedMetadataRealm() { From f784477cf8ecd21c327f2bad16fd20ccecc88a79 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Thu, 20 Apr 2023 07:14:56 +0200 Subject: [PATCH 2/9] Formatting and tests --- .../kotlin/io/realm/kotlin/mongodb/App.kt | 3 +- .../kotlin/mongodb/AuthenticationChange.kt | 42 ++++++++----------- .../realm/kotlin/mongodb/internal/AppImpl.kt | 9 ++-- .../internal/AuthenticationChangeImpl.kt | 21 ++++++++++ .../realm/kotlin/mongodb/internal/UserImpl.kt | 8 ++-- .../kotlin/test/mongodb/shared/AppTests.kt | 19 +++++---- 6 files changed, 61 insertions(+), 41 deletions(-) create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AuthenticationChangeImpl.kt diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt index 879b7961d3..6269e21c5e 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt @@ -23,7 +23,6 @@ import io.realm.kotlin.mongodb.exceptions.AuthException import io.realm.kotlin.mongodb.exceptions.InvalidCredentialsException import io.realm.kotlin.mongodb.internal.AppConfigurationImpl import io.realm.kotlin.mongodb.internal.AppImpl -import io.realm.kotlin.mongodb.sync.ConnectionStateChange import kotlinx.coroutines.flow.Flow /** @@ -109,7 +108,7 @@ public interface App { * * @return a [Flow] of authentication events for users associated with this app. */ - public fun authenticationChangeAsFlow(): Flow; + public fun authenticationChangeAsFlow(): Flow /** * Close the app instance and release all underlying resources. diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt index ae3fd3c7e6..2ea0e9c4b8 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt @@ -3,38 +3,32 @@ package io.realm.kotlin.mongodb /** * TODO */ -public data class AuthenticationChange( - /** - * TODO - */ - public val type: Type, +public sealed interface AuthenticationChange { /** * TODO */ public val user: User -) { - /** * TODO */ - public enum class Type { - /** - * TODO - */ - LOGGED_IN, - /** - * TODO - */ - LOGGED_OUT - } - + public fun didLogIn(): Boolean /** * TODO */ - public fun didLogIn(): Boolean = (type == Type.LOGGED_IN) + public fun didLogOut(): Boolean +} - /** - * TODO - */ - public fun didLogOut(): Boolean = (type == Type.LOGGED_OUT) -} \ No newline at end of file +/** + * TODO + */ +public interface LoggedIn : AuthenticationChange + +/** + * TODO + */ +public interface LoggedOut : AuthenticationChange + +/** + * TODO + */ +public interface Removed : AuthenticationChange diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt index b21972df35..0dff589c7e 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt @@ -32,7 +32,6 @@ import io.realm.kotlin.mongodb.auth.EmailPasswordAuth import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.cancel // TODO Public due to being a transitive dependency to UserImpl public class AppImpl( @@ -85,11 +84,15 @@ public class AppImpl( } private suspend fun reportUserLoggedIn(user: User) { - authenticationChangeFlow.emit(AuthenticationChange(AuthenticationChange.Type.LOGGED_IN, user)) + authenticationChangeFlow.emit(LoggedInImpl(user)) } internal suspend fun reportUserLoggedOut(user: User) { - authenticationChangeFlow.emit(AuthenticationChange(AuthenticationChange.Type.LOGGED_OUT, user)) + authenticationChangeFlow.emit(LoggedOutImpl(user)) + } + + internal suspend fun reportUserRemoved(user: User) { + authenticationChangeFlow.emit(RemovedImpl(user)) } override fun authenticationChangeAsFlow(): Flow { diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AuthenticationChangeImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AuthenticationChangeImpl.kt new file mode 100644 index 0000000000..e3ae8ceb87 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AuthenticationChangeImpl.kt @@ -0,0 +1,21 @@ +package io.realm.kotlin.mongodb.internal + +import io.realm.kotlin.mongodb.LoggedIn +import io.realm.kotlin.mongodb.LoggedOut +import io.realm.kotlin.mongodb.Removed +import io.realm.kotlin.mongodb.User + +internal class LoggedInImpl(override val user: User) : LoggedIn { + override fun didLogIn(): Boolean = (user.state == User.State.LOGGED_IN) + override fun didLogOut(): Boolean = (user.state == User.State.LOGGED_OUT || user.state == User.State.REMOVED) +} + +internal class LoggedOutImpl(override val user: User) : LoggedOut { + override fun didLogIn(): Boolean = (user.state == User.State.LOGGED_IN) + override fun didLogOut(): Boolean = (user.state == User.State.LOGGED_OUT || user.state == User.State.REMOVED) +} + +internal class RemovedImpl(override val user: User) : Removed { + override fun didLogIn(): Boolean = (user.state == User.State.LOGGED_IN) + override fun didLogOut(): Boolean = (user.state == User.State.LOGGED_OUT || user.state == User.State.REMOVED) +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/UserImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/UserImpl.kt index e3a1737e88..c1fa23cc5f 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/UserImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/UserImpl.kt @@ -113,7 +113,7 @@ public class UserImpl( } override suspend fun remove(): User { - val reportLogOut = loggedIn + val reportRemoved = loggedIn Channel>(1).use { channel -> RealmInterop.realm_app_remove_user( app.nativePointer, @@ -124,8 +124,8 @@ public class UserImpl( ) return@use channel.receive() .getOrThrow().also { - if (reportLogOut) { - app.reportUserLoggedOut(this) + if (reportRemoved) { + app.reportUserRemoved(this) } } } @@ -146,7 +146,7 @@ public class UserImpl( ) return@use channel.receive() .getOrThrow().also { - app.reportUserLoggedOut(this) + app.reportUserRemoved(this) } } } diff --git a/packages/test-sync/src/androidAndroidTest/kotlin/io/realm/kotlin/test/mongodb/shared/AppTests.kt b/packages/test-sync/src/androidAndroidTest/kotlin/io/realm/kotlin/test/mongodb/shared/AppTests.kt index af04dcac85..1d2b78751c 100644 --- a/packages/test-sync/src/androidAndroidTest/kotlin/io/realm/kotlin/test/mongodb/shared/AppTests.kt +++ b/packages/test-sync/src/androidAndroidTest/kotlin/io/realm/kotlin/test/mongodb/shared/AppTests.kt @@ -22,11 +22,15 @@ import io.realm.kotlin.entities.sync.ChildPk import io.realm.kotlin.entities.sync.ParentPk import io.realm.kotlin.internal.platform.appFilesDirectory import io.realm.kotlin.internal.platform.fileExists +import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.AppConfiguration import io.realm.kotlin.mongodb.AuthenticationChange import io.realm.kotlin.mongodb.AuthenticationProvider import io.realm.kotlin.mongodb.Credentials +import io.realm.kotlin.mongodb.LoggedIn +import io.realm.kotlin.mongodb.LoggedOut +import io.realm.kotlin.mongodb.Removed import io.realm.kotlin.mongodb.User import io.realm.kotlin.mongodb.exceptions.InvalidCredentialsException import io.realm.kotlin.mongodb.sync.SyncConfiguration @@ -40,7 +44,6 @@ import io.realm.kotlin.test.util.TestHelper.randomEmail import io.realm.kotlin.test.util.receiveOrFail import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.runBlocking import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Ignore @@ -276,14 +279,14 @@ class AppTests { val user1 = app.login(Credentials.anonymous()) val loggedInEvent = c.receiveOrFail() - assertEquals(AuthenticationChange.Type.LOGGED_IN, loggedInEvent.type) + assertTrue(loggedInEvent is LoggedIn) assertSame(user1, loggedInEvent.user) assertTrue(loggedInEvent.didLogIn()) assertFalse(loggedInEvent.didLogOut()) user1.logOut() val loggedOutEvent = c.receiveOrFail() - assertEquals(AuthenticationChange.Type.LOGGED_OUT, loggedOutEvent.type) + assertTrue(loggedOutEvent is LoggedOut) assertSame(user1, loggedOutEvent.user) assertTrue(loggedOutEvent.didLogOut()) assertFalse(loggedOutEvent.didLogIn()) @@ -293,7 +296,7 @@ class AppTests { val user2 = app.login(Credentials.anonymous()) val reloginEvent = c.receiveOrFail() assertEquals(user2, reloginEvent.user) - assertEquals(AuthenticationChange.Type.LOGGED_IN, reloginEvent.type) + assertTrue(reloginEvent is LoggedIn) job.cancel() c.close() @@ -309,11 +312,11 @@ class AppTests { } val user1 = app.login(Credentials.anonymous(reuseExisting = true)) val loggedInEvent = c.receiveOrFail() - assertEquals(AuthenticationChange.Type.LOGGED_IN, loggedInEvent.type) + assertTrue(loggedInEvent is LoggedIn) user1.remove() val loggedOutEvent = c.receiveOrFail() - assertEquals(AuthenticationChange.Type.LOGGED_OUT, loggedOutEvent.type) + assertTrue(loggedOutEvent is Removed) assertSame(user1, loggedOutEvent.user) assertTrue(loggedOutEvent.didLogOut()) assertFalse(loggedOutEvent.didLogIn()) @@ -336,11 +339,11 @@ class AppTests { } val user = app.login(Credentials.anonymous(reuseExisting = true)) val loggedInEvent = c.receiveOrFail() - assertEquals(AuthenticationChange.Type.LOGGED_IN, loggedInEvent.type) + assertTrue(loggedInEvent is LoggedIn) user.delete() val loggedOutEvent = c.receiveOrFail() - assertEquals(AuthenticationChange.Type.LOGGED_OUT, loggedOutEvent.type) + assertTrue(loggedOutEvent is Removed) assertSame(user, loggedOutEvent.user) assertTrue(loggedOutEvent.didLogOut()) assertFalse(loggedOutEvent.didLogIn()) From 5fbe2dd509299958abb4e91cdfe6a1663613b7cf Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Thu, 20 Apr 2023 07:39:49 +0200 Subject: [PATCH 3/9] Add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f69b1da85..2791cf3a4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ### Enhancements * Avoid tracking unreferenced realm versions through the garbage collector. (Issue [#1234](https://github.com/realm/realm-kotlin/issues/1234)) * [Sync] All tokens, passwords and custom function arguments are now obfuscated by default, even if `LogLevel` is set to DEBUG, TRACE or ALL. (Issue [#410](https://github.com/realm/realm-kotlin/issues/410)) +* [Sync] Add support for `App.authenticationChangeAsFlow()` which make it possible to listen to authentication changes like "LoggedIn", "LoggedOut" and "Removed" across all users of the app. (Issue [#749](https://github.com/realm/realm-kotlin/issues/749)). ### Fixed * None. From 0c72d79f632d5c3279d0acaba6daf7ccc9fc38af Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Thu, 20 Apr 2023 14:36:55 +0200 Subject: [PATCH 4/9] Redo API and docs based on feedback --- .../kotlin/io/realm/kotlin/mongodb/App.kt | 2 +- .../kotlin/mongodb/AuthenticationChange.kt | 36 ++++++++++++------- .../internal/AuthenticationChangeImpl.kt | 17 ++------- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt index 6269e21c5e..4ab2cdd7fb 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt @@ -104,7 +104,7 @@ public interface App { /** * Create a [Flow] of [AuthenticationChange]-events to receive notifications of updates to all - * app user auth states like login and logout. + * app user authentication states, like login and logout. * * @return a [Flow] of authentication events for users associated with this app. */ diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt index 2ea0e9c4b8..aa87ad715d 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt @@ -1,34 +1,44 @@ package io.realm.kotlin.mongodb +import kotlinx.coroutines.flow.Flow + /** - * TODO + * This sealed class describe the possible events that can be observed on the [Flow] created by + * calling [App.authenticationChangeAsFlow]. + * + * The specific states are represented by these subclasses [LoggedIn], [LoggedOut] and + * [Removed]. + * + * Changes can thus be consumed this way: + * + * ``` + * app.authenticationChangeAsFlow().asFlow().collect { change: AuthenticationChange -> + * when(change) { + * is LoggedIn -> handleLogin(change.user) + * is LoggedOut -> handleLogOut(change.user) + * is Removed -> handleRemove(change.user) + * } + * } + * ``` */ public sealed interface AuthenticationChange { /** - * TODO + * A reference to the [User] that the event happened to. */ public val user: User - /** - * TODO - */ - public fun didLogIn(): Boolean - /** - * TODO - */ - public fun didLogOut(): Boolean } /** - * TODO + * Event emitted when a user logs into the app. */ public interface LoggedIn : AuthenticationChange /** - * TODO + * Event emitted when a user is logged out. */ public interface LoggedOut : AuthenticationChange /** - * TODO + * Event emitted when a user is removed, which also logs them out. */ public interface Removed : AuthenticationChange diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AuthenticationChangeImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AuthenticationChangeImpl.kt index e3ae8ceb87..0ee5b9a474 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AuthenticationChangeImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AuthenticationChangeImpl.kt @@ -5,17 +5,6 @@ import io.realm.kotlin.mongodb.LoggedOut import io.realm.kotlin.mongodb.Removed import io.realm.kotlin.mongodb.User -internal class LoggedInImpl(override val user: User) : LoggedIn { - override fun didLogIn(): Boolean = (user.state == User.State.LOGGED_IN) - override fun didLogOut(): Boolean = (user.state == User.State.LOGGED_OUT || user.state == User.State.REMOVED) -} - -internal class LoggedOutImpl(override val user: User) : LoggedOut { - override fun didLogIn(): Boolean = (user.state == User.State.LOGGED_IN) - override fun didLogOut(): Boolean = (user.state == User.State.LOGGED_OUT || user.state == User.State.REMOVED) -} - -internal class RemovedImpl(override val user: User) : Removed { - override fun didLogIn(): Boolean = (user.state == User.State.LOGGED_IN) - override fun didLogOut(): Boolean = (user.state == User.State.LOGGED_OUT || user.state == User.State.REMOVED) -} +internal class LoggedInImpl(override val user: User) : LoggedIn +internal class LoggedOutImpl(override val user: User) : LoggedOut +internal class RemovedImpl(override val user: User) : Removed From 18ea31bd6a3b602d0f93e02821651836a514540d Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Thu, 20 Apr 2023 15:42:13 +0200 Subject: [PATCH 5/9] Fix tests --- .../io/realm/kotlin/test/mongodb/shared/AppTests.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/test-sync/src/androidAndroidTest/kotlin/io/realm/kotlin/test/mongodb/shared/AppTests.kt b/packages/test-sync/src/androidAndroidTest/kotlin/io/realm/kotlin/test/mongodb/shared/AppTests.kt index 1d2b78751c..65b43f4caa 100644 --- a/packages/test-sync/src/androidAndroidTest/kotlin/io/realm/kotlin/test/mongodb/shared/AppTests.kt +++ b/packages/test-sync/src/androidAndroidTest/kotlin/io/realm/kotlin/test/mongodb/shared/AppTests.kt @@ -281,15 +281,11 @@ class AppTests { val loggedInEvent = c.receiveOrFail() assertTrue(loggedInEvent is LoggedIn) assertSame(user1, loggedInEvent.user) - assertTrue(loggedInEvent.didLogIn()) - assertFalse(loggedInEvent.didLogOut()) user1.logOut() val loggedOutEvent = c.receiveOrFail() assertTrue(loggedOutEvent is LoggedOut) assertSame(user1, loggedOutEvent.user) - assertTrue(loggedOutEvent.didLogOut()) - assertFalse(loggedOutEvent.didLogIn()) // Repeating logout does not trigger a new event user1.logOut() @@ -318,8 +314,6 @@ class AppTests { val loggedOutEvent = c.receiveOrFail() assertTrue(loggedOutEvent is Removed) assertSame(user1, loggedOutEvent.user) - assertTrue(loggedOutEvent.didLogOut()) - assertFalse(loggedOutEvent.didLogIn()) job.cancel() c.close() @@ -345,8 +339,6 @@ class AppTests { val loggedOutEvent = c.receiveOrFail() assertTrue(loggedOutEvent is Removed) assertSame(user, loggedOutEvent.user) - assertTrue(loggedOutEvent.didLogOut()) - assertFalse(loggedOutEvent.didLogIn()) job.cancel() c.close() From e7fae82d05a790fbb303a13eed6490887ad7f620 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Mon, 24 Apr 2023 14:18:15 +0200 Subject: [PATCH 6/9] PR feedback --- .../kotlin/mongodb/AuthenticationChange.kt | 7 ++- .../realm/kotlin/mongodb/internal/AppImpl.kt | 19 ++++---- .../realm/kotlin/mongodb/internal/UserImpl.kt | 46 +++++++++++-------- 3 files changed, 41 insertions(+), 31 deletions(-) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt index aa87ad715d..049296cc2d 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt @@ -23,7 +23,12 @@ import kotlinx.coroutines.flow.Flow */ public sealed interface AuthenticationChange { /** - * A reference to the [User] that the event happened to. + * A reference to the [User] this event happened to. + * + * *Warning:* This is the live user object, so the [User.state]] might have diverged from the + * event it is associated with, i.e. if a users logs out and back in while the event is + * propagating, the state of the user might be [User.State.LOGGED_IN], even though it was + * reported as a [LoggedOut] event. */ public val user: User } diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt index 0dff589c7e..3a5e528b13 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt @@ -78,21 +78,18 @@ public class AppImpl( ) return channel.receive() .getOrThrow().also { user: User -> - reportUserLoggedIn(user) + reportAuthenticationChange(user, User.State.LOGGED_IN) } } } - private suspend fun reportUserLoggedIn(user: User) { - authenticationChangeFlow.emit(LoggedInImpl(user)) - } - - internal suspend fun reportUserLoggedOut(user: User) { - authenticationChangeFlow.emit(LoggedOutImpl(user)) - } - - internal suspend fun reportUserRemoved(user: User) { - authenticationChangeFlow.emit(RemovedImpl(user)) + internal suspend fun reportAuthenticationChange(user: User, change: User.State) { + val event: AuthenticationChange = when (change) { + User.State.LOGGED_OUT -> LoggedOutImpl(user) + User.State.LOGGED_IN -> LoggedInImpl(user) + User.State.REMOVED -> RemovedImpl(user) + } + authenticationChangeFlow.emit(event) } override fun authenticationChangeAsFlow(): Flow { diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/UserImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/UserImpl.kt index c1fa23cc5f..f11ed6f66f 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/UserImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/UserImpl.kt @@ -94,38 +94,46 @@ public class UserImpl( } override suspend fun logOut() { - val reportLoggedOut = loggedIn - Channel>(1).use { channel -> + Channel>(1).use { channel -> + val reportLoggedOut = loggedIn RealmInterop.realm_app_log_out( app.nativePointer, nativePointer, - channelResultCallback(channel) { - // No-op + channelResultCallback(channel) { + if (reportLoggedOut) { + User.State.LOGGED_OUT + } else { + null + } } ) return@use channel.receive() - .getOrThrow().also { - if (reportLoggedOut) { - app.reportUserLoggedOut(this) + .getOrThrow().also { state: User.State? -> + if (state != null) { + app.reportAuthenticationChange(this, state) } } } } override suspend fun remove(): User { - val reportRemoved = loggedIn - Channel>(1).use { channel -> + Channel>(1).use { channel -> + val reportRemoved = loggedIn RealmInterop.realm_app_remove_user( app.nativePointer, nativePointer, - channelResultCallback(channel) { - // No-op + channelResultCallback(channel) { + if (reportRemoved) { + User.State.REMOVED + } else { + null + } } ) return@use channel.receive() - .getOrThrow().also { - if (reportRemoved) { - app.reportUserRemoved(this) + .getOrThrow().also { state: User.State? -> + if (state != null) { + app.reportAuthenticationChange(this, state) } } } @@ -136,17 +144,17 @@ public class UserImpl( if (state != User.State.LOGGED_IN) { throw IllegalStateException("User must be logged in, in order to be deleted.") } - Channel>(1).use { channel -> + Channel>(1).use { channel -> RealmInterop.realm_app_delete_user( app.nativePointer, nativePointer, - channelResultCallback(channel) { - // No-op + channelResultCallback(channel) { + User.State.REMOVED } ) return@use channel.receive() - .getOrThrow().also { - app.reportUserRemoved(this) + .getOrThrow().also { state: User.State -> + app.reportAuthenticationChange(this, state) } } } From b68e3f43c087b20515e97f43bff0e7b92a4cc7ad Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Mon, 24 Apr 2023 17:28:29 +0200 Subject: [PATCH 7/9] Throw if reporting auth changes are blocked for too long --- .../realm/kotlin/mongodb/internal/AppImpl.kt | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt index 3a5e528b13..abb9ac900d 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt @@ -29,6 +29,7 @@ import io.realm.kotlin.mongodb.AuthenticationChange import io.realm.kotlin.mongodb.Credentials import io.realm.kotlin.mongodb.User import io.realm.kotlin.mongodb.auth.EmailPasswordAuth +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -40,7 +41,17 @@ public class AppImpl( internal val nativePointer: RealmAppPointer private val networkTransport: NetworkTransport - private val authenticationChangeFlow = MutableSharedFlow() + + // Allow some delay between events being reported and them being consumed. + // When the (somewhat arbitrary) limit is hit, we will throw an exception, since we assume the + // consumer is doing something wrong. This is also needed because we don't + // want to block user events like logout, delete and remove. + @Suppress("MagicNumber") + private val authenticationChangeFlow = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 8, + onBufferOverflow = BufferOverflow.SUSPEND + ) init { val appResources: Pair> = configuration.createNativeApp() @@ -83,13 +94,19 @@ public class AppImpl( } } - internal suspend fun reportAuthenticationChange(user: User, change: User.State) { + internal fun reportAuthenticationChange(user: User, change: User.State) { val event: AuthenticationChange = when (change) { User.State.LOGGED_OUT -> LoggedOutImpl(user) User.State.LOGGED_IN -> LoggedInImpl(user) User.State.REMOVED -> RemovedImpl(user) } - authenticationChangeFlow.emit(event) + if (!authenticationChangeFlow.tryEmit(event)) { + throw IllegalStateException( + "It wasn't possible to emit authentication changes " + + "because a consuming flow was blocked. Increase dispatcher processing resources " + + "or buffer `App.authenticationChangeAsFlow()` with buffer(...)." + ) + } } override fun authenticationChangeAsFlow(): Flow { From 655e8d30bcf3f0f5d6a9db7c66ecd6aa7300ca66 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Mon, 24 Apr 2023 18:11:04 +0200 Subject: [PATCH 8/9] Add test for hitting flow capacity --- .../kotlin/test/mongodb/shared/AppTests.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/test-sync/src/androidAndroidTest/kotlin/io/realm/kotlin/test/mongodb/shared/AppTests.kt b/packages/test-sync/src/androidAndroidTest/kotlin/io/realm/kotlin/test/mongodb/shared/AppTests.kt index 65b43f4caa..4018eca13c 100644 --- a/packages/test-sync/src/androidAndroidTest/kotlin/io/realm/kotlin/test/mongodb/shared/AppTests.kt +++ b/packages/test-sync/src/androidAndroidTest/kotlin/io/realm/kotlin/test/mongodb/shared/AppTests.kt @@ -44,6 +44,8 @@ import io.realm.kotlin.test.util.TestHelper.randomEmail import io.realm.kotlin.test.util.receiveOrFail import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Ignore @@ -344,6 +346,28 @@ class AppTests { c.close() } + @Test + fun authenticationChangeAsFlow_throwsWhenExceedCapacity() = runBlocking { + val latch = Mutex(locked = true) + val job = async { + app.authenticationChangeAsFlow().collect { + // Block `flow` from collecting any more events beside the first. + latch.withLock { + // Allow flow to continue + } + } + } + // Logging in 9 users should hit the capacity of the flow, causing the next + // login to fail. + repeat(9) { + app.createUserAndLogIn() + } + assertFailsWith { + app.createUserAndLogIn() + } + job.cancel() + } + @Test fun encryptedMetadataRealm() { // Create new test app with a random encryption key From e98a659f7ba74c2e49674bf1f342dc9440ba1398 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Tue, 25 Apr 2023 17:12:12 +0200 Subject: [PATCH 9/9] PR feedback --- .../kotlin/io/realm/kotlin/mongodb/App.kt | 2 +- .../kotlin/mongodb/AuthenticationChange.kt | 19 +++++++++++++++++-- .../kotlin/io/realm/kotlin/mongodb/User.kt | 6 ++++++ .../internal/AuthenticationChangeImpl.kt | 15 +++++++++++++++ 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt index 4ab2cdd7fb..9acb2eb1a6 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt @@ -104,7 +104,7 @@ public interface App { /** * Create a [Flow] of [AuthenticationChange]-events to receive notifications of updates to all - * app user authentication states, like login and logout. + * app user authentication states: login, logout and removal. * * @return a [Flow] of authentication events for users associated with this app. */ diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt index 049296cc2d..6b89fc69f1 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AuthenticationChange.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.realm.kotlin.mongodb import kotlinx.coroutines.flow.Flow @@ -12,7 +27,7 @@ import kotlinx.coroutines.flow.Flow * Changes can thus be consumed this way: * * ``` - * app.authenticationChangeAsFlow().asFlow().collect { change: AuthenticationChange -> + * app.authenticationChangeAsFlow().collect { change: AuthenticationChange -> * when(change) { * is LoggedIn -> handleLogin(change.user) * is LoggedOut -> handleLogOut(change.user) @@ -25,7 +40,7 @@ public sealed interface AuthenticationChange { /** * A reference to the [User] this event happened to. * - * *Warning:* This is the live user object, so the [User.state]] might have diverged from the + * *Warning:* This is the live user object, so the [User.state] might have diverged from the * event it is associated with, i.e. if a users logs out and back in while the event is * propagating, the state of the user might be [User.State.LOGGED_IN], even though it was * reported as a [LoggedOut] event. diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/User.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/User.kt index 690e63dc9a..fa4d7e3742 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/User.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/User.kt @@ -128,6 +128,8 @@ public interface User { * * @throws io.realm.kotlin.mongodb.exceptions.ServiceException if a failure occurred when * communicating with App Services. See [AppException] for details. + * @throws IllegalStateException if a consumer listening to [App.authenticationChangeAsFlow] + * is too slow consuming events. */ // FIXME add references to allUsers and remove when ready // * All other users will be marked as [User.State.LOGGED_OUT] @@ -147,6 +149,8 @@ public interface User { * @throws IllegalStateException if the user was already removed. * @throws io.realm.kotlin.mongodb.exceptions.ServiceException if a failure occurred when * communicating with App Services. See [AppException] for details. + * @throws IllegalStateException if a consumer listening to [App.authenticationChangeAsFlow] + * is too slow consuming events. */ // TODO Document how this method behave if offline public suspend fun remove(): User @@ -163,6 +167,8 @@ public interface User { * @throws IllegalStateException if the user was already removed or not logged in. * @throws io.realm.kotlin.mongodb.exceptions.ServiceException if a failure occurred when * communicating with App Services. See [AppException] for details. + * @throws IllegalStateException if a consumer listening to [App.authenticationChangeAsFlow] + * is too slow consuming events. */ public suspend fun delete() diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AuthenticationChangeImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AuthenticationChangeImpl.kt index 0ee5b9a474..4f2e880d5e 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AuthenticationChangeImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AuthenticationChangeImpl.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.realm.kotlin.mongodb.internal import io.realm.kotlin.mongodb.LoggedIn