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

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

/*
 * 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.InternalConfiguration
import io.realm.kotlin.internal.MutableLiveRealmImpl
import io.realm.kotlin.internal.RealmImpl
import io.realm.kotlin.internal.TypedFrozenRealmImpl
import io.realm.kotlin.internal.interop.AsyncOpenCallback
import io.realm.kotlin.internal.interop.FrozenRealmPointer
import io.realm.kotlin.internal.interop.LiveRealmPointer
import io.realm.kotlin.internal.interop.RealmAppPointer
import io.realm.kotlin.internal.interop.RealmAsyncOpenTaskPointer
import io.realm.kotlin.internal.interop.RealmConfigurationPointer
import io.realm.kotlin.internal.interop.RealmInterop
import io.realm.kotlin.internal.interop.RealmSyncConfigurationPointer
import io.realm.kotlin.internal.interop.RealmSyncSessionPointer
import io.realm.kotlin.internal.interop.SyncAfterClientResetHandler
import io.realm.kotlin.internal.interop.SyncBeforeClientResetHandler
import io.realm.kotlin.internal.interop.SyncErrorCallback
import io.realm.kotlin.internal.interop.sync.SyncError
import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode
import io.realm.kotlin.internal.platform.freeze
import io.realm.kotlin.mongodb.exceptions.ClientResetRequiredException
import io.realm.kotlin.mongodb.exceptions.DownloadingRealmTimeOutException
import io.realm.kotlin.mongodb.subscriptions
import io.realm.kotlin.mongodb.sync.DiscardUnsyncedChangesStrategy
import io.realm.kotlin.mongodb.sync.InitialRemoteDataConfiguration
import io.realm.kotlin.mongodb.sync.InitialSubscriptionsConfiguration
import io.realm.kotlin.mongodb.sync.ManuallyRecoverUnsyncedChangesStrategy
import io.realm.kotlin.mongodb.sync.SyncClientResetStrategy
import io.realm.kotlin.mongodb.sync.SyncConfiguration
import io.realm.kotlin.mongodb.sync.SyncMode
import io.realm.kotlin.mongodb.sync.SyncSession
import kotlinx.atomicfu.AtomicRef
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.mongodb.kbson.BsonValue

@Suppress("LongParameterList")
internal class SyncConfigurationImpl(
    private val configuration: InternalConfiguration,
    internal val partitionValue: BsonValue?,
    override val user: UserImpl,
    override val errorHandler: SyncSession.ErrorHandler,
    override val syncClientResetStrategy: SyncClientResetStrategy,
    override val initialSubscriptions: InitialSubscriptionsConfiguration?,
    override val initialRemoteData: InitialRemoteDataConfiguration?
) : InternalConfiguration by configuration, SyncConfiguration {

    override suspend fun openRealm(realm: RealmImpl): Pair {
        // Partition-based Realms with `waitForInitialRemoteData` enabled will use
        // async open first do download the server side Realm. This is much much faster than
        // creating the Realm locally first and then downloading (and integrating) changes into
        // that.
        if (partitionValue != null && initialRemoteData != null) {
            // Channel to work around not being able to use `suspendCoroutine` to wrap the callback, as
            // that results in the `Continuation` being frozen, which breaks it.
            val channel = Channel(1)
            val taskPointer: AtomicRef = atomic(null)
            try {
                val result: Any = withTimeout(initialRemoteData.timeout.inWholeMilliseconds) {
                    withContext(realm.configuration.notificationDispatcher) {
                        val callback = AsyncOpenCallback { error: Throwable? ->
                            if (error != null) {
                                channel.trySend(error)
                            } else {
                                channel.trySend(true)
                            }
                        }.freeze()

                        val configPtr = createNativeConfiguration()
                        taskPointer.value = RealmInterop.realm_open_synchronized(configPtr)
                        RealmInterop.realm_async_open_task_start(taskPointer.value!!, callback)
                        channel.receive()
                    }
                }
                when (result) {
                    is Boolean -> { /* Do nothing, opening the Realm will follow below */ }
                    is Throwable -> throw result
                    else -> throw IllegalStateException("Unexpected value: $result")
                }
            } catch (ex: TimeoutCancellationException) {
                taskPointer.value?.let { ptr: RealmAsyncOpenTaskPointer ->
                    RealmInterop.realm_async_open_task_cancel(ptr)
                }
                throw DownloadingRealmTimeOutException(this)
            } finally {
                channel.close()
            }
        }

        // Open the local Realm file. This will also include any data potentially downloaded
        // by Async Open above.
        return configuration.openRealm(realm)
    }

    override suspend fun initializeRealmData(realm: RealmImpl, realmFileCreated: Boolean) {
        // Create or update subscriptions for Flexible Sync realms as needed.
        initialSubscriptions?.let { initialSubscriptionsConfig ->
            if (initialSubscriptionsConfig.rerunOnOpen || realmFileCreated) {
                realm.subscriptions.update {
                    with(initialSubscriptions.callback) {
                        write(realm)
                    }
                }
            }
        }

        // Download subscription data if needed. Partition-base realms can only configure
        // `waitForInitialRemoteData` which is being accounted for when calling `openRealm`, so that
        // case is ignored here.
        if (initialRemoteData != null && initialSubscriptions != null) {
            val updateExistingFile = initialSubscriptions.rerunOnOpen && !realmFileCreated
            if (realmFileCreated || updateExistingFile) {
                val success: Boolean =
                    realm.subscriptions.waitForSynchronization(initialRemoteData.timeout)
                if (!success) {
                    throw DownloadingRealmTimeOutException(this)
                }
            }
        }

        // Last, run any local Realm initialization logic
        configuration.initializeRealmData(realm, realmFileCreated)
    }

    override fun createNativeConfiguration(): RealmConfigurationPointer {
        val ptr: RealmConfigurationPointer = configuration.createNativeConfiguration()
        return syncInitializer(ptr)
    }

    private val syncInitializer: (RealmConfigurationPointer) -> RealmConfigurationPointer

    private interface ClientResetStrategyHelper {
        fun initialize(nativeSyncConfig: RealmSyncConfigurationPointer)
        fun onSyncError(session: SyncSession, appPointer: RealmAppPointer, error: SyncError)
    }

    private class DiscardUnsyncedChangesHelper constructor(
        val strategy: DiscardUnsyncedChangesStrategy,
        val configuration: InternalConfiguration
    ) : ClientResetStrategyHelper {
        override fun initialize(nativeSyncConfig: RealmSyncConfigurationPointer) {
            RealmInterop.realm_sync_config_set_resync_mode(
                nativeSyncConfig,
                SyncSessionResyncMode.RLM_SYNC_SESSION_RESYNC_MODE_DISCARD_LOCAL
            )

            val onBefore: SyncBeforeClientResetHandler = object : SyncBeforeClientResetHandler {
                override fun onBeforeReset(realmBefore: FrozenRealmPointer) {
                    strategy.onBeforeReset(TypedFrozenRealmImpl(realmBefore, configuration))
                }
            }

            RealmInterop.realm_sync_config_set_before_client_reset_handler(
                nativeSyncConfig,
                onBefore
            )

            val onAfter: SyncAfterClientResetHandler = object : SyncAfterClientResetHandler {
                override fun onAfterReset(
                    realmBefore: FrozenRealmPointer,
                    realmAfter: LiveRealmPointer,
                    didRecover: Boolean
                ) {
                    // Needed to allow writes on the Mutable after Realm
                    RealmInterop.realm_begin_write(realmAfter)

                    @Suppress("TooGenericExceptionCaught")
                    try {
                        strategy.onAfterReset(
                            TypedFrozenRealmImpl(realmBefore, configuration),
                            MutableLiveRealmImpl(realmAfter, configuration)
                        )

                        // Callback completed successfully we can safely commit the changes
                        // user might have cancelled the transaction manually
                        if (RealmInterop.realm_is_in_transaction(realmAfter)) {
                            RealmInterop.realm_commit(realmAfter)
                        }
                    } catch (exception: Throwable) {
                        // Cancel the transaction
                        // user might have cancelled the transaction manually
                        if (RealmInterop.realm_is_in_transaction(realmAfter)) {
                            RealmInterop.realm_rollback(realmAfter)
                        }
                        // Rethrow so core can send it over again
                        throw exception
                    }
                }
            }

            RealmInterop.realm_sync_config_set_after_client_reset_handler(
                nativeSyncConfig,
                onAfter
            )
        }

        override fun onSyncError(
            session: SyncSession,
            appPointer: RealmAppPointer,
            error: SyncError
        ) {
            // If there is a user exception we appoint it as the cause of the client reset
            strategy.onError(
                session,
                ClientResetRequiredException(appPointer, error)
            )
        }
    }

    private class ManuallyRecoverUnsyncedChangesHelper(
        val strategy: ManuallyRecoverUnsyncedChangesStrategy
    ) : ClientResetStrategyHelper {
        override fun initialize(nativeSyncConfig: RealmSyncConfigurationPointer) {
            RealmInterop.realm_sync_config_set_resync_mode(
                nativeSyncConfig,
                SyncSessionResyncMode.RLM_SYNC_SESSION_RESYNC_MODE_MANUAL
            )
        }

        override fun onSyncError(
            session: SyncSession,
            appPointer: RealmAppPointer,
            error: SyncError
        ) {
            strategy.onClientReset(
                session,
                ClientResetRequiredException(appPointer, error)
            )
        }
    }

    init {
        // We need to freeze `errorHandler` reference on initial thread
        val userErrorHandler = errorHandler
        val resetStrategy = syncClientResetStrategy.freeze()
        val frozenAppPointer = user.app.nativePointer.freeze()

        val initializerHelper = when (resetStrategy) {
            is DiscardUnsyncedChangesStrategy ->
                DiscardUnsyncedChangesHelper(resetStrategy, configuration)
            is ManuallyRecoverUnsyncedChangesStrategy ->
                ManuallyRecoverUnsyncedChangesHelper(resetStrategy)
            else -> throw IllegalArgumentException("Unsupported client reset strategy: $resetStrategy")
        }

        val errorCallback =
            SyncErrorCallback { pointer: RealmSyncSessionPointer, error: SyncError ->
                val session = SyncSessionImpl(pointer)
                val syncError = convertSyncError(error)

                // Notify before/after callbacks too if error is client reset
                if (error.isClientResetRequested) {
                    initializerHelper.onSyncError(session, frozenAppPointer, error)
                } else {
                    userErrorHandler.onError(session, syncError)
                }
            }.freeze()

        syncInitializer = { nativeConfig: RealmConfigurationPointer ->
            val nativeSyncConfig: RealmSyncConfigurationPointer = if (partitionValue == null) {
                RealmInterop.realm_flx_sync_config_new(user.nativePointer)
            } else {
                RealmInterop.realm_sync_config_new(
                    user.nativePointer,
                    partitionValue.toJson()
                )
            }

            RealmInterop.realm_sync_config_set_error_handler(
                nativeSyncConfig,
                errorCallback
            )

            // Do any initialization required for the strategies
            initializerHelper.initialize(nativeSyncConfig)

            RealmInterop.realm_config_set_sync_config(nativeConfig, nativeSyncConfig)

            nativeConfig
        }
    }

    override val syncMode: SyncMode =
        if (partitionValue == null) SyncMode.FLEXIBLE else SyncMode.PARTITION_BASED
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy