All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.io.realm.kotlin.mongodb.internal.AppImpl.kt Maven / Gradle / Ivy

Go to download

Sync Library code for Realm Kotlin. This artifact is not supposed to be consumed directly, but through 'io.realm.kotlin:gradle-plugin:1.5.2' instead.

The newest version!
/*
 * Copyright 2021 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.internal.interop.RealmAppPointer
import io.realm.kotlin.internal.interop.RealmInterop
import io.realm.kotlin.internal.interop.RealmUserPointer
import io.realm.kotlin.internal.interop.sync.NetworkTransport
import io.realm.kotlin.internal.interop.sync.WebSocketTransport
import io.realm.kotlin.internal.toDuration
import io.realm.kotlin.internal.util.DispatcherHolder
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.AppConfiguration
import io.realm.kotlin.mongodb.AuthenticationChange
import io.realm.kotlin.mongodb.Credentials
import io.realm.kotlin.mongodb.User
import io.realm.kotlin.mongodb.annotations.ExperimentalEdgeServerApi
import io.realm.kotlin.mongodb.auth.EmailPasswordAuth
import io.realm.kotlin.mongodb.sync.Sync
import io.realm.kotlin.types.RealmInstant
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

public data class AppResources(
    val dispatcherHolder: DispatcherHolder,
    val networkTransport: NetworkTransport,
    val websocketTransport: WebSocketTransport?,
    val realmAppPointer: RealmAppPointer
)

// TODO Public due to being a transitive dependency to UserImpl
public class AppImpl(
    override val configuration: AppConfigurationImpl,
) : App {

    internal val nativePointer: RealmAppPointer
    internal val appNetworkDispatcher: DispatcherHolder
    private val networkTransport: NetworkTransport
    private val websocketTransport: WebSocketTransport?

    private var lastOnlineStateReported: Duration? = null
    private var lastConnectedState: Boolean? = null // null = unknown, true = connected, false = disconnected
    @Suppress("MagicNumber")
    private val reconnectThreshold = 5.seconds

    @ExperimentalEdgeServerApi
    override val baseUrl: String
        get() = RealmInterop.realm_app_get_base_url(nativePointer)

    @ExperimentalEdgeServerApi
    override suspend fun updateBaseUrl(baseUrl: String?) {
        Channel>(1).use { channel ->
            RealmInterop.realm_app_update_base_url(
                app = nativePointer,
                baseUrl = baseUrl?.trimEnd('/'), // trailing slashes are not handled properly in core
                callback = channelResultCallback(channel) {
                    // No-op
                }
            )
            channel.receive().getOrThrow()
        }
    }

    @Suppress("invisible_member", "invisible_reference", "MagicNumber")
    private val connectionListener = NetworkStateObserver.ConnectionListener { connectionAvailable ->
        // In an ideal world, we would be able to reliably detect the network coming and
        // going. Unfortunately that does not seem to be case (at least on Android).
        //
        // So instead of assuming that we have always detect the device going offline first,
        // we just tell Realm Core to reconnect when we detect the network has come back.
        //
        // Due to the way network interfaces are re-enabled on Android, we might see multiple
        // "isOnline" messages in short order. So in order to prevent resetting the network
        // too often we throttle messages, so a reconnect can only happen ever 5 seconds.
        configuration.logger.debug("Network state change detected. ConnectionAvailable = $connectionAvailable")
        val now: Duration = RealmInstant.now().toDuration()
        if (connectionAvailable && (lastOnlineStateReported == null || now.minus(lastOnlineStateReported!!) > reconnectThreshold)
        ) {
            configuration.logger.info("Trigger network reconnect.")
            try {
                sync.reconnect()
            } catch (ex: Exception) {
                configuration.logger.error(ex.toString())
            }
            lastOnlineStateReported = now
        }
        lastConnectedState = connectionAvailable
    }

    // 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: AppResources = configuration.createNativeApp()
        appNetworkDispatcher = appResources.dispatcherHolder
        networkTransport = appResources.networkTransport
        websocketTransport = appResources.websocketTransport
        nativePointer = appResources.realmAppPointer
        NetworkStateObserver.addListener(connectionListener)
    }

    override val emailPasswordAuth: EmailPasswordAuth by lazy { EmailPasswordAuthImpl(nativePointer) }

    override val currentUser: User?
        get() = RealmInterop.realm_app_get_current_user(nativePointer)
            ?.let { UserImpl(it, this) }
    override val sync: Sync by lazy { SyncImpl(nativePointer) }

    override fun allUsers(): List =
        RealmInterop.realm_app_get_all_users(nativePointer)
            .map { ptr: RealmUserPointer ->
                UserImpl(ptr, this)
            }

    override suspend fun login(credentials: Credentials): User {
        // suspendCoroutine doesn't allow freezing callback capturing continuation
        // ... and cannot be resumed on another thread (we probably also want to guarantee that we
        // are resuming on the same dispatcher), so run our own implementation using a channel
        Channel>(1).use { channel ->
            RealmInterop.realm_app_log_in_with_credentials(
                nativePointer,
                when (credentials) {
                    is CredentialsImpl -> credentials.nativePointer
                    is CustomEJsonCredentialsImpl -> credentials.nativePointer(this)
                    else -> throw IllegalArgumentException("Argument 'credentials' is of an invalid type ${credentials::class.simpleName}")
                },
                channelResultCallback(channel) { userPointer ->
                    UserImpl(userPointer, this)
                }
            )
            return channel.receive()
                .getOrThrow().also { user: User ->
                    reportAuthenticationChange(user, User.State.LOGGED_IN)
                }
        }
    }

    override fun switchUser(user: User) {
        Validation.isType(user)
        RealmInterop.realm_app_switch_user(this.nativePointer, user.nativePointer)
    }

    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)
        }
        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 {
        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.
        //
        // This is not required as the pointers will otherwise be released by the GC, but it can
        // be beneficial in order to reason about the lifecycle of the Sync thread and dispatchers.
        networkTransport.close()
        nativePointer.release()
        NetworkStateObserver.removeListener(connectionListener)
    }

    internal companion object {
        // This method is used to inject bundleId to the sync configuration. The
        // SyncLoweringExtension is replacing calls to App.create(appId) with calls to this method.
        internal fun create(appId: String, bundleId: String): App {
            Validation.checkEmpty(appId, "appId")
            return App.create(AppConfiguration.Builder(appId).build(bundleId))
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy