From 88c95650d51bf7ff71e8a9bb380f411de511f79e Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 25 Sep 2023 13:26:43 +0100 Subject: [PATCH 01/45] Using platform networking for Sync Websocket --- buildSrc/src/main/kotlin/Config.kt | 4 +- .../kotlin/internal/interop/RealmInterop.kt | 29 +- .../interop/sync/WebSocketTransport.kt | 88 ++++++ .../kotlin/internal/interop/RealmInterop.kt | 29 +- packages/cinterop/src/native/realm.def | 4 + .../kotlin/internal/interop/RealmInterop.kt | 193 ++++++++++- packages/jni-swig-stub/realm.i | 2 +- .../src/main/jni/realm_api_helpers.cpp | 240 ++++++++++++++ .../src/main/jni/realm_api_helpers.h | 12 + .../util/CoroutineDispatcherFactory.kt | 2 +- packages/library-sync/build.gradle.kts | 3 +- .../realm/kotlin/mongodb/AppConfiguration.kt | 22 ++ .../mongodb/internal/AppConfigurationImpl.kt | 19 +- .../realm/kotlin/mongodb/internal/AppImpl.kt | 19 +- .../mongodb/internal/HttpClientCache.kt | 1 + .../internal/KtorWebSocketTransport.kt | 299 ++++++++++++++++++ .../kotlin/mongodb/sync/SyncConfiguration.kt | 1 - .../mongodb/internal/HttpClientCache.kt | 17 +- .../mongodb/internal/HttpClientCache.kt | 6 +- .../io/realm/kotlin/test/mongodb/TestApp.kt | 8 +- .../test/mongodb/common/SyncedRealmTests.kt | 31 +- 21 files changed, 990 insertions(+), 39 deletions(-) create mode 100644 packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 783db71ece..cf8f766b89 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -115,7 +115,7 @@ object Versions { const val buildkonfig = "0.13.3" // https://github.com/yshrsmz/BuildKonfig // Not currently used, so mostly here for documentation. Core requires minimum 3.15, but 3.18.1 is available through the Android SDK. // Build also tested successfully with 3.21.4 (latest release). - const val cmake = "3.22.1" + const val cmake = "3.27.4" const val coroutines = "1.7.0" // https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core const val datetime = "0.4.0" // https://github.com/Kotlin/kotlinx-datetime const val detektPlugin = "1.22.0-RC2" // https://github.com/detekt/detekt @@ -130,7 +130,7 @@ object Versions { const val latestKotlin = "1.9.0-Beta" // https://kotlinlang.org/docs/eap.html#build-details const val kotlinCompileTesting = "1.5.0" // https://github.com/tschuchortdev/kotlin-compile-testing const val ktlint = "0.45.2" // https://github.com/pinterest/ktlint - const val ktor = "2.1.2" // https://github.com/ktorio/ktor + const val ktor = "2.3.4" // https://github.com/ktorio/ktor const val nexusPublishPlugin = "1.1.0" // https://github.com/gradle-nexus/publish-plugin const val okio = "3.2.0" // https://square.github.io/okio/#releases const val relinker = "1.4.5" // https://github.com/KeepSafe/ReLinker diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 33332f2450..b216b6bd8f 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -31,6 +31,7 @@ import io.realm.kotlin.internal.interop.sync.ProtocolClientErrorCode import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode import io.realm.kotlin.internal.interop.sync.SyncUserIdentity +import io.realm.kotlin.internal.interop.sync.WebSocketTransport import kotlinx.coroutines.CoroutineDispatcher import org.mongodb.kbson.ObjectId import kotlin.jvm.JvmInline @@ -101,9 +102,15 @@ interface RealmUserT : CapiT interface RealmNetworkTransportT : CapiT interface RealmSyncSessionT : CapiT interface RealmSubscriptionT : CapiT +interface RealmSyncSocketObserverPointerT : CapiT +interface RealmSyncSocketCallbackPointerT : CapiT + interface RealmBaseSubscriptionSet : CapiT +interface RealmSyncSocket : CapiT interface RealmSubscriptionSetT : RealmBaseSubscriptionSet interface RealmMutableSubscriptionSetT : RealmBaseSubscriptionSet +interface RealmSyncSocketT : RealmSyncSocket + // Public type aliases binding to internal verbose type safe type definitions. This should allow us // to easily change implementation details later on. typealias RealmAsyncOpenTaskPointer = NativePointer @@ -119,7 +126,11 @@ typealias RealmSubscriptionPointer = NativePointer typealias RealmBaseSubscriptionSetPointer = NativePointer typealias RealmSubscriptionSetPointer = NativePointer typealias RealmMutableSubscriptionSetPointer = NativePointer - +typealias RealmSyncSocketPointer = NativePointer +typealias RealmSyncSocketObserverPointer = NativePointer +typealias RealmSyncSocketCallbackPointer = NativePointer +typealias RealmWebsocketHandlerCallbackPointer = NativePointer +typealias RealmWebsocketProviderPointer = NativePointer /** * Class for grouping and normalizing values we want to send as part of * logging in Sync Users. @@ -490,7 +501,6 @@ expect object RealmInterop { fun realm_app_link_credentials(app: RealmAppPointer, user: RealmUserPointer, credentials: RealmCredentialsPointer, callback: AppCallback) fun realm_clear_cached_apps() fun realm_app_sync_client_get_default_file_path_for_realm( - app: RealmAppPointer, syncConfig: RealmSyncConfigurationPointer, overriddenName: String? ): String @@ -786,4 +796,19 @@ expect object RealmInterop { fun realm_sync_subscriptionset_commit( mutableSubscriptionSet: RealmMutableSubscriptionSetPointer ): RealmSubscriptionSetPointer + + fun realm_sync_set_websocket_transport( + syncClientConfig: RealmSyncClientConfigurationPointer, + webSocketTransport: WebSocketTransport + ) + + fun realm_sync_socket_callback_complete(nativePointer: RealmWebsocketHandlerCallbackPointer, cancelled: Boolean = false, status: Int = 0/* ok */, reason: String = "") + + fun realm_sync_socket_websocket_connected(nativePointer: RealmWebsocketProviderPointer, protocol: String) + + fun realm_sync_socket_websocket_error(nativePointer: RealmWebsocketProviderPointer) + + fun realm_sync_socket_websocket_message(nativePointer: RealmWebsocketProviderPointer, data: ByteArray) + + fun realm_sync_socket_websocket_closed(nativePointer: RealmWebsocketProviderPointer, wasClean: Boolean, errorCode: Int, reason: String = "") } diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt new file mode 100644 index 0000000000..423eb550f1 --- /dev/null +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt @@ -0,0 +1,88 @@ +package io.realm.kotlin.internal.interop.sync + +import io.realm.kotlin.internal.interop.RealmInterop +import io.realm.kotlin.internal.interop.RealmWebsocketProviderPointer +import io.realm.kotlin.internal.interop.RealmWebsocketHandlerCallbackPointer +import kotlinx.coroutines.Job + +interface WebSocketTransport { + fun post(handlerCallback: RealmWebsocketHandlerCallbackPointer) + + fun createTimer( + delayInMilliseconds: Long, + handlerCallback: RealmWebsocketHandlerCallbackPointer + ): CancellableTimer + + fun connect( + observer: WebSocketObserver, + path: String, + address: String, + port: Long, + isSsl: Boolean, + numProtocols: Long, + supportedProtocols: String + ): WebSocketClient + + fun write( + webSocketClient: WebSocketClient, + data: ByteArray, + length: Long, + handlerCallback: RealmWebsocketHandlerCallbackPointer + ) + + fun runCallback( + handlerCallback: RealmWebsocketHandlerCallbackPointer, + status: Int = 0/* ok */, + reason: String = "" + ) { + RealmInterop.realm_sync_socket_callback_complete( + handlerCallback, + cancelled = false, + status, + reason + ) + } + + fun close() +} + +class CancellableTimer( + private val job: Job, + private val handlerCallback: RealmWebsocketHandlerCallbackPointer +) { + fun cancel() { + // avoid double delete, if the Job has completed then the callback function was already been invoked and deleted from the heap + if (!job.isCompleted && !job.isCancelled) { + job.cancel() + RealmInterop.realm_sync_socket_callback_complete(handlerCallback, cancelled = true) + } + } +} + +interface WebSocketClient { + fun send(message: ByteArray, handlerCallback: RealmWebsocketHandlerCallbackPointer) + fun closeWebsocket() +} + +class WebSocketObserver(private val webSocketObserverPointer: RealmWebsocketProviderPointer) { + fun onConnected(protocol: String) { + RealmInterop.realm_sync_socket_websocket_connected(webSocketObserverPointer, protocol) + } + + fun onError() { + RealmInterop.realm_sync_socket_websocket_error(webSocketObserverPointer) + } + + fun onNewMessage(data: ByteArray) { + RealmInterop.realm_sync_socket_websocket_message(webSocketObserverPointer, data) + } + + fun onClose(wasClean: Boolean, errorCode: Int, reason: String) { + RealmInterop.realm_sync_socket_websocket_closed( + webSocketObserverPointer, + wasClean, + errorCode, + reason + ) + } +} \ No newline at end of file diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 88b254ca0b..6164a1c660 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -31,6 +31,7 @@ import io.realm.kotlin.internal.interop.sync.ProtocolClientErrorCode import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode import io.realm.kotlin.internal.interop.sync.SyncUserIdentity +import io.realm.kotlin.internal.interop.sync.WebSocketTransport import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart @@ -1156,7 +1157,6 @@ actual object RealmInterop { } actual fun realm_app_sync_client_get_default_file_path_for_realm( - app: RealmAppPointer, syncConfig: RealmSyncConfigurationPointer, overriddenName: String? ): String { @@ -1973,6 +1973,33 @@ actual object RealmInterop { return LongPointerWrapper(realmc.realm_sync_subscription_set_commit(mutableSubscriptionSet.cptr())) } + actual fun realm_sync_set_websocket_transport( + syncClientConfig: RealmSyncClientConfigurationPointer, + webSocketTransport: WebSocketTransport + ) { + realmc.realm_sync_websocket_new(syncClientConfig.cptr(), webSocketTransport) + } + + actual fun realm_sync_socket_callback_complete(nativePointer: RealmWebsocketHandlerCallbackPointer, cancelled: Boolean, status: Int, reason: String) { + realmc.realm_sync_websocket_callback_complete(cancelled, nativePointer.cptr(), status, reason) + } + + actual fun realm_sync_socket_websocket_connected(nativePointer: RealmWebsocketProviderPointer, protocol: String) { + realmc.realm_sync_websocket_connected(nativePointer.cptr(), protocol) + } + + actual fun realm_sync_socket_websocket_error(nativePointer: RealmWebsocketProviderPointer) { + realmc.realm_sync_websocket_error(nativePointer.cptr()) + } + + actual fun realm_sync_socket_websocket_message(nativePointer: RealmWebsocketProviderPointer, data: ByteArray) {//data: String, size: ULong + realmc.realm_sync_websocket_message(nativePointer.cptr(), data, data.size.toLong()) + } + + actual fun realm_sync_socket_websocket_closed(nativePointer: RealmWebsocketProviderPointer, wasClean: Boolean, errorCode: Int, reason: String) { + realmc.realm_sync_websocket_closed(nativePointer.cptr(), wasClean, errorCode, reason) + } + fun NativePointer.cptr(): Long { return (this as LongPointerWrapper).ptr } diff --git a/packages/cinterop/src/native/realm.def b/packages/cinterop/src/native/realm.def index d5f34c6379..46dbca1270 100644 --- a/packages/cinterop/src/native/realm.def +++ b/packages/cinterop/src/native/realm.def @@ -12,6 +12,10 @@ headerFilter = realm.h realm/error_codes.h // libraryPaths.ios_x64 = ../external/core/build-macos_x64/src/realm/object-store/c_api ../external/core/build-macos_x64/src/realm ../external/core/build-macos_x64/src/realm/parser ../external/core/build-macos_x64/src/realm/object-store/ linkerOpts = -lcompression -lz -framework Foundation -framework CoreFoundation -framework Security strictEnums = realm_errno realm_error_category realm_sync_errno_client realm_sync_errno_connection realm_sync_errno_session + +// We don't want to convert Websocket binary data to String +noStringConversion = realm_sync_socket_websocket_message + --- #include #include diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 96a5d86e00..0b81cbc98d 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -38,6 +38,9 @@ import io.realm.kotlin.internal.interop.sync.SyncErrorCode import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode import io.realm.kotlin.internal.interop.sync.SyncUserIdentity +import io.realm.kotlin.internal.interop.sync.WebSocketClient +import io.realm.kotlin.internal.interop.sync.WebSocketObserver +import io.realm.kotlin.internal.interop.sync.WebSocketTransport import kotlinx.atomicfu.AtomicBoolean import kotlinx.atomicfu.atomic import kotlinx.cinterop.AutofreeScope @@ -62,11 +65,13 @@ import kotlinx.cinterop.alloc import kotlinx.cinterop.allocArray import kotlinx.cinterop.asStableRef import kotlinx.cinterop.cValue +import kotlinx.cinterop.cValuesOf import kotlinx.cinterop.convert import kotlinx.cinterop.cstr import kotlinx.cinterop.get import kotlinx.cinterop.getBytes import kotlinx.cinterop.memScoped +import kotlinx.cinterop.objcPtr import kotlinx.cinterop.pointed import kotlinx.cinterop.ptr import kotlinx.cinterop.readBytes @@ -74,6 +79,7 @@ import kotlinx.cinterop.readValue import kotlinx.cinterop.refTo import kotlinx.cinterop.set import kotlinx.cinterop.staticCFunction +import kotlinx.cinterop.toCValues import kotlinx.cinterop.toKString import kotlinx.cinterop.useContents import kotlinx.cinterop.usePinned @@ -81,6 +87,8 @@ import kotlinx.cinterop.value import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job +import kotlinx.coroutines.Runnable import kotlinx.coroutines.launch import org.mongodb.kbson.BsonObjectId import org.mongodb.kbson.ObjectId @@ -125,6 +133,10 @@ import realm_wrapper.realm_sync_error_code_t import realm_wrapper.realm_sync_session_resync_mode import realm_wrapper.realm_sync_session_state_e import realm_wrapper.realm_sync_session_stop_policy_e +import realm_wrapper.realm_sync_socket_callback_t +import realm_wrapper.realm_sync_socket_t +import realm_wrapper.realm_sync_socket_timer_t +import realm_wrapper.realm_sync_socket_websocket_t import realm_wrapper.realm_t import realm_wrapper.realm_user_identity import realm_wrapper.realm_user_t @@ -2199,7 +2211,6 @@ actual object RealmInterop { } actual fun realm_app_sync_client_get_default_file_path_for_realm( - app: RealmAppPointer, syncConfig: RealmSyncConfigurationPointer, overriddenName: String? ): String { @@ -2660,6 +2671,186 @@ actual object RealmInterop { ) ) } + + actual fun realm_sync_set_websocket_transport( + syncClientConfig: RealmSyncClientConfigurationPointer, + webSocketTransport: WebSocketTransport + ) { + val realmSyncSocketNew: CPointer? = realm_wrapper.realm_sync_socket_new( + userdata = StableRef.create(webSocketTransport).asCPointer(), + userdata_free = staticCFunction { userdata: CPointer? -> + disposeUserData(userdata) + }, + post_func = staticCFunction { userdata: CPointer?, syncSocketCallback: CPointer? -> + safeUserData(userdata).let { websocketTransport -> + // schedule execution of the FunctionHandler + + //TODO: should we Move the source object to a destination object? like .NET + /// void post(FunctionHandler&& handler) final { + // s_post_work(m_managed_provider, new FunctionHandler(std::move(handler))); + // } + +// val destinationObjMemory = nativeHeap.alloc() +// websocketTransport.post(CPointerWrapper(syncSocketCallback)) +// val p :RealmSyncSocketCallbackPointer? = null +// val toLong = syncSocketCallback.toLong() +// syncSocketCallback?.pointed +// websocketTransport.post(Runnable { (syncSocketCallback as CPointer Unit>>)?.invoke() }) +// StableRef.create(syncSocketCallback.rawValue).asCPointer() + // I'm not de-referencing it correctly causing a BAD_EXEC + // TODO pass in as argument the status/error messsage +// websocketTransport.post(Runnable { realm_wrapper.realm_sync_socket_callback_complete(syncSocketCallback, 0, "") }) + websocketTransport.post(CPointerWrapper(syncSocketCallback)) + +// (syncSocketCallback as CPointer Unit>>).invoke() + } + }, + create_timer_func = staticCFunction?,uint64_t, CPointer?, CPointer?> { userdata: CPointer?, delayInMilliseconds: uint64_t, syncSocketCallback: CPointer? -> + safeUserData(userdata).let { ws -> + // schedule the callback after the delay parameter and return SyncTimer to be able to cancel it + // .NET creates the Job in C# then send the object as C opaque pointer + /* + var timer = new Timer(TimeSpan.FromMilliseconds(delay_milliseconds), native_callback, provider._workQueue); + return GCHandle.ToIntPtr(GCHandle.Alloc(timer)); + + private static void CancelTimer(IntPtr managed_timer) + { + var handle = GCHandle.FromIntPtr(managed_timer); + try + { + ((Timer)handle.Target!).Cancel(); + } + finally + { + handle.Free(); + } + */ + // in our case we can get the CPointer value of the allocated object (outside Arena) + // TODO return CancelTimer then return it as a NativePointer (use StableRef.create(networkTransport).asCPointer()) + // + val job: Job = ws.createTimer(delayInMilliseconds.toLong(), CPointerWrapper(syncSocketCallback)) + StableRef.create(job).asCPointer() +// val invoke: realm_sync_socket_timer_t? = f?.invoke() +// invoke + // return void* as COpaquePointer + } +// val objcPtr: CPointer? = (a?.objcPtr() as CPointer<*>) +// null as CPointer<*> + }, + cancel_timer_func = staticCFunction { userdata: CPointer?, timer: realm_sync_socket_timer_t? -> +// safeUserData(userdata).let { ws -> + val job: StableRef? = timer?.asStableRef() + job?.get().run { + if (this != null) { + this.cancel() + job!!.dispose() + } + } +// } + }, + free_timer_func = staticCFunction { userdata: CPointer?, timer: realm_sync_socket_timer_t? -> Unit }, + websocket_connect_func = staticCFunction { userdata: CPointer?, endpoint: CValue, observer: CPointer? -> + safeUserData(userdata).let { websocketTransport -> + endpoint.useContents { + // When Ktor connects it should invoke: + // RLM_API void realm_sync_socket_websocket_connected(realm_websocket_observer_t* realm_websocket_observer, const char* protocol) + // on Error RLM_API void realm_sync_socket_websocket_error(realm_websocket_observer_t* realm_websocket_observer) + // on new Message RLM_API void realm_sync_socket_websocket_message(realm_websocket_observer_t* realm_websocket_observer, const char* data, size_t data_size) + // on Close RLM_API void realm_sync_socket_websocket_closed(realm_websocket_observer_t* realm_websocket_observer, bool was_clean, + // realm_web_socket_errno_e code, const char* reason) + + /* + WebSocketEndpoint { + using port_type = sync::port_type; + std::string address; // Host address + port_type port; // Host port number + std::string path; // Includes access token in query. + std::vector protocols; // Array of one or more websocket protocols + bool is_ssl; // true if SSL should be used + */ +// val protocols1: CPointer>>>? = this.protocols + val supportedProtocols = mutableListOf() + for (i in 0 until this.num_protocols.toInt()) { + val protocol: CPointer>? = this.protocols?.get(i) + supportedProtocols.add(protocol.safeKString()) + } + // CPointer>? -> safeKString() +// val data: CPointer>? = protocols1.readBytes() +// val readBytes: ByteArray? = data?.readBytes(this.size.toInt()) +// readBytes?.decodeToString(0, size.toInt(), throwOnInvalidSequence = false)!! + + //TODO return KTor WebSocket as native pointer maybe use the instance as StableRef.create(networkTransport).asCPointer() +// val observerWebSocketConnected : (String) -> Unit = { } +// val observerWebSocketError : () -> Unit = { realm_wrapper.realm_sync_socket_websocket_error(observer) } +// val observerWebSocketNewMessage: (data: ByteArray) -> Unit = { data -> +// println(">>>>>>>>>>>>>>>>>>>>>> observerWebSocketNewMessage message = ${data.toKString()}") +// realm_wrapper.realm_sync_socket_websocket_message(observer, data.toKString(throwOnInvalidSequence = true), data.size.toULong()) } +// val observerWebSocketClose: (wasClean: Boolean, errorCode: UInt, reason: String) -> Unit = { wasClean: Boolean, errorCode: UInt, reason: String -> realm_wrapper.realm_sync_socket_websocket_closed(observer, wasClean, errorCode, reason) } + + val managedObserver = WebSocketObserver(CPointerWrapper(observer)) +// override fun onConnected(protocol: String) { +// realm_wrapper.realm_sync_socket_websocket_connected(observer, protocol) +// } +// +// override fun onError() { +// realm_wrapper.realm_sync_socket_websocket_error(observer) +// } +// +// override fun onNewMessage(data: ByteArray) { +// realm_wrapper.realm_sync_socket_websocket_message(observer, data.toKString(throwOnInvalidSequence = true), data.size.toULong()) +// } +// +// override fun onClose( +// wasClean: Boolean, +// errorCode: UInt, +// reason: String +// ) { +// realm_wrapper.realm_sync_socket_websocket_closed(observer, wasClean, errorCode, reason) +// } +// } + + val webSocketClient: WebSocketClient = websocketTransport.connect(managedObserver, this.path.safeKString(), this.address.safeKString(), this.port.toLong(), this.is_ssl, this.num_protocols.toLong(), supportedProtocols.joinToString(", ")) + val webSocketClientPointer: CPointer = StableRef.create(webSocketClient).asCPointer() + webSocketClientPointer + } + } +// null as realm_wrapper.realm_sync_socket_websocket_t? + }, + websocket_write_func = staticCFunction { userdata: CPointer?, websocket: realm_sync_socket_websocket_t?, data: CPointer?, length: size_t, callback: CPointer? -> + safeUserData(userdata).let { websocketTransport -> + safeUserData(websocket).let { webSocketClient -> + data?.readBytes(length.toInt())?.run { + websocketTransport.write(webSocketClient, this, length.toLong(), CPointerWrapper(callback) /*TODO change status and reason if there's an error sending the Frame*/) + } + } + } + Unit + }, + websocket_free_func = staticCFunction { userdata: CPointer?, websocket: realm_sync_socket_websocket_t? -> Unit } + ) + realm_wrapper.realm_sync_client_config_set_sync_socket(syncClientConfig.cptr(), realmSyncSocketNew) + } + + actual fun realm_sync_socket_callback_complete(nativePointer: RealmWebsocketHandlerCallbackPointer, cancelled: Boolean, status: Int, reason: String) { + //TODO use cancelled to cancel or free + realm_wrapper.realm_sync_socket_callback_complete(nativePointer.cptr(), status.toUInt(), reason) + } + + actual fun realm_sync_socket_websocket_connected(nativePointer: RealmWebsocketProviderPointer, protocol: String) { + realm_wrapper.realm_sync_socket_websocket_connected(nativePointer.cptr(), protocol) + } + + actual fun realm_sync_socket_websocket_error(nativePointer: RealmWebsocketProviderPointer) { + realm_wrapper.realm_sync_socket_websocket_error(nativePointer.cptr()) + } + + actual fun realm_sync_socket_websocket_message(nativePointer: RealmWebsocketProviderPointer, data: ByteArray) { + realm_wrapper.realm_sync_socket_websocket_message(nativePointer.cptr(), data.toCValues(), data.size.toULong()) + } + + actual fun realm_sync_socket_websocket_closed(nativePointer: RealmWebsocketProviderPointer, wasClean: Boolean, errorCode: Int, reason: String) { + realm_wrapper.realm_sync_socket_websocket_closed(nativePointer.cptr(), wasClean, errorCode.toUInt(), reason) + } @Suppress("LongParameterList") actual fun realm_app_config_new( diff --git a/packages/jni-swig-stub/realm.i b/packages/jni-swig-stub/realm.i index 9bf2bedd97..f10652d6b4 100644 --- a/packages/jni-swig-stub/realm.i +++ b/packages/jni-swig-stub/realm.i @@ -301,7 +301,7 @@ return $jnicall; realm_flx_sync_mutable_subscription_set_t*, realm_flx_sync_subscription_desc_t*, realm_set_t*, realm_async_open_task_t*, realm_dictionary_t*, realm_sync_session_connection_state_notification_token_t*, - realm_dictionary_changes_t* }; + realm_dictionary_changes_t*, realm_sync_socket_t* }; // For all functions returning a pointer or bool, check for null/false and throw an error if // realm_get_last_error returns true. diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index 9848fc99c4..536471e1f4 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -680,6 +680,246 @@ realm_http_transport_t* realm_network_transport_new(jobject network_transport) { }); } +// *** BEGIN - WebSocket Client (Platform Networking) *** // + +static void websocket_post_func(realm_userdata_t userdata, + realm_sync_socket_callback_t* realm_callback) { + auto jenv = get_env(true); + static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); + static JavaMethod native_pointer_constructor(jenv, native_pointer_class, "", "(JZ)V"); + + auto* lambda = new std::function([realm_callback=std::move(realm_callback)](bool cancelled, int status, const char* reason) { + realm_sync_socket_post_complete(realm_callback, realm_errno_e::RLM_ERR_NONE, ""); + }); + jobject pointer = jenv->NewObject(native_pointer_class, native_pointer_constructor, + reinterpret_cast(lambda), false); + + static JavaClass jvm_websocket_transport_class (jenv, "io/realm/kotlin/internal/interop/sync/WebSocketTransport"); + static JavaMethod post_method (jenv, jvm_websocket_transport_class, "post", + "(Lio/realm/kotlin/internal/interop/NativePointer;)V"); + jobject websocket_transport = static_cast(userdata); + + if (jenv->ExceptionCheck()) { + jthrowable exception = jenv->ExceptionOccurred(); + + jclass clazz = jenv->GetObjectClass(exception); + jmethodID get_message = jenv->GetMethodID(clazz, + "getMessage", + "()Ljava/lang/String;"); + jstring message = (jstring) jenv->CallObjectMethod(exception, get_message); + auto str = jenv->GetStringUTFChars(message, NULL); + } + jenv->CallVoidMethod(websocket_transport, post_method, pointer); + + if (jenv->ExceptionCheck()) { + jthrowable exception = jenv->ExceptionOccurred(); + + jclass clazz = jenv->GetObjectClass(exception); + jmethodID get_message = jenv->GetMethodID(clazz, + "getMessage", + "()Ljava/lang/String;"); + jstring message = (jstring) jenv->CallObjectMethod(exception, get_message); + auto str = jenv->GetStringUTFChars(message, NULL); + } + + jenv->DeleteLocalRef(pointer); +} + +static realm_sync_socket_timer_t websocket_create_timer_func( + realm_userdata_t userdata, uint64_t delay_ms, realm_sync_socket_callback_t* realm_callback) { + auto jenv = get_env(true); + static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); + static JavaMethod native_pointer_constructor(jenv, native_pointer_class, "", "(JZ)V"); + + auto* lambda = new std::function([realm_callback= std::move(realm_callback)](bool cancel, int status, const char* reason) { + if (cancel) { + realm_sync_socket_timer_canceled(realm_callback); + } else { + realm_sync_socket_timer_complete(realm_callback, realm_errno_e::RLM_ERR_NONE, "");// TODO should we use status and reason? + } + }); + jobject pointer = jenv->NewObject(native_pointer_class, native_pointer_constructor, + reinterpret_cast(lambda), false); + + static JavaClass jvm_websocket_transport_class (jenv, "io/realm/kotlin/internal/interop/sync/WebSocketTransport"); + static JavaMethod create_timer_method (jenv, jvm_websocket_transport_class, "createTimer", + "(JLio/realm/kotlin/internal/interop/NativePointer;)Lio/realm/kotlin/internal/interop/sync/CancellableTimer;"); + jobject websocket_transport = static_cast(userdata); + jobject cancellable_timer = jenv->CallObjectMethod(websocket_transport, create_timer_method, jlong(delay_ms), pointer); + + jenv->DeleteLocalRef(pointer); + return reinterpret_cast(jenv->NewGlobalRef(cancellable_timer)); +} + +static void websocket_cancel_timer_func(realm_userdata_t userdata, + realm_sync_socket_timer_t timer_userdata) { + + if (timer_userdata != nullptr) { + nullptr)); + auto jenv = get_env(true); + jobject cancellable_timer = static_cast(timer_userdata); + + static JavaClass cancellable_timer_class(jenv, "io/realm/kotlin/internal/interop/sync/CancellableTimer"); + static JavaMethod cancel_method(jenv, cancellable_timer_class, "cancel", + "()V"); + jenv->CallVoidMethod(cancellable_timer, cancel_method); + + jenv->DeleteGlobalRef(cancellable_timer); + } +} + +static realm_sync_socket_websocket_t websocket_connect_func( + realm_userdata_t userdata, realm_websocket_endpoint_t endpoint, + realm_websocket_observer_t* realm_websocket_observer) { + + auto jenv = get_env(true); + + static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); + static JavaMethod native_pointer_constructor(jenv, native_pointer_class, "", "(JZ)V"); + jobject observer_pointer = jenv->NewObject(native_pointer_class, native_pointer_constructor, + reinterpret_cast(realm_websocket_observer), false); + + static JavaClass websocket_observer_class(jenv, "io/realm/kotlin/internal/interop/sync/WebSocketObserver"); + static JavaMethod websocket_observer_constructor(jenv, websocket_observer_class, "", + "(Lio/realm/kotlin/internal/interop/NativePointer;)V"); + jobject websocket_observer = jenv->NewObject(websocket_observer_class, websocket_observer_constructor, observer_pointer); + + static JavaClass websocket_transport_class(jenv, "io/realm/kotlin/internal/interop/sync/WebSocketTransport"); + static JavaMethod connect_method(jenv, websocket_transport_class, "connect", + "(Lio/realm/kotlin/internal/interop/sync/WebSocketObserver;Ljava/lang/String;Ljava/lang/String;JZJLjava/lang/String;)Lio/realm/kotlin/internal/interop/sync/WebSocketClient;"); + jobject websocket_transport = static_cast(userdata); + + std::ostringstream supported_protocol; + for (size_t i = 0; i < endpoint.num_protocols; ++i) { + supported_protocol << endpoint.protocols[i] << ", "; + } + + jobject websocket_client = jenv->CallObjectMethod(websocket_transport, connect_method, + websocket_observer, + to_jstring(jenv, endpoint.path), + to_jstring(jenv, endpoint.address), + jlong(endpoint.port), + endpoint.is_ssl, + jlong(endpoint.num_protocols), + to_jstring(jenv, supported_protocol.str().c_str())); + realm_sync_socket_websocket_t global_websocket_ref = reinterpret_cast(jenv->NewGlobalRef(websocket_client)); + + jenv->DeleteLocalRef(websocket_observer); + jenv->DeleteLocalRef(observer_pointer); + + return global_websocket_ref; +} + +static void websocket_async_write_func(realm_userdata_t userdata, + realm_sync_socket_websocket_t websocket_userdata, + const char* data, size_t size, + realm_sync_socket_callback_t* realm_callback) { + auto jenv = get_env(true); + + static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); + static JavaMethod native_pointer_constructor(jenv, native_pointer_class, "", "(JZ)V"); + auto* lambda = new std::function([realm_callback=std::move(realm_callback)](bool cancelled, int status, const char* reason) { + realm_sync_socket_post_complete(realm_callback, static_cast(status), "foo"); + }); + jobject callback_pointer = jenv->NewObject(native_pointer_class, native_pointer_constructor, + reinterpret_cast(lambda), false); + + static jclass websocket_transport_class = jenv->FindClass("io/realm/kotlin/internal/interop/sync/WebSocketTransport"); + static jmethodID write_method = jenv->GetMethodID(websocket_transport_class, "write", + "(Lio/realm/kotlin/internal/interop/sync/WebSocketClient;[BJLio/realm/kotlin/internal/interop/NativePointer;)V"); + jobject websocket_transport = static_cast(userdata); + + jbyteArray byteArray = jenv->NewByteArray(size); + jenv->SetByteArrayRegion(byteArray, 0, size, reinterpret_cast(data)); + + jenv->CallVoidMethod(websocket_transport, write_method, + static_cast(websocket_userdata), + byteArray, + jlong(size), + callback_pointer); + + jenv->DeleteLocalRef(byteArray); +} + +static void realm_sync_websocket_free(realm_userdata_t userdata, + realm_sync_socket_websocket_t websocket_userdata) { + if (websocket_userdata != nullptr) { + auto jenv = get_env(true); + static jclass websocket_client_class = jenv->FindClass("io/realm/kotlin/internal/interop/sync/WebSocketClient"); + static jmethodID close_method = jenv->GetMethodID(websocket_client_class, "closeWebsocket", "()V"); + + jobject websocket_client = static_cast(websocket_userdata); + jenv->CallVoidMethod(websocket_client, close_method); + + jenv->DeleteGlobalRef(websocket_client); + } +} + +static void realm_sync_userdata_free(realm_userdata_t userdata) { + if (userdata != nullptr) { + auto jenv = get_env(true); + + static jclass websocket_client_class = jenv->FindClass("io/realm/kotlin/internal/interop/sync/WebSocketTransport"); + static jmethodID close_method = jenv->GetMethodID(websocket_client_class, "close", "()V"); + + jobject websocket_transport = static_cast(userdata); + jenv->CallVoidMethod(websocket_transport, close_method); + + jenv->DeleteGlobalRef(websocket_transport); + } +} + +// This should run in the context of CoroutineScope +void realm_sync_websocket_callback_complete(bool cancelled, int64_t lambda_ptr, int status, const char* reason) { + std::function* callback = reinterpret_cast*>(lambda_ptr); + (*callback)(cancelled, status, reason); + delete callback; +} + +void realm_sync_websocket_connected(int64_t observer_ptr, const char* protocol) { + realm_sync_socket_websocket_connected(reinterpret_cast(observer_ptr), protocol); +} + +void realm_sync_websocket_error(int64_t observer_ptr) { + realm_sync_socket_websocket_error(reinterpret_cast(observer_ptr)); +} + +void realm_sync_websocket_message(int64_t observer_ptr, jbyteArray data, size_t size) { + auto jenv = get_env(true); + jbyte* byteData = jenv->GetByteArrayElements(data, NULL); + std::unique_ptr charData(new char[size]); // not null terminated (used in util::Span with size parameter) + std::memcpy(charData.get(), byteData, size); + realm_sync_socket_websocket_message(reinterpret_cast(observer_ptr), charData.get(), size); + jenv->ReleaseByteArrayElements(data, byteData, JNI_ABORT); +} + +void realm_sync_websocket_closed(int64_t observer_ptr, bool was_clean, int error_code, const char* reason) { + realm_sync_socket_websocket_closed(reinterpret_cast(observer_ptr), was_clean, static_cast(error_code), reason); +} + +realm_sync_socket_t* realm_sync_websocket_new(int64_t sync_client_config_ptr, jobject websocket_transport) { + auto jenv = get_env(false); // Always called from JVM + + // get pointer use it inside unique_ptr and set it inside sync_client so it can manage its lifecycle + + realm_sync_socket_t* socket_provider = realm_sync_socket_new(jenv->NewGlobalRef(websocket_transport), /*userdata*/ + realm_sync_userdata_free/*userdata_free*/, + websocket_post_func/*post_func*/, + websocket_create_timer_func/*create_timer_func*/, + websocket_cancel_timer_func/*cancel_timer_func*/, + [](realm_userdata_t userdata, + realm_sync_socket_timer_t timer_userdata){ + }/*free_timer_func*/, + websocket_connect_func/*websocket_connect_func*/, + websocket_async_write_func/*websocket_write_func*/, + realm_sync_websocket_free/*websocket_free_func*/); + realm_sync_client_config_set_sync_socket(reinterpret_cast(sync_client_config_ptr)/*config*/, socket_provider); + realm_release(socket_provider); + return socket_provider; +} + +// *** END - WebSocket Client (Platform Networking) *** // + void set_log_callback(jint j_log_level, jobject log_callback) { auto jenv = get_env(false); auto log_level = static_cast(j_log_level); diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h index 0854631d72..9128477b73 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h @@ -134,4 +134,16 @@ realm_sync_thread_destroyed(realm_userdata_t userdata); void realm_sync_thread_error(realm_userdata_t userdata, const char* error); +realm_sync_socket_t* realm_sync_websocket_new(int64_t sync_client_config_ptr, jobject websocket_transport); + +void realm_sync_websocket_callback_complete(bool cancelled, int64_t lambda_ptr, int status, const char* reason); + +void realm_sync_websocket_connected(int64_t observer_ptr, const char* protocol); + +void realm_sync_websocket_error(int64_t observer_ptr); + +void realm_sync_websocket_message(int64_t observer_ptr, jbyteArray data, size_t size); + +void realm_sync_websocket_closed(int64_t observer_ptr, bool was_clean, int error_code, const char* reason); + #endif //TEST_REALM_API_HELPERS_H diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/util/CoroutineDispatcherFactory.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/util/CoroutineDispatcherFactory.kt index a7cddce0c3..5abde21e59 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/util/CoroutineDispatcherFactory.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/util/CoroutineDispatcherFactory.kt @@ -59,7 +59,7 @@ public fun interface CoroutineDispatcherFactory { /** * Returns the dispatcher from the factory configuration, creating it if needed. - * If a dispatcher is created, calling this method multiple times wille create a + * If a dispatcher is created, calling this method multiple times will create a * new dispatcher for each call. */ public fun create(): DispatcherHolder diff --git a/packages/library-sync/build.gradle.kts b/packages/library-sync/build.gradle.kts index 13a714ad9a..6cbf7f950e 100644 --- a/packages/library-sync/build.gradle.kts +++ b/packages/library-sync/build.gradle.kts @@ -78,7 +78,8 @@ kotlin { val jvm by creating { dependsOn(commonMain) dependencies { - implementation("io.ktor:ktor-client-okhttp:${Versions.ktor}") + // TODO revert back to okhttp when https://youtrack.jetbrains.com/issue/KTOR-6266 is fixed + implementation("io.ktor:ktor-client-cio:${Versions.ktor}") } } val jvmMain by getting { diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt index 7b1f958385..8b6cc50da8 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt @@ -22,6 +22,7 @@ import io.realm.kotlin.annotations.ExperimentalRealmSerializerApi import io.realm.kotlin.internal.ContextLogger import io.realm.kotlin.internal.interop.sync.MetadataMode import io.realm.kotlin.internal.interop.sync.NetworkTransport +import io.realm.kotlin.internal.interop.sync.WebSocketTransport import io.realm.kotlin.internal.platform.appFilesDirectory import io.realm.kotlin.internal.platform.canWrite import io.realm.kotlin.internal.platform.directoryExists @@ -37,6 +38,7 @@ import io.realm.kotlin.mongodb.ext.customData import io.realm.kotlin.mongodb.ext.profile import io.realm.kotlin.mongodb.internal.AppConfigurationImpl import io.realm.kotlin.mongodb.internal.KtorNetworkTransport +import io.realm.kotlin.mongodb.internal.KtorWebSocketTransport import io.realm.kotlin.mongodb.internal.LogObfuscatorImpl import io.realm.kotlin.mongodb.sync.SyncConfiguration import kotlinx.coroutines.CoroutineDispatcher @@ -141,6 +143,7 @@ public interface AppConfiguration { private var syncRootDirectory: String = appFilesDirectory() private var userLoggers: List = listOf() private var networkTransport: NetworkTransport? = null + private var websocketTransport: WebSocketTransport? = null private var appName: String? = null private var appVersion: String? = null @@ -149,6 +152,7 @@ public interface AppConfiguration { private var httpLogObfuscator: HttpLogObfuscator? = LogObfuscatorImpl private val customRequestHeaders = mutableMapOf() private var authorizationHeaderName: String = DEFAULT_AUTHORIZATION_HEADER_NAME + private var usePlatformNetworking: Boolean = false /** * Sets the encryption key used to encrypt the user metadata Realm only. Individual @@ -325,6 +329,15 @@ public interface AppConfiguration { this.ejson = ejson } + /** + * Platform Networking offer improved support for proxies and firewalls that require authentication, + * instead of Realm's built-in WebSocket client for Sync traffic. This will become the default in a future version. + */ + public fun usePlatformNetworking(): Builder = + apply { + this.usePlatformNetworking = true + } + /** * Allows defining a custom network transport. It is used by some tests that require simulating * network responses. @@ -393,6 +406,14 @@ public interface AppConfiguration { ) } + val websocketTransport: ((DispatcherHolder) -> WebSocketTransport)? = if (usePlatformNetworking) + { dispatcherHolder -> + websocketTransport ?: KtorWebSocketTransport( + timeoutMs = 60000, + dispatcherHolder = dispatcherHolder + ) + } else null + return AppConfigurationImpl( appId = appId, baseUrl = baseUrl, @@ -402,6 +423,7 @@ public interface AppConfiguration { else MetadataMode.RLM_SYNC_CLIENT_METADATA_MODE_ENCRYPTED, appNetworkDispatcherFactory = appNetworkDispatcherFactory, networkTransportFactory = networkTransport, + webSocketTransportFactory = websocketTransport, syncRootDirectory = syncRootDirectory, logger = logConfig, appName = appName, diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt index 98f9edb155..7db12f6a87 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt @@ -24,6 +24,7 @@ import io.realm.kotlin.internal.interop.RealmSyncClientConfigurationPointer import io.realm.kotlin.internal.interop.SyncConnectionParams import io.realm.kotlin.internal.interop.sync.MetadataMode import io.realm.kotlin.internal.interop.sync.NetworkTransport +import io.realm.kotlin.internal.interop.sync.WebSocketTransport import io.realm.kotlin.internal.platform.DEVICE_MANUFACTURER import io.realm.kotlin.internal.platform.DEVICE_MODEL import io.realm.kotlin.internal.platform.OS_VERSION @@ -46,6 +47,7 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) override val encryptionKey: ByteArray?, private val appNetworkDispatcherFactory: CoroutineDispatcherFactory, internal val networkTransportFactory: (dispatcher: DispatcherHolder) -> NetworkTransport, + internal val webSocketTransportFactory: ((DispatcherHolder) -> WebSocketTransport)?, override val metadataMode: MetadataMode, override val syncRootDirectory: String, public val logger: LogConfiguration?, @@ -72,6 +74,7 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) // effect should be the same val appDispatcher = appNetworkDispatcherFactory.create() val networkTransport = networkTransportFactory(appDispatcher) + val websocketTransport = webSocketTransportFactory?.invoke(appDispatcher) val appConfigPointer: RealmAppConfigurationPointer = initializeRealmAppConfig(appName, appVersion, bundleId, networkTransport) var applicationInfo: String? = null @@ -85,12 +88,15 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) } val sdkInfo = "RealmKotlin/$SDK_VERSION" val synClientConfig: RealmSyncClientConfigurationPointer = initializeSyncClientConfig( + websocketTransport, sdkInfo, applicationInfo.toString() ) - return Triple( + + return AppResources( appDispatcher, networkTransport, + websocketTransport, RealmInterop.realm_app_get( appConfigPointer, synClientConfig, @@ -144,7 +150,11 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) ) } - private fun initializeSyncClientConfig(sdkInfo: String?, applicationInfo: String?): RealmSyncClientConfigurationPointer = + private fun initializeSyncClientConfig( + webSocketTransport: WebSocketTransport?, + sdkInfo: String?, + applicationInfo: String? + ): RealmSyncClientConfigurationPointer = RealmInterop.realm_sync_client_config_new() .also { syncClientConfig -> // Initialize client configuration first @@ -181,6 +191,11 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) it ) } + + // Use platform networking for Sync client WebSockets if provided + webSocketTransport?.let { + RealmInterop.realm_sync_set_websocket_transport(syncClientConfig, it) + } } internal companion object { 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 260a220332..a65330c7c5 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 @@ -20,6 +20,7 @@ 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.util.DispatcherHolder import io.realm.kotlin.internal.util.Validation import io.realm.kotlin.internal.util.use @@ -34,7 +35,12 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -internal typealias AppResources = Triple +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( @@ -44,6 +50,7 @@ public class AppImpl( internal val nativePointer: RealmAppPointer internal val appNetworkDispatcher: DispatcherHolder private val networkTransport: NetworkTransport + private val websocketTransport: WebSocketTransport? // 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 @@ -58,9 +65,10 @@ public class AppImpl( init { val appResources: AppResources = configuration.createNativeApp() - appNetworkDispatcher = appResources.first - networkTransport = appResources.second - nativePointer = appResources.third + appNetworkDispatcher = appResources.dispatcherHolder + networkTransport = appResources.networkTransport + websocketTransport = appResources.websocketTransport + nativePointer = appResources.realmAppPointer } override val emailPasswordAuth: EmailPasswordAuth by lazy { EmailPasswordAuthImpl(nativePointer) } @@ -130,6 +138,9 @@ public class AppImpl( // be beneficial in order to reason about the lifecycle of the Sync thread and dispatchers. networkTransport.close() nativePointer.release() + // It's important to close the transport *after* we delete the App, since SyncSession dtor + // still relies on the event loop (powered by the coroutines) to post function handler to be executed. + websocketTransport?.close() } internal companion object { diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt index d918aa4d6a..44148064d0 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt @@ -43,4 +43,5 @@ internal expect class HttpClientCache(timeoutMs: Long, customLogger: Logger? = n fun close() // Close any resources stored in the cache. } +// TODO use a la private val clientCache: HttpClientCache = HttpClientCache(timeoutMs, logger) public expect fun createPlatformClient(block: HttpClientConfig<*>.() -> Unit): HttpClient diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt new file mode 100644 index 0000000000..9bf566c86f --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt @@ -0,0 +1,299 @@ +package io.realm.kotlin.mongodb.internal + +import io.ktor.client.HttpClient +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.websocket.ClientWebSocketSession +import io.ktor.client.plugins.websocket.WebSockets +import io.ktor.client.plugins.websocket.webSocket +import io.ktor.client.request.header +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.http.URLProtocol +import io.ktor.websocket.CloseReason +import io.ktor.websocket.Frame +import io.ktor.websocket.close +import io.ktor.websocket.readReason +import io.ktor.websocket.readText +import io.realm.kotlin.internal.ContextLogger +import io.realm.kotlin.internal.interop.RealmWebsocketHandlerCallbackPointer +import io.realm.kotlin.internal.interop.sync.CancellableTimer +import io.realm.kotlin.internal.interop.sync.WebSocketClient +import io.realm.kotlin.internal.interop.sync.WebSocketObserver +import io.realm.kotlin.internal.interop.sync.WebSocketTransport +import io.realm.kotlin.internal.util.DispatcherHolder +import kotlinx.coroutines.CompletableJob +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.consumeEach +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch + +public class KtorWebSocketTransport( + timeoutMs: Long, + private val dispatcherHolder: DispatcherHolder +) : WebSocketTransport { + + private val logger = ContextLogger("WebSocket") + private val client: HttpClient by lazy { createWebSocketClient(timeoutMs) } + private val transportJob: CompletableJob by lazy { Job() } + private val scope: CoroutineScope by lazy { CoroutineScope(dispatcherHolder.dispatcher + transportJob) } + + override fun post(handlerCallback: RealmWebsocketHandlerCallbackPointer) { + scope.launch { + runCallback(handlerCallback) + } + } + + override fun createTimer( + delayInMilliseconds: Long, + handlerCallback: RealmWebsocketHandlerCallbackPointer + ): CancellableTimer = CancellableTimer(scope.launch { + (this as Job).invokeOnCompletion { completionHandler -> + // Only run the callback if it was not cancelled in the meantime + when (completionHandler) { + null -> { + runCallback(handlerCallback) + } + } + } + delay(delayInMilliseconds) + + }, handlerCallback) + + override fun connect( + observer: WebSocketObserver, + path: String, + address: String, + port: Long, + isSsl: Boolean, + numProtocols: Long, + supportedProtocols: String + ): WebSocketClient { + + return object : WebSocketClient { + private val websocketJob: CompletableJob by lazy { Job() } + private val websocketExceptionHandler: CoroutineExceptionHandler by lazy { + CoroutineExceptionHandler { _, exception: Throwable -> + logger.error(exception) + closeWebsocket() + } + } + private val scope: CoroutineScope by lazy { CoroutineScope(dispatcherHolder.dispatcher + websocketJob + websocketExceptionHandler) } + + private val writeFlow: MutableSharedFlow> = + MutableSharedFlow() + + private val binaryBuffer = FrameBuffer { + // Observer could be invalid at this point? + observer.onNewMessage(it) + } + + private lateinit var session: ClientWebSocketSession + + init { + openConnection() + } + + private fun openConnection() { + scope.launch { + client.webSocket( + method = HttpMethod.Get, + host = address, + port = port.toInt(), + path = path, + request = { + url.protocol = if (isSsl) URLProtocol.WSS else URLProtocol.WS + header(HttpHeaders.SecWebSocketProtocol, supportedProtocols) + }, + ) { + session = this + // it's unlikely to get a WebSocketSession without a successful protocol switch + // but we're double checking the status here + if (call.response.status != HttpStatusCode.SwitchingProtocols) { + observer.onError() + observer.onClose( + wasClean = false, + errorCode = 4401/*RLM_ERR_WEBSOCKET_CONNECTION_FAILED */, + reason = "Websocket server responded with status code ${call.response.status} instead of ${HttpStatusCode.SwitchingProtocols}" + ) + } else { + when (val selectedProtocol = + call.response.headers[HttpHeaders.SecWebSocketProtocol]) { + null -> { + observer.onError() + observer.onClose( + false, + 1002 /*RLM_ERR_WEBSOCKET_PROTOCOLERROR*/, + "${HttpHeaders.SecWebSocketProtocol} header not returned. Sync server didn't return supported protocol" + + ". Supported protocols are = $supportedProtocols" + ) + close( + CloseReason( + CloseReason.Codes.PROTOCOL_ERROR, + "Server didn't select a supported protocol. Supported protocols are = $supportedProtocols" + ) + ) // Sends a [Frame.Close]. + } + + else -> { + scope.launch { + observer.onConnected(selectedProtocol) + } + } + } + + // Writing messages to WebSocket + scope.launch { + writeFlow.collect { + try { + // There's no fragmentation needed when sending frames from client + // so 'fin' should always be `true` + outgoing.send(Frame.Binary(true, it.first)) + scope.launch { + runCallback(it.second) + } + } catch (e: Exception) { + runCallback( + it.second, + 4403 /*RLM_ERR_WEBSOCKET_WRITE_ERROR*/, + e.message.toString() + ) + } + } + } + + // Reading messages from WebSocket + scope.launch { + incoming.consumeEach { + when (val frame = it) { + is Frame.Binary -> { + binaryBuffer.appendAndSend(frame) + } + + is Frame.Close -> { + // It's important to rely properly the error code from the server. + // The server will report auth errors (and a few other error types) + // as websocket application-level errors after establishing the socket, rather than failing at the HTTP layer. + // since the websocket spec does not allow the HTTP status code from the response to be + // passed back to the client from the websocket implementation (example instruct a refresh token + // via a 401 HTTP response is not possible) see https://jira.mongodb.org/browse/BAAS-10531. + // In order to provide a reasonable response that the Sync Client can react upon, the private range of websocket close status codes + // 4000-4999, can be used to return a more specific error. + val errorCode: Int = + frame.readReason()?.code?.toInt() + ?: 0/*RLM_ERR_WEBSOCKET_OK*/ + val reason: String = frame.readReason()?.toString() + ?: "Received Close from Websocket server" + + observer.onClose(true, errorCode, reason) + close( + CloseReason( + CloseReason.Codes.NORMAL, + "Server closed the Websocket" + ) + ) + } + + is Frame.Text -> { + logger.warn("Received unexpected text WebSocket message ${frame.readText()}") + } + + // Raw WebSocket Frames (i.e Frame.Ping & Frame.Pong) are handled automatically with the client + // (Core doesn't care about these in the new API) + else -> { + logger.warn("Received unexpected message from server, Frame type = ${frame.frameType}") + } + } + } + } + websocketJob.join() // otherwise the client will send end the session and send a Close + } + } + } + } + + override fun send( + message: ByteArray, + handlerCallback: RealmWebsocketHandlerCallbackPointer + ) { + scope.launch { + writeFlow.emit(Pair(message, handlerCallback)) + } + } + + override fun closeWebsocket() { + if (::session.isInitialized) { + session.cancel() // Terminate the WebSocket session, connect needs to be called again + } + websocketJob.cancel() // Cancel all scheduled jobs + } + } + } + + override fun write( + webSocketClient: WebSocketClient, + data: ByteArray, + length: Long, + handlerCallback: RealmWebsocketHandlerCallbackPointer + ) { + webSocketClient.send(data, handlerCallback) + } + + override fun close() { + transportJob.cancel() + client.close() + } + + private fun createWebSocketClient(timeoutMs: Long): HttpClient { + return createPlatformClient { + install(HttpTimeout) { + connectTimeoutMillis = timeoutMs + requestTimeoutMillis = timeoutMs + socketTimeoutMillis = timeoutMs + } + install(WebSockets) + } + } +} + +/** + * Helper class that handles Frame [fragmentation](https://www.rfc-editor.org/rfc/rfc6455#section-5.4). + * Core expect a full binary frame to process a changeset, however the server can choose to split websocket Frames. + * We need to buffer them until we receive the `Frame.fin == true` flag. + * + * Note: Core doesn't send fragmented Frames, so this buffering only needed when reading from the websocket. + */ +private class FrameBuffer(val sendDefragmentedMessageToObserver: (binaryMessage: ByteArray) -> Unit) { + private val buffer = mutableListOf() + private var currentSize = 0 + + fun appendAndSend(frame: Frame) { + if (frame.data.isNotEmpty()) { + buffer.add(frame.data) + currentSize += frame.data.size + } + + if (frame.fin) { + // Append fragmented Frames and flush the buffer + sendDefragmentedMessageToObserver(flush()) + } + } + + private fun flush(): ByteArray { + val entireFrame = ByteArray(currentSize) + var currentIndex = 0 + + for (fragment in buffer) { + fragment.copyInto(entireFrame, destinationOffset = currentIndex) + currentIndex += fragment.size + } + + buffer.clear() + currentSize = 0 + return entireFrame + } +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt index 6b1e865ae4..b3b14d80b7 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt @@ -600,7 +600,6 @@ public interface SyncConfiguration : Configuration { ) }.let { auxSyncConfig -> RealmInterop.realm_app_sync_client_get_default_file_path_for_realm( - (user as UserImpl).app.nativePointer, auxSyncConfig, name ) diff --git a/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt b/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt index 61e8a2b3d4..d06b001986 100644 --- a/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt +++ b/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt @@ -1,10 +1,13 @@ @file:JvmName("HttpClientCacheJVM") package io.realm.kotlin.mongodb.internal -import io.ktor.client.HttpClient -import io.ktor.client.HttpClientConfig -import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.* +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.* import io.ktor.client.plugins.logging.Logger +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.util.* /** * Cache HttpClient on Android and JVM. @@ -21,12 +24,8 @@ internal actual class HttpClientCache actual constructor(timeoutMs: Long, custom } public actual fun createPlatformClient(block: HttpClientConfig<*>.() -> Unit): HttpClient { - return HttpClient(OkHttp) { - engine { - config { - retryOnConnectionFailure(true) - } - } + // Revert to OkHttp when https://youtrack.jetbrains.com/issue/KTOR-6266 is fixed + return HttpClient(CIO) { this.apply(block) } } diff --git a/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt b/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt index 1ff1d158e5..2dbd8aa975 100644 --- a/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt +++ b/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt @@ -4,6 +4,7 @@ import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig import io.ktor.client.engine.darwin.Darwin import io.ktor.client.plugins.logging.Logger +import io.ktor.client.plugins.websocket.WebSockets /** * Cache HttpClient on iOS. @@ -19,5 +20,8 @@ internal actual class HttpClientCache actual constructor(timeoutMs: Long, custom } public actual fun createPlatformClient(block: HttpClientConfig<*>.() -> Unit): HttpClient { - return HttpClient(Darwin, block) + return HttpClient(Darwin) { + install(WebSockets) + this.apply(block) + } } diff --git a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt index f203b8c712..ac947b7724 100644 --- a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt @@ -97,7 +97,7 @@ open class TestApp private constructor( builder = builder, networkTransport = networkTransport, ejson = ejson, - initialSetup = initialSetup + initialSetup = initialSetup, ) ) @@ -118,10 +118,13 @@ open class TestApp private constructor( deleteAllUsers() } + app.close() + + // Tearing down the SyncSession still relies on the the event loop (powered by the coroutines) of the platform networking + // to post Function Handler, so we need to close it after we close the App if (dispatcher is CloseableCoroutineDispatcher) { dispatcher.close() } - app.close() // Close network client resources closeClient() @@ -172,6 +175,7 @@ open class TestApp private constructor( .baseUrl(TEST_SERVER_BASE_URL) .networkTransport(networkTransport) .ejson(ejson) + .usePlatformNetworking() .apply { if (logLevel != null) { log( diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index 38e0fa85b4..a547f1e5c9 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -111,7 +111,6 @@ class SyncedRealmTests { fun setup() { partitionValue = TestHelper.randomPartitionValue() app = TestApp() - val (email, password) = randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) @@ -153,7 +152,7 @@ class SyncedRealmTests { override fun onError(session: SyncSession, error: SyncException) { fail("Realm 1: $error") } - } + }, ) Realm.open(config1).use { realm1 -> val config2 = createSyncConfig( @@ -196,7 +195,6 @@ class SyncedRealmTests { realm1.write { copyToRealm(child) } - val childResults = channel.receiveOrFail() val childPk = childResults.list[0] assertEquals("CHILD_A", childPk._id) @@ -1103,7 +1101,8 @@ class SyncedRealmTests { FlexEmbeddedObject::class ), initialSubscriptions = { realm: Realm -> - realm.query("section = $0", section).subscribe(name = "parentSubscription") + realm.query("section = $0", section) + .subscribe(name = "parentSubscription") } ) val syncConfig2 = createFlexibleSyncConfig( @@ -1145,7 +1144,10 @@ class SyncedRealmTests { // As is data assertEquals(1, flexRealm2.query().count().find()) - assertEquals("User1Object", flexRealm2.query().first().find()!!.name) + assertEquals( + "User1Object", + flexRealm2.query().first().find()!!.name + ) flexRealm2.subscriptions.waitForSynchronization(30.seconds) flexRealm2.write { @@ -1230,10 +1232,11 @@ class SyncedRealmTests { // Reading the object means we received it from the other Realm withTimeout(30.seconds) { - val obj: FlexParentObject = realm1.query("section = $0", section).asFlow() - .map { it.list } - .filter { it.isNotEmpty() } - .first().first() + val obj: FlexParentObject = + realm1.query("section = $0", section).asFlow() + .map { it.list } + .filter { it.isNotEmpty() } + .first().first() assertEquals(section, obj.section) // 1. Local write to work around https://github.com/realm/realm-kotlin/issues/1070 @@ -1293,8 +1296,14 @@ class SyncedRealmTests { flexSyncRealm.syncSession.uploadAllLocalChanges() } assertTrue(customLogger.logs.isNotEmpty()) - assertTrue(customLogger.logs.any { it.contains("Connection[1]: Negotiated protocol version:") }, "Missing Connection[1]") - assertTrue(customLogger.logs.any { it.contains("MyCustomApp/1.0.0") }, "Missing MyCustomApp/1.0.0") + assertTrue( + customLogger.logs.any { it.contains("Connection[1]: Negotiated protocol version:") }, + "Missing Connection[1]" + ) + assertTrue( + customLogger.logs.any { it.contains("MyCustomApp/1.0.0") }, + "Missing MyCustomApp/1.0.0" + ) flexApp.close() } From 24708595af50e3c7d2fe60dcf31f71fc536d9740 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Tue, 26 Sep 2023 14:23:17 +0100 Subject: [PATCH 02/45] Disabling removed C-API method --- .../kotlin/internal/interop/RealmInterop.kt | 3 +- .../kotlin/internal/interop/RealmInterop.kt | 48 +++++++++---------- packages/external/core | 2 +- .../src/main/jni/realm_api_helpers.cpp | 3 +- .../kotlin/test/mongodb/common/UserTests.kt | 18 +++---- 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 86700b7789..d75587988a 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -1116,7 +1116,8 @@ actual object RealmInterop { } actual fun realm_user_get_auth_provider(user: RealmUserPointer): AuthProvider { - return AuthProvider.of(realmc.realm_user_get_auth_provider(user.cptr())) + TODO("No longer valid") +// return AuthProvider.of(realmc.realm_user_get_auth_provider(user.cptr())) } actual fun realm_user_get_access_token(user: RealmUserPointer): String { diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index e817fbec02..d1cc91a15d 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -2270,30 +2270,30 @@ actual object RealmInterop { .also { realm_wrapper.realm_free(cPath) } } - actual fun realm_user_get_all_identities(user: RealmUserPointer): List { - memScoped { - val count = AuthProvider.values().size - val properties = allocArray(count) - val outCount = alloc() - realm_wrapper.realm_user_get_all_identities( - user.cptr(), - properties, - count.convert(), - outCount.ptr - ) - outCount.value.toLong().let { count -> - return if (count > 0) { - (0 until outCount.value.toLong()).map { - with(properties[it]) { - SyncUserIdentity(this.id!!.toKString(), AuthProvider.of(this.provider_type)) - } - } - } else { - emptyList() - } - } - } - } +// actual fun realm_user_get_all_identities(user: RealmUserPointer): List { +// memScoped { +// val count = AuthProvider.values().size +// val properties = allocArray(count) +// val outCount = alloc() +// realm_wrapper.realm_user_get_all_identities( +// user.cptr(), +// properties, +// count.convert(), +// outCount.ptr +// ) +// outCount.value.toLong().let { count -> +// return if (count > 0) { +// (0 until outCount.value.toLong()).map { +// with(properties[it]) { +// SyncUserIdentity(this.id!!.toKString(), AuthProvider.of(this.provider_type)) +// } +// } +// } else { +// emptyList() +// } +// } +// } +// } actual fun realm_user_get_identity(user: RealmUserPointer): String { return realm_wrapper.realm_user_get_identity(user.cptr()).safeKString("identity") diff --git a/packages/external/core b/packages/external/core index fe012487a9..a9c8f9948e 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit fe012487a9af0f8ed2b3aaa1ec40710bffbfeaf1 +Subproject commit a9c8f9948eb10b1514b8e91dcb04791bca35173b diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index f04e3df987..3a877ce015 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -751,7 +751,6 @@ static void websocket_cancel_timer_func(realm_userdata_t userdata, realm_sync_socket_timer_t timer_userdata) { if (timer_userdata != nullptr) { - nullptr)); auto jenv = get_env(true); jobject cancellable_timer = static_cast(timer_userdata); @@ -815,7 +814,7 @@ static void websocket_async_write_func(realm_userdata_t userdata, static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); static JavaMethod native_pointer_constructor(jenv, native_pointer_class, "", "(JZ)V"); auto* lambda = new std::function([realm_callback=std::move(realm_callback)](bool cancelled, int status, const char* reason) { - realm_sync_socket_post_complete(realm_callback, static_cast(status), "foo"); + realm_sync_socket_write_complete(realm_callback, static_cast(status), ""); }); jobject callback_pointer = jenv->NewObject(native_pointer_class, native_pointer_constructor, reinterpret_cast(lambda), false); diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt index eb622cf8c0..6cd0ae2562 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt @@ -122,15 +122,15 @@ class UserTests { } } - @Test - fun getProviderType() = runBlocking { - val email = randomEmail() - val emailUser = createUserAndLogin(email, "123456") - assertEquals(AuthenticationProvider.EMAIL_PASSWORD, emailUser.provider) - emailUser.logOut() - // AuthenticationProvider is not removed once user is logged out - assertEquals(AuthenticationProvider.EMAIL_PASSWORD, emailUser.provider) - } +// @Test +// fun getProviderType() = runBlocking { +// val email = randomEmail() +// val emailUser = createUserAndLogin(email, "123456") +// assertEquals(AuthenticationProvider.EMAIL_PASSWORD, emailUser.provider) +// emailUser.logOut() +// // AuthenticationProvider is not removed once user is logged out +// assertEquals(AuthenticationProvider.EMAIL_PASSWORD, emailUser.provider) +// } @Test fun getAccessToken() = runBlocking { From 824e049ec7036c7fd38983278e910b91990fe4e4 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 5 Oct 2023 10:22:58 +0100 Subject: [PATCH 03/45] Completing appropriately the Sync callbacks --- .../kotlin/internal/interop/ErrorCode.kt | 1 + .../kotlin/internal/interop/RealmInterop.kt | 6 +- .../interop/sync/ProtocolErrorCode.kt | 17 +- .../interop/sync/WebSocketTransport.kt | 22 +- .../kotlin/internal/interop/RealmInterop.kt | 10 +- .../interop/sync/ProtocolErrorCode.kt | 47 +++- packages/cinterop/src/native/realm.def | 2 +- .../kotlin/internal/interop/ErrorCode.kt | 2 +- .../kotlin/internal/interop/RealmInterop.kt | 261 +++++++----------- .../interop/sync/ProtocolErrorCode.kt | 55 +++- packages/external/core | 2 +- .../src/main/jni/realm_api_helpers.cpp | 63 ++--- .../internal/KtorWebSocketTransport.kt | 102 ++++--- 13 files changed, 328 insertions(+), 262 deletions(-) diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 31db69c4c8..4560a9f889 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -187,3 +187,4 @@ expect enum class ErrorCode : CodeDescription { fun of(nativeValue: Int): ErrorCode? } } + diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 0b621ce4eb..00b64d550a 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -30,6 +30,8 @@ import io.realm.kotlin.internal.interop.sync.ProgressDirection import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode import io.realm.kotlin.internal.interop.sync.SyncUserIdentity import io.realm.kotlin.internal.interop.sync.WebSocketTransport +import io.realm.kotlin.internal.interop.sync.WebsocketErrorCode +import io.realm.kotlin.internal.interop.sync.WebsocketCallbackResult import kotlinx.coroutines.CoroutineDispatcher import org.mongodb.kbson.ObjectId import kotlin.jvm.JvmInline @@ -802,7 +804,7 @@ expect object RealmInterop { webSocketTransport: WebSocketTransport ) - fun realm_sync_socket_callback_complete(nativePointer: RealmWebsocketHandlerCallbackPointer, cancelled: Boolean = false, status: Int = 0/* ok */, reason: String = "") + fun realm_sync_socket_callback_complete(nativePointer: RealmWebsocketHandlerCallbackPointer, cancelled: Boolean = false, status: WebsocketCallbackResult = WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_SUCCESS, reason: String = "") fun realm_sync_socket_websocket_connected(nativePointer: RealmWebsocketProviderPointer, protocol: String) @@ -810,5 +812,5 @@ expect object RealmInterop { fun realm_sync_socket_websocket_message(nativePointer: RealmWebsocketProviderPointer, data: ByteArray) - fun realm_sync_socket_websocket_closed(nativePointer: RealmWebsocketProviderPointer, wasClean: Boolean, errorCode: Int, reason: String = "") + fun realm_sync_socket_websocket_closed(nativePointer: RealmWebsocketProviderPointer, wasClean: Boolean, errorCode: WebsocketErrorCode, reason: String = "") } diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt index b7984a1d84..d1868984ab 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt @@ -121,6 +121,21 @@ expect enum class WebsocketErrorCode : CodeDescription { RLM_ERR_WEBSOCKET_FATAL_ERROR; companion object { - internal fun of(nativeValue: Int): WebsocketErrorCode? + fun of(nativeValue: Int): WebsocketErrorCode? + } +} + +expect enum class WebsocketCallbackResult : CodeDescription { + RLM_ERR_SYNC_SOCKET_SUCCESS, + RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED, + RLM_ERR_SYNC_SOCKET_RUNTIME, + RLM_ERR_SYNC_SOCKET_OUT_OF_MEMORY, + RLM_ERR_SYNC_SOCKET_ADDRESS_SPACE_EXHAUSTED, + RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED, + RLM_ERR_SYNC_SOCKET_NOT_SUPPORTED, + RLM_ERR_SYNC_SOCKET_INVALID_ARGUMENT; + + companion object { + fun of(nativeValue: Int): WebsocketCallbackResult? } } diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt index 423eb550f1..eb9543378e 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt @@ -1,8 +1,8 @@ package io.realm.kotlin.internal.interop.sync import io.realm.kotlin.internal.interop.RealmInterop -import io.realm.kotlin.internal.interop.RealmWebsocketProviderPointer import io.realm.kotlin.internal.interop.RealmWebsocketHandlerCallbackPointer +import io.realm.kotlin.internal.interop.RealmWebsocketProviderPointer import kotlinx.coroutines.Job interface WebSocketTransport { @@ -10,7 +10,7 @@ interface WebSocketTransport { fun createTimer( delayInMilliseconds: Long, - handlerCallback: RealmWebsocketHandlerCallbackPointer + handlerCallback: RealmWebsocketHandlerCallbackPointer, ): CancellableTimer fun connect( @@ -32,12 +32,13 @@ interface WebSocketTransport { fun runCallback( handlerCallback: RealmWebsocketHandlerCallbackPointer, - status: Int = 0/* ok */, + cancelled: Boolean = false, + status: WebsocketCallbackResult = WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_SUCCESS, reason: String = "" ) { RealmInterop.realm_sync_socket_callback_complete( handlerCallback, - cancelled = false, + cancelled, status, reason ) @@ -48,14 +49,11 @@ interface WebSocketTransport { class CancellableTimer( private val job: Job, - private val handlerCallback: RealmWebsocketHandlerCallbackPointer + private val cancelCallback: () -> Unit ) { fun cancel() { - // avoid double delete, if the Job has completed then the callback function was already been invoked and deleted from the heap - if (!job.isCompleted && !job.isCancelled) { - job.cancel() - RealmInterop.realm_sync_socket_callback_complete(handlerCallback, cancelled = true) - } + job.cancel() + cancelCallback() } } @@ -77,7 +75,7 @@ class WebSocketObserver(private val webSocketObserverPointer: RealmWebsocketProv RealmInterop.realm_sync_socket_websocket_message(webSocketObserverPointer, data) } - fun onClose(wasClean: Boolean, errorCode: Int, reason: String) { + fun onClose(wasClean: Boolean, errorCode: WebsocketErrorCode, reason: String) { RealmInterop.realm_sync_socket_websocket_closed( webSocketObserverPointer, wasClean, @@ -85,4 +83,4 @@ class WebSocketObserver(private val webSocketObserverPointer: RealmWebsocketProv reason ) } -} \ No newline at end of file +} diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index d75587988a..f06b2cdcee 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -30,6 +30,8 @@ import io.realm.kotlin.internal.interop.sync.ProgressDirection import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode import io.realm.kotlin.internal.interop.sync.SyncUserIdentity import io.realm.kotlin.internal.interop.sync.WebSocketTransport +import io.realm.kotlin.internal.interop.sync.WebsocketCallbackResult +import io.realm.kotlin.internal.interop.sync.WebsocketErrorCode import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -1985,8 +1987,8 @@ actual object RealmInterop { realmc.realm_sync_websocket_new(syncClientConfig.cptr(), webSocketTransport) } - actual fun realm_sync_socket_callback_complete(nativePointer: RealmWebsocketHandlerCallbackPointer, cancelled: Boolean, status: Int, reason: String) { - realmc.realm_sync_websocket_callback_complete(cancelled, nativePointer.cptr(), status, reason) + actual fun realm_sync_socket_callback_complete(nativePointer: RealmWebsocketHandlerCallbackPointer, cancelled: Boolean, status: WebsocketCallbackResult, reason: String) { + realmc.realm_sync_websocket_callback_complete(cancelled, nativePointer.cptr(), status.nativeValue, reason) } actual fun realm_sync_socket_websocket_connected(nativePointer: RealmWebsocketProviderPointer, protocol: String) { @@ -2001,8 +2003,8 @@ actual object RealmInterop { realmc.realm_sync_websocket_message(nativePointer.cptr(), data, data.size.toLong()) } - actual fun realm_sync_socket_websocket_closed(nativePointer: RealmWebsocketProviderPointer, wasClean: Boolean, errorCode: Int, reason: String) { - realmc.realm_sync_websocket_closed(nativePointer.cptr(), wasClean, errorCode, reason) + actual fun realm_sync_socket_websocket_closed(nativePointer: RealmWebsocketProviderPointer, wasClean: Boolean, errorCode: WebsocketErrorCode, reason: String) { + realmc.realm_sync_websocket_closed(nativePointer.cptr(), wasClean, errorCode.nativeValue, reason) } fun NativePointer.cptr(): Long { diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt index 8ba8028fa8..3a1a74f450 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt @@ -19,6 +19,7 @@ package io.realm.kotlin.internal.interop.sync import io.realm.kotlin.internal.interop.CodeDescription import io.realm.kotlin.internal.interop.realm_sync_errno_connection_e import io.realm.kotlin.internal.interop.realm_sync_errno_session_e +import io.realm.kotlin.internal.interop.realm_sync_socket_callback_result_e import io.realm.kotlin.internal.interop.realm_web_socket_errno_e actual enum class SyncConnectionErrorCode( @@ -129,7 +130,51 @@ actual enum class WebsocketErrorCode( RLM_ERR_WEBSOCKET_FATAL_ERROR("FatalError", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_FATAL_ERROR); actual companion object { - internal actual fun of(nativeValue: Int): WebsocketErrorCode? = + actual fun of(nativeValue: Int): WebsocketErrorCode? = + values().firstOrNull { value -> + value.nativeValue == nativeValue + } + } +} + + +actual enum class WebsocketCallbackResult(override val description: String, override val nativeValue: Int) : CodeDescription { + + RLM_ERR_SYNC_SOCKET_SUCCESS( + "Websocket callback success", + realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_SUCCESS + ), + RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED( + "Websocket callback aborted", + realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED + ), + RLM_ERR_SYNC_SOCKET_RUNTIME( + "Websocket Runtime error", + realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_RUNTIME + ), + RLM_ERR_SYNC_SOCKET_OUT_OF_MEMORY( + "Websocket out of memory ", + realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_OUT_OF_MEMORY + ), + RLM_ERR_SYNC_SOCKET_ADDRESS_SPACE_EXHAUSTED( + "Websocket address space exhausted", + realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_ADDRESS_SPACE_EXHAUSTED + ), + RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED( + "Websocket connection closed", + realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED + ), + RLM_ERR_SYNC_SOCKET_NOT_SUPPORTED( + "Websocket not supported", + realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_NOT_SUPPORTED + ), + RLM_ERR_SYNC_SOCKET_INVALID_ARGUMENT( + "Websocket invalid argument", + realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_INVALID_ARGUMENT + ); + + actual companion object { + actual fun of(nativeValue: Int): WebsocketCallbackResult? = values().firstOrNull { value -> value.nativeValue == nativeValue } diff --git a/packages/cinterop/src/native/realm.def b/packages/cinterop/src/native/realm.def index d9705ccafe..01f59930d9 100644 --- a/packages/cinterop/src/native/realm.def +++ b/packages/cinterop/src/native/realm.def @@ -11,7 +11,7 @@ headerFilter = realm.h realm/error_codes.h // libraryPaths.macos_x64 = ../external/core/build-macos_x64/src/realm/object-store/c_api ../external/core/build-macos_x64/src/realm ../external/core/build-macos_x64/src/realm/parser ../external/core/build-macos_x64/src/realm/object-store/ // libraryPaths.ios_x64 = ../external/core/build-macos_x64/src/realm/object-store/c_api ../external/core/build-macos_x64/src/realm ../external/core/build-macos_x64/src/realm/parser ../external/core/build-macos_x64/src/realm/object-store/ linkerOpts = -lcompression -lz -framework Foundation -framework CoreFoundation -framework Security -strictEnums = realm_errno realm_error_category realm_sync_errno_client realm_sync_errno_connection realm_sync_errno_session realm_web_socket_errno +strictEnums = realm_errno realm_error_category realm_sync_errno_client realm_sync_errno_connection realm_sync_errno_session realm_web_socket_errno realm_sync_socket_callback_result // We don't want to convert Websocket binary data to String noStringConversion = realm_sync_socket_websocket_message diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index aadd8db464..07d2f18a88 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -20,7 +20,7 @@ import realm_wrapper.realm_errno actual enum class ErrorCode( override val description: String, - private val nativeError: realm_errno + nativeError: realm_errno ) : CodeDescription { RLM_ERR_NONE("None", realm_errno.RLM_ERR_NONE), RLM_ERR_RUNTIME("Runtime", realm_errno.RLM_ERR_RUNTIME), diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index d1cc91a15d..9ce926f646 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -22,6 +22,7 @@ import io.realm.kotlin.internal.interop.Constants.ENCRYPTION_KEY_LENGTH import io.realm.kotlin.internal.interop.sync.ApiKeyWrapper import io.realm.kotlin.internal.interop.sync.AppError import io.realm.kotlin.internal.interop.sync.AuthProvider +import io.realm.kotlin.internal.interop.sync.CancellableTimer import io.realm.kotlin.internal.interop.sync.CoreCompensatingWriteInfo import io.realm.kotlin.internal.interop.sync.CoreConnectionState import io.realm.kotlin.internal.interop.sync.CoreSubscriptionSetState @@ -37,6 +38,7 @@ import io.realm.kotlin.internal.interop.sync.SyncUserIdentity import io.realm.kotlin.internal.interop.sync.WebSocketClient import io.realm.kotlin.internal.interop.sync.WebSocketObserver import io.realm.kotlin.internal.interop.sync.WebSocketTransport +import io.realm.kotlin.internal.interop.sync.WebsocketErrorCode import kotlinx.atomicfu.AtomicBoolean import kotlinx.atomicfu.atomic import kotlinx.cinterop.AutofreeScope @@ -61,13 +63,11 @@ import kotlinx.cinterop.alloc import kotlinx.cinterop.allocArray import kotlinx.cinterop.asStableRef import kotlinx.cinterop.cValue -import kotlinx.cinterop.cValuesOf import kotlinx.cinterop.convert import kotlinx.cinterop.cstr import kotlinx.cinterop.get import kotlinx.cinterop.getBytes import kotlinx.cinterop.memScoped -import kotlinx.cinterop.objcPtr import kotlinx.cinterop.pointed import kotlinx.cinterop.ptr import kotlinx.cinterop.readBytes @@ -82,9 +82,6 @@ import kotlinx.cinterop.usePinned import kotlinx.cinterop.value import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Job -import kotlinx.coroutines.Runnable import kotlinx.coroutines.launch import org.mongodb.kbson.BsonObjectId import org.mongodb.kbson.ObjectId @@ -133,7 +130,6 @@ import realm_wrapper.realm_sync_socket_t import realm_wrapper.realm_sync_socket_timer_t import realm_wrapper.realm_sync_socket_websocket_t import realm_wrapper.realm_t -import realm_wrapper.realm_user_identity import realm_wrapper.realm_user_t import realm_wrapper.realm_value_t import realm_wrapper.realm_value_type @@ -2270,7 +2266,8 @@ actual object RealmInterop { .also { realm_wrapper.realm_free(cPath) } } -// actual fun realm_user_get_all_identities(user: RealmUserPointer): List { + actual fun realm_user_get_all_identities(user: RealmUserPointer): List { + TODO("Not Valid") // memScoped { // val count = AuthProvider.values().size // val properties = allocArray(count) @@ -2293,14 +2290,15 @@ actual object RealmInterop { // } // } // } -// } + } actual fun realm_user_get_identity(user: RealmUserPointer): String { return realm_wrapper.realm_user_get_identity(user.cptr()).safeKString("identity") } actual fun realm_user_get_auth_provider(user: RealmUserPointer): AuthProvider { - return AuthProvider.of(realm_wrapper.realm_user_get_auth_provider(user.cptr())) + TODO() +// return AuthProvider.of(realm_wrapper.realm_user_get_auth_provider(user.cptr())) } actual fun realm_user_is_logged_in(user: RealmUserPointer): Boolean { @@ -2723,164 +2721,109 @@ actual object RealmInterop { syncClientConfig: RealmSyncClientConfigurationPointer, webSocketTransport: WebSocketTransport ) { - val realmSyncSocketNew: CPointer? = realm_wrapper.realm_sync_socket_new( - userdata = StableRef.create(webSocketTransport).asCPointer(), - userdata_free = staticCFunction { userdata: CPointer? -> - disposeUserData(userdata) - }, - post_func = staticCFunction { userdata: CPointer?, syncSocketCallback: CPointer? -> - safeUserData(userdata).let { websocketTransport -> - // schedule execution of the FunctionHandler - - //TODO: should we Move the source object to a destination object? like .NET - /// void post(FunctionHandler&& handler) final { - // s_post_work(m_managed_provider, new FunctionHandler(std::move(handler))); - // } - -// val destinationObjMemory = nativeHeap.alloc() -// websocketTransport.post(CPointerWrapper(syncSocketCallback)) -// val p :RealmSyncSocketCallbackPointer? = null -// val toLong = syncSocketCallback.toLong() -// syncSocketCallback?.pointed -// websocketTransport.post(Runnable { (syncSocketCallback as CPointer Unit>>)?.invoke() }) -// StableRef.create(syncSocketCallback.rawValue).asCPointer() - // I'm not de-referencing it correctly causing a BAD_EXEC - // TODO pass in as argument the status/error messsage -// websocketTransport.post(Runnable { realm_wrapper.realm_sync_socket_callback_complete(syncSocketCallback, 0, "") }) - websocketTransport.post(CPointerWrapper(syncSocketCallback)) - -// (syncSocketCallback as CPointer Unit>>).invoke() - } - }, - create_timer_func = staticCFunction?,uint64_t, CPointer?, CPointer?> { userdata: CPointer?, delayInMilliseconds: uint64_t, syncSocketCallback: CPointer? -> - safeUserData(userdata).let { ws -> - // schedule the callback after the delay parameter and return SyncTimer to be able to cancel it - // .NET creates the Job in C# then send the object as C opaque pointer - /* - var timer = new Timer(TimeSpan.FromMilliseconds(delay_milliseconds), native_callback, provider._workQueue); - return GCHandle.ToIntPtr(GCHandle.Alloc(timer)); - - private static void CancelTimer(IntPtr managed_timer) - { - var handle = GCHandle.FromIntPtr(managed_timer); - try - { - ((Timer)handle.Target!).Cancel(); - } - finally - { - handle.Free(); + val realmSyncSocketNew: CPointer? = + realm_wrapper.realm_sync_socket_new( + userdata = StableRef.create(webSocketTransport).asCPointer(), + userdata_free = staticCFunction { userdata: CPointer? -> + safeUserData(userdata).close() + disposeUserData(userdata) + }, + post_func = staticCFunction { userdata: CPointer?, syncSocketCallback: CPointer? -> + val callback: WebsocketFunctionHandlerCallback = { cancelled , _, _ -> + realm_wrapper.realm_sync_socket_post_complete(syncSocketCallback, + if (cancelled) WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED.asNativeEnum else WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_SUCCESS.asNativeEnum, + "") + } + + safeUserData(userdata).post( + CPointerWrapper(StableRef.create(callback).asCPointer()) + ) + }, + create_timer_func = staticCFunction { userdata: CPointer?, delayInMilliseconds: uint64_t, syncSocketCallback: CPointer? -> + val callback: WebsocketFunctionHandlerCallback = { cancelled, _, _ -> + if (cancelled) { + realm_wrapper.realm_sync_socket_timer_canceled(syncSocketCallback) + } else { + realm_wrapper.realm_sync_socket_timer_complete(syncSocketCallback, WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_SUCCESS.asNativeEnum, "") + } + } + + safeUserData(userdata).let { ws -> + val job: CancellableTimer = ws.createTimer( + delayInMilliseconds.toLong(), + CPointerWrapper(StableRef.create(callback).asCPointer()) + ) + StableRef.create(job).asCPointer() + } + }, + cancel_timer_func = staticCFunction { userdata: CPointer?, timer: realm_sync_socket_timer_t? -> + safeUserData(timer).cancel() + }, + free_timer_func = staticCFunction { userdata: CPointer?, timer: realm_sync_socket_timer_t? -> + disposeUserData(timer) + }, + websocket_connect_func = staticCFunction { userdata: CPointer?, endpoint: CValue, observer: CPointer? -> + safeUserData(userdata).let { websocketTransport -> + endpoint.useContents { + val managedObserver = WebSocketObserver(CPointerWrapper(observer)) + + val supportedProtocols = mutableListOf() + for (i in 0 until this.num_protocols.toInt()) { + val protocol: CPointer>? = this.protocols?.get(i) + supportedProtocols.add(protocol.safeKString()) } - */ - // in our case we can get the CPointer value of the allocated object (outside Arena) - // TODO return CancelTimer then return it as a NativePointer (use StableRef.create(networkTransport).asCPointer()) - // - val job: Job = ws.createTimer(delayInMilliseconds.toLong(), CPointerWrapper(syncSocketCallback)) - StableRef.create(job).asCPointer() -// val invoke: realm_sync_socket_timer_t? = f?.invoke() -// invoke - // return void* as COpaquePointer - } -// val objcPtr: CPointer? = (a?.objcPtr() as CPointer<*>) -// null as CPointer<*> - }, - cancel_timer_func = staticCFunction { userdata: CPointer?, timer: realm_sync_socket_timer_t? -> -// safeUserData(userdata).let { ws -> - val job: StableRef? = timer?.asStableRef() - job?.get().run { - if (this != null) { - this.cancel() - job!!.dispose() + val webSocketClient: WebSocketClient = websocketTransport.connect( + managedObserver, + this.path.safeKString(), + this.address.safeKString(), + this.port.toLong(), + this.is_ssl, + this.num_protocols.toLong(), + supportedProtocols.joinToString(", ") + ) + StableRef.create(webSocketClient).asCPointer() } } -// } - }, - free_timer_func = staticCFunction { userdata: CPointer?, timer: realm_sync_socket_timer_t? -> Unit }, - websocket_connect_func = staticCFunction { userdata: CPointer?, endpoint: CValue, observer: CPointer? -> - safeUserData(userdata).let { websocketTransport -> - endpoint.useContents { - // When Ktor connects it should invoke: - // RLM_API void realm_sync_socket_websocket_connected(realm_websocket_observer_t* realm_websocket_observer, const char* protocol) - // on Error RLM_API void realm_sync_socket_websocket_error(realm_websocket_observer_t* realm_websocket_observer) - // on new Message RLM_API void realm_sync_socket_websocket_message(realm_websocket_observer_t* realm_websocket_observer, const char* data, size_t data_size) - // on Close RLM_API void realm_sync_socket_websocket_closed(realm_websocket_observer_t* realm_websocket_observer, bool was_clean, - // realm_web_socket_errno_e code, const char* reason) - - /* - WebSocketEndpoint { - using port_type = sync::port_type; - std::string address; // Host address - port_type port; // Host port number - std::string path; // Includes access token in query. - std::vector protocols; // Array of one or more websocket protocols - bool is_ssl; // true if SSL should be used - */ -// val protocols1: CPointer>>>? = this.protocols - val supportedProtocols = mutableListOf() - for (i in 0 until this.num_protocols.toInt()) { - val protocol: CPointer>? = this.protocols?.get(i) - supportedProtocols.add(protocol.safeKString()) + }, + websocket_write_func = staticCFunction { userdata: CPointer?, websocket: realm_sync_socket_websocket_t?, data: CPointer?, length: size_t, callback: CPointer? -> + val postWriteCallback: WebsocketFunctionHandlerCallback = + { cancelled, status, reason -> + realm_wrapper.realm_sync_socket_write_complete( + callback, + if (cancelled) WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED.asNativeEnum else WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_SUCCESS.asNativeEnum, + reason + ) } - // CPointer>? -> safeKString() -// val data: CPointer>? = protocols1.readBytes() -// val readBytes: ByteArray? = data?.readBytes(this.size.toInt()) -// readBytes?.decodeToString(0, size.toInt(), throwOnInvalidSequence = false)!! - - //TODO return KTor WebSocket as native pointer maybe use the instance as StableRef.create(networkTransport).asCPointer() -// val observerWebSocketConnected : (String) -> Unit = { } -// val observerWebSocketError : () -> Unit = { realm_wrapper.realm_sync_socket_websocket_error(observer) } -// val observerWebSocketNewMessage: (data: ByteArray) -> Unit = { data -> -// println(">>>>>>>>>>>>>>>>>>>>>> observerWebSocketNewMessage message = ${data.toKString()}") -// realm_wrapper.realm_sync_socket_websocket_message(observer, data.toKString(throwOnInvalidSequence = true), data.size.toULong()) } -// val observerWebSocketClose: (wasClean: Boolean, errorCode: UInt, reason: String) -> Unit = { wasClean: Boolean, errorCode: UInt, reason: String -> realm_wrapper.realm_sync_socket_websocket_closed(observer, wasClean, errorCode, reason) } - - val managedObserver = WebSocketObserver(CPointerWrapper(observer)) -// override fun onConnected(protocol: String) { -// realm_wrapper.realm_sync_socket_websocket_connected(observer, protocol) -// } -// -// override fun onError() { -// realm_wrapper.realm_sync_socket_websocket_error(observer) -// } -// -// override fun onNewMessage(data: ByteArray) { -// realm_wrapper.realm_sync_socket_websocket_message(observer, data.toKString(throwOnInvalidSequence = true), data.size.toULong()) -// } -// -// override fun onClose( -// wasClean: Boolean, -// errorCode: UInt, -// reason: String -// ) { -// realm_wrapper.realm_sync_socket_websocket_closed(observer, wasClean, errorCode, reason) -// } -// } - val webSocketClient: WebSocketClient = websocketTransport.connect(managedObserver, this.path.safeKString(), this.address.safeKString(), this.port.toLong(), this.is_ssl, this.num_protocols.toLong(), supportedProtocols.joinToString(", ")) - val webSocketClientPointer: CPointer = StableRef.create(webSocketClient).asCPointer() - webSocketClientPointer - } - } -// null as realm_wrapper.realm_sync_socket_websocket_t? - }, - websocket_write_func = staticCFunction { userdata: CPointer?, websocket: realm_sync_socket_websocket_t?, data: CPointer?, length: size_t, callback: CPointer? -> - safeUserData(userdata).let { websocketTransport -> - safeUserData(websocket).let { webSocketClient -> - data?.readBytes(length.toInt())?.run { - websocketTransport.write(webSocketClient, this, length.toLong(), CPointerWrapper(callback) /*TODO change status and reason if there's an error sending the Frame*/) + safeUserData(userdata).let { websocketTransport -> + safeUserData(websocket).let { webSocketClient -> + data?.readBytes(length.toInt())?.run { + websocketTransport.write( + webSocketClient, + this, + length.toLong(), + CPointerWrapper(StableRef.create(postWriteCallback).asCPointer()) + ) + } } } + Unit + }, + websocket_free_func = staticCFunction { userdata: CPointer?, websocket: realm_sync_socket_websocket_t? -> + safeUserData(websocket).closeWebsocket() + disposeUserData(websocket) } - Unit - }, - websocket_free_func = staticCFunction { userdata: CPointer?, websocket: realm_sync_socket_websocket_t? -> Unit } + ) + realm_wrapper.realm_sync_client_config_set_sync_socket( + syncClientConfig.cptr(), + realmSyncSocketNew ) - realm_wrapper.realm_sync_client_config_set_sync_socket(syncClientConfig.cptr(), realmSyncSocketNew) + realm_release(realmSyncSocketNew) } - actual fun realm_sync_socket_callback_complete(nativePointer: RealmWebsocketHandlerCallbackPointer, cancelled: Boolean, status: Int, reason: String) { - //TODO use cancelled to cancel or free - realm_wrapper.realm_sync_socket_callback_complete(nativePointer.cptr(), status.toUInt(), reason) + actual fun realm_sync_socket_callback_complete(nativePointer: RealmWebsocketHandlerCallbackPointer, cancelled: Boolean, status: WebsocketErrorCode, reason: String) { + safeUserData(nativePointer.cptr())(cancelled, status, reason) + disposeUserData(nativePointer.cptr()) } actual fun realm_sync_socket_websocket_connected(nativePointer: RealmWebsocketProviderPointer, protocol: String) { @@ -2895,8 +2838,8 @@ actual object RealmInterop { realm_wrapper.realm_sync_socket_websocket_message(nativePointer.cptr(), data.toCValues(), data.size.toULong()) } - actual fun realm_sync_socket_websocket_closed(nativePointer: RealmWebsocketProviderPointer, wasClean: Boolean, errorCode: Int, reason: String) { - realm_wrapper.realm_sync_socket_websocket_closed(nativePointer.cptr(), wasClean, errorCode.toUInt(), reason) + actual fun realm_sync_socket_websocket_closed(nativePointer: RealmWebsocketProviderPointer, wasClean: Boolean, errorCode: WebsocketErrorCode, reason: String) { + realm_wrapper.realm_sync_socket_websocket_closed(nativePointer.cptr(), wasClean, errorCode.asNativeEnum, reason) } @Suppress("LongParameterList") @@ -3612,6 +3555,8 @@ actual object RealmInterop { } } +private typealias WebsocketFunctionHandlerCallback = (Boolean, WebsocketErrorCode, String) -> Unit + fun realm_value_t.asByteArray(): ByteArray { if (this.type != realm_value_type.RLM_TYPE_BINARY) { error("Value is not of type ByteArray: $this.type") diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt index 20817a0ae5..2854bfaef5 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt @@ -16,8 +16,10 @@ package io.realm.kotlin.internal.interop.sync import io.realm.kotlin.internal.interop.CodeDescription +import realm_wrapper.realm_errno import realm_wrapper.realm_sync_errno_connection import realm_wrapper.realm_sync_errno_session +import realm_wrapper.realm_sync_socket_callback_result import realm_wrapper.realm_web_socket_errno actual enum class SyncConnectionErrorCode( @@ -131,8 +133,59 @@ actual enum class WebsocketErrorCode( override val nativeValue: Int = errorCode.value.toInt() + val asNativeEnum: realm_web_socket_errno = errorCode + + actual companion object { + actual fun of(nativeValue: Int): WebsocketErrorCode? = + values().firstOrNull { value -> + value.nativeValue == nativeValue + } + } +} + +actual enum class WebsocketCallbackResult( + override val description: String, + nativeError: realm_sync_socket_callback_result +) : CodeDescription { + + RLM_ERR_SYNC_SOCKET_SUCCESS( + "Websocket callback success", + realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_SUCCESS + ), + RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED( + "Websocket callback aborted", + realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED + ), + RLM_ERR_SYNC_SOCKET_RUNTIME( + "Websocket Runtime error", + realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_RUNTIME + ), + RLM_ERR_SYNC_SOCKET_OUT_OF_MEMORY( + "Websocket out of memory ", + realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_OUT_OF_MEMORY + ), + RLM_ERR_SYNC_SOCKET_ADDRESS_SPACE_EXHAUSTED( + "Websocket address space exhausted", + realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_ADDRESS_SPACE_EXHAUSTED + ), + RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED( + "Websocket connection closed", + realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED + ), + RLM_ERR_SYNC_SOCKET_NOT_SUPPORTED( + "Websocket not supported", + realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_NOT_SUPPORTED + ), + RLM_ERR_SYNC_SOCKET_INVALID_ARGUMENT( + "Websocket invalid argument", + realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_INVALID_ARGUMENT + ); + + override val nativeValue: Int = nativeError.value.toInt() + val asNativeEnum: realm_sync_socket_callback_result = nativeError + actual companion object { - internal actual fun of(nativeValue: Int): WebsocketErrorCode? = + actual fun of(nativeValue: Int): WebsocketCallbackResult? = values().firstOrNull { value -> value.nativeValue == nativeValue } diff --git a/packages/external/core b/packages/external/core index a9c8f9948e..9fe0653fef 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit a9c8f9948eb10b1514b8e91dcb04791bca35173b +Subproject commit 9fe0653fef672f14c771019ba27d54d568f622d0 diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index 3a877ce015..8adbb6e273 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -678,14 +678,18 @@ realm_http_transport_t* realm_network_transport_new(jobject network_transport) { // *** BEGIN - WebSocket Client (Platform Networking) *** // +using WebsocketFunctionHandlerCallback = std::function; + static void websocket_post_func(realm_userdata_t userdata, realm_sync_socket_callback_t* realm_callback) { auto jenv = get_env(true); static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); static JavaMethod native_pointer_constructor(jenv, native_pointer_class, "", "(JZ)V"); - auto* lambda = new std::function([realm_callback=std::move(realm_callback)](bool cancelled, int status, const char* reason) { - realm_sync_socket_post_complete(realm_callback, realm_errno_e::RLM_ERR_NONE, ""); + WebsocketFunctionHandlerCallback* lambda = new WebsocketFunctionHandlerCallback([realm_callback=std::move(realm_callback)](bool cancelled, int status, const char* reason) { + realm_sync_socket_post_complete(realm_callback, + cancelled ? realm_sync_socket_callback_result::RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED : realm_sync_socket_callback_result::RLM_ERR_SYNC_SOCKET_SUCCESS, + ""); }); jobject pointer = jenv->NewObject(native_pointer_class, native_pointer_constructor, reinterpret_cast(lambda), false); @@ -694,46 +698,28 @@ static void websocket_post_func(realm_userdata_t userdata, static JavaMethod post_method (jenv, jvm_websocket_transport_class, "post", "(Lio/realm/kotlin/internal/interop/NativePointer;)V"); jobject websocket_transport = static_cast(userdata); - - if (jenv->ExceptionCheck()) { - jthrowable exception = jenv->ExceptionOccurred(); - - jclass clazz = jenv->GetObjectClass(exception); - jmethodID get_message = jenv->GetMethodID(clazz, - "getMessage", - "()Ljava/lang/String;"); - jstring message = (jstring) jenv->CallObjectMethod(exception, get_message); - auto str = jenv->GetStringUTFChars(message, NULL); - } jenv->CallVoidMethod(websocket_transport, post_method, pointer); - if (jenv->ExceptionCheck()) { - jthrowable exception = jenv->ExceptionOccurred(); - - jclass clazz = jenv->GetObjectClass(exception); - jmethodID get_message = jenv->GetMethodID(clazz, - "getMessage", - "()Ljava/lang/String;"); - jstring message = (jstring) jenv->CallObjectMethod(exception, get_message); - auto str = jenv->GetStringUTFChars(message, NULL); - } - jenv->DeleteLocalRef(pointer); } static realm_sync_socket_timer_t websocket_create_timer_func( realm_userdata_t userdata, uint64_t delay_ms, realm_sync_socket_callback_t* realm_callback) { auto jenv = get_env(true); - static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); + static JavaClass native_pointer_class(jenv, + "io/realm/kotlin/internal/interop/LongPointerWrapper"); static JavaMethod native_pointer_constructor(jenv, native_pointer_class, "", "(JZ)V"); - - auto* lambda = new std::function([realm_callback= std::move(realm_callback)](bool cancel, int status, const char* reason) { - if (cancel) { - realm_sync_socket_timer_canceled(realm_callback); - } else { - realm_sync_socket_timer_complete(realm_callback, realm_errno_e::RLM_ERR_NONE, "");// TODO should we use status and reason? - } - }); + WebsocketFunctionHandlerCallback *lambda = new WebsocketFunctionHandlerCallback( + [realm_callback = std::move(realm_callback)](bool cancel, int status, + const char *reason) { + if (cancel) { + realm_sync_socket_timer_canceled(realm_callback); + } else { + realm_sync_socket_timer_complete(realm_callback, + realm_sync_socket_callback_result::RLM_ERR_SYNC_SOCKET_SUCCESS, + ""); + } + }); jobject pointer = jenv->NewObject(native_pointer_class, native_pointer_constructor, reinterpret_cast(lambda), false); @@ -813,8 +799,10 @@ static void websocket_async_write_func(realm_userdata_t userdata, static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); static JavaMethod native_pointer_constructor(jenv, native_pointer_class, "", "(JZ)V"); - auto* lambda = new std::function([realm_callback=std::move(realm_callback)](bool cancelled, int status, const char* reason) { - realm_sync_socket_write_complete(realm_callback, static_cast(status), ""); + WebsocketFunctionHandlerCallback* lambda = new WebsocketFunctionHandlerCallback([realm_callback=std::move(realm_callback)](bool cancelled, int status, const char* reason) { + realm_sync_socket_write_complete(realm_callback, + cancelled ? realm_sync_socket_callback_result::RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED: realm_sync_socket_callback_result::RLM_ERR_SYNC_SOCKET_SUCCESS, + ""); }); jobject callback_pointer = jenv->NewObject(native_pointer_class, native_pointer_constructor, reinterpret_cast(lambda), false); @@ -866,7 +854,7 @@ static void realm_sync_userdata_free(realm_userdata_t userdata) { // This should run in the context of CoroutineScope void realm_sync_websocket_callback_complete(bool cancelled, int64_t lambda_ptr, int status, const char* reason) { - std::function* callback = reinterpret_cast*>(lambda_ptr); + WebsocketFunctionHandlerCallback* callback = reinterpret_cast(lambda_ptr); (*callback)(cancelled, status, reason); delete callback; } @@ -894,9 +882,6 @@ void realm_sync_websocket_closed(int64_t observer_ptr, bool was_clean, int error realm_sync_socket_t* realm_sync_websocket_new(int64_t sync_client_config_ptr, jobject websocket_transport) { auto jenv = get_env(false); // Always called from JVM - - // get pointer use it inside unique_ptr and set it inside sync_client so it can manage its lifecycle - realm_sync_socket_t* socket_provider = realm_sync_socket_new(jenv->NewGlobalRef(websocket_transport), /*userdata*/ realm_sync_userdata_free/*userdata_free*/, websocket_post_func/*post_func*/, diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt index 9bf566c86f..cfc313934b 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt @@ -21,49 +21,68 @@ import io.realm.kotlin.internal.interop.sync.CancellableTimer import io.realm.kotlin.internal.interop.sync.WebSocketClient import io.realm.kotlin.internal.interop.sync.WebSocketObserver import io.realm.kotlin.internal.interop.sync.WebSocketTransport +import io.realm.kotlin.internal.interop.sync.WebsocketErrorCode import io.realm.kotlin.internal.util.DispatcherHolder +import kotlinx.atomicfu.AtomicRef +import kotlinx.atomicfu.atomic import kotlinx.coroutines.CompletableJob import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking public class KtorWebSocketTransport( timeoutMs: Long, private val dispatcherHolder: DispatcherHolder ) : WebSocketTransport { - private val logger = ContextLogger("WebSocket") + private val logger = ContextLogger("Websocket") private val client: HttpClient by lazy { createWebSocketClient(timeoutMs) } private val transportJob: CompletableJob by lazy { Job() } private val scope: CoroutineScope by lazy { CoroutineScope(dispatcherHolder.dispatcher + transportJob) } override fun post(handlerCallback: RealmWebsocketHandlerCallbackPointer) { scope.launch { - runCallback(handlerCallback) + (this as Job).invokeOnCompletion { completionHandler: Throwable? -> + // Only run the callback if it was not cancelled in the meantime + when (completionHandler) { + null -> runCallback(handlerCallback) + else -> runCallback( + handlerCallback, + cancelled = true + ) + } + } } } override fun createTimer( delayInMilliseconds: Long, handlerCallback: RealmWebsocketHandlerCallbackPointer - ): CancellableTimer = CancellableTimer(scope.launch { - (this as Job).invokeOnCompletion { completionHandler -> - // Only run the callback if it was not cancelled in the meantime - when (completionHandler) { - null -> { - runCallback(handlerCallback) + ): CancellableTimer { + val atomic: AtomicRef = atomic(handlerCallback) + return CancellableTimer(scope.launch { + delay(delayInMilliseconds) + atomic.getAndSet(null)?.run { + runCallback(handlerCallback) + } + }) { // Cancel lambda + scope.launch { + atomic.getAndSet(null)?.run { + runCallback(handlerCallback, cancelled = true) } } } - delay(delayInMilliseconds) - - }, handlerCallback) + } + @OptIn(ExperimentalCoroutinesApi::class) override fun connect( observer: WebSocketObserver, path: String, @@ -83,15 +102,12 @@ public class KtorWebSocketTransport( } } private val scope: CoroutineScope by lazy { CoroutineScope(dispatcherHolder.dispatcher + websocketJob + websocketExceptionHandler) } - - private val writeFlow: MutableSharedFlow> = - MutableSharedFlow() + private val writeChannel = + Channel>(capacity = UNLIMITED) private val binaryBuffer = FrameBuffer { - // Observer could be invalid at this point? observer.onNewMessage(it) } - private lateinit var session: ClientWebSocketSession init { @@ -117,7 +133,7 @@ public class KtorWebSocketTransport( observer.onError() observer.onClose( wasClean = false, - errorCode = 4401/*RLM_ERR_WEBSOCKET_CONNECTION_FAILED */, + errorCode = WebsocketErrorCode.RLM_ERR_WEBSOCKET_CONNECTION_FAILED, reason = "Websocket server responded with status code ${call.response.status} instead of ${HttpStatusCode.SwitchingProtocols}" ) } else { @@ -127,7 +143,7 @@ public class KtorWebSocketTransport( observer.onError() observer.onClose( false, - 1002 /*RLM_ERR_WEBSOCKET_PROTOCOLERROR*/, + WebsocketErrorCode.RLM_ERR_WEBSOCKET_PROTOCOLERROR, "${HttpHeaders.SecWebSocketProtocol} header not returned. Sync server didn't return supported protocol" + ". Supported protocols are = $supportedProtocols" ) @@ -148,21 +164,11 @@ public class KtorWebSocketTransport( // Writing messages to WebSocket scope.launch { - writeFlow.collect { - try { - // There's no fragmentation needed when sending frames from client - // so 'fin' should always be `true` - outgoing.send(Frame.Binary(true, it.first)) - scope.launch { - runCallback(it.second) - } - } catch (e: Exception) { - runCallback( - it.second, - 4403 /*RLM_ERR_WEBSOCKET_WRITE_ERROR*/, - e.message.toString() - ) - } + writeChannel.consumeEach { + // There's no fragmentation needed when sending frames from client + // so 'fin' should always be `true` + outgoing.send(Frame.Binary(true, it.first)) + runCallback(it.second) } } @@ -183,9 +189,10 @@ public class KtorWebSocketTransport( // via a 401 HTTP response is not possible) see https://jira.mongodb.org/browse/BAAS-10531. // In order to provide a reasonable response that the Sync Client can react upon, the private range of websocket close status codes // 4000-4999, can be used to return a more specific error. - val errorCode: Int = + val errorCode: WebsocketErrorCode = frame.readReason()?.code?.toInt() - ?: 0/*RLM_ERR_WEBSOCKET_OK*/ + ?.let { code -> WebsocketErrorCode.of(code) } + ?: WebsocketErrorCode.RLM_ERR_WEBSOCKET_OK val reason: String = frame.readReason()?.toString() ?: "Received Close from Websocket server" @@ -220,16 +227,29 @@ public class KtorWebSocketTransport( message: ByteArray, handlerCallback: RealmWebsocketHandlerCallbackPointer ) { - scope.launch { - writeFlow.emit(Pair(message, handlerCallback)) - } + writeChannel.trySend(Pair(message, handlerCallback)) } override fun closeWebsocket() { if (::session.isInitialized) { - session.cancel() // Terminate the WebSocket session, connect needs to be called again + session.cancel() // Terminate the WebSocket session, connect needs to be called again. + } + // Collect unprocessed writes and cancel them (mainly to avoid leaking the FunctionHandler). + while (true) { + val result = writeChannel.tryReceive() + if (result.isSuccess) { + result.getOrNull()?.run { + runBlocking(scope.coroutineContext) { + runCallback(handlerCallback = second, cancelled = true) + } + } + } else { + // No more elements in the channel + break + } } - websocketJob.cancel() // Cancel all scheduled jobs + writeChannel.close() + websocketJob.cancel() // Cancel all scheduled jobs. } } } From e521d625a0d6d2986a313953acc332fce8a53523 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Fri, 6 Oct 2023 10:37:29 +0100 Subject: [PATCH 04/45] Fixes --- .../kotlin/internal/interop/RealmInterop.kt | 2 +- .../interop/sync/WebSocketTransport.kt | 4 ++-- .../kotlin/internal/interop/RealmInterop.kt | 4 ++-- .../src/main/jni/realm_api_helpers.cpp | 5 +++-- .../src/main/jni/realm_api_helpers.h | 2 +- .../internal/KtorWebSocketTransport.kt | 21 ++++++++++--------- .../test/mongodb/common/SyncedRealmTests.kt | 3 ++- 7 files changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 00b64d550a..f63caff361 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -810,7 +810,7 @@ expect object RealmInterop { fun realm_sync_socket_websocket_error(nativePointer: RealmWebsocketProviderPointer) - fun realm_sync_socket_websocket_message(nativePointer: RealmWebsocketProviderPointer, data: ByteArray) + fun realm_sync_socket_websocket_message(nativePointer: RealmWebsocketProviderPointer, data: ByteArray) : Boolean fun realm_sync_socket_websocket_closed(nativePointer: RealmWebsocketProviderPointer, wasClean: Boolean, errorCode: WebsocketErrorCode, reason: String = "") } diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt index eb9543378e..1e214f0331 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt @@ -71,8 +71,8 @@ class WebSocketObserver(private val webSocketObserverPointer: RealmWebsocketProv RealmInterop.realm_sync_socket_websocket_error(webSocketObserverPointer) } - fun onNewMessage(data: ByteArray) { - RealmInterop.realm_sync_socket_websocket_message(webSocketObserverPointer, data) + fun onNewMessage(data: ByteArray) : Boolean { + return RealmInterop.realm_sync_socket_websocket_message(webSocketObserverPointer, data) } fun onClose(wasClean: Boolean, errorCode: WebsocketErrorCode, reason: String) { diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index f06b2cdcee..daffb071bb 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -1999,8 +1999,8 @@ actual object RealmInterop { realmc.realm_sync_websocket_error(nativePointer.cptr()) } - actual fun realm_sync_socket_websocket_message(nativePointer: RealmWebsocketProviderPointer, data: ByteArray) {//data: String, size: ULong - realmc.realm_sync_websocket_message(nativePointer.cptr(), data, data.size.toLong()) + actual fun realm_sync_socket_websocket_message(nativePointer: RealmWebsocketProviderPointer, data: ByteArray) : Boolean { + return realmc.realm_sync_websocket_message(nativePointer.cptr(), data, data.size.toLong()) } actual fun realm_sync_socket_websocket_closed(nativePointer: RealmWebsocketProviderPointer, wasClean: Boolean, errorCode: WebsocketErrorCode, reason: String) { diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index 8adbb6e273..473773906f 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -867,13 +867,14 @@ void realm_sync_websocket_error(int64_t observer_ptr) { realm_sync_socket_websocket_error(reinterpret_cast(observer_ptr)); } -void realm_sync_websocket_message(int64_t observer_ptr, jbyteArray data, size_t size) { +bool realm_sync_websocket_message(int64_t observer_ptr, jbyteArray data, size_t size) { auto jenv = get_env(true); jbyte* byteData = jenv->GetByteArrayElements(data, NULL); std::unique_ptr charData(new char[size]); // not null terminated (used in util::Span with size parameter) std::memcpy(charData.get(), byteData, size); - realm_sync_socket_websocket_message(reinterpret_cast(observer_ptr), charData.get(), size); + bool close_websocket = !realm_sync_socket_websocket_message(reinterpret_cast(observer_ptr), charData.get(), size); jenv->ReleaseByteArrayElements(data, byteData, JNI_ABORT); + return close_websocket; } void realm_sync_websocket_closed(int64_t observer_ptr, bool was_clean, int error_code, const char* reason) { diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h index 25430bdded..ce26351b5f 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h @@ -145,7 +145,7 @@ void realm_sync_websocket_connected(int64_t observer_ptr, const char* protocol); void realm_sync_websocket_error(int64_t observer_ptr); -void realm_sync_websocket_message(int64_t observer_ptr, jbyteArray data, size_t size); +bool realm_sync_websocket_message(int64_t observer_ptr, jbyteArray data, size_t size); void realm_sync_websocket_closed(int64_t observer_ptr, bool was_clean, int error_code, const char* reason); diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt index cfc313934b..177cafa755 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt @@ -177,7 +177,10 @@ public class KtorWebSocketTransport( incoming.consumeEach { when (val frame = it) { is Frame.Binary -> { - binaryBuffer.appendAndSend(frame) + val shouldCloseSocket = binaryBuffer.appendAndSend(frame) + if (shouldCloseSocket) { + closeWebsocket() + } } is Frame.Close -> { @@ -197,12 +200,6 @@ public class KtorWebSocketTransport( ?: "Received Close from Websocket server" observer.onClose(true, errorCode, reason) - close( - CloseReason( - CloseReason.Codes.NORMAL, - "Server closed the Websocket" - ) - ) } is Frame.Text -> { @@ -287,11 +284,14 @@ public class KtorWebSocketTransport( * * Note: Core doesn't send fragmented Frames, so this buffering only needed when reading from the websocket. */ -private class FrameBuffer(val sendDefragmentedMessageToObserver: (binaryMessage: ByteArray) -> Unit) { +private class FrameBuffer(val sendDefragmentedMessageToObserver: (binaryMessage: ByteArray) -> Boolean) { private val buffer = mutableListOf() private var currentSize = 0 - fun appendAndSend(frame: Frame) { + /** + * @return True if we should close the Websocket after this write. + */ + fun appendAndSend(frame: Frame) : Boolean { if (frame.data.isNotEmpty()) { buffer.add(frame.data) currentSize += frame.data.size @@ -299,8 +299,9 @@ private class FrameBuffer(val sendDefragmentedMessageToObserver: (binaryMessage: if (frame.fin) { // Append fragmented Frames and flush the buffer - sendDefragmentedMessageToObserver(flush()) + return sendDefragmentedMessageToObserver(flush()) } + return false } private fun flush(): ByteArray { diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index e486bd9e4d..41c3c4657b 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -1260,7 +1260,8 @@ class SyncedRealmTests { } assertTrue(customLogger.logs.isNotEmpty()) assertTrue(customLogger.logs.any { it.contains("Connection[1]: Negotiated protocol version:") }, "Missing Connection[1]") - assertTrue(customLogger.logs.any { it.contains("MyCustomApp/1.0.0") }, "Missing MyCustomApp/1.0.0") + // user_agent is not populated in the new socket provider + // assertTrue(customLogger.logs.any { it.contains("MyCustomApp/1.0.0") }, "Missing MyCustomApp/1.0.0") } } From 698e86a9d5e0a31020178bee1a4b52373b9cbb53 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Fri, 6 Oct 2023 12:55:53 +0100 Subject: [PATCH 05/45] Fixing Swig --- .../io/realm/kotlin/internal/interop/RealmInterop.kt | 9 +++++---- packages/jni-swig-stub/realm.i | 6 +++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 9ce926f646..9bbc58883f 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -38,6 +38,7 @@ import io.realm.kotlin.internal.interop.sync.SyncUserIdentity import io.realm.kotlin.internal.interop.sync.WebSocketClient import io.realm.kotlin.internal.interop.sync.WebSocketObserver import io.realm.kotlin.internal.interop.sync.WebSocketTransport +import io.realm.kotlin.internal.interop.sync.WebsocketCallbackResult import io.realm.kotlin.internal.interop.sync.WebsocketErrorCode import kotlinx.atomicfu.AtomicBoolean import kotlinx.atomicfu.atomic @@ -2821,7 +2822,7 @@ actual object RealmInterop { realm_release(realmSyncSocketNew) } - actual fun realm_sync_socket_callback_complete(nativePointer: RealmWebsocketHandlerCallbackPointer, cancelled: Boolean, status: WebsocketErrorCode, reason: String) { + actual fun realm_sync_socket_callback_complete(nativePointer: RealmWebsocketHandlerCallbackPointer, cancelled: Boolean, status: WebsocketCallbackResult, reason: String) { safeUserData(nativePointer.cptr())(cancelled, status, reason) disposeUserData(nativePointer.cptr()) } @@ -2834,8 +2835,8 @@ actual object RealmInterop { realm_wrapper.realm_sync_socket_websocket_error(nativePointer.cptr()) } - actual fun realm_sync_socket_websocket_message(nativePointer: RealmWebsocketProviderPointer, data: ByteArray) { - realm_wrapper.realm_sync_socket_websocket_message(nativePointer.cptr(), data.toCValues(), data.size.toULong()) + actual fun realm_sync_socket_websocket_message(nativePointer: RealmWebsocketProviderPointer, data: ByteArray) : Boolean { + return realm_wrapper.realm_sync_socket_websocket_message(nativePointer.cptr(), data.toCValues(), data.size.toULong()) } actual fun realm_sync_socket_websocket_closed(nativePointer: RealmWebsocketProviderPointer, wasClean: Boolean, errorCode: WebsocketErrorCode, reason: String) { @@ -3555,7 +3556,7 @@ actual object RealmInterop { } } -private typealias WebsocketFunctionHandlerCallback = (Boolean, WebsocketErrorCode, String) -> Unit +private typealias WebsocketFunctionHandlerCallback = (Boolean, WebsocketCallbackResult, String) -> Unit fun realm_value_t.asByteArray(): ByteArray { if (this.type != realm_value_type.RLM_TYPE_BINARY) { diff --git a/packages/jni-swig-stub/realm.i b/packages/jni-swig-stub/realm.i index 8ce3632de6..8bc6d3b223 100644 --- a/packages/jni-swig-stub/realm.i +++ b/packages/jni-swig-stub/realm.i @@ -330,6 +330,11 @@ bool realm_object_is_valid(const realm_object_t*); } jresult = (jboolean)result; } + +%typemap(javaimports) realm_sync_socket_callback_result %{ +import static io.realm.kotlin.internal.interop.realm_errno_e.*; +%} + // Just showcasing a wrapping concept. Maybe we should just go with `long` (apply void* as above) //%typemap(jstype) realm_t* "LongPointerWrapper" //%typemap(javain) realm_t* "$javainput.ptr()" @@ -431,4 +436,3 @@ bool realm_object_is_valid(const realm_object_t*); %include "realm.h" %include "realm/error_codes.h" %include "src/main/jni/realm_api_helpers.h" - From d64b0b2e987831d07d56a76848b5d7bcbb3ada48 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Fri, 6 Oct 2023 13:34:55 +0100 Subject: [PATCH 06/45] Fixing indentation --- .../kotlin/internal/interop/ErrorCode.kt | 1 - .../kotlin/internal/interop/RealmInterop.kt | 7 ++-- .../interop/sync/WebSocketTransport.kt | 3 +- .../kotlin/internal/interop/RealmInterop.kt | 5 ++- .../interop/sync/ProtocolErrorCode.kt | 1 - .../kotlin/internal/interop/RealmInterop.kt | 29 +++++++++++++---- .../interop/sync/ProtocolErrorCode.kt | 1 - .../realm/kotlin/mongodb/AppConfiguration.kt | 4 +-- .../mongodb/internal/AppConfigurationImpl.kt | 2 +- .../internal/KtorWebSocketTransport.kt | 32 ++++++++++--------- .../mongodb/internal/HttpClientCache.kt | 7 ++-- 11 files changed, 55 insertions(+), 37 deletions(-) diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 4560a9f889..31db69c4c8 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -187,4 +187,3 @@ expect enum class ErrorCode : CodeDescription { fun of(nativeValue: Int): ErrorCode? } } - diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index f63caff361..5b28847954 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -30,8 +30,8 @@ import io.realm.kotlin.internal.interop.sync.ProgressDirection import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode import io.realm.kotlin.internal.interop.sync.SyncUserIdentity import io.realm.kotlin.internal.interop.sync.WebSocketTransport -import io.realm.kotlin.internal.interop.sync.WebsocketErrorCode import io.realm.kotlin.internal.interop.sync.WebsocketCallbackResult +import io.realm.kotlin.internal.interop.sync.WebsocketErrorCode import kotlinx.coroutines.CoroutineDispatcher import org.mongodb.kbson.ObjectId import kotlin.jvm.JvmInline @@ -810,7 +810,10 @@ expect object RealmInterop { fun realm_sync_socket_websocket_error(nativePointer: RealmWebsocketProviderPointer) - fun realm_sync_socket_websocket_message(nativePointer: RealmWebsocketProviderPointer, data: ByteArray) : Boolean + fun realm_sync_socket_websocket_message( + nativePointer: RealmWebsocketProviderPointer, + data: ByteArray + ): Boolean fun realm_sync_socket_websocket_closed(nativePointer: RealmWebsocketProviderPointer, wasClean: Boolean, errorCode: WebsocketErrorCode, reason: String = "") } diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt index 1e214f0331..14f7458a35 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt @@ -13,6 +13,7 @@ interface WebSocketTransport { handlerCallback: RealmWebsocketHandlerCallbackPointer, ): CancellableTimer + @Suppress("LongParameterList") fun connect( observer: WebSocketObserver, path: String, @@ -71,7 +72,7 @@ class WebSocketObserver(private val webSocketObserverPointer: RealmWebsocketProv RealmInterop.realm_sync_socket_websocket_error(webSocketObserverPointer) } - fun onNewMessage(data: ByteArray) : Boolean { + fun onNewMessage(data: ByteArray): Boolean { return RealmInterop.realm_sync_socket_websocket_message(webSocketObserverPointer, data) } diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index daffb071bb..bd0911c35a 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -1999,7 +1999,10 @@ actual object RealmInterop { realmc.realm_sync_websocket_error(nativePointer.cptr()) } - actual fun realm_sync_socket_websocket_message(nativePointer: RealmWebsocketProviderPointer, data: ByteArray) : Boolean { + actual fun realm_sync_socket_websocket_message( + nativePointer: RealmWebsocketProviderPointer, + data: ByteArray + ): Boolean { return realmc.realm_sync_websocket_message(nativePointer.cptr(), data, data.size.toLong()) } diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt index 3a1a74f450..c01be29fc1 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt @@ -137,7 +137,6 @@ actual enum class WebsocketErrorCode( } } - actual enum class WebsocketCallbackResult(override val description: String, override val nativeValue: Int) : CodeDescription { RLM_ERR_SYNC_SOCKET_SUCCESS( diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 9bbc58883f..35dbf6b246 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -2730,10 +2730,12 @@ actual object RealmInterop { disposeUserData(userdata) }, post_func = staticCFunction { userdata: CPointer?, syncSocketCallback: CPointer? -> - val callback: WebsocketFunctionHandlerCallback = { cancelled , _, _ -> - realm_wrapper.realm_sync_socket_post_complete(syncSocketCallback, + val callback: WebsocketFunctionHandlerCallback = { cancelled, _, _ -> + realm_wrapper.realm_sync_socket_post_complete( + syncSocketCallback, if (cancelled) WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED.asNativeEnum else WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_SUCCESS.asNativeEnum, - "") + "" + ) } safeUserData(userdata).post( @@ -2745,7 +2747,11 @@ actual object RealmInterop { if (cancelled) { realm_wrapper.realm_sync_socket_timer_canceled(syncSocketCallback) } else { - realm_wrapper.realm_sync_socket_timer_complete(syncSocketCallback, WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_SUCCESS.asNativeEnum, "") + realm_wrapper.realm_sync_socket_timer_complete( + syncSocketCallback, + WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_SUCCESS.asNativeEnum, + "" + ) } } @@ -2803,7 +2809,9 @@ actual object RealmInterop { webSocketClient, this, length.toLong(), - CPointerWrapper(StableRef.create(postWriteCallback).asCPointer()) + CPointerWrapper( + StableRef.create(postWriteCallback).asCPointer() + ) ) } } @@ -2835,8 +2843,15 @@ actual object RealmInterop { realm_wrapper.realm_sync_socket_websocket_error(nativePointer.cptr()) } - actual fun realm_sync_socket_websocket_message(nativePointer: RealmWebsocketProviderPointer, data: ByteArray) : Boolean { - return realm_wrapper.realm_sync_socket_websocket_message(nativePointer.cptr(), data.toCValues(), data.size.toULong()) + actual fun realm_sync_socket_websocket_message( + nativePointer: RealmWebsocketProviderPointer, + data: ByteArray + ): Boolean { + return realm_wrapper.realm_sync_socket_websocket_message( + nativePointer.cptr(), + data.toCValues(), + data.size.toULong() + ) } actual fun realm_sync_socket_websocket_closed(nativePointer: RealmWebsocketProviderPointer, wasClean: Boolean, errorCode: WebsocketErrorCode, reason: String) { diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt index 2854bfaef5..805184bc5c 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt @@ -16,7 +16,6 @@ package io.realm.kotlin.internal.interop.sync import io.realm.kotlin.internal.interop.CodeDescription -import realm_wrapper.realm_errno import realm_wrapper.realm_sync_errno_connection import realm_wrapper.realm_sync_errno_session import realm_wrapper.realm_sync_socket_callback_result diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt index 8b6cc50da8..75e9a0606e 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt @@ -406,8 +406,8 @@ public interface AppConfiguration { ) } - val websocketTransport: ((DispatcherHolder) -> WebSocketTransport)? = if (usePlatformNetworking) - { dispatcherHolder -> + val websocketTransport: ((DispatcherHolder) -> WebSocketTransport)? = + if (usePlatformNetworking) { dispatcherHolder -> websocketTransport ?: KtorWebSocketTransport( timeoutMs = 60000, dispatcherHolder = dispatcherHolder diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt index 7fad855d3b..ca3d0b7bdb 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt @@ -150,7 +150,7 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) webSocketTransport: WebSocketTransport?, sdkInfo: String?, applicationInfo: String? - ): RealmSyncClientConfigurationPointer = + ): RealmSyncClientConfigurationPointer = RealmInterop.realm_sync_client_config_new() .also { syncClientConfig -> // Initialize client configuration first diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt index 177cafa755..d21d3615f7 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt @@ -28,7 +28,6 @@ import kotlinx.atomicfu.atomic import kotlinx.coroutines.CompletableJob import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel @@ -55,8 +54,7 @@ public class KtorWebSocketTransport( when (completionHandler) { null -> runCallback(handlerCallback) else -> runCallback( - handlerCallback, - cancelled = true + handlerCallback, cancelled = true ) } } @@ -68,12 +66,14 @@ public class KtorWebSocketTransport( handlerCallback: RealmWebsocketHandlerCallbackPointer ): CancellableTimer { val atomic: AtomicRef = atomic(handlerCallback) - return CancellableTimer(scope.launch { - delay(delayInMilliseconds) - atomic.getAndSet(null)?.run { - runCallback(handlerCallback) + return CancellableTimer( + scope.launch { + delay(delayInMilliseconds) + atomic.getAndSet(null)?.run { + runCallback(handlerCallback) + } } - }) { // Cancel lambda + ) { // Cancel lambda scope.launch { atomic.getAndSet(null)?.run { runCallback(handlerCallback, cancelled = true) @@ -82,7 +82,6 @@ public class KtorWebSocketTransport( } } - @OptIn(ExperimentalCoroutinesApi::class) override fun connect( observer: WebSocketObserver, path: String, @@ -114,6 +113,7 @@ public class KtorWebSocketTransport( openConnection() } + @Suppress("LongMethod") private fun openConnection() { scope.launch { client.webSocket( @@ -137,15 +137,16 @@ public class KtorWebSocketTransport( reason = "Websocket server responded with status code ${call.response.status} instead of ${HttpStatusCode.SwitchingProtocols}" ) } else { - when (val selectedProtocol = - call.response.headers[HttpHeaders.SecWebSocketProtocol]) { + when ( + val selectedProtocol = + call.response.headers[HttpHeaders.SecWebSocketProtocol] + ) { null -> { observer.onError() observer.onClose( false, WebsocketErrorCode.RLM_ERR_WEBSOCKET_PROTOCOLERROR, - "${HttpHeaders.SecWebSocketProtocol} header not returned. Sync server didn't return supported protocol" + - ". Supported protocols are = $supportedProtocols" + "${HttpHeaders.SecWebSocketProtocol} header not returned. Sync server didn't return supported protocol" + ". Supported protocols are = $supportedProtocols" ) close( CloseReason( @@ -177,7 +178,8 @@ public class KtorWebSocketTransport( incoming.consumeEach { when (val frame = it) { is Frame.Binary -> { - val shouldCloseSocket = binaryBuffer.appendAndSend(frame) + val shouldCloseSocket = + binaryBuffer.appendAndSend(frame) if (shouldCloseSocket) { closeWebsocket() } @@ -291,7 +293,7 @@ private class FrameBuffer(val sendDefragmentedMessageToObserver: (binaryMessage: /** * @return True if we should close the Websocket after this write. */ - fun appendAndSend(frame: Frame) : Boolean { + fun appendAndSend(frame: Frame): Boolean { if (frame.data.isNotEmpty()) { buffer.add(frame.data) currentSize += frame.data.size diff --git a/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt b/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt index d06b001986..73a5138713 100644 --- a/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt +++ b/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt @@ -1,13 +1,10 @@ @file:JvmName("HttpClientCacheJVM") package io.realm.kotlin.mongodb.internal -import io.ktor.client.* +import io.ktor.client.HttpClient +import io.ktor.client.HttpClientConfig import io.ktor.client.engine.cio.CIO -import io.ktor.client.plugins.* import io.ktor.client.plugins.logging.Logger -import io.ktor.client.request.* -import io.ktor.http.* -import io.ktor.util.* /** * Cache HttpClient on Android and JVM. From 16b62ab13804011a6dc6cd57d0eacc9a56140d1c Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Fri, 6 Oct 2023 16:14:38 +0100 Subject: [PATCH 07/45] Fixing test --- .../kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt index 6079e3edea..8bb6a4df18 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt @@ -391,7 +391,7 @@ class AppTests { Realm.open(syncConfig).close() // Create a configuration pointing to the metadata Realm for that app - val lastSetSchemaVersion = 6L + val lastSetSchemaVersion = 7L val metadataDir = "${app.configuration.syncRootDirectory}/mongodb-realm/${app.configuration.appId}/server-utility/metadata/" val config = RealmConfiguration .Builder(setOf()) From 3505926eb788eb2c490988c5aa73224b2f4342b9 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Fri, 6 Oct 2023 23:19:34 +0100 Subject: [PATCH 08/45] Adding a flag to run new platform selectively on CI --- Jenkinsfile | 6 ++--- packages/test-sync/build.gradle.kts | 2 ++ .../io/realm/kotlin/test/mongodb/TestApp.kt | 25 +++++++++++-------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index c3ff69b098..3110be989e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -188,7 +188,7 @@ pipeline { "integrationtest", { forwardAdbPorts() - testAndCollect("packages", "cleanAllTests -PincludeSdkModules=false connectedAndroidTest") + testAndCollect("packages", "cleanAllTests -PREALM_USE_PLATFORM_NETWORKING=true -PincludeSdkModules=false connectedAndroidTest") } ) } @@ -212,7 +212,7 @@ pipeline { steps { testWithServer([ { - testAndCollect("packages", 'cleanAllTests jvmTest -PincludeSdkModules=false ') + testAndCollect("packages", 'cleanAllTests jvmTest -PREALM_USE_PLATFORM_NETWORKING=true -PincludeSdkModules=false ') } ]) } @@ -232,7 +232,7 @@ pipeline { steps { testWithServer([ { - testAndCollect("packages", 'cleanAllTests :test-sync:connectedAndroidtest -PincludeSdkModules=false -PtestBuildType=debugMinified') + testAndCollect("packages", 'cleanAllTests :test-sync:connectedAndroidtest -PREALM_USE_PLATFORM_NETWORKING=true -PincludeSdkModules=false -PtestBuildType=debugMinified') } ]) sh 'rm mapping.zip || true' diff --git a/packages/test-sync/build.gradle.kts b/packages/test-sync/build.gradle.kts index d04f38661f..a5bdfb3bbb 100644 --- a/packages/test-sync/build.gradle.kts +++ b/packages/test-sync/build.gradle.kts @@ -17,6 +17,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetWithSimulatorTests import com.codingfeline.buildkonfig.compiler.FieldSpec.Type +import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly plugins { id("org.jetbrains.kotlin.multiplatform") @@ -313,6 +314,7 @@ buildkonfig { buildConfigField(Type.STRING, "privateApiKey", "") } buildConfigField(Type.STRING, "clusterName", getPropertyValue("syncTestClusterName") ?: "") + buildConfigField(Type.BOOLEAN, "usePlatformNetworking", getPropertyValue("REALM_USE_PLATFORM_NETWORKING")?.toLowerCaseAsciiOnly() ?: "false") } } diff --git a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt index 171fc61dfc..c946abff20 100644 --- a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt @@ -195,20 +195,25 @@ open class TestApp private constructor( } @Suppress("invisible_member", "invisible_reference") - var config = AppConfiguration.Builder(appAdmin.clientAppId) + val config = AppConfiguration.Builder(appAdmin.clientAppId) .baseUrl(TEST_SERVER_BASE_URL) .networkTransport(networkTransport) .ejson(ejson) - .usePlatformNetworking() - .apply { - if (logLevel != null) { - log( - logLevel, - if (customLogger == null) emptyList() - else listOf(customLogger) - ) - } + if (SyncServerConfig.usePlatformNetworking) { + println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> REALM_USE_PLATFORM_NETWORKING is set") + config.usePlatformNetworking() + } else { + println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> REALM_USE_PLATFORM_NETWORKING is NOT SET") + } + config.apply { + if (logLevel != null) { + log( + logLevel, + if (customLogger == null) emptyList() + else listOf(customLogger) + ) } + } val app = App.create( builder(config) From aa38a7417c449a7597b394106619d353aca68621 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 9 Oct 2023 15:15:18 +0100 Subject: [PATCH 09/45] Fixing tests/cleanup --- .../kotlin/internal/interop/RealmInterop.kt | 46 +++++++++---------- .../io/realm/kotlin/test/mongodb/TestApp.kt | 3 -- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 35dbf6b246..e7773afbd3 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -131,6 +131,7 @@ import realm_wrapper.realm_sync_socket_t import realm_wrapper.realm_sync_socket_timer_t import realm_wrapper.realm_sync_socket_websocket_t import realm_wrapper.realm_t +import realm_wrapper.realm_user_identity import realm_wrapper.realm_user_t import realm_wrapper.realm_value_t import realm_wrapper.realm_value_type @@ -2268,29 +2269,28 @@ actual object RealmInterop { } actual fun realm_user_get_all_identities(user: RealmUserPointer): List { - TODO("Not Valid") -// memScoped { -// val count = AuthProvider.values().size -// val properties = allocArray(count) -// val outCount = alloc() -// realm_wrapper.realm_user_get_all_identities( -// user.cptr(), -// properties, -// count.convert(), -// outCount.ptr -// ) -// outCount.value.toLong().let { count -> -// return if (count > 0) { -// (0 until outCount.value.toLong()).map { -// with(properties[it]) { -// SyncUserIdentity(this.id!!.toKString(), AuthProvider.of(this.provider_type)) -// } -// } -// } else { -// emptyList() -// } -// } -// } + memScoped { + val count = AuthProvider.values().size + val properties = allocArray(count) + val outCount = alloc() + realm_wrapper.realm_user_get_all_identities( + user.cptr(), + properties, + count.convert(), + outCount.ptr + ) + outCount.value.toLong().let { count -> + return if (count > 0) { + (0 until outCount.value.toLong()).map { + with(properties[it]) { + SyncUserIdentity(this.id!!.toKString(), AuthProvider.of(this.provider_type)) + } + } + } else { + emptyList() + } + } + } } actual fun realm_user_get_identity(user: RealmUserPointer): String { diff --git a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt index c946abff20..e779f127c1 100644 --- a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt @@ -200,10 +200,7 @@ open class TestApp private constructor( .networkTransport(networkTransport) .ejson(ejson) if (SyncServerConfig.usePlatformNetworking) { - println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> REALM_USE_PLATFORM_NETWORKING is set") config.usePlatformNetworking() - } else { - println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> REALM_USE_PLATFORM_NETWORKING is NOT SET") } config.apply { if (logLevel != null) { From 01e6d623d3362b77847a40f9e487732be4b2f9d1 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Tue, 10 Oct 2023 11:49:12 +0100 Subject: [PATCH 10/45] Using daemon thread for sync client while Core add support to Thread Observer to detach it --- .../src/main/jni/realm_api_helpers.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index 473773906f..0f7cc7d95f 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -682,7 +682,7 @@ using WebsocketFunctionHandlerCallback = std::function", "(JZ)V"); @@ -705,7 +705,7 @@ static void websocket_post_func(realm_userdata_t userdata, static realm_sync_socket_timer_t websocket_create_timer_func( realm_userdata_t userdata, uint64_t delay_ms, realm_sync_socket_callback_t* realm_callback) { - auto jenv = get_env(true); + auto jenv = get_env(true, true); // attach as daemon thread static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); static JavaMethod native_pointer_constructor(jenv, native_pointer_class, "", "(JZ)V"); @@ -737,7 +737,7 @@ static void websocket_cancel_timer_func(realm_userdata_t userdata, realm_sync_socket_timer_t timer_userdata) { if (timer_userdata != nullptr) { - auto jenv = get_env(true); + auto jenv = get_env(true, true); // attach as daemon thread jobject cancellable_timer = static_cast(timer_userdata); static JavaClass cancellable_timer_class(jenv, "io/realm/kotlin/internal/interop/sync/CancellableTimer"); @@ -753,7 +753,7 @@ static realm_sync_socket_websocket_t websocket_connect_func( realm_userdata_t userdata, realm_websocket_endpoint_t endpoint, realm_websocket_observer_t* realm_websocket_observer) { - auto jenv = get_env(true); + auto jenv = get_env(true, true); // attach as daemon thread static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); static JavaMethod native_pointer_constructor(jenv, native_pointer_class, "", "(JZ)V"); @@ -795,7 +795,7 @@ static void websocket_async_write_func(realm_userdata_t userdata, realm_sync_socket_websocket_t websocket_userdata, const char* data, size_t size, realm_sync_socket_callback_t* realm_callback) { - auto jenv = get_env(true); + auto jenv = get_env(true, true); // attach as daemon thread static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); static JavaMethod native_pointer_constructor(jenv, native_pointer_class, "", "(JZ)V"); @@ -827,7 +827,7 @@ static void websocket_async_write_func(realm_userdata_t userdata, static void realm_sync_websocket_free(realm_userdata_t userdata, realm_sync_socket_websocket_t websocket_userdata) { if (websocket_userdata != nullptr) { - auto jenv = get_env(true); + auto jenv = get_env(true, true); // attach as daemon thread static jclass websocket_client_class = jenv->FindClass("io/realm/kotlin/internal/interop/sync/WebSocketClient"); static jmethodID close_method = jenv->GetMethodID(websocket_client_class, "closeWebsocket", "()V"); @@ -840,7 +840,7 @@ static void realm_sync_websocket_free(realm_userdata_t userdata, static void realm_sync_userdata_free(realm_userdata_t userdata) { if (userdata != nullptr) { - auto jenv = get_env(true); + auto jenv = get_env(true, true); // attach as daemon thread static jclass websocket_client_class = jenv->FindClass("io/realm/kotlin/internal/interop/sync/WebSocketTransport"); static jmethodID close_method = jenv->GetMethodID(websocket_client_class, "close", "()V"); @@ -868,7 +868,7 @@ void realm_sync_websocket_error(int64_t observer_ptr) { } bool realm_sync_websocket_message(int64_t observer_ptr, jbyteArray data, size_t size) { - auto jenv = get_env(true); + auto jenv = get_env(true, true); // attach as daemon thread jbyte* byteData = jenv->GetByteArrayElements(data, NULL); std::unique_ptr charData(new char[size]); // not null terminated (used in util::Span with size parameter) std::memcpy(charData.get(), byteData, size); From c92871cf36434ce8dc4a594892df0d4a339d814f Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Tue, 10 Oct 2023 23:37:29 +0100 Subject: [PATCH 11/45] Proguard fix --- .../src/main/jni/realm_api_helpers.cpp | 17 +++++++++-------- .../proguard-rules-consumer-common.pro | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index 0f7cc7d95f..2fe83ed427 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -682,6 +682,7 @@ using WebsocketFunctionHandlerCallback = std::function", "(JZ)V"); @@ -705,7 +706,8 @@ static void websocket_post_func(realm_userdata_t userdata, static realm_sync_socket_timer_t websocket_create_timer_func( realm_userdata_t userdata, uint64_t delay_ms, realm_sync_socket_callback_t* realm_callback) { - auto jenv = get_env(true, true); // attach as daemon thread + // called from main thread/event loop which should be already attached to JVM + auto jenv = get_env(false); static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); static JavaMethod native_pointer_constructor(jenv, native_pointer_class, "", "(JZ)V"); @@ -735,9 +737,8 @@ static realm_sync_socket_timer_t websocket_create_timer_func( static void websocket_cancel_timer_func(realm_userdata_t userdata, realm_sync_socket_timer_t timer_userdata) { - if (timer_userdata != nullptr) { - auto jenv = get_env(true, true); // attach as daemon thread + auto jenv = get_env(false); jobject cancellable_timer = static_cast(timer_userdata); static JavaClass cancellable_timer_class(jenv, "io/realm/kotlin/internal/interop/sync/CancellableTimer"); @@ -753,7 +754,7 @@ static realm_sync_socket_websocket_t websocket_connect_func( realm_userdata_t userdata, realm_websocket_endpoint_t endpoint, realm_websocket_observer_t* realm_websocket_observer) { - auto jenv = get_env(true, true); // attach as daemon thread + auto jenv = get_env(false); static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); static JavaMethod native_pointer_constructor(jenv, native_pointer_class, "", "(JZ)V"); @@ -795,7 +796,7 @@ static void websocket_async_write_func(realm_userdata_t userdata, realm_sync_socket_websocket_t websocket_userdata, const char* data, size_t size, realm_sync_socket_callback_t* realm_callback) { - auto jenv = get_env(true, true); // attach as daemon thread + auto jenv = get_env(false); static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); static JavaMethod native_pointer_constructor(jenv, native_pointer_class, "", "(JZ)V"); @@ -827,7 +828,7 @@ static void websocket_async_write_func(realm_userdata_t userdata, static void realm_sync_websocket_free(realm_userdata_t userdata, realm_sync_socket_websocket_t websocket_userdata) { if (websocket_userdata != nullptr) { - auto jenv = get_env(true, true); // attach as daemon thread + auto jenv = get_env(false); static jclass websocket_client_class = jenv->FindClass("io/realm/kotlin/internal/interop/sync/WebSocketClient"); static jmethodID close_method = jenv->GetMethodID(websocket_client_class, "closeWebsocket", "()V"); @@ -840,7 +841,7 @@ static void realm_sync_websocket_free(realm_userdata_t userdata, static void realm_sync_userdata_free(realm_userdata_t userdata) { if (userdata != nullptr) { - auto jenv = get_env(true, true); // attach as daemon thread + auto jenv = get_env(false); static jclass websocket_client_class = jenv->FindClass("io/realm/kotlin/internal/interop/sync/WebSocketTransport"); static jmethodID close_method = jenv->GetMethodID(websocket_client_class, "close", "()V"); @@ -868,7 +869,7 @@ void realm_sync_websocket_error(int64_t observer_ptr) { } bool realm_sync_websocket_message(int64_t observer_ptr, jbyteArray data, size_t size) { - auto jenv = get_env(true, true); // attach as daemon thread + auto jenv = get_env(false); jbyte* byteData = jenv->GetByteArrayElements(data, NULL); std::unique_ptr charData(new char[size]); // not null terminated (used in util::Span with size parameter) std::memcpy(charData.get(), byteData, size); diff --git a/packages/library-base/proguard-rules-consumer-common.pro b/packages/library-base/proguard-rules-consumer-common.pro index 3eb86435de..3d73db1d9a 100644 --- a/packages/library-base/proguard-rules-consumer-common.pro +++ b/packages/library-base/proguard-rules-consumer-common.pro @@ -127,6 +127,20 @@ *; } +# Platform networking callback +-keep class io.realm.kotlin.internal.interop.sync.WebSocketTransport { + *; +} +-keep class io.realm.kotlin.internal.interop.sync.CancellableTimer { + *; +} +-keep class io.realm.kotlin.internal.interop.sync.WebSocketClient { + *; +} +-keep class io.realm.kotlin.internal.interop.sync.WebSocketObserver { + *; +} + # Un-comment for debugging #-printconfiguration /tmp/full-r8-config.txt #-keepattributes LineNumberTable,SourceFile From 2c6807590a8ba9f1c9f5726f54e11b2cc3353dbc Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 11 Oct 2023 13:10:59 +0100 Subject: [PATCH 12/45] Update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc76998202..1b5b02583a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * [Sync] If calling a function on App Services that resulted in a redirect, it would only redirect for GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) * [Sync] If calling a function on App Services that resulted in a redirect, it would only redirect for GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) +* [Sync] Added option to use managed WebSockets via Ktor instead of Realm's built-in WebSocket client for Sync traffic. Managed WebSockets offer improved support for proxies and firewalls that require authentication. This feature is currently opt-in and can be enabled by using `AppConfiguration.usePlatformNetworking()`. Managed WebSockets will become the default in a future version. (PR [#1528](https://github.com/realm/realm-kotlin/pull/1528)). Currently only JVM and Android platforms are supported. ### Compatibility * File format: Generates Realms with file format v23. @@ -32,7 +33,7 @@ GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) * Minimum Android SDK: 16. ### Internal -* None. +* Updated to Realm Core 13.22.0, commit 9fe0653fef672f14c771019ba27d54d568f622d0. ## 1.11.1 (2023-09-07) From bb4105b1b9c8399ee7dc63ea415ee7ac95b38f3a Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 12 Oct 2023 11:48:15 +0100 Subject: [PATCH 13/45] Initial PR feedback --- CHANGELOG.md | 2 ++ .../kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt | 10 ++++++++++ .../realm/kotlin/mongodb/internal/HttpClientCache.kt | 1 - 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b5b02583a..6a9bbaee3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) ### Internal * Updated to Realm Core 13.22.0, commit 9fe0653fef672f14c771019ba27d54d568f622d0. +* Update to Ktor 2.3.4. +* Switched Ktor engine to CIO for Android and JVM to work around https://youtrack.jetbrains.com/issue/KTOR-6266. Revert to OkHttp when the issue is fixed. ## 1.11.1 (2023-09-07) diff --git a/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt b/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt index 1effc3cd32..5eedd9daa1 100644 --- a/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt +++ b/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt @@ -28,6 +28,7 @@ import io.realm.kotlin.internal.interop.realm_sync_errno_connection_e import io.realm.kotlin.internal.interop.realm_sync_errno_session_e import io.realm.kotlin.internal.interop.realm_sync_session_resync_mode_e import io.realm.kotlin.internal.interop.realm_sync_session_state_e +import io.realm.kotlin.internal.interop.realm_sync_socket_callback_result_e import io.realm.kotlin.internal.interop.realm_user_state_e import io.realm.kotlin.internal.interop.realm_web_socket_errno_e import io.realm.kotlin.internal.interop.sync.AuthProvider @@ -38,6 +39,7 @@ import io.realm.kotlin.internal.interop.sync.MetadataMode import io.realm.kotlin.internal.interop.sync.SyncConnectionErrorCode import io.realm.kotlin.internal.interop.sync.SyncSessionErrorCode import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode +import io.realm.kotlin.internal.interop.sync.WebsocketCallbackResult import io.realm.kotlin.internal.interop.sync.WebsocketErrorCode import org.junit.Test import kotlin.reflect.KClass @@ -135,6 +137,14 @@ class SyncEnumTests { } } + @Test + fun websocketResultCode() { + checkEnum(realm_sync_socket_callback_result_e::class) { nativeValue -> + WebsocketCallbackResult.of(nativeValue) + } + } + + private inline fun checkEnum( enumClass: KClass, mapNativeValue: (Int) -> T? diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt index c733b5d5c8..208769c6f9 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt @@ -48,5 +48,4 @@ internal expect class HttpClientCache(timeoutMs: Long, customLogger: Logger? = n fun close() // Close any resources stored in the cache. } -// TODO use a la private val clientCache: HttpClientCache = HttpClientCache(timeoutMs, logger) public expect fun createPlatformClient(block: HttpClientConfig<*>.() -> Unit): HttpClient From 4de15a9c52655da11692a87e884bbec953365554 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 12 Oct 2023 11:58:04 +0100 Subject: [PATCH 14/45] Fix indentation --- .../kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt b/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt index 5eedd9daa1..4d12f28409 100644 --- a/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt +++ b/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt @@ -144,7 +144,6 @@ class SyncEnumTests { } } - private inline fun checkEnum( enumClass: KClass, mapNativeValue: (Int) -> T? From da49df3791cd282b02cce91cdc8d04d4bf9f1d11 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 12 Oct 2023 14:05:57 +0100 Subject: [PATCH 15/45] Bump Core --- packages/external/core | 2 +- packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/external/core b/packages/external/core index 9fe0653fef..efc8ac30e6 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit 9fe0653fef672f14c771019ba27d54d568f622d0 +Subproject commit efc8ac30e61e1feec687f519130783b96a684873 diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index 2fe83ed427..e67bbebfd7 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -681,7 +681,7 @@ realm_http_transport_t* realm_network_transport_new(jobject network_transport) { using WebsocketFunctionHandlerCallback = std::function; static void websocket_post_func(realm_userdata_t userdata, - realm_sync_socket_callback_t* realm_callback) { + realm_sync_socket_post_callback_t* realm_callback) { // Some calls to 'post' happens from the external commit helper which is not necessarily attached yet to a JVM thread auto jenv = get_env(true, true); // attach as daemon thread static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); @@ -705,7 +705,7 @@ static void websocket_post_func(realm_userdata_t userdata, } static realm_sync_socket_timer_t websocket_create_timer_func( - realm_userdata_t userdata, uint64_t delay_ms, realm_sync_socket_callback_t* realm_callback) { + realm_userdata_t userdata, uint64_t delay_ms, realm_sync_socket_timer_callback_t* realm_callback) { // called from main thread/event loop which should be already attached to JVM auto jenv = get_env(false); static JavaClass native_pointer_class(jenv, @@ -795,7 +795,7 @@ static realm_sync_socket_websocket_t websocket_connect_func( static void websocket_async_write_func(realm_userdata_t userdata, realm_sync_socket_websocket_t websocket_userdata, const char* data, size_t size, - realm_sync_socket_callback_t* realm_callback) { + realm_sync_socket_write_callback_t* realm_callback) { auto jenv = get_env(false); static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); From 7133797711950bfe1d7eec2cb30ad696823879bb Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 12 Oct 2023 14:47:22 +0100 Subject: [PATCH 16/45] Fixing renamed types --- .../io/realm/kotlin/internal/interop/RealmInterop.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index e7773afbd3..9bac79bd14 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -126,10 +126,12 @@ import realm_wrapper.realm_sync_client_metadata_mode import realm_wrapper.realm_sync_session_resync_mode import realm_wrapper.realm_sync_session_state_e import realm_wrapper.realm_sync_session_stop_policy_e -import realm_wrapper.realm_sync_socket_callback_t +import realm_wrapper.realm_sync_socket_post_callback_t import realm_wrapper.realm_sync_socket_t +import realm_wrapper.realm_sync_socket_timer_callback_t import realm_wrapper.realm_sync_socket_timer_t import realm_wrapper.realm_sync_socket_websocket_t +import realm_wrapper.realm_sync_socket_write_callback_t import realm_wrapper.realm_t import realm_wrapper.realm_user_identity import realm_wrapper.realm_user_t @@ -2729,7 +2731,7 @@ actual object RealmInterop { safeUserData(userdata).close() disposeUserData(userdata) }, - post_func = staticCFunction { userdata: CPointer?, syncSocketCallback: CPointer? -> + post_func = staticCFunction { userdata: CPointer?, syncSocketCallback: CPointer? -> val callback: WebsocketFunctionHandlerCallback = { cancelled, _, _ -> realm_wrapper.realm_sync_socket_post_complete( syncSocketCallback, @@ -2742,7 +2744,7 @@ actual object RealmInterop { CPointerWrapper(StableRef.create(callback).asCPointer()) ) }, - create_timer_func = staticCFunction { userdata: CPointer?, delayInMilliseconds: uint64_t, syncSocketCallback: CPointer? -> + create_timer_func = staticCFunction { userdata: CPointer?, delayInMilliseconds: uint64_t, syncSocketCallback: CPointer? -> val callback: WebsocketFunctionHandlerCallback = { cancelled, _, _ -> if (cancelled) { realm_wrapper.realm_sync_socket_timer_canceled(syncSocketCallback) @@ -2792,7 +2794,7 @@ actual object RealmInterop { } } }, - websocket_write_func = staticCFunction { userdata: CPointer?, websocket: realm_sync_socket_websocket_t?, data: CPointer?, length: size_t, callback: CPointer? -> + websocket_write_func = staticCFunction { userdata: CPointer?, websocket: realm_sync_socket_websocket_t?, data: CPointer?, length: size_t, callback: CPointer? -> val postWriteCallback: WebsocketFunctionHandlerCallback = { cancelled, status, reason -> realm_wrapper.realm_sync_socket_write_complete( From 5098395395721de57b4647a9c35c4591e93925f4 Mon Sep 17 00:00:00 2001 From: Clemente Date: Thu, 12 Oct 2023 16:28:18 +0200 Subject: [PATCH 17/45] Bump core to version 13.23.0 --- CHANGELOG.md | 2 +- .../kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt | 1 - .../kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt | 4 ---- .../kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt | 4 ---- packages/external/core | 2 +- .../src/commonMain/kotlin/io/realm/kotlin/mongodb/User.kt | 1 + .../kotlin/io/realm/kotlin/mongodb/internal/UserImpl.kt | 2 +- 7 files changed, 4 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc76998202..702996027e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) * Minimum Android SDK: 16. ### Internal -* None. +* Updated to Realm Core 13.23.0, commit f8604b9fd3d2982008a1d3f5ff35e52ee9098d5b. ## 1.11.1 (2023-09-07) diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 1cd6c153c9..293190fe4d 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -534,7 +534,6 @@ expect object RealmInterop { // User fun realm_user_get_all_identities(user: RealmUserPointer): List fun realm_user_get_identity(user: RealmUserPointer): String - fun realm_user_get_auth_provider(user: RealmUserPointer): AuthProvider fun realm_user_get_access_token(user: RealmUserPointer): String fun realm_user_get_refresh_token(user: RealmUserPointer): String fun realm_user_get_device_id(user: RealmUserPointer): String diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index c2b2a07d8f..cd48ae8854 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -1114,10 +1114,6 @@ actual object RealmInterop { return realmc.realm_user_get_identity(user.cptr()) } - actual fun realm_user_get_auth_provider(user: RealmUserPointer): AuthProvider { - return AuthProvider.of(realmc.realm_user_get_auth_provider(user.cptr())) - } - actual fun realm_user_get_access_token(user: RealmUserPointer): String { return realmc.realm_user_get_access_token(user.cptr()) } diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 46facf20d2..54feccf235 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -2287,10 +2287,6 @@ actual object RealmInterop { return realm_wrapper.realm_user_get_identity(user.cptr()).safeKString("identity") } - actual fun realm_user_get_auth_provider(user: RealmUserPointer): AuthProvider { - return AuthProvider.of(realm_wrapper.realm_user_get_auth_provider(user.cptr())) - } - actual fun realm_user_is_logged_in(user: RealmUserPointer): Boolean { return realm_wrapper.realm_user_is_logged_in(user.cptr()) } diff --git a/packages/external/core b/packages/external/core index c258e2681b..f8604b9fd3 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit c258e2681bca5fb33bbd23c112493817b43bfa86 +Subproject commit f8604b9fd3d2982008a1d3f5ff35e52ee9098d5b 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 fa4d7e3742..1fc0d1cb32 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 @@ -83,6 +83,7 @@ public interface User { * Returns the provider type used to log the user in. * If a user logs out, the authentication provider last used to log the user in will still be returned. */ + @Deprecated("Property not stable, users might have multiple providers.", ReplaceWith("identities")) public val provider: AuthenticationProvider /** 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 c10d3a4971..ec96b5d864 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 @@ -49,7 +49,7 @@ public class UserImpl( override val loggedIn: Boolean get() = RealmInterop.realm_user_is_logged_in(nativePointer) override val provider: AuthenticationProvider - get() = AuthenticationProviderImpl.fromId(RealmInterop.realm_user_get_auth_provider(nativePointer)) + get() = identities.first().provider override val accessToken: String get() = RealmInterop.realm_user_get_access_token(nativePointer) override val refreshToken: String From c0180c5581feaa59eabf73fa6976a3206bdd61f2 Mon Sep 17 00:00:00 2001 From: Clemente Date: Mon, 16 Oct 2023 13:32:36 +0200 Subject: [PATCH 18/45] Remove workaround for realm file name --- .../io/realm/kotlin/mongodb/sync/SyncConfiguration.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt index 6b1e865ae4..675fbf08a2 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt @@ -23,7 +23,6 @@ import io.realm.kotlin.TypedRealm import io.realm.kotlin.internal.ConfigurationImpl import io.realm.kotlin.internal.ContextLogger import io.realm.kotlin.internal.ObjectIdImpl -import io.realm.kotlin.internal.REALM_FILE_EXTENSION import io.realm.kotlin.internal.interop.RealmInterop import io.realm.kotlin.internal.interop.SchemaMode import io.realm.kotlin.internal.platform.PATH_SEPARATOR @@ -606,12 +605,7 @@ public interface SyncConfiguration : Configuration { ) } - // Remove .realm extension if user has overridden filename manually - return if (name != null) { - absolutePath.removeSuffix(REALM_FILE_EXTENSION) - } else { - absolutePath - } + return absolutePath } } From 5234ba4de76bb97c08ffb3bd1465296a124b401b Mon Sep 17 00:00:00 2001 From: Clemente Date: Mon, 16 Oct 2023 14:38:41 +0200 Subject: [PATCH 19/45] Update metadata schema version --- .../kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt index 6079e3edea..8bb6a4df18 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt @@ -391,7 +391,7 @@ class AppTests { Realm.open(syncConfig).close() // Create a configuration pointing to the metadata Realm for that app - val lastSetSchemaVersion = 6L + val lastSetSchemaVersion = 7L val metadataDir = "${app.configuration.syncRootDirectory}/mongodb-realm/${app.configuration.appId}/server-utility/metadata/" val config = RealmConfiguration .Builder(setOf()) From ed8e5f9c1e216196c9f3c5d5c09d8fca1afae82e Mon Sep 17 00:00:00 2001 From: Clemente Date: Mon, 16 Oct 2023 15:17:26 +0200 Subject: [PATCH 20/45] Update name test cases --- .../io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt index 77a80463d5..859a47cafb 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt @@ -1261,8 +1261,8 @@ class SyncConfigTests { val config: SyncConfiguration = SyncConfiguration.Builder(user, partitionValue, setOf()) .name(fileName) .build() - val suffix = pathOf("", "mongodb-realm", user.app.configuration.appId, user.id, fileName) + val suffix = pathOf("", "mongodb-realm", user.app.configuration.appId, user.id, "$fileName.realm") assertTrue(config.path.endsWith(suffix), "${config.path} failed.") - assertEquals(fileName, config.name, "${config.name} failed.") + assertEquals("$fileName.realm", config.name, "${config.name} failed.") } } From 8da19a02805019dca8f22f61c909596c1d19b0d1 Mon Sep 17 00:00:00 2001 From: Clemente Date: Tue, 17 Oct 2023 11:44:19 +0200 Subject: [PATCH 21/45] Update test cases --- CHANGELOG.md | 2 +- packages/external/core | 2 +- .../kotlin/io/realm/kotlin/test/common/RealmTests.kt | 4 ++-- .../io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt | 7 +++++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 702996027e..c399e39a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) * Minimum Android SDK: 16. ### Internal -* Updated to Realm Core 13.23.0, commit f8604b9fd3d2982008a1d3f5ff35e52ee9098d5b. +* Updated to Realm Core 13.23.1, commit c569bec4d04da84030d94f376437bc4efda3686b. ## 1.11.1 (2023-09-07) diff --git a/packages/external/core b/packages/external/core index f8604b9fd3..c569bec4d0 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit f8604b9fd3d2982008a1d3f5ff35e52ee9098d5b +Subproject commit c569bec4d04da84030d94f376437bc4efda3686b diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmTests.kt index a398bc409e..c3f518fc01 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmTests.kt @@ -441,7 +441,7 @@ class RealmTests { @Suppress("LongMethod") fun deleteRealm() { val fileSystem = FileSystem.SYSTEM - val testDir = PlatformUtils.createTempDir("test_dir") + val testDir = PlatformUtils.createTempDir() val testDirPath = testDir.toPath() assertTrue(fileSystem.exists(testDirPath)) @@ -510,7 +510,7 @@ class RealmTests { @Test fun deleteRealm_fileDoesNotExists() { val fileSystem = FileSystem.SYSTEM - val testDir = PlatformUtils.createTempDir("test_dir") + val testDir = PlatformUtils.createTempDir() val configuration = RealmConfiguration.Builder(schema = setOf(Parent::class, Child::class)) .directory(testDir) .build() diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt index 859a47cafb..2635dc277d 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt @@ -1261,8 +1261,11 @@ class SyncConfigTests { val config: SyncConfiguration = SyncConfiguration.Builder(user, partitionValue, setOf()) .name(fileName) .build() - val suffix = pathOf("", "mongodb-realm", user.app.configuration.appId, user.id, "$fileName.realm") + + val expectedFilename = if (fileName.endsWith(".realm")) fileName else "$fileName.realm" + + val suffix = pathOf("", "mongodb-realm", user.app.configuration.appId, user.id, expectedFilename) assertTrue(config.path.endsWith(suffix), "${config.path} failed.") - assertEquals("$fileName.realm", config.name, "${config.name} failed.") + assertEquals(expectedFilename, config.name, "${config.name} failed.") } } From 19bc7c858868dcce016faad959525e9405c1b6b4 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Tue, 17 Oct 2023 11:13:33 +0100 Subject: [PATCH 22/45] Update core --- packages/external/core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/external/core b/packages/external/core index efc8ac30e6..b33f7e683d 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit efc8ac30e61e1feec687f519130783b96a684873 +Subproject commit b33f7e683d0079af6d9a9c3d3e3d7c8b16bed70c From ed1b5a9e7cf50e826d9259f9503779ef0ae6e5fe Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Tue, 17 Oct 2023 13:09:20 +0100 Subject: [PATCH 23/45] Disabling path tests until they are fixed on main --- .../test/mongodb/common/FlexibleSyncConfigurationTests.kt | 3 ++- .../test/mongodb/common/SyncClientResetIntegrationTests.kt | 3 +++ .../io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt | 2 ++ .../io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt index 841def7936..4016df9ed9 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt @@ -129,7 +129,7 @@ class FlexibleSyncConfigurationTests { // val config: SyncConfiguration = SyncConfiguration.defaultConfig(user) // assertFailsWith { config.partitionValue } // } - + @Ignore @Test fun defaultPath() { val user: User = app.asTestApp.createUserAndLogin() @@ -220,6 +220,7 @@ class FlexibleSyncConfigurationTests { // assertTrue(config.syncClientResetStrategy is ManuallyRecoverUnsyncedChangesStrategy) // } + @Ignore @Test fun overrideDefaultPath() { val user: User = app.asTestApp.createUserAndLogin() diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt index 16b68768c6..e3eb516b0e 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt @@ -59,6 +59,7 @@ import kotlin.random.Random import kotlin.reflect.KClass import kotlin.test.AfterTest import kotlin.test.BeforeTest +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -448,6 +449,7 @@ class SyncClientResetIntegrationTests { } } + @Ignore @Test fun discardUnsyncedChanges_discards_attemptRecover_pbs() { performPbsTest { syncMode, app, user, builder -> @@ -455,6 +457,7 @@ class SyncClientResetIntegrationTests { } } + @Ignore @Test fun discardUnsyncedChanges_discards_attemptRecover_flx() { performFlxTest { syncMode, app, user, builder -> diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt index 77a80463d5..e0fb9283fb 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt @@ -495,6 +495,7 @@ class SyncConfigTests { nameAssertions("my-file-name") } + @Ignore @Test fun name_withDotRealmFileExtension() { nameAssertions("my-file-name.realm") @@ -505,6 +506,7 @@ class SyncConfigTests { nameAssertions("my-file-name.database") } + @Ignore @Test fun name_similarToDefaultObjectStoreName() { nameAssertions("s_partition-9482732795133669400.realm") diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index 41c3c4657b..3ace9c82c0 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -1330,6 +1330,7 @@ class SyncedRealmTests { println("Partition based sync bundled realm is in ${config2.path}") } + @Ignore @Test fun initialRealm_partitionBasedSync() { val (email, password) = randomEmail() to "password1234" From dc02b106c32269beb083b6562045a75d44808762 Mon Sep 17 00:00:00 2001 From: Clemente Date: Wed, 18 Oct 2023 12:31:24 +0200 Subject: [PATCH 24/45] Bump core commit --- CHANGELOG.md | 2 +- packages/external/core | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c399e39a4d..fc12a4621e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) * Minimum Android SDK: 16. ### Internal -* Updated to Realm Core 13.23.1, commit c569bec4d04da84030d94f376437bc4efda3686b. +* Updated to Realm Core 13.23.1, commit 3618b2e9d679cd2880be8df17b79d4cc6d71ff76. ## 1.11.1 (2023-09-07) diff --git a/packages/external/core b/packages/external/core index c569bec4d0..3618b2e9d6 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit c569bec4d04da84030d94f376437bc4efda3686b +Subproject commit 3618b2e9d679cd2880be8df17b79d4cc6d71ff76 From 98e718dc6cf1f42b93f65294aaf54cc36866d27c Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 18 Oct 2023 11:35:32 +0100 Subject: [PATCH 25/45] Update packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Claus Rørbech --- .../kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt index 75e9a0606e..4f5a3c3712 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt @@ -333,9 +333,9 @@ public interface AppConfiguration { * Platform Networking offer improved support for proxies and firewalls that require authentication, * instead of Realm's built-in WebSocket client for Sync traffic. This will become the default in a future version. */ - public fun usePlatformNetworking(): Builder = + public fun usePlatformNetworking(enable: Boolean=true): Builder = apply { - this.usePlatformNetworking = true + this.usePlatformNetworking = enable } /** From 8fc52a9d80ce7783df8cb1f1efaa6de967fe38db Mon Sep 17 00:00:00 2001 From: Clemente Date: Wed, 18 Oct 2023 14:25:47 +0200 Subject: [PATCH 26/45] Fix client reset flaky test --- .../test/mongodb/common/SyncClientResetIntegrationTests.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt index 16b68768c6..7a1ba648c2 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt @@ -531,7 +531,11 @@ class SyncClientResetIntegrationTests { assertEquals(ClientResetEvents.ON_BEFORE_RESET, channel.receiveOrFail()) assertEquals(ClientResetEvents.ON_AFTER_RESET, channel.receiveOrFail()) - // TODO We must not need this. Force updating the instance pointer. + // Object count down to 0 just after the reset + assertEquals(0, objectChannel.receiveOrFail().list.size) + + // TODO https://github.com/realm/realm-core/issues/7065 + // We must not need this. Force updating the instance pointer. realm.write { } // Validate Realm instance has been correctly updated From ee4fe0383d289ca3214777c4b38dd698c744c1d8 Mon Sep 17 00:00:00 2001 From: Clemente Date: Wed, 18 Oct 2023 14:41:37 +0200 Subject: [PATCH 27/45] bump baas --- dependencies.list | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dependencies.list b/dependencies.list index ba8ab7b56a..abe16e6f95 100644 --- a/dependencies.list +++ b/dependencies.list @@ -1,11 +1,11 @@ # Version of MongoDB Realm used by integration tests # See https://github.com/realm/ci/packages/147854 for available versions -MONGODB_REALM_SERVER=2023-09-22 +MONGODB_REALM_SERVER=2023-10-10 # `BAAS` and `BAAS-UI` projects commit hashes matching MONGODB_REALM_SERVER image version # note that the MONGODB_REALM_SERVER image is a nightly build, find the matching commits # for that date within the following repositories: # https://github.com/10gen/baas/ # https://github.com/10gen/baas-ui/ -REALM_BAAS_GIT_HASH=0b7562d0401d72c909369030dc29332542614ba3 -REALM_BAAS_UI_GIT_HASH=24baee4eb0e9736969a00a7bfac849565bca17f4 +REALM_BAAS_GIT_HASH=8246fc548763eb908b8090df864e9924e3330a0d +REALM_BAAS_UI_GIT_HASH=8a1843be2bf24f2faa705c5470a5bdd8d954f7ea From bacf3b088b28f222380dba0b5c2f567f1c205e13 Mon Sep 17 00:00:00 2001 From: Clemente Date: Wed, 18 Oct 2023 15:07:47 +0200 Subject: [PATCH 28/45] Fix test case --- .../kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt index f3093f2e6a..3160fd094d 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt @@ -331,7 +331,7 @@ class FlexibleSyncIntegrationTests { val exception: CompensatingWriteException = channel.receiveOrFail() - assertTrue(exception.message!!.startsWith("[Sync][CompensatingWrite(1033)] Client attempted a write that is outside of permissions or query filters; it has been reverted Logs:"), exception.message) + assertTrue(exception.message!!.startsWith("[Sync][CompensatingWrite(1033)] Client attempted a write that is not allowed; it has been reverted Logs:"), exception.message) assertEquals(1, exception.writes.size) exception.writes[0].run { From f0b5f6a45e648e67837d6195fee8279b6e5be86e Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 18 Oct 2023 14:14:30 +0100 Subject: [PATCH 29/45] PR feedback --- Jenkinsfile | 6 +- .../src/jvm/jni/java_class_global_def.hpp | 7 + .../kotlin/internal/interop/RealmInterop.kt | 173 +++++++++--------- .../src/main/jni/realm_api_helpers.cpp | 53 +++--- .../realm/kotlin/mongodb/AppConfiguration.kt | 2 +- .../internal/KtorWebSocketTransport.kt | 10 +- packages/test-sync/build.gradle.kts | 2 +- .../io/realm/kotlin/test/mongodb/TestApp.kt | 22 +-- 8 files changed, 141 insertions(+), 134 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 3110be989e..9c9a8b23b3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -188,7 +188,7 @@ pipeline { "integrationtest", { forwardAdbPorts() - testAndCollect("packages", "cleanAllTests -PREALM_USE_PLATFORM_NETWORKING=true -PincludeSdkModules=false connectedAndroidTest") + testAndCollect("packages", "cleanAllTests -PsyncUsePlatformNetworking=true -PincludeSdkModules=false connectedAndroidTest") } ) } @@ -212,7 +212,7 @@ pipeline { steps { testWithServer([ { - testAndCollect("packages", 'cleanAllTests jvmTest -PREALM_USE_PLATFORM_NETWORKING=true -PincludeSdkModules=false ') + testAndCollect("packages", 'cleanAllTests jvmTest -PsyncUsePlatformNetworking=true -PincludeSdkModules=false ') } ]) } @@ -232,7 +232,7 @@ pipeline { steps { testWithServer([ { - testAndCollect("packages", 'cleanAllTests :test-sync:connectedAndroidtest -PREALM_USE_PLATFORM_NETWORKING=true -PincludeSdkModules=false -PtestBuildType=debugMinified') + testAndCollect("packages", 'cleanAllTests :test-sync:connectedAndroidtest -PsyncUsePlatformNetworking=true -PincludeSdkModules=false -PtestBuildType=debugMinified') } ]) sh 'rm mapping.zip || true' diff --git a/packages/cinterop/src/jvm/jni/java_class_global_def.hpp b/packages/cinterop/src/jvm/jni/java_class_global_def.hpp index 92b9d2562e..58e48efb37 100644 --- a/packages/cinterop/src/jvm/jni/java_class_global_def.hpp +++ b/packages/cinterop/src/jvm/jni/java_class_global_def.hpp @@ -66,6 +66,7 @@ class JavaClassGlobalDef { , m_io_realm_kotlin_internal_interop_app_callback(env, "io/realm/kotlin/internal/interop/AppCallback", false) , m_io_realm_kotlin_internal_interop_connection_state_change_callback(env, "io/realm/kotlin/internal/interop/ConnectionStateChangeCallback", false) , m_io_realm_kotlin_internal_interop_sync_thread_observer(env, "io/realm/kotlin/internal/interop/SyncThreadObserver", false) + , m_io_realm_kotlin_internal_interop_sync_websocket_client(env, "io/realm/kotlin/internal/interop/sync/WebSocketClient", false) { } @@ -92,6 +93,7 @@ class JavaClassGlobalDef { jni_util::JavaClass m_io_realm_kotlin_internal_interop_app_callback; jni_util::JavaClass m_io_realm_kotlin_internal_interop_connection_state_change_callback; jni_util::JavaClass m_io_realm_kotlin_internal_interop_sync_thread_observer; + jni_util::JavaClass m_io_realm_kotlin_internal_interop_sync_websocket_client; inline static std::unique_ptr& instance() { @@ -228,6 +230,11 @@ class JavaClassGlobalDef { return jni_util::JavaMethod(env, instance()->m_kotlin_jvm_functions_function1, "invoke", "(Ljava/lang/Object;)Ljava/lang/Object;"); } + + inline static const jni_util::JavaClass& sync_websocket_client() + { + return instance()->m_io_realm_kotlin_internal_interop_sync_websocket_client; + } }; } // namespace realm diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 9bac79bd14..11d6276579 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -2724,107 +2724,110 @@ actual object RealmInterop { syncClientConfig: RealmSyncClientConfigurationPointer, webSocketTransport: WebSocketTransport ) { - val realmSyncSocketNew: CPointer? = - realm_wrapper.realm_sync_socket_new( - userdata = StableRef.create(webSocketTransport).asCPointer(), - userdata_free = staticCFunction { userdata: CPointer? -> - safeUserData(userdata).close() - disposeUserData(userdata) - }, - post_func = staticCFunction { userdata: CPointer?, syncSocketCallback: CPointer? -> - val callback: WebsocketFunctionHandlerCallback = { cancelled, _, _ -> - realm_wrapper.realm_sync_socket_post_complete( - syncSocketCallback, - if (cancelled) WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED.asNativeEnum else WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_SUCCESS.asNativeEnum, - "" - ) - } - - safeUserData(userdata).post( - CPointerWrapper(StableRef.create(callback).asCPointer()) - ) - }, - create_timer_func = staticCFunction { userdata: CPointer?, delayInMilliseconds: uint64_t, syncSocketCallback: CPointer? -> - val callback: WebsocketFunctionHandlerCallback = { cancelled, _, _ -> - if (cancelled) { - realm_wrapper.realm_sync_socket_timer_canceled(syncSocketCallback) - } else { - realm_wrapper.realm_sync_socket_timer_complete( + val realmSyncSocketNew: CPointer = + checkedPointerResult( + realm_wrapper.realm_sync_socket_new( + userdata = StableRef.create(webSocketTransport).asCPointer(), + userdata_free = staticCFunction { userdata: CPointer? -> + safeUserData(userdata).close() + disposeUserData(userdata) + }, + post_func = staticCFunction { userdata: CPointer?, syncSocketCallback: CPointer? -> + val callback: WebsocketFunctionHandlerCallback = { cancelled, _, _ -> + realm_wrapper.realm_sync_socket_post_complete( syncSocketCallback, - WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_SUCCESS.asNativeEnum, + if (cancelled) WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED.asNativeEnum else WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_SUCCESS.asNativeEnum, "" ) } - } - safeUserData(userdata).let { ws -> - val job: CancellableTimer = ws.createTimer( - delayInMilliseconds.toLong(), + safeUserData(userdata).post( CPointerWrapper(StableRef.create(callback).asCPointer()) ) - StableRef.create(job).asCPointer() - } - }, - cancel_timer_func = staticCFunction { userdata: CPointer?, timer: realm_sync_socket_timer_t? -> - safeUserData(timer).cancel() - }, - free_timer_func = staticCFunction { userdata: CPointer?, timer: realm_sync_socket_timer_t? -> - disposeUserData(timer) - }, - websocket_connect_func = staticCFunction { userdata: CPointer?, endpoint: CValue, observer: CPointer? -> - safeUserData(userdata).let { websocketTransport -> - endpoint.useContents { - val managedObserver = WebSocketObserver(CPointerWrapper(observer)) - - val supportedProtocols = mutableListOf() - for (i in 0 until this.num_protocols.toInt()) { - val protocol: CPointer>? = this.protocols?.get(i) - supportedProtocols.add(protocol.safeKString()) + }, + create_timer_func = staticCFunction { userdata: CPointer?, delayInMilliseconds: uint64_t, syncSocketCallback: CPointer? -> + val callback: WebsocketFunctionHandlerCallback = { cancelled, _, _ -> + if (cancelled) { + realm_wrapper.realm_sync_socket_timer_canceled(syncSocketCallback) + } else { + realm_wrapper.realm_sync_socket_timer_complete( + syncSocketCallback, + WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_SUCCESS.asNativeEnum, + "" + ) } - val webSocketClient: WebSocketClient = websocketTransport.connect( - managedObserver, - this.path.safeKString(), - this.address.safeKString(), - this.port.toLong(), - this.is_ssl, - this.num_protocols.toLong(), - supportedProtocols.joinToString(", ") - ) - StableRef.create(webSocketClient).asCPointer() } - } - }, - websocket_write_func = staticCFunction { userdata: CPointer?, websocket: realm_sync_socket_websocket_t?, data: CPointer?, length: size_t, callback: CPointer? -> - val postWriteCallback: WebsocketFunctionHandlerCallback = - { cancelled, status, reason -> - realm_wrapper.realm_sync_socket_write_complete( - callback, - if (cancelled) WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED.asNativeEnum else WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_SUCCESS.asNativeEnum, - reason + + safeUserData(userdata).let { ws -> + val job: CancellableTimer = ws.createTimer( + delayInMilliseconds.toLong(), + CPointerWrapper(StableRef.create(callback).asCPointer()) ) + StableRef.create(job).asCPointer() } + }, + cancel_timer_func = staticCFunction { userdata: CPointer?, timer: realm_sync_socket_timer_t? -> + safeUserData(timer).cancel() + }, + free_timer_func = staticCFunction { userdata: CPointer?, timer: realm_sync_socket_timer_t? -> + disposeUserData(timer) + }, + websocket_connect_func = staticCFunction { userdata: CPointer?, endpoint: CValue, observer: CPointer? -> + safeUserData(userdata).let { websocketTransport -> + endpoint.useContents { + val managedObserver = WebSocketObserver(CPointerWrapper(observer)) + + val supportedProtocols = mutableListOf() + for (i in 0 until this.num_protocols.toInt()) { + val protocol: CPointer>? = + this.protocols?.get(i) + supportedProtocols.add(protocol.safeKString()) + } + val webSocketClient: WebSocketClient = websocketTransport.connect( + managedObserver, + this.path.safeKString(), + this.address.safeKString(), + this.port.toLong(), + this.is_ssl, + this.num_protocols.toLong(), + supportedProtocols.joinToString(", ") + ) + StableRef.create(webSocketClient).asCPointer() + } + } + }, + websocket_write_func = staticCFunction { userdata: CPointer?, websocket: realm_sync_socket_websocket_t?, data: CPointer?, length: size_t, callback: CPointer? -> + val postWriteCallback: WebsocketFunctionHandlerCallback = + { cancelled, status, reason -> + realm_wrapper.realm_sync_socket_write_complete( + callback, + if (cancelled) WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED.asNativeEnum else WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_SUCCESS.asNativeEnum, + reason + ) + } - safeUserData(userdata).let { websocketTransport -> - safeUserData(websocket).let { webSocketClient -> - data?.readBytes(length.toInt())?.run { - websocketTransport.write( - webSocketClient, - this, - length.toLong(), - CPointerWrapper( - StableRef.create(postWriteCallback).asCPointer() + safeUserData(userdata).let { websocketTransport -> + safeUserData(websocket).let { webSocketClient -> + data?.readBytes(length.toInt())?.run { + websocketTransport.write( + webSocketClient, + this, + length.toLong(), + CPointerWrapper( + StableRef.create(postWriteCallback).asCPointer() + ) ) - ) + } } } + Unit + }, + websocket_free_func = staticCFunction { userdata: CPointer?, websocket: realm_sync_socket_websocket_t? -> + safeUserData(websocket).closeWebsocket() + disposeUserData(websocket) } - Unit - }, - websocket_free_func = staticCFunction { userdata: CPointer?, websocket: realm_sync_socket_websocket_t? -> - safeUserData(websocket).closeWebsocket() - disposeUserData(websocket) - } - ) + ) + ) ?: error("Couldn't create Sync Socket") realm_wrapper.realm_sync_client_config_set_sync_socket( syncClientConfig.cptr(), realmSyncSocketNew diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index e67bbebfd7..9bd753a41d 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -684,33 +684,28 @@ static void websocket_post_func(realm_userdata_t userdata, realm_sync_socket_post_callback_t* realm_callback) { // Some calls to 'post' happens from the external commit helper which is not necessarily attached yet to a JVM thread auto jenv = get_env(true, true); // attach as daemon thread - static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); - static JavaMethod native_pointer_constructor(jenv, native_pointer_class, "", "(JZ)V"); WebsocketFunctionHandlerCallback* lambda = new WebsocketFunctionHandlerCallback([realm_callback=std::move(realm_callback)](bool cancelled, int status, const char* reason) { realm_sync_socket_post_complete(realm_callback, cancelled ? realm_sync_socket_callback_result::RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED : realm_sync_socket_callback_result::RLM_ERR_SYNC_SOCKET_SUCCESS, ""); }); - jobject pointer = jenv->NewObject(native_pointer_class, native_pointer_constructor, - reinterpret_cast(lambda), false); + jobject lambda_callback_pointer_wrapper = wrap_pointer(jenv,reinterpret_cast(lambda)); - static JavaClass jvm_websocket_transport_class (jenv, "io/realm/kotlin/internal/interop/sync/WebSocketTransport"); - static JavaMethod post_method (jenv, jvm_websocket_transport_class, "post", + static JavaMethod post_method (jenv, JavaClassGlobalDef::sync_websocket_client(), "post", "(Lio/realm/kotlin/internal/interop/NativePointer;)V"); jobject websocket_transport = static_cast(userdata); - jenv->CallVoidMethod(websocket_transport, post_method, pointer); + jenv->CallVoidMethod(websocket_transport, post_method, lambda_callback_pointer_wrapper); + jni_check_exception(jenv); - jenv->DeleteLocalRef(pointer); + jenv->DeleteLocalRef(lambda_callback_pointer_wrapper); } static realm_sync_socket_timer_t websocket_create_timer_func( realm_userdata_t userdata, uint64_t delay_ms, realm_sync_socket_timer_callback_t* realm_callback) { // called from main thread/event loop which should be already attached to JVM auto jenv = get_env(false); - static JavaClass native_pointer_class(jenv, - "io/realm/kotlin/internal/interop/LongPointerWrapper"); - static JavaMethod native_pointer_constructor(jenv, native_pointer_class, "", "(JZ)V"); + WebsocketFunctionHandlerCallback *lambda = new WebsocketFunctionHandlerCallback( [realm_callback = std::move(realm_callback)](bool cancel, int status, const char *reason) { @@ -722,16 +717,15 @@ static realm_sync_socket_timer_t websocket_create_timer_func( ""); } }); - jobject pointer = jenv->NewObject(native_pointer_class, native_pointer_constructor, - reinterpret_cast(lambda), false); + jobject lambda_callback_pointer_wrapper = wrap_pointer(jenv,reinterpret_cast(lambda)); - static JavaClass jvm_websocket_transport_class (jenv, "io/realm/kotlin/internal/interop/sync/WebSocketTransport"); - static JavaMethod create_timer_method (jenv, jvm_websocket_transport_class, "createTimer", + static JavaMethod create_timer_method (jenv, JavaClassGlobalDef::sync_websocket_client(), "createTimer", "(JLio/realm/kotlin/internal/interop/NativePointer;)Lio/realm/kotlin/internal/interop/sync/CancellableTimer;"); jobject websocket_transport = static_cast(userdata); - jobject cancellable_timer = jenv->CallObjectMethod(websocket_transport, create_timer_method, jlong(delay_ms), pointer); + jobject cancellable_timer = jenv->CallObjectMethod(websocket_transport, create_timer_method, jlong(delay_ms), lambda_callback_pointer_wrapper); + jni_check_exception(jenv); - jenv->DeleteLocalRef(pointer); + jenv->DeleteLocalRef(lambda_callback_pointer_wrapper); return reinterpret_cast(jenv->NewGlobalRef(cancellable_timer)); } @@ -745,6 +739,7 @@ static void websocket_cancel_timer_func(realm_userdata_t userdata, static JavaMethod cancel_method(jenv, cancellable_timer_class, "cancel", "()V"); jenv->CallVoidMethod(cancellable_timer, cancel_method); + jni_check_exception(jenv); jenv->DeleteGlobalRef(cancellable_timer); } @@ -756,18 +751,14 @@ static realm_sync_socket_websocket_t websocket_connect_func( auto jenv = get_env(false); - static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); - static JavaMethod native_pointer_constructor(jenv, native_pointer_class, "", "(JZ)V"); - jobject observer_pointer = jenv->NewObject(native_pointer_class, native_pointer_constructor, - reinterpret_cast(realm_websocket_observer), false); + jobject observer_pointer = wrap_pointer(jenv,reinterpret_cast(realm_websocket_observer)); static JavaClass websocket_observer_class(jenv, "io/realm/kotlin/internal/interop/sync/WebSocketObserver"); static JavaMethod websocket_observer_constructor(jenv, websocket_observer_class, "", "(Lio/realm/kotlin/internal/interop/NativePointer;)V"); jobject websocket_observer = jenv->NewObject(websocket_observer_class, websocket_observer_constructor, observer_pointer); - static JavaClass websocket_transport_class(jenv, "io/realm/kotlin/internal/interop/sync/WebSocketTransport"); - static JavaMethod connect_method(jenv, websocket_transport_class, "connect", + static JavaMethod connect_method(jenv, JavaClassGlobalDef::sync_websocket_client(), "connect", "(Lio/realm/kotlin/internal/interop/sync/WebSocketObserver;Ljava/lang/String;Ljava/lang/String;JZJLjava/lang/String;)Lio/realm/kotlin/internal/interop/sync/WebSocketClient;"); jobject websocket_transport = static_cast(userdata); @@ -784,6 +775,8 @@ static realm_sync_socket_websocket_t websocket_connect_func( endpoint.is_ssl, jlong(endpoint.num_protocols), to_jstring(jenv, supported_protocol.str().c_str())); + jni_check_exception(jenv); + realm_sync_socket_websocket_t global_websocket_ref = reinterpret_cast(jenv->NewGlobalRef(websocket_client)); jenv->DeleteLocalRef(websocket_observer); @@ -798,15 +791,12 @@ static void websocket_async_write_func(realm_userdata_t userdata, realm_sync_socket_write_callback_t* realm_callback) { auto jenv = get_env(false); - static JavaClass native_pointer_class(jenv, "io/realm/kotlin/internal/interop/LongPointerWrapper"); - static JavaMethod native_pointer_constructor(jenv, native_pointer_class, "", "(JZ)V"); WebsocketFunctionHandlerCallback* lambda = new WebsocketFunctionHandlerCallback([realm_callback=std::move(realm_callback)](bool cancelled, int status, const char* reason) { realm_sync_socket_write_complete(realm_callback, cancelled ? realm_sync_socket_callback_result::RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED: realm_sync_socket_callback_result::RLM_ERR_SYNC_SOCKET_SUCCESS, ""); }); - jobject callback_pointer = jenv->NewObject(native_pointer_class, native_pointer_constructor, - reinterpret_cast(lambda), false); + jobject lambda_callback_pointer_wrapper = wrap_pointer(jenv,reinterpret_cast(lambda)); static jclass websocket_transport_class = jenv->FindClass("io/realm/kotlin/internal/interop/sync/WebSocketTransport"); static jmethodID write_method = jenv->GetMethodID(websocket_transport_class, "write", @@ -820,9 +810,12 @@ static void websocket_async_write_func(realm_userdata_t userdata, static_cast(websocket_userdata), byteArray, jlong(size), - callback_pointer); + lambda_callback_pointer_wrapper); + jni_check_exception(jenv); jenv->DeleteLocalRef(byteArray); + jenv->DeleteLocalRef(lambda_callback_pointer_wrapper); + } static void realm_sync_websocket_free(realm_userdata_t userdata, @@ -834,6 +827,7 @@ static void realm_sync_websocket_free(realm_userdata_t userdata, jobject websocket_client = static_cast(websocket_userdata); jenv->CallVoidMethod(websocket_client, close_method); + jni_check_exception(jenv); jenv->DeleteGlobalRef(websocket_client); } @@ -848,6 +842,7 @@ static void realm_sync_userdata_free(realm_userdata_t userdata) { jobject websocket_transport = static_cast(userdata); jenv->CallVoidMethod(websocket_transport, close_method); + jni_check_exception(jenv); jenv->DeleteGlobalRef(websocket_transport); } @@ -895,6 +890,8 @@ realm_sync_socket_t* realm_sync_websocket_new(int64_t sync_client_config_ptr, jo websocket_connect_func/*websocket_connect_func*/, websocket_async_write_func/*websocket_write_func*/, realm_sync_websocket_free/*websocket_free_func*/); + jni_check_exception(jenv); + realm_sync_client_config_set_sync_socket(reinterpret_cast(sync_client_config_ptr)/*config*/, socket_provider); realm_release(socket_provider); return socket_provider; diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt index 4f5a3c3712..4e55158fda 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt @@ -333,7 +333,7 @@ public interface AppConfiguration { * Platform Networking offer improved support for proxies and firewalls that require authentication, * instead of Realm's built-in WebSocket client for Sync traffic. This will become the default in a future version. */ - public fun usePlatformNetworking(enable: Boolean=true): Builder = + public fun usePlatformNetworking(enable: Boolean = true): Builder = apply { this.usePlatformNetworking = enable } diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt index d21d3615f7..9b4a3e4067 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt @@ -65,18 +65,18 @@ public class KtorWebSocketTransport( delayInMilliseconds: Long, handlerCallback: RealmWebsocketHandlerCallbackPointer ): CancellableTimer { - val atomic: AtomicRef = atomic(handlerCallback) + val atomicCallback: AtomicRef = atomic(handlerCallback) return CancellableTimer( scope.launch { delay(delayInMilliseconds) - atomic.getAndSet(null)?.run { - runCallback(handlerCallback) + atomicCallback.getAndSet(null)?.run { + runCallback(this) } } ) { // Cancel lambda scope.launch { - atomic.getAndSet(null)?.run { - runCallback(handlerCallback, cancelled = true) + atomicCallback.getAndSet(null)?.run { + runCallback(this, cancelled = true) } } } diff --git a/packages/test-sync/build.gradle.kts b/packages/test-sync/build.gradle.kts index a5bdfb3bbb..f61536655a 100644 --- a/packages/test-sync/build.gradle.kts +++ b/packages/test-sync/build.gradle.kts @@ -314,7 +314,7 @@ buildkonfig { buildConfigField(Type.STRING, "privateApiKey", "") } buildConfigField(Type.STRING, "clusterName", getPropertyValue("syncTestClusterName") ?: "") - buildConfigField(Type.BOOLEAN, "usePlatformNetworking", getPropertyValue("REALM_USE_PLATFORM_NETWORKING")?.toLowerCaseAsciiOnly() ?: "false") + buildConfigField(Type.BOOLEAN, "usePlatformNetworking", getPropertyValue("syncUsePlatformNetworking") ?: "false") } } diff --git a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt index e779f127c1..4577d0d366 100644 --- a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt @@ -199,18 +199,18 @@ open class TestApp private constructor( .baseUrl(TEST_SERVER_BASE_URL) .networkTransport(networkTransport) .ejson(ejson) - if (SyncServerConfig.usePlatformNetworking) { - config.usePlatformNetworking() - } - config.apply { - if (logLevel != null) { - log( - logLevel, - if (customLogger == null) emptyList() - else listOf(customLogger) - ) + .apply { + if (logLevel != null) { + log( + logLevel, + if (customLogger == null) emptyList() + else listOf(customLogger) + ) + } + if (SyncServerConfig.usePlatformNetworking) { + usePlatformNetworking() + } } - } val app = App.create( builder(config) From dd37a22f188d3e4855152e0e9b64cb1dcc56178b Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 19 Oct 2023 16:19:18 +0100 Subject: [PATCH 30/45] Fixing tests --- .../src/jvm/jni/java_class_global_def.hpp | 9 +++++++-- .../src/main/jni/realm_api_helpers.cpp | 15 ++++++--------- .../common/FlexibleSyncConfigurationTests.kt | 2 -- .../common/FlexibleSyncIntegrationTests.kt | 2 +- .../common/SyncClientResetIntegrationTests.kt | 3 --- .../test/mongodb/common/SyncConfigTests.kt | 2 -- .../test/mongodb/common/SyncedRealmTests.kt | 1 - .../kotlin/test/mongodb/common/UserTests.kt | 18 +++++++++--------- 8 files changed, 23 insertions(+), 29 deletions(-) diff --git a/packages/cinterop/src/jvm/jni/java_class_global_def.hpp b/packages/cinterop/src/jvm/jni/java_class_global_def.hpp index 58e48efb37..8797d971a0 100644 --- a/packages/cinterop/src/jvm/jni/java_class_global_def.hpp +++ b/packages/cinterop/src/jvm/jni/java_class_global_def.hpp @@ -66,6 +66,7 @@ class JavaClassGlobalDef { , m_io_realm_kotlin_internal_interop_app_callback(env, "io/realm/kotlin/internal/interop/AppCallback", false) , m_io_realm_kotlin_internal_interop_connection_state_change_callback(env, "io/realm/kotlin/internal/interop/ConnectionStateChangeCallback", false) , m_io_realm_kotlin_internal_interop_sync_thread_observer(env, "io/realm/kotlin/internal/interop/SyncThreadObserver", false) + , m_io_realm_kotlin_internal_interop_sync_websocket_transport(env, "io/realm/kotlin/internal/interop/sync/WebSocketTransport", false) , m_io_realm_kotlin_internal_interop_sync_websocket_client(env, "io/realm/kotlin/internal/interop/sync/WebSocketClient", false) { } @@ -93,6 +94,7 @@ class JavaClassGlobalDef { jni_util::JavaClass m_io_realm_kotlin_internal_interop_app_callback; jni_util::JavaClass m_io_realm_kotlin_internal_interop_connection_state_change_callback; jni_util::JavaClass m_io_realm_kotlin_internal_interop_sync_thread_observer; + jni_util::JavaClass m_io_realm_kotlin_internal_interop_sync_websocket_transport; jni_util::JavaClass m_io_realm_kotlin_internal_interop_sync_websocket_client; inline static std::unique_ptr& instance() @@ -231,8 +233,11 @@ class JavaClassGlobalDef { "(Ljava/lang/Object;)Ljava/lang/Object;"); } - inline static const jni_util::JavaClass& sync_websocket_client() - { + inline static const jni_util::JavaClass& sync_websocket_transport() { + return instance()->m_io_realm_kotlin_internal_interop_sync_websocket_transport; + } + + inline static const jni_util::JavaClass& sync_websocket_client() { return instance()->m_io_realm_kotlin_internal_interop_sync_websocket_client; } }; diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index 9bd753a41d..9bb83409b6 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -692,7 +692,7 @@ static void websocket_post_func(realm_userdata_t userdata, }); jobject lambda_callback_pointer_wrapper = wrap_pointer(jenv,reinterpret_cast(lambda)); - static JavaMethod post_method (jenv, JavaClassGlobalDef::sync_websocket_client(), "post", + static JavaMethod post_method(jenv, JavaClassGlobalDef::sync_websocket_transport(), "post", "(Lio/realm/kotlin/internal/interop/NativePointer;)V"); jobject websocket_transport = static_cast(userdata); jenv->CallVoidMethod(websocket_transport, post_method, lambda_callback_pointer_wrapper); @@ -719,7 +719,7 @@ static realm_sync_socket_timer_t websocket_create_timer_func( }); jobject lambda_callback_pointer_wrapper = wrap_pointer(jenv,reinterpret_cast(lambda)); - static JavaMethod create_timer_method (jenv, JavaClassGlobalDef::sync_websocket_client(), "createTimer", + static JavaMethod create_timer_method (jenv, JavaClassGlobalDef::sync_websocket_transport(), "createTimer", "(JLio/realm/kotlin/internal/interop/NativePointer;)Lio/realm/kotlin/internal/interop/sync/CancellableTimer;"); jobject websocket_transport = static_cast(userdata); jobject cancellable_timer = jenv->CallObjectMethod(websocket_transport, create_timer_method, jlong(delay_ms), lambda_callback_pointer_wrapper); @@ -758,7 +758,7 @@ static realm_sync_socket_websocket_t websocket_connect_func( "(Lio/realm/kotlin/internal/interop/NativePointer;)V"); jobject websocket_observer = jenv->NewObject(websocket_observer_class, websocket_observer_constructor, observer_pointer); - static JavaMethod connect_method(jenv, JavaClassGlobalDef::sync_websocket_client(), "connect", + static JavaMethod connect_method(jenv, JavaClassGlobalDef::sync_websocket_transport(), "connect", "(Lio/realm/kotlin/internal/interop/sync/WebSocketObserver;Ljava/lang/String;Ljava/lang/String;JZJLjava/lang/String;)Lio/realm/kotlin/internal/interop/sync/WebSocketClient;"); jobject websocket_transport = static_cast(userdata); @@ -798,8 +798,7 @@ static void websocket_async_write_func(realm_userdata_t userdata, }); jobject lambda_callback_pointer_wrapper = wrap_pointer(jenv,reinterpret_cast(lambda)); - static jclass websocket_transport_class = jenv->FindClass("io/realm/kotlin/internal/interop/sync/WebSocketTransport"); - static jmethodID write_method = jenv->GetMethodID(websocket_transport_class, "write", + static jmethodID write_method = jenv->GetMethodID(JavaClassGlobalDef::sync_websocket_transport(), "write", "(Lio/realm/kotlin/internal/interop/sync/WebSocketClient;[BJLio/realm/kotlin/internal/interop/NativePointer;)V"); jobject websocket_transport = static_cast(userdata); @@ -822,8 +821,7 @@ static void realm_sync_websocket_free(realm_userdata_t userdata, realm_sync_socket_websocket_t websocket_userdata) { if (websocket_userdata != nullptr) { auto jenv = get_env(false); - static jclass websocket_client_class = jenv->FindClass("io/realm/kotlin/internal/interop/sync/WebSocketClient"); - static jmethodID close_method = jenv->GetMethodID(websocket_client_class, "closeWebsocket", "()V"); + static jmethodID close_method = jenv->GetMethodID(JavaClassGlobalDef::sync_websocket_client(), "closeWebsocket", "()V"); jobject websocket_client = static_cast(websocket_userdata); jenv->CallVoidMethod(websocket_client, close_method); @@ -837,8 +835,7 @@ static void realm_sync_userdata_free(realm_userdata_t userdata) { if (userdata != nullptr) { auto jenv = get_env(false); - static jclass websocket_client_class = jenv->FindClass("io/realm/kotlin/internal/interop/sync/WebSocketTransport"); - static jmethodID close_method = jenv->GetMethodID(websocket_client_class, "close", "()V"); + static jmethodID close_method = jenv->GetMethodID(JavaClassGlobalDef::sync_websocket_transport(), "close", "()V"); jobject websocket_transport = static_cast(userdata); jenv->CallVoidMethod(websocket_transport, close_method); diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt index 4016df9ed9..be13d1f51f 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt @@ -129,7 +129,6 @@ class FlexibleSyncConfigurationTests { // val config: SyncConfiguration = SyncConfiguration.defaultConfig(user) // assertFailsWith { config.partitionValue } // } - @Ignore @Test fun defaultPath() { val user: User = app.asTestApp.createUserAndLogin() @@ -220,7 +219,6 @@ class FlexibleSyncConfigurationTests { // assertTrue(config.syncClientResetStrategy is ManuallyRecoverUnsyncedChangesStrategy) // } - @Ignore @Test fun overrideDefaultPath() { val user: User = app.asTestApp.createUserAndLogin() diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt index 3160fd094d..f3093f2e6a 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt @@ -331,7 +331,7 @@ class FlexibleSyncIntegrationTests { val exception: CompensatingWriteException = channel.receiveOrFail() - assertTrue(exception.message!!.startsWith("[Sync][CompensatingWrite(1033)] Client attempted a write that is not allowed; it has been reverted Logs:"), exception.message) + assertTrue(exception.message!!.startsWith("[Sync][CompensatingWrite(1033)] Client attempted a write that is outside of permissions or query filters; it has been reverted Logs:"), exception.message) assertEquals(1, exception.writes.size) exception.writes[0].run { diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt index 46406fcbfa..06f9940072 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt @@ -59,7 +59,6 @@ import kotlin.random.Random import kotlin.reflect.KClass import kotlin.test.AfterTest import kotlin.test.BeforeTest -import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -451,7 +450,6 @@ class SyncClientResetIntegrationTests { channel.close() } - @Ignore @Test fun discardUnsyncedChanges_discards_attemptRecover_pbs() { performPbsTest { syncMode, app, user, builder -> @@ -459,7 +457,6 @@ class SyncClientResetIntegrationTests { } } - @Ignore @Test fun discardUnsyncedChanges_discards_attemptRecover_flx() { performFlxTest { syncMode, app, user, builder -> diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt index 8094c08663..2635dc277d 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt @@ -495,7 +495,6 @@ class SyncConfigTests { nameAssertions("my-file-name") } - @Ignore @Test fun name_withDotRealmFileExtension() { nameAssertions("my-file-name.realm") @@ -506,7 +505,6 @@ class SyncConfigTests { nameAssertions("my-file-name.database") } - @Ignore @Test fun name_similarToDefaultObjectStoreName() { nameAssertions("s_partition-9482732795133669400.realm") diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index 3ace9c82c0..41c3c4657b 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -1330,7 +1330,6 @@ class SyncedRealmTests { println("Partition based sync bundled realm is in ${config2.path}") } - @Ignore @Test fun initialRealm_partitionBasedSync() { val (email, password) = randomEmail() to "password1234" diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt index 6cd0ae2562..eb622cf8c0 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt @@ -122,15 +122,15 @@ class UserTests { } } -// @Test -// fun getProviderType() = runBlocking { -// val email = randomEmail() -// val emailUser = createUserAndLogin(email, "123456") -// assertEquals(AuthenticationProvider.EMAIL_PASSWORD, emailUser.provider) -// emailUser.logOut() -// // AuthenticationProvider is not removed once user is logged out -// assertEquals(AuthenticationProvider.EMAIL_PASSWORD, emailUser.provider) -// } + @Test + fun getProviderType() = runBlocking { + val email = randomEmail() + val emailUser = createUserAndLogin(email, "123456") + assertEquals(AuthenticationProvider.EMAIL_PASSWORD, emailUser.provider) + emailUser.logOut() + // AuthenticationProvider is not removed once user is logged out + assertEquals(AuthenticationProvider.EMAIL_PASSWORD, emailUser.provider) + } @Test fun getAccessToken() = runBlocking { From 073e4be8d60201d5c5dc3563357b86e2ef865034 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Fri, 20 Oct 2023 10:15:36 +0100 Subject: [PATCH 31/45] Fixing test --- .../kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt index f3093f2e6a..3160fd094d 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt @@ -331,7 +331,7 @@ class FlexibleSyncIntegrationTests { val exception: CompensatingWriteException = channel.receiveOrFail() - assertTrue(exception.message!!.startsWith("[Sync][CompensatingWrite(1033)] Client attempted a write that is outside of permissions or query filters; it has been reverted Logs:"), exception.message) + assertTrue(exception.message!!.startsWith("[Sync][CompensatingWrite(1033)] Client attempted a write that is not allowed; it has been reverted Logs:"), exception.message) assertEquals(1, exception.writes.size) exception.writes[0].run { From 0e41956b71f9b9dd80e13e68b7213d2ac4187ec6 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 23 Oct 2023 13:01:54 +0100 Subject: [PATCH 32/45] - Update Core - Closing JVM scheduler in dtors --- CHANGELOG.md | 2 +- .../realm/kotlin/internal/interop/RealmInterop.kt | 2 +- packages/external/core | 2 +- .../src/main/jni/realm_api_helpers.cpp | 3 ++- .../io/realm/kotlin/internal/ConfigurationImpl.kt | 13 +++++-------- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 658e41da3e..815e1d705b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,7 @@ GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) * Minimum Android SDK: 16. ### Internal -* Updated to Realm Core 13.23.1, commit 9fe0653fef672f14c771019ba27d54d568f622d0. +* Updated to Realm Core 13.23.1, commit 43853ebe18bd2a6873e7f300d18b6a5e614da627. * Update to Ktor 2.3.4. * Switched Ktor engine to CIO for Android and JVM to work around https://youtrack.jetbrains.com/issue/KTOR-6266. Revert to OkHttp when the issue is fixed. diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index aa3d754bbd..ca84afea3f 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -185,7 +185,7 @@ actual object RealmInterop { } actual fun realm_create_scheduler(): RealmSchedulerPointer = - LongPointerWrapper(realmc.realm_create_generic_scheduler()) + LongPointerWrapper(realmc.realm_create_generic_scheduler(), managed = true) actual fun realm_create_scheduler(dispatcher: CoroutineDispatcher): RealmSchedulerPointer = LongPointerWrapper(realmc.realm_create_scheduler(JVMScheduler(dispatcher))) diff --git a/packages/external/core b/packages/external/core index b33f7e683d..43853ebe18 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit b33f7e683d0079af6d9a9c3d3e3d7c8b16bed70c +Subproject commit 43853ebe18bd2a6873e7f300d18b6a5e614da627 diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index 9bb83409b6..c418a2d5ea 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -269,6 +269,7 @@ class CustomJVMScheduler { ~CustomJVMScheduler() { get_env(true)->DeleteGlobalRef(m_jvm_dispatch_scheduler); + delete m_scheduler; } void set_scheduler(realm_scheduler_t* scheduler) { @@ -278,7 +279,7 @@ class CustomJVMScheduler { void notify() { // There is currently no signaling of creation/tear down of the core notifier thread, so we // just attach it as a daemon thread here on first notification to allow the JVM to - // shutdown propertly. See https://github.com/realm/realm-core/issues/6429 + // shutdown property. See https://github.com/realm/realm-core/issues/6429 auto jenv = get_env(true, true, "core-notifier"); jni_check_exception(jenv); jenv->CallVoidMethod(m_jvm_dispatch_scheduler, m_notify_method, diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt index 3463f478c0..da4d486485 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt @@ -109,14 +109,11 @@ public open class ConfigurationImpl( override suspend fun openRealm(realm: RealmImpl): Pair { val configPtr = realm.configuration.createNativeConfiguration() - return RealmInterop.realm_create_scheduler() - .use { scheduler -> - val (dbPointer, fileCreated) = RealmInterop.realm_open(configPtr, scheduler) - val liveRealmReference = LiveRealmReference(realm, dbPointer) - val frozenReference = liveRealmReference.snapshot(realm) - liveRealmReference.close() - frozenReference to fileCreated - } + val (dbPointer, fileCreated) = RealmInterop.realm_open(configPtr, RealmInterop.realm_create_scheduler()) + val liveRealmReference = LiveRealmReference(realm, dbPointer) + val frozenReference = liveRealmReference.snapshot(realm) + liveRealmReference.close() + return frozenReference to fileCreated } override suspend fun initializeRealmData(realm: RealmImpl, realmFileCreated: Boolean) { From 0105a73e8faccdf77660e57c43f862df02f6c89f Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 23 Oct 2023 13:58:11 +0100 Subject: [PATCH 33/45] unused import --- .../kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt index da4d486485..2d5598bbd9 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt @@ -36,7 +36,6 @@ import io.realm.kotlin.internal.interop.RealmConfigurationPointer import io.realm.kotlin.internal.interop.RealmInterop import io.realm.kotlin.internal.interop.RealmSchemaPointer import io.realm.kotlin.internal.interop.SchemaMode -import io.realm.kotlin.internal.interop.use import io.realm.kotlin.internal.platform.appFilesDirectory import io.realm.kotlin.internal.platform.prepareRealmFilePath import io.realm.kotlin.internal.platform.realmObjectCompanionOrThrow From aecef9387d59e7ab7e26c8aba93cd77858be49e2 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Tue, 24 Oct 2023 12:44:48 +0100 Subject: [PATCH 34/45] Reverting changes --- .../realm/kotlin/internal/interop/RealmInterop.kt | 2 +- .../src/main/jni/realm_api_helpers.cpp | 1 - .../io/realm/kotlin/internal/ConfigurationImpl.kt | 14 +++++++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index ca84afea3f..aa3d754bbd 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -185,7 +185,7 @@ actual object RealmInterop { } actual fun realm_create_scheduler(): RealmSchedulerPointer = - LongPointerWrapper(realmc.realm_create_generic_scheduler(), managed = true) + LongPointerWrapper(realmc.realm_create_generic_scheduler()) actual fun realm_create_scheduler(dispatcher: CoroutineDispatcher): RealmSchedulerPointer = LongPointerWrapper(realmc.realm_create_scheduler(JVMScheduler(dispatcher))) diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index c418a2d5ea..0ab8144461 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -269,7 +269,6 @@ class CustomJVMScheduler { ~CustomJVMScheduler() { get_env(true)->DeleteGlobalRef(m_jvm_dispatch_scheduler); - delete m_scheduler; } void set_scheduler(realm_scheduler_t* scheduler) { diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt index 2d5598bbd9..3463f478c0 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt @@ -36,6 +36,7 @@ import io.realm.kotlin.internal.interop.RealmConfigurationPointer import io.realm.kotlin.internal.interop.RealmInterop import io.realm.kotlin.internal.interop.RealmSchemaPointer import io.realm.kotlin.internal.interop.SchemaMode +import io.realm.kotlin.internal.interop.use import io.realm.kotlin.internal.platform.appFilesDirectory import io.realm.kotlin.internal.platform.prepareRealmFilePath import io.realm.kotlin.internal.platform.realmObjectCompanionOrThrow @@ -108,11 +109,14 @@ public open class ConfigurationImpl( override suspend fun openRealm(realm: RealmImpl): Pair { val configPtr = realm.configuration.createNativeConfiguration() - val (dbPointer, fileCreated) = RealmInterop.realm_open(configPtr, RealmInterop.realm_create_scheduler()) - val liveRealmReference = LiveRealmReference(realm, dbPointer) - val frozenReference = liveRealmReference.snapshot(realm) - liveRealmReference.close() - return frozenReference to fileCreated + return RealmInterop.realm_create_scheduler() + .use { scheduler -> + val (dbPointer, fileCreated) = RealmInterop.realm_open(configPtr, scheduler) + val liveRealmReference = LiveRealmReference(realm, dbPointer) + val frozenReference = liveRealmReference.snapshot(realm) + liveRealmReference.close() + frozenReference to fileCreated + } } override suspend fun initializeRealmData(realm: RealmImpl, realmFileCreated: Boolean) { From 1120c1b1656a57b509a2ae9fec03c1d91cc84141 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 1 Nov 2023 11:09:58 +0000 Subject: [PATCH 35/45] - Revert engines to OkHttp for JVM and Android and CIO for Darwin - Update to CMake 3.27.7 - Enabling sync tests for Darwin --- CHANGELOG.md | 7 ++++--- Jenkinsfile | 4 ++-- buildSrc/src/main/kotlin/Config.kt | 2 +- packages/library-sync/build.gradle.kts | 5 ++--- .../io/realm/kotlin/mongodb/internal/HttpClientCache.kt | 5 ++--- .../io/realm/kotlin/mongodb/internal/HttpClientCache.kt | 4 ++-- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 815e1d705b..1a7897ae5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,11 +14,11 @@ This release upgrades the Sync metadata in a way that is not compatible with old * Fix error in `RealmAny.equals` that would sometimes return `true` when comparing RealmAnys wrapping same type but different values. (Issue [#1523](https://github.com/realm/realm-kotlin/pull/1523)) * [Sync] If calling a function on App Services that resulted in a redirect, it would only redirect for GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) * [Sync] Manual client reset on Windows would not trigger correctly when run inside `onManualResetFallback`. (Issue [#1515](https://github.com/realm/realm-kotlin/pull/1515)) -* [Sync] `ClientResetRequiredException.executeClientReset()` now returns a boolean indicating if the manual reset fully succeded or not. (Issue [#1515](https://github.com/realm/realm-kotlin/pull/1515)) +* [Sync] `ClientResetRequiredException.executeClientReset()` now returns a boolean indicating if the manual reset fully succeeded or not. (Issue [#1515](https://github.com/realm/realm-kotlin/pull/1515)) * [Sync] If calling a function on App Services that resulted in a redirect, it would only redirect for GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) * [Sync] If calling a function on App Services that resulted in a redirect, it would only redirect for GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) -* [Sync] Added option to use managed WebSockets via Ktor instead of Realm's built-in WebSocket client for Sync traffic. Managed WebSockets offer improved support for proxies and firewalls that require authentication. This feature is currently opt-in and can be enabled by using `AppConfiguration.usePlatformNetworking()`. Managed WebSockets will become the default in a future version. (PR [#1528](https://github.com/realm/realm-kotlin/pull/1528)). Currently only JVM and Android platforms are supported. +* [Sync] Added option to use managed WebSockets via Ktor instead of Realm's built-in WebSocket client for Sync traffic. Managed WebSockets offer improved support for proxies and firewalls that require authentication. This feature is currently opt-in and can be enabled by using `AppConfiguration.usePlatformNetworking()`. Managed WebSockets will become the default in a future version. (PR [#1528](https://github.com/realm/realm-kotlin/pull/1528)). ### Compatibility * File format: Generates Realms with file format v23. @@ -37,7 +37,8 @@ GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) ### Internal * Updated to Realm Core 13.23.1, commit 43853ebe18bd2a6873e7f300d18b6a5e614da627. * Update to Ktor 2.3.4. -* Switched Ktor engine to CIO for Android and JVM to work around https://youtrack.jetbrains.com/issue/KTOR-6266. Revert to OkHttp when the issue is fixed. +* Switched Ktor engine to CIO for Darwin to work around https://youtrack.jetbrains.com/issue/KTOR-6267. +* Updated to CMake 3.27.7 ## 1.11.1 (2023-09-07) diff --git a/Jenkinsfile b/Jenkinsfile index 9c9a8b23b3..cbed1be899 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -202,7 +202,7 @@ pipeline { // This will overwrite previous test results, but should be ok as we would not get here // if previous stages failed. { - testAndCollect("packages", "cleanAllTests macosTest -PincludeSdkModules=false") + testAndCollect("packages", "cleanAllTests -PsyncUsePlatformNetworking=true macosTest -PincludeSdkModules=false") }, ]) } @@ -222,7 +222,7 @@ pipeline { steps { testWithServer([ { - testAndCollect("packages", "cleanAllTests iosTest -PincludeSdkModules=false") + testAndCollect("packages", "cleanAllTests -PsyncUsePlatformNetworking=true iosTest -PincludeSdkModules=false") } ]) } diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 8da42b0f3a..0be4731780 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -115,7 +115,7 @@ object Versions { const val buildkonfig = "0.13.3" // https://github.com/yshrsmz/BuildKonfig // Not currently used, so mostly here for documentation. Core requires minimum 3.15, but 3.18.1 is available through the Android SDK. // Build also tested successfully with 3.21.4 (latest release). - const val cmake = "3.22.1" + const val cmake = "3.27.7" const val coroutines = "1.7.0" // https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core const val datetime = "0.4.0" // https://github.com/Kotlin/kotlinx-datetime const val detektPlugin = "1.22.0-RC2" // https://github.com/detekt/detekt diff --git a/packages/library-sync/build.gradle.kts b/packages/library-sync/build.gradle.kts index 7042f9a9bb..d462decd32 100644 --- a/packages/library-sync/build.gradle.kts +++ b/packages/library-sync/build.gradle.kts @@ -78,8 +78,7 @@ kotlin { val jvm by creating { dependsOn(commonMain) dependencies { - // TODO revert back to okhttp when https://youtrack.jetbrains.com/issue/KTOR-6266 is fixed - implementation("io.ktor:ktor-client-cio:${Versions.ktor}") + implementation("io.ktor:ktor-client-okhttp:${Versions.ktor}") } } val jvmMain by getting { @@ -96,7 +95,7 @@ kotlin { val nativeDarwin by creating { dependsOn(commonMain) dependencies { - implementation("io.ktor:ktor-client-darwin:${Versions.ktor}") + implementation("io.ktor:ktor-client-cio:${Versions.ktor}") } } val macosX64Main by getting { dependsOn(nativeDarwin) } diff --git a/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt b/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt index 73a5138713..af14352554 100644 --- a/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt +++ b/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt @@ -3,7 +3,7 @@ package io.realm.kotlin.mongodb.internal import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig -import io.ktor.client.engine.cio.CIO +import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.logging.Logger /** @@ -21,8 +21,7 @@ internal actual class HttpClientCache actual constructor(timeoutMs: Long, custom } public actual fun createPlatformClient(block: HttpClientConfig<*>.() -> Unit): HttpClient { - // Revert to OkHttp when https://youtrack.jetbrains.com/issue/KTOR-6266 is fixed - return HttpClient(CIO) { + return HttpClient(OkHttp) { this.apply(block) } } diff --git a/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt b/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt index 2dbd8aa975..59a98e1717 100644 --- a/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt +++ b/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt @@ -2,7 +2,7 @@ package io.realm.kotlin.mongodb.internal import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig -import io.ktor.client.engine.darwin.Darwin +import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.websocket.WebSockets @@ -20,7 +20,7 @@ internal actual class HttpClientCache actual constructor(timeoutMs: Long, custom } public actual fun createPlatformClient(block: HttpClientConfig<*>.() -> Unit): HttpClient { - return HttpClient(Darwin) { + return HttpClient(CIO) { install(WebSockets) this.apply(block) } From fed3971c760a76cd012911203a38c036c6aacc2d Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 1 Nov 2023 16:36:35 +0000 Subject: [PATCH 36/45] Disabling Darwin Sync tests --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index cbed1be899..9c9a8b23b3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -202,7 +202,7 @@ pipeline { // This will overwrite previous test results, but should be ok as we would not get here // if previous stages failed. { - testAndCollect("packages", "cleanAllTests -PsyncUsePlatformNetworking=true macosTest -PincludeSdkModules=false") + testAndCollect("packages", "cleanAllTests macosTest -PincludeSdkModules=false") }, ]) } @@ -222,7 +222,7 @@ pipeline { steps { testWithServer([ { - testAndCollect("packages", "cleanAllTests -PsyncUsePlatformNetworking=true iosTest -PincludeSdkModules=false") + testAndCollect("packages", "cleanAllTests iosTest -PincludeSdkModules=false") } ]) } From fdb717a24618abd997227edb1cbf73acad301e4d Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 1 Nov 2023 17:01:45 +0000 Subject: [PATCH 37/45] - Update Core commit - Fix merge error --- .../interop/sync/ProtocolErrorCode.kt | 15 ------ .../interop/sync/ProtocolErrorCode.kt | 43 ---------------- .../interop/sync/ProtocolErrorCode.kt | 49 ------------------- packages/external/core | 2 +- 4 files changed, 1 insertion(+), 108 deletions(-) diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt index 6d0106ca2c..a182d610a8 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt @@ -125,21 +125,6 @@ expect enum class WebsocketErrorCode : CodeDescription { } } -expect enum class WebsocketCallbackResult : CodeDescription { - RLM_ERR_SYNC_SOCKET_SUCCESS, - RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED, - RLM_ERR_SYNC_SOCKET_RUNTIME, - RLM_ERR_SYNC_SOCKET_OUT_OF_MEMORY, - RLM_ERR_SYNC_SOCKET_ADDRESS_SPACE_EXHAUSTED, - RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED, - RLM_ERR_SYNC_SOCKET_NOT_SUPPORTED, - RLM_ERR_SYNC_SOCKET_INVALID_ARGUMENT; - - companion object { - fun of(nativeValue: Int): WebsocketCallbackResult? - } -} - /** * Wrapper for C-API `realm_sync_socket_callback_result` * See https://github.com/realm/realm-core/blob/master/src/realm/error_codes.h#L298 diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt index d6b01118b6..c01be29fc1 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt @@ -179,46 +179,3 @@ actual enum class WebsocketCallbackResult(override val description: String, over } } } - -actual enum class WebsocketCallbackResult(override val description: String, override val nativeValue: Int) : CodeDescription { - - RLM_ERR_SYNC_SOCKET_SUCCESS( - "Websocket callback success", - realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_SUCCESS - ), - RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED( - "Websocket callback aborted", - realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED - ), - RLM_ERR_SYNC_SOCKET_RUNTIME( - "Websocket Runtime error", - realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_RUNTIME - ), - RLM_ERR_SYNC_SOCKET_OUT_OF_MEMORY( - "Websocket out of memory ", - realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_OUT_OF_MEMORY - ), - RLM_ERR_SYNC_SOCKET_ADDRESS_SPACE_EXHAUSTED( - "Websocket address space exhausted", - realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_ADDRESS_SPACE_EXHAUSTED - ), - RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED( - "Websocket connection closed", - realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED - ), - RLM_ERR_SYNC_SOCKET_NOT_SUPPORTED( - "Websocket not supported", - realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_NOT_SUPPORTED - ), - RLM_ERR_SYNC_SOCKET_INVALID_ARGUMENT( - "Websocket invalid argument", - realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_INVALID_ARGUMENT - ); - - actual companion object { - actual fun of(nativeValue: Int): WebsocketCallbackResult? = - values().firstOrNull { value -> - value.nativeValue == nativeValue - } - } -} diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt index 5a2adab448..805184bc5c 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt @@ -190,52 +190,3 @@ actual enum class WebsocketCallbackResult( } } } - -actual enum class WebsocketCallbackResult( - override val description: String, - nativeError: realm_sync_socket_callback_result -) : CodeDescription { - - RLM_ERR_SYNC_SOCKET_SUCCESS( - "Websocket callback success", - realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_SUCCESS - ), - RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED( - "Websocket callback aborted", - realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED - ), - RLM_ERR_SYNC_SOCKET_RUNTIME( - "Websocket Runtime error", - realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_RUNTIME - ), - RLM_ERR_SYNC_SOCKET_OUT_OF_MEMORY( - "Websocket out of memory ", - realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_OUT_OF_MEMORY - ), - RLM_ERR_SYNC_SOCKET_ADDRESS_SPACE_EXHAUSTED( - "Websocket address space exhausted", - realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_ADDRESS_SPACE_EXHAUSTED - ), - RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED( - "Websocket connection closed", - realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED - ), - RLM_ERR_SYNC_SOCKET_NOT_SUPPORTED( - "Websocket not supported", - realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_NOT_SUPPORTED - ), - RLM_ERR_SYNC_SOCKET_INVALID_ARGUMENT( - "Websocket invalid argument", - realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_INVALID_ARGUMENT - ); - - override val nativeValue: Int = nativeError.value.toInt() - val asNativeEnum: realm_sync_socket_callback_result = nativeError - - actual companion object { - actual fun of(nativeValue: Int): WebsocketCallbackResult? = - values().firstOrNull { value -> - value.nativeValue == nativeValue - } - } -} diff --git a/packages/external/core b/packages/external/core index 43853ebe18..e6271d7230 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit 43853ebe18bd2a6873e7f300d18b6a5e614da627 +Subproject commit e6271d72308b40399890060f58a88cf568c2ee22 From 50639dbcf89c707e46d1e5757741938c588a1b3f Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Sat, 25 Nov 2023 18:24:04 +0000 Subject: [PATCH 38/45] Reworked the Ktor Websocket Trasnport to use OkHttp for JVM and Android. KTOR doesn't expose Close Frames which are necessary to rely any error code to Core (invalid token for example) CIO client also has issues with Android doze https://youtrack.jetbrains.com/issue/KTOR-5993 --- .../interop/sync/WebSocketTransport.kt | 17 +- .../kotlin/internal/interop/RealmInterop.kt | 2 +- .../src/main/jni/realm_api_helpers.cpp | 2 +- .../kotlin/internal/RealmListInternal.kt | 2 +- .../realm/kotlin/mongodb/AppConfiguration.kt | 4 +- .../realm/kotlin/mongodb/internal/AppImpl.kt | 3 - .../internal/KtorWebSocketTransport.kt | 322 ------------------ .../internal/RealmWebSocketTransport.kt | 105 ++++++ .../mongodb/internal/OkHttpWebsocketClient.kt | 215 ++++++++++++ .../mongodb/internal/NetworkStateObserver.kt | 16 + .../io/realm/kotlin/test/mongodb/TestApp.kt | 2 +- .../test/mongodb/common/SyncedRealmTests.kt | 10 +- .../kotlin/test/mongodb/jvm/RealmTests.kt | 8 +- 13 files changed, 365 insertions(+), 343 deletions(-) delete mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmWebSocketTransport.kt create mode 100644 packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt index 14f7458a35..d614e91ab1 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt @@ -38,10 +38,7 @@ interface WebSocketTransport { reason: String = "" ) { RealmInterop.realm_sync_socket_callback_complete( - handlerCallback, - cancelled, - status, - reason + handlerCallback, cancelled, status, reason ) } @@ -60,7 +57,12 @@ class CancellableTimer( interface WebSocketClient { fun send(message: ByteArray, handlerCallback: RealmWebsocketHandlerCallbackPointer) - fun closeWebsocket() + fun close() +} + +interface WebsocketEngine { + fun shutdown() + fun getEngine(timeoutMs: Long = 0): T } class WebSocketObserver(private val webSocketObserverPointer: RealmWebsocketProviderPointer) { @@ -78,10 +80,7 @@ class WebSocketObserver(private val webSocketObserverPointer: RealmWebsocketProv fun onClose(wasClean: Boolean, errorCode: WebsocketErrorCode, reason: String) { RealmInterop.realm_sync_socket_websocket_closed( - webSocketObserverPointer, - wasClean, - errorCode, - reason + webSocketObserverPointer, wasClean, errorCode, reason ) } } diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 87c6965f86..5e8ad20068 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -2818,7 +2818,7 @@ actual object RealmInterop { Unit }, websocket_free_func = staticCFunction { userdata: CPointer?, websocket: realm_sync_socket_websocket_t? -> - safeUserData(websocket).closeWebsocket() + safeUserData(websocket).close() disposeUserData(websocket) } ) diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index 0ab8144461..ad7b0b12f9 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -821,7 +821,7 @@ static void realm_sync_websocket_free(realm_userdata_t userdata, realm_sync_socket_websocket_t websocket_userdata) { if (websocket_userdata != nullptr) { auto jenv = get_env(false); - static jmethodID close_method = jenv->GetMethodID(JavaClassGlobalDef::sync_websocket_client(), "closeWebsocket", "()V"); + static jmethodID close_method = jenv->GetMethodID(JavaClassGlobalDef::sync_websocket_client(), "close", "()V"); jobject websocket_client = static_cast(websocket_userdata); jenv->CallVoidMethod(websocket_client, close_method); diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt index 2b4d12221b..e1e10aa510 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt @@ -207,7 +207,7 @@ internal interface ListOperator : CollectionOperator { fun get(index: Int): E - // TODO OPTIMIZE We technically don't need update policy and cache for primitie lists but right now RealmObjectHelper.assign doesn't know how to differentiate the calls to the operator + // TODO OPTIMIZE We technically don't need update policy and cache for primitive lists but right now RealmObjectHelper.assign doesn't know how to differentiate the calls to the operator fun insert( index: Int, element: E, diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt index 4e55158fda..6f401039a9 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt @@ -38,8 +38,8 @@ import io.realm.kotlin.mongodb.ext.customData import io.realm.kotlin.mongodb.ext.profile import io.realm.kotlin.mongodb.internal.AppConfigurationImpl import io.realm.kotlin.mongodb.internal.KtorNetworkTransport -import io.realm.kotlin.mongodb.internal.KtorWebSocketTransport import io.realm.kotlin.mongodb.internal.LogObfuscatorImpl +import io.realm.kotlin.mongodb.internal.RealmWebSocketTransport import io.realm.kotlin.mongodb.sync.SyncConfiguration import kotlinx.coroutines.CoroutineDispatcher import org.mongodb.kbson.ExperimentalKBsonSerializerApi @@ -408,7 +408,7 @@ public interface AppConfiguration { val websocketTransport: ((DispatcherHolder) -> WebSocketTransport)? = if (usePlatformNetworking) { dispatcherHolder -> - websocketTransport ?: KtorWebSocketTransport( + websocketTransport ?: RealmWebSocketTransport( timeoutMs = 60000, dispatcherHolder = dispatcherHolder ) 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 201b44980a..ffd161b575 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 @@ -178,9 +178,6 @@ public class AppImpl( networkTransport.close() nativePointer.release() NetworkStateObserver.removeListener(connectionListener) - // It's important to close the transport *after* we delete the App, since SyncSession dtor - // still relies on the event loop (powered by the coroutines) to post function handler to be executed. - websocketTransport?.close() } internal companion object { diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt deleted file mode 100644 index 9b4a3e4067..0000000000 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/KtorWebSocketTransport.kt +++ /dev/null @@ -1,322 +0,0 @@ -package io.realm.kotlin.mongodb.internal - -import io.ktor.client.HttpClient -import io.ktor.client.plugins.HttpTimeout -import io.ktor.client.plugins.websocket.ClientWebSocketSession -import io.ktor.client.plugins.websocket.WebSockets -import io.ktor.client.plugins.websocket.webSocket -import io.ktor.client.request.header -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpMethod -import io.ktor.http.HttpStatusCode -import io.ktor.http.URLProtocol -import io.ktor.websocket.CloseReason -import io.ktor.websocket.Frame -import io.ktor.websocket.close -import io.ktor.websocket.readReason -import io.ktor.websocket.readText -import io.realm.kotlin.internal.ContextLogger -import io.realm.kotlin.internal.interop.RealmWebsocketHandlerCallbackPointer -import io.realm.kotlin.internal.interop.sync.CancellableTimer -import io.realm.kotlin.internal.interop.sync.WebSocketClient -import io.realm.kotlin.internal.interop.sync.WebSocketObserver -import io.realm.kotlin.internal.interop.sync.WebSocketTransport -import io.realm.kotlin.internal.interop.sync.WebsocketErrorCode -import io.realm.kotlin.internal.util.DispatcherHolder -import kotlinx.atomicfu.AtomicRef -import kotlinx.atomicfu.atomic -import kotlinx.coroutines.CompletableJob -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED -import kotlinx.coroutines.channels.consumeEach -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking - -public class KtorWebSocketTransport( - timeoutMs: Long, - private val dispatcherHolder: DispatcherHolder -) : WebSocketTransport { - - private val logger = ContextLogger("Websocket") - private val client: HttpClient by lazy { createWebSocketClient(timeoutMs) } - private val transportJob: CompletableJob by lazy { Job() } - private val scope: CoroutineScope by lazy { CoroutineScope(dispatcherHolder.dispatcher + transportJob) } - - override fun post(handlerCallback: RealmWebsocketHandlerCallbackPointer) { - scope.launch { - (this as Job).invokeOnCompletion { completionHandler: Throwable? -> - // Only run the callback if it was not cancelled in the meantime - when (completionHandler) { - null -> runCallback(handlerCallback) - else -> runCallback( - handlerCallback, cancelled = true - ) - } - } - } - } - - override fun createTimer( - delayInMilliseconds: Long, - handlerCallback: RealmWebsocketHandlerCallbackPointer - ): CancellableTimer { - val atomicCallback: AtomicRef = atomic(handlerCallback) - return CancellableTimer( - scope.launch { - delay(delayInMilliseconds) - atomicCallback.getAndSet(null)?.run { - runCallback(this) - } - } - ) { // Cancel lambda - scope.launch { - atomicCallback.getAndSet(null)?.run { - runCallback(this, cancelled = true) - } - } - } - } - - override fun connect( - observer: WebSocketObserver, - path: String, - address: String, - port: Long, - isSsl: Boolean, - numProtocols: Long, - supportedProtocols: String - ): WebSocketClient { - - return object : WebSocketClient { - private val websocketJob: CompletableJob by lazy { Job() } - private val websocketExceptionHandler: CoroutineExceptionHandler by lazy { - CoroutineExceptionHandler { _, exception: Throwable -> - logger.error(exception) - closeWebsocket() - } - } - private val scope: CoroutineScope by lazy { CoroutineScope(dispatcherHolder.dispatcher + websocketJob + websocketExceptionHandler) } - private val writeChannel = - Channel>(capacity = UNLIMITED) - - private val binaryBuffer = FrameBuffer { - observer.onNewMessage(it) - } - private lateinit var session: ClientWebSocketSession - - init { - openConnection() - } - - @Suppress("LongMethod") - private fun openConnection() { - scope.launch { - client.webSocket( - method = HttpMethod.Get, - host = address, - port = port.toInt(), - path = path, - request = { - url.protocol = if (isSsl) URLProtocol.WSS else URLProtocol.WS - header(HttpHeaders.SecWebSocketProtocol, supportedProtocols) - }, - ) { - session = this - // it's unlikely to get a WebSocketSession without a successful protocol switch - // but we're double checking the status here - if (call.response.status != HttpStatusCode.SwitchingProtocols) { - observer.onError() - observer.onClose( - wasClean = false, - errorCode = WebsocketErrorCode.RLM_ERR_WEBSOCKET_CONNECTION_FAILED, - reason = "Websocket server responded with status code ${call.response.status} instead of ${HttpStatusCode.SwitchingProtocols}" - ) - } else { - when ( - val selectedProtocol = - call.response.headers[HttpHeaders.SecWebSocketProtocol] - ) { - null -> { - observer.onError() - observer.onClose( - false, - WebsocketErrorCode.RLM_ERR_WEBSOCKET_PROTOCOLERROR, - "${HttpHeaders.SecWebSocketProtocol} header not returned. Sync server didn't return supported protocol" + ". Supported protocols are = $supportedProtocols" - ) - close( - CloseReason( - CloseReason.Codes.PROTOCOL_ERROR, - "Server didn't select a supported protocol. Supported protocols are = $supportedProtocols" - ) - ) // Sends a [Frame.Close]. - } - - else -> { - scope.launch { - observer.onConnected(selectedProtocol) - } - } - } - - // Writing messages to WebSocket - scope.launch { - writeChannel.consumeEach { - // There's no fragmentation needed when sending frames from client - // so 'fin' should always be `true` - outgoing.send(Frame.Binary(true, it.first)) - runCallback(it.second) - } - } - - // Reading messages from WebSocket - scope.launch { - incoming.consumeEach { - when (val frame = it) { - is Frame.Binary -> { - val shouldCloseSocket = - binaryBuffer.appendAndSend(frame) - if (shouldCloseSocket) { - closeWebsocket() - } - } - - is Frame.Close -> { - // It's important to rely properly the error code from the server. - // The server will report auth errors (and a few other error types) - // as websocket application-level errors after establishing the socket, rather than failing at the HTTP layer. - // since the websocket spec does not allow the HTTP status code from the response to be - // passed back to the client from the websocket implementation (example instruct a refresh token - // via a 401 HTTP response is not possible) see https://jira.mongodb.org/browse/BAAS-10531. - // In order to provide a reasonable response that the Sync Client can react upon, the private range of websocket close status codes - // 4000-4999, can be used to return a more specific error. - val errorCode: WebsocketErrorCode = - frame.readReason()?.code?.toInt() - ?.let { code -> WebsocketErrorCode.of(code) } - ?: WebsocketErrorCode.RLM_ERR_WEBSOCKET_OK - val reason: String = frame.readReason()?.toString() - ?: "Received Close from Websocket server" - - observer.onClose(true, errorCode, reason) - } - - is Frame.Text -> { - logger.warn("Received unexpected text WebSocket message ${frame.readText()}") - } - - // Raw WebSocket Frames (i.e Frame.Ping & Frame.Pong) are handled automatically with the client - // (Core doesn't care about these in the new API) - else -> { - logger.warn("Received unexpected message from server, Frame type = ${frame.frameType}") - } - } - } - } - websocketJob.join() // otherwise the client will send end the session and send a Close - } - } - } - } - - override fun send( - message: ByteArray, - handlerCallback: RealmWebsocketHandlerCallbackPointer - ) { - writeChannel.trySend(Pair(message, handlerCallback)) - } - - override fun closeWebsocket() { - if (::session.isInitialized) { - session.cancel() // Terminate the WebSocket session, connect needs to be called again. - } - // Collect unprocessed writes and cancel them (mainly to avoid leaking the FunctionHandler). - while (true) { - val result = writeChannel.tryReceive() - if (result.isSuccess) { - result.getOrNull()?.run { - runBlocking(scope.coroutineContext) { - runCallback(handlerCallback = second, cancelled = true) - } - } - } else { - // No more elements in the channel - break - } - } - writeChannel.close() - websocketJob.cancel() // Cancel all scheduled jobs. - } - } - } - - override fun write( - webSocketClient: WebSocketClient, - data: ByteArray, - length: Long, - handlerCallback: RealmWebsocketHandlerCallbackPointer - ) { - webSocketClient.send(data, handlerCallback) - } - - override fun close() { - transportJob.cancel() - client.close() - } - - private fun createWebSocketClient(timeoutMs: Long): HttpClient { - return createPlatformClient { - install(HttpTimeout) { - connectTimeoutMillis = timeoutMs - requestTimeoutMillis = timeoutMs - socketTimeoutMillis = timeoutMs - } - install(WebSockets) - } - } -} - -/** - * Helper class that handles Frame [fragmentation](https://www.rfc-editor.org/rfc/rfc6455#section-5.4). - * Core expect a full binary frame to process a changeset, however the server can choose to split websocket Frames. - * We need to buffer them until we receive the `Frame.fin == true` flag. - * - * Note: Core doesn't send fragmented Frames, so this buffering only needed when reading from the websocket. - */ -private class FrameBuffer(val sendDefragmentedMessageToObserver: (binaryMessage: ByteArray) -> Boolean) { - private val buffer = mutableListOf() - private var currentSize = 0 - - /** - * @return True if we should close the Websocket after this write. - */ - fun appendAndSend(frame: Frame): Boolean { - if (frame.data.isNotEmpty()) { - buffer.add(frame.data) - currentSize += frame.data.size - } - - if (frame.fin) { - // Append fragmented Frames and flush the buffer - return sendDefragmentedMessageToObserver(flush()) - } - return false - } - - private fun flush(): ByteArray { - val entireFrame = ByteArray(currentSize) - var currentIndex = 0 - - for (fragment in buffer) { - fragment.copyInto(entireFrame, destinationOffset = currentIndex) - currentIndex += fragment.size - } - - buffer.clear() - currentSize = 0 - return entireFrame - } -} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmWebSocketTransport.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmWebSocketTransport.kt new file mode 100644 index 0000000000..1d891037e3 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmWebSocketTransport.kt @@ -0,0 +1,105 @@ +package io.realm.kotlin.mongodb.internal + +import io.realm.kotlin.internal.interop.RealmWebsocketHandlerCallbackPointer +import io.realm.kotlin.internal.interop.sync.CancellableTimer +import io.realm.kotlin.internal.interop.sync.WebSocketClient +import io.realm.kotlin.internal.interop.sync.WebSocketObserver +import io.realm.kotlin.internal.interop.sync.WebSocketTransport +import io.realm.kotlin.internal.interop.sync.WebsocketEngine +import io.realm.kotlin.internal.util.DispatcherHolder +import kotlinx.atomicfu.AtomicRef +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.CompletableJob +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +public class RealmWebSocketTransport( + public val timeoutMs: Long, + public val dispatcherHolder: DispatcherHolder +) : WebSocketTransport { + private val transportJob: CompletableJob by lazy { Job() } + public val scope: CoroutineScope by lazy { CoroutineScope(dispatcherHolder.dispatcher + transportJob) } + private val websocketClients = mutableListOf() + public val engine: WebsocketEngine by lazy { websocketEngine() } + + override fun post(handlerCallback: RealmWebsocketHandlerCallbackPointer) { + scope.launch { + (this as Job).invokeOnCompletion { completionHandler: Throwable? -> + // Only run the callback if it was not cancelled in the meantime + when (completionHandler) { + null -> runCallback(handlerCallback) + else -> runCallback( + handlerCallback, cancelled = true + ) + } + } + } + } + + override fun createTimer( + delayInMilliseconds: Long, + handlerCallback: RealmWebsocketHandlerCallbackPointer + ): CancellableTimer { + val atomicCallback: AtomicRef = + atomic(handlerCallback) + return CancellableTimer( + scope.launch { + delay(delayInMilliseconds) + atomicCallback.getAndSet(null)?.run { // this -> callback pointer + runCallback(this) + } + } + ) { + scope.launch { + atomicCallback.getAndSet(null)?.run { // this -> callback pointer + runCallback(this, cancelled = true) + } + } + } + } + + override fun connect( + observer: WebSocketObserver, + path: String, + address: String, + port: Long, + isSsl: Boolean, + numProtocols: Long, + supportedProtocols: String, + ): WebSocketClient = platformWebsocketClient( + observer, path, address, port, isSsl, supportedProtocols, this + ).also { + websocketClients.add(it) + } + + override fun write( + webSocketClient: WebSocketClient, + data: ByteArray, + length: Long, + handlerCallback: RealmWebsocketHandlerCallbackPointer + ) { + webSocketClient.send(data, handlerCallback) + } + + override fun close() { + websocketClients.forEach { it.close() } + websocketClients.clear() + engine.shutdown() + scope.cancel() + } +} + +public expect fun websocketEngine(): WebsocketEngine +@Suppress("LongParameterList") +public expect fun platformWebsocketClient( + observer: WebSocketObserver, + path: String, + address: String, + port: Long, + isSsl: Boolean, + supportedProtocols: String, + transport: RealmWebSocketTransport +): WebSocketClient diff --git a/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt b/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt new file mode 100644 index 0000000000..18db80004e --- /dev/null +++ b/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt @@ -0,0 +1,215 @@ +package io.realm.kotlin.mongodb.internal + +import io.realm.kotlin.internal.ContextLogger +import io.realm.kotlin.internal.interop.RealmWebsocketHandlerCallbackPointer +import io.realm.kotlin.internal.interop.sync.WebSocketClient +import io.realm.kotlin.internal.interop.sync.WebSocketObserver +import io.realm.kotlin.internal.interop.sync.WebsocketCallbackResult +import io.realm.kotlin.internal.interop.sync.WebsocketEngine +import io.realm.kotlin.internal.interop.sync.WebsocketErrorCode +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener +import okio.ByteString +import okio.ByteString.Companion.toByteString +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference + +@Suppress("LongParameterList") +public class OkHttpWebsocketClient( + private val observer: WebSocketObserver, + path: String, + address: String, + port: Long, + isSsl: Boolean, + supportedProtocols: String, + websocketEngine: WebsocketEngine, + timeoutMs: Long, + private val scope: CoroutineScope, + private val runCallback: ( + handlerCallback: RealmWebsocketHandlerCallbackPointer, + cancelled: Boolean, + status: WebsocketCallbackResult, + reason: String + ) -> Unit +) : WebSocketClient, WebSocketListener() { + + private val logger = ContextLogger("Websocket") + private val okHttpClient: OkHttpClient = websocketEngine.getEngine(timeoutMs) + private lateinit var webSocket: WebSocket + private val isClosed: AtomicBoolean = AtomicBoolean(false) + + init { + val websocketURL = "${if (isSsl) "wss" else "ws"}://$address:$port$path" + val request: Request = Request.Builder().url(websocketURL) + .addHeader("Sec-WebSocket-Protocol", supportedProtocols).build() + + scope.launch { + okHttpClient.newWebSocket(request, this@OkHttpWebsocketClient) + } + } + + override fun onOpen(webSocket: WebSocket, response: Response) { + super.onOpen(webSocket, response) + logger.warn("onOpen websocket ${webSocket.request().url}, response status code ${response.code}") + + this.webSocket = webSocket + + if (!isClosed.get()) { + response.header("Sec-WebSocket-Protocol")?.let { selectedProtocol -> + scope.launch { + if (!isClosed.get()) { // The session could have been paused in the meantime which will cause the WebSocket to be destroyed, as well as the observer so avoid invoking connect on a deleted CAPIWebSocketObserver + observer.onConnected(selectedProtocol) + } + } + } + } + } + + override fun onMessage(webSocket: WebSocket, bytes: ByteString) { + super.onMessage(webSocket, bytes) + if (!isClosed.get()) { + scope.launch { + if (!isClosed.get()) { // socket could have been closed in the meantime + val shouldClose: Boolean = observer.onNewMessage(bytes.toByteArray()) + if (shouldClose) { + webSocket.close( + WebsocketErrorCode.RLM_ERR_WEBSOCKET_OK.nativeValue, + "websocket should be closed after last message received" + ) + } + } + } + } + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + super.onClosing(webSocket, code, reason) + logger.warn("onClosing code = $code reason = $reason") + } + + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { + super.onClosed(webSocket, code, reason) + logger.warn("onClosed code = $code reason = $reason") + if (!isClosed.getAndSet(true)) { + scope.launch { + if (!isClosed.getAndSet(true)) { + // It's important to rely properly the error code from the server. + // The server will report auth errors (and a few other error types) + // as websocket application-level errors after establishing the socket, rather than failing at the HTTP layer. + // since the websocket spec does not allow the HTTP status code from the response to be + // passed back to the client from the websocket implementation (example instruct a refresh token + // via a 401 HTTP response is not possible) see https://jira.mongodb.org/browse/BAAS-10531. + // In order to provide a reasonable response that the Sync Client can react upon, the private range of websocket close status codes + // 4000-4999, can be used to return a more specific error. + WebsocketErrorCode.of(code)?.let { errorCode -> + observer.onClose( + true, errorCode, reason + ) + } ?: run { + observer.onClose( + true, + WebsocketErrorCode.RLM_ERR_WEBSOCKET_FATAL_ERROR, + "Unknown error code $code. original reason $reason" + ) + } + } + } + } + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + super.onFailure(webSocket, t, response) + logger.warn("onFailure throwable ${t.message} websocket closed? = ${isClosed.get()}") + if (!isClosed.get()) { + scope.launch { + observer.onError() + } + } + } + + override fun send(message: ByteArray, handlerCallback: RealmWebsocketHandlerCallbackPointer) { + scope.launch { + try { + webSocket.send(message.toByteString()) + runCallback( + handlerCallback, false, WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_SUCCESS, "" + ) + } catch (e: Exception) { + runCallback( + handlerCallback, + false, + WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_RUNTIME, + "Sending Frame exception: ${e.message}" + ) + } + } + } + + override fun close() { + logger.warn("close") + if (!isClosed.getAndSet(true) && ::webSocket.isInitialized) { // Generalise :webSocket.isInitialized whenever it is used + scope.launch { + webSocket.close( + WebsocketErrorCode.RLM_ERR_WEBSOCKET_OK.nativeValue, "client closing websocket" + ) + } + } + } +} + +private object OkHttpEngine : WebsocketEngine { + private var engine: AtomicReference = AtomicReference(null) + + override fun shutdown() { + engine.getAndSet(null)?.dispatcher?.executorService?.shutdownNow() + } + + override fun getEngine(timeoutMs: Long): T { + if (engine.get() == null) { // FIXME check atomicity + engine.set( + OkHttpClient.Builder().connectTimeout(timeoutMs, TimeUnit.MILLISECONDS) + .readTimeout(timeoutMs, TimeUnit.MILLISECONDS) + .callTimeout(timeoutMs, TimeUnit.MILLISECONDS) + .writeTimeout(timeoutMs, TimeUnit.MILLISECONDS).build() + ) + } + + @Suppress("UNCHECKED_CAST") return engine.get() as T + } +} + +public actual fun websocketEngine(): WebsocketEngine { + return OkHttpEngine +} +@Suppress("LongParameterList") +public actual fun platformWebsocketClient( + observer: WebSocketObserver, + path: String, + address: String, + port: Long, + isSsl: Boolean, + supportedProtocols: String, + transport: RealmWebSocketTransport +): WebSocketClient { + return OkHttpWebsocketClient( + observer, + path, + address, + port, + isSsl, + supportedProtocols, + transport.engine, + transport.timeoutMs, + transport.scope + ) { handlerCallback: RealmWebsocketHandlerCallbackPointer, cancelled: Boolean, status: WebsocketCallbackResult, reason: String -> + transport.scope.launch { + transport.runCallback(handlerCallback, cancelled, status, reason) + } + } +} diff --git a/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt b/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt index 75cb4b9d68..94a80526e6 100644 --- a/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt +++ b/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt @@ -1,8 +1,24 @@ package io.realm.kotlin.mongodb.internal +import io.realm.kotlin.internal.interop.sync.WebSocketClient +import io.realm.kotlin.internal.interop.sync.WebSocketObserver +import io.realm.kotlin.internal.interop.sync.WebsocketEngine + internal actual fun registerSystemNetworkObserver() { // This is handled automatically by Realm Core which will also call `Sync.reconnect()` // automatically. So on iOS/macOS we do not do anything. // See https://github.com/realm/realm-core/blob/a678c36a85cf299f745f68f8b5ceff364d714181/src/realm/object-store/sync/impl/sync_client.hpp#L82C3-L82C3 // for further details. } + +public actual fun platformWebsocketClient( + observer: WebSocketObserver, + path: String, + address: String, + port: Long, + isSsl: Boolean, + supportedProtocols: String, + transport: RealmWebSocketTransport +): WebSocketClient = TODO() + +public actual fun websocketEngine(): WebsocketEngine = TODO() diff --git a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt index 4577d0d366..91b7ff6aa2 100644 --- a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt @@ -45,7 +45,7 @@ import kotlinx.coroutines.CoroutineDispatcher import org.mongodb.kbson.ExperimentalKBsonSerializerApi import org.mongodb.kbson.serialization.EJson -val TEST_APP_PARTITION = syncServerAppName("pbs") // With Partion-based Sync +val TEST_APP_PARTITION = syncServerAppName("pbs") // With Partition-based Sync val TEST_APP_FLEX = syncServerAppName("flx") // With Flexible Sync val TEST_APP_CLUSTER_NAME = SyncServerConfig.clusterName diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index fe2851414b..07d64f982e 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -944,10 +944,12 @@ class SyncedRealmTests { val (email1, password1) = randomEmail() to "password1234" val user = flexApp.createUserAndLogIn(email1, password1) val localConfig = createWriteCopyLocalConfig("local.realm") - val syncConfig = createPartitionSyncConfig( + val syncConfig = createFlexibleSyncConfig( user = user, name = "sync.realm", - partitionValue = partitionValue, + initialSubscriptions = { + it.query().subscribe(name = "parentSubscription") + } ) Realm.open(syncConfig).use { flexSyncRealm: Realm -> flexSyncRealm.writeBlocking { @@ -957,6 +959,10 @@ class SyncedRealmTests { } ) } + + flexSyncRealm.syncSession.uploadAllLocalChanges(30.seconds) + flexSyncRealm.syncSession.downloadAllServerChanges(30.seconds) + // Copy to local Realm flexSyncRealm.writeCopyTo(localConfig) } diff --git a/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt b/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt index 8c0d0d7f94..6dc3c149a2 100644 --- a/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt +++ b/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt @@ -21,9 +21,11 @@ import io.realm.kotlin.entities.sync.ChildPk import io.realm.kotlin.entities.sync.ParentPk import io.realm.kotlin.mongodb.Credentials import io.realm.kotlin.mongodb.sync.SyncConfiguration +import io.realm.kotlin.mongodb.syncSession import io.realm.kotlin.test.mongodb.TestApp import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.TestHelper +import io.realm.kotlin.test.util.use import kotlinx.coroutines.runBlocking import kotlin.test.Test import kotlin.test.assertEquals @@ -41,7 +43,11 @@ class RealmTests { val app = TestApp("cleanupAllRealmThreadsOnClose") val user = app.login(Credentials.anonymous()) val configuration = SyncConfiguration.create(user, TestHelper.randomPartitionValue(), setOf(ParentPk::class, ChildPk::class)) - Realm.open(configuration).close() + Realm.open(configuration).use { + // we make sure Schema is exchanged correctly + it.syncSession.uploadAllLocalChanges() + it.syncSession.downloadAllServerChanges() + } app.close() // Wait max 30 seconds for threads to settle From 042eb8a42a3476265bafd33ed3d5a69afe9c7c9c Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 30 Nov 2023 23:02:22 +0000 Subject: [PATCH 39/45] - Cancelled callback when Websocket is closed - Fixing CMake conf --- packages/cinterop/src/jvm/CMakeLists.txt | 2 +- .../mongodb/internal/OkHttpWebsocketClient.kt | 61 +++++++++++++------ 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/packages/cinterop/src/jvm/CMakeLists.txt b/packages/cinterop/src/jvm/CMakeLists.txt index 28087655d3..874f7226d8 100644 --- a/packages/cinterop/src/jvm/CMakeLists.txt +++ b/packages/cinterop/src/jvm/CMakeLists.txt @@ -15,7 +15,7 @@ if (CMAKE_SYSTEM_NAME MATCHES "^Windows") elseif (CMAKE_SYSTEM_NAME MATCHES "^Android") MESSAGE("Building JNI for Android") - set(CAPI_BUILD "${CMAKE_SOURCE_DIR}/../../../external/core}/build-android-${ANDROID_ABI}-${CMAKE_BUILD_TYPE}") + set(CAPI_BUILD "${CMAKE_SOURCE_DIR}/../../../external/core/build-android-${ANDROID_ABI}-${CMAKE_BUILD_TYPE}") set(REALM_INCLUDE_DIRS ${CAPI_BUILD}/src ${CINTEROP_JNI} ${SWIG_JNI_GENERATED} ${SWIG_JNI_HELPERS}) set(REALM_TARGET_LINK_LIBS log android RealmFFIStatic Realm::ObjectStore) diff --git a/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt b/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt index 18db80004e..0af77ec811 100644 --- a/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt +++ b/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt @@ -90,12 +90,12 @@ public class OkHttpWebsocketClient( override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { super.onClosing(webSocket, code, reason) - logger.warn("onClosing code = $code reason = $reason") + logger.debug("onClosing code = $code reason = $reason") } override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { super.onClosed(webSocket, code, reason) - logger.warn("onClosed code = $code reason = $reason") + logger.debug("onClosed code = $code reason = $reason") if (!isClosed.getAndSet(true)) { scope.launch { if (!isClosed.getAndSet(true)) { @@ -125,34 +125,58 @@ public class OkHttpWebsocketClient( override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { super.onFailure(webSocket, t, response) - logger.warn("onFailure throwable ${t.message} websocket closed? = ${isClosed.get()}") + logger.debug("onFailure throwable ${t.message} websocket closed? = ${isClosed.get()}") if (!isClosed.get()) { scope.launch { - observer.onError() + if (!isClosed.get()) { + observer.onError() + } } } } override fun send(message: ByteArray, handlerCallback: RealmWebsocketHandlerCallbackPointer) { - scope.launch { - try { - webSocket.send(message.toByteString()) - runCallback( - handlerCallback, false, WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_SUCCESS, "" - ) - } catch (e: Exception) { - runCallback( - handlerCallback, - false, - WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_RUNTIME, - "Sending Frame exception: ${e.message}" - ) + if (!isClosed.get()) { + scope.launch { + try { + if (!isClosed.get()) { + webSocket.send(message.toByteString()) + runCallback( + handlerCallback, + false, + WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_SUCCESS, + "" + ) + } else { + runCallback( + handlerCallback, + false, + WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED, + "Connection already closed" + ) + } + + } catch (e: Exception) { + runCallback( + handlerCallback, + false, + WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_RUNTIME, + "Sending Frame exception: ${e.message}" + ) + } } + } else { + runCallback( + handlerCallback, + false, + WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED, + "Connection already closed" + ) } } override fun close() { - logger.warn("close") + logger.debug("close") if (!isClosed.getAndSet(true) && ::webSocket.isInitialized) { // Generalise :webSocket.isInitialized whenever it is used scope.launch { webSocket.close( @@ -187,6 +211,7 @@ private object OkHttpEngine : WebsocketEngine { public actual fun websocketEngine(): WebsocketEngine { return OkHttpEngine } + @Suppress("LongParameterList") public actual fun platformWebsocketClient( observer: WebSocketObserver, From 71359d35d9ac3335ac56b9f9634d5ffe07a38a2d Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 4 Dec 2023 21:33:02 +0000 Subject: [PATCH 40/45] fixing compile error --- .../kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt index 531efabc9d..25399a9cdc 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt @@ -470,7 +470,7 @@ public interface AppConfiguration { else MetadataMode.RLM_SYNC_CLIENT_METADATA_MODE_ENCRYPTED, appNetworkDispatcherFactory = appNetworkDispatcherFactory, networkTransportFactory = networkTransport, - webSocketTransport = websocketTransport, + websocketTransport = websocketTransport, syncRootDirectory = syncRootDirectory, logger = logConfig, appName = appName, From a55e9ed2ac134d2c6b3b65eef8272588ed3d01cf Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Tue, 5 Dec 2023 23:18:11 +0000 Subject: [PATCH 41/45] fixing test --- .../io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index 8e469976f0..41be369765 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -959,9 +959,6 @@ class SyncedRealmTests { ) } - flexSyncRealm.syncSession.uploadAllLocalChanges(30.seconds) - flexSyncRealm.syncSession.downloadAllServerChanges(30.seconds) - // Copy to local Realm flexSyncRealm.writeCopyTo(localConfig) } From 96be9c1a42b8d52099cf43b404f86f17393a1c53 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 6 Dec 2023 10:35:46 +0000 Subject: [PATCH 42/45] PR feedback --- .../internal/interop/sync/SyncUserIdentity.kt | 16 ++++++++++++++ .../interop/sync/WebSocketTransport.kt | 15 +++++++++++++ packages/library-sync/build.gradle.kts | 2 +- .../mongodb/internal/OkHttpWebsocketClient.kt | 20 +++++++++++------- .../mongodb/internal/HttpClientCache.kt | 8 ++----- packages/test-sync/build.gradle.kts | 1 - .../common/SyncClientResetIntegrationTests.kt | 21 ++++++------------- 7 files changed, 53 insertions(+), 30 deletions(-) diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncUserIdentity.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncUserIdentity.kt index c45b3b9916..3fbca71c76 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncUserIdentity.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncUserIdentity.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2022 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.internal.interop.sync /** diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt index 4ede51de32..e7339de602 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.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.internal.interop.sync import io.realm.kotlin.internal.interop.RealmInterop diff --git a/packages/library-sync/build.gradle.kts b/packages/library-sync/build.gradle.kts index d462decd32..d2f3afe90e 100644 --- a/packages/library-sync/build.gradle.kts +++ b/packages/library-sync/build.gradle.kts @@ -95,7 +95,7 @@ kotlin { val nativeDarwin by creating { dependsOn(commonMain) dependencies { - implementation("io.ktor:ktor-client-cio:${Versions.ktor}") + implementation("io.ktor:ktor-client-darwin:${Versions.ktor}") } } val macosX64Main by getting { dependsOn(nativeDarwin) } diff --git a/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt b/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt index 0653b321a7..f79f0c90c5 100644 --- a/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt +++ b/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt @@ -18,6 +18,7 @@ import okio.ByteString import okio.ByteString.Companion.toByteString import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean +import kotlin.random.Random @Suppress("LongParameterList") public class OkHttpWebsocketClient( @@ -42,7 +43,7 @@ public class OkHttpWebsocketClient( ) -> Unit ) : WebSocketClient, WebSocketListener() { - private val logger = ContextLogger("Websocket") + private val logger = ContextLogger("Websocket-${Random.nextInt()}") /** * [WebsocketEngine] responsible of establishing the connection, sending and receiving websocket Frames. @@ -91,6 +92,7 @@ public class OkHttpWebsocketClient( override fun onMessage(webSocket: WebSocket, bytes: ByteString) { super.onMessage(webSocket, bytes) + logger.debug("onMessage: ${bytes.toByteArray().decodeToString()} isClosed = ${isClosed.get()} observerIsClosed = ${observerIsClosed.get()}") runIfNotClosing { val shouldClose: Boolean = observer.onNewMessage(bytes.toByteArray()) @@ -147,6 +149,8 @@ public class OkHttpWebsocketClient( } override fun send(message: ByteArray, handlerCallback: RealmWebsocketHandlerCallbackPointer) { + logger.debug("send: ${message.decodeToString()} isClosed = ${isClosed.get()} observerIsClosed = ${observerIsClosed.get()}") + // send any queued Frames even if the Core observer is closed, but only if the websocket is still open, this can be a message like 'unbind' // which instruct the Sync server to terminate the Sync Session (server will respond by 'unbound'). if (!isClosed.get()) { @@ -178,12 +182,14 @@ public class OkHttpWebsocketClient( } } } else { - runCallback( - handlerCallback, - observerIsClosed.get(), // if the Core observer is closed we run this callback as cancelled (to free underlying resources) - WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED, - "Connection already closed" - ) + scope.launch { + runCallback( + handlerCallback, + observerIsClosed.get(), // if the Core observer is closed we run this callback as cancelled (to free underlying resources) + WebsocketCallbackResult.RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED, + "Connection already closed" + ) + } } } diff --git a/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt b/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt index 59a98e1717..1ff1d158e5 100644 --- a/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt +++ b/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt @@ -2,9 +2,8 @@ package io.realm.kotlin.mongodb.internal import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig -import io.ktor.client.engine.cio.CIO +import io.ktor.client.engine.darwin.Darwin import io.ktor.client.plugins.logging.Logger -import io.ktor.client.plugins.websocket.WebSockets /** * Cache HttpClient on iOS. @@ -20,8 +19,5 @@ internal actual class HttpClientCache actual constructor(timeoutMs: Long, custom } public actual fun createPlatformClient(block: HttpClientConfig<*>.() -> Unit): HttpClient { - return HttpClient(CIO) { - install(WebSockets) - this.apply(block) - } + return HttpClient(Darwin, block) } diff --git a/packages/test-sync/build.gradle.kts b/packages/test-sync/build.gradle.kts index f61536655a..07f49567ce 100644 --- a/packages/test-sync/build.gradle.kts +++ b/packages/test-sync/build.gradle.kts @@ -17,7 +17,6 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetWithSimulatorTests import com.codingfeline.buildkonfig.compiler.FieldSpec.Type -import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly plugins { id("org.jetbrains.kotlin.multiplatform") diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt index 2e037246a9..01fd16cb75 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt @@ -413,46 +413,37 @@ class SyncClientResetIntegrationTests { Realm.open(config).use { realm -> runBlocking { - println("<<<<<<<<<<<<<<<<< S1") realm.syncSession.downloadAllServerChanges(defaultTimeout) - println("<<<<<<<<<<<<<<<<< S2") + // This channel helps to validate that the Realm gets updated val objectChannel: Channel> = newChannel() + val job = async { - println("<<<<<<<<<<<<<<<<< S3") getObjects(realm) .asFlow() .collect { - println("<<<<<<<<<<<<<<<<< SEND $it") objectChannel.trySend(it) } } - println("<<<<<<<<<<<<<<<<< S4") // No initial data assertEquals(0, objectChannel.receiveOrFail().list.size) - println("<<<<<<<<<<<<<<<<< S5") + app.triggerClientReset(syncMode, realm.syncSession, user.id) { - println("<<<<<<<<<<<<<<<<< S6") insertElement(realm) - println("<<<<<<<<<<<<<<<<< S7") assertEquals(1, objectChannel.receiveOrFail().list.size) - println("<<<<<<<<<<<<<<<<< S8") } - println("<<<<<<<<<<<<<<<<< S9") + // Validate that the client reset was triggered successfully assertEquals(ClientResetEvents.ON_BEFORE_RESET, channel.receiveOrFail()) - println("<<<<<<<<<<<<<<<<< S10") assertEquals(ClientResetEvents.ON_AFTER_RESET, channel.receiveOrFail()) - println("<<<<<<<<<<<<<<<<< S11") + // TODO We must not need this. Force updating the instance pointer. realm.write { } - println("<<<<<<<<<<<<<<<<< S12") + // Validate Realm instance has been correctly updated assertEquals(0, objectChannel.receiveOrFail().list.size) - println("<<<<<<<<<<<<<<<<< S13") objectChannel.close() - println("<<<<<<<<<<<<<<<<< S14") job.cancel() } } From 0ae12f80fc8d110e1926e778e8f5c30ecc4f5a8e Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 6 Dec 2023 11:27:08 +0000 Subject: [PATCH 43/45] Added removed configuration --- .../kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt | 2 ++ .../io/realm/kotlin/mongodb/internal/HttpClientCache.kt | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt index 25399a9cdc..0c3cf46938 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt @@ -380,6 +380,8 @@ public interface AppConfiguration { /** * Platform Networking offer improved support for proxies and firewalls that require authentication, * instead of Realm's built-in WebSocket client for Sync traffic. This will become the default in a future version. + * + * Note: Only Android and JVM targets are supported so far. */ public fun usePlatformNetworking(enable: Boolean = true): Builder = apply { diff --git a/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt b/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt index af14352554..61e8a2b3d4 100644 --- a/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt +++ b/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt @@ -22,6 +22,11 @@ internal actual class HttpClientCache actual constructor(timeoutMs: Long, custom public actual fun createPlatformClient(block: HttpClientConfig<*>.() -> Unit): HttpClient { return HttpClient(OkHttp) { + engine { + config { + retryOnConnectionFailure(true) + } + } this.apply(block) } } From d491fee118d8cce4da32cc5e39923ccfd820ccfb Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 11 Dec 2023 14:36:24 +0000 Subject: [PATCH 44/45] PR feedback --- .../interop/sync/WebSocketTransport.kt | 65 +++++++++++++++++++ .../src/main/jni/realm_api_helpers.cpp | 6 +- .../internal/RealmWebSocketTransport.kt | 2 +- .../mongodb/internal/OkHttpWebsocketClient.kt | 14 ++-- .../kotlin/test/mongodb/jvm/RealmTests.kt | 6 +- 5 files changed, 77 insertions(+), 16 deletions(-) diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt index e7339de602..136a535c05 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/WebSocketTransport.kt @@ -20,14 +20,33 @@ import io.realm.kotlin.internal.interop.RealmWebsocketHandlerCallbackPointer import io.realm.kotlin.internal.interop.RealmWebsocketProviderPointer import kotlinx.coroutines.Job +/** + * Interface to be implemented by the websocket provider. This helps un-bundle the implementation + * from Core to leverage the platform capabilities (Proxy, firewall, vpn etc.). + */ interface WebSocketTransport { + /** + * Submit a handler function to be executed by the event loop. + */ fun post(handlerCallback: RealmWebsocketHandlerCallbackPointer) + /** + * Create and register a new timer whose handler function will be posted + * to the event loop when the provided delay expires. + * @return [CancellableTimer] to be called if the timer is to be cancelled before the delay. + */ fun createTimer( delayInMilliseconds: Long, handlerCallback: RealmWebsocketHandlerCallbackPointer, ): CancellableTimer + /** + * Create a new websocket pointed to the server indicated by endpoint and + * connect to the server. Any events that occur during the execution of the + * websocket will call directly to the handlers provided by the observer (new messages, error, close events) + * + * @return [WebSocketClient] instance to be used by Core to send data, and signal a close session. + */ @Suppress("LongParameterList") fun connect( observer: WebSocketObserver, @@ -39,6 +58,10 @@ interface WebSocketTransport { supportedSyncProtocols: String ): WebSocketClient + /** + * Writes to the previously created websocket in [connect] the binary data. The provided [handlerCallback] needs + * to run in the event loop after a successful write or in case of an error. + */ fun write( webSocketClient: WebSocketClient, data: ByteArray, @@ -46,6 +69,10 @@ interface WebSocketTransport { handlerCallback: RealmWebsocketHandlerCallbackPointer ) + /** + * This helper function run the provided function pointer. It needs to be called within the same event loop context (thread) + * as the rest of the other functions. + */ fun runCallback( handlerCallback: RealmWebsocketHandlerCallbackPointer, cancelled: Boolean = false, @@ -57,9 +84,16 @@ interface WebSocketTransport { ) } + /** + * Core signal the transport, all websockets previously created with [connect] would have been closed at this point + * this is useful to do any resource cleanup like shutting down the engine or closing coroutine dispatcher. + */ fun close() } +/** + * Cancel a previously scheduled timer created via [WebSocketTransport.createTimer]. + */ class CancellableTimer( private val job: Job, private val cancelCallback: () -> Unit @@ -70,29 +104,60 @@ class CancellableTimer( } } +/** + * Define an interface to interact with the websocket created via [WebSocketTransport.connect]. + * This will be called from Core. + */ interface WebSocketClient { + /** + * Send a binary Frame to the remote peer. + */ fun send(message: ByteArray, handlerCallback: RealmWebsocketHandlerCallbackPointer) + + /** + * Close the websocket. + */ fun close() } +/** + * Defines an abstraction of the underlying Http engine used to create the websocket. + * This abstraction is needed in order to deterministically create and shutdown the engine at the transport level. + * All websocket within the same App share the same transport and by definition the same engine. + */ interface WebsocketEngine { fun shutdown() fun getInstance(): T } +/** + * Wrapper around Core callback pointer (observer). This will delegate calls for all incoming messages from the remote peer. + */ class WebSocketObserver(private val webSocketObserverPointer: RealmWebsocketProviderPointer) { + /** + * Communicate the negotiated Sync protocol. + */ fun onConnected(protocol: String) { RealmInterop.realm_sync_socket_websocket_connected(webSocketObserverPointer, protocol) } + /** + * Notify an error. + */ fun onError() { RealmInterop.realm_sync_socket_websocket_error(webSocketObserverPointer) } + /** + * Forward received message to Core. + */ fun onNewMessage(data: ByteArray): Boolean { return RealmInterop.realm_sync_socket_websocket_message(webSocketObserverPointer, data) } + /** + * Notify closure message. + */ fun onClose(wasClean: Boolean, errorCode: WebsocketErrorCode, reason: String) { RealmInterop.realm_sync_socket_websocket_closed( webSocketObserverPointer, wasClean, errorCode, reason diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index b258a6ba1a..8dff759129 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -768,7 +768,8 @@ static void websocket_post_func(realm_userdata_t userdata, } static realm_sync_socket_timer_t websocket_create_timer_func( - realm_userdata_t userdata, uint64_t delay_ms, realm_sync_socket_timer_callback_t* realm_callback) { + realm_userdata_t userdata, uint64_t delay_ms, + realm_sync_socket_timer_callback_t *realm_callback) { // called from main thread/event loop which should be already attached to JVM auto jenv = get_env(false); @@ -802,8 +803,7 @@ static void websocket_cancel_timer_func(realm_userdata_t userdata, jobject cancellable_timer = static_cast(timer_userdata); static JavaClass cancellable_timer_class(jenv, "io/realm/kotlin/internal/interop/sync/CancellableTimer"); - static JavaMethod cancel_method(jenv, cancellable_timer_class, "cancel", - "()V"); + static JavaMethod cancel_method(jenv, cancellable_timer_class, "cancel", "()V"); jenv->CallVoidMethod(cancellable_timer, cancel_method); jni_check_exception(jenv); diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmWebSocketTransport.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmWebSocketTransport.kt index 4035832915..2f30169e2e 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmWebSocketTransport.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmWebSocketTransport.kt @@ -30,8 +30,8 @@ public class RealmWebSocketTransport( override fun post(handlerCallback: RealmWebsocketHandlerCallbackPointer) { scope.launch { (this as Job).invokeOnCompletion { completionHandler: Throwable? -> - // Only run the callback if it was not cancelled in the meantime when (completionHandler) { + // Only run the callback successfully if it was not cancelled in the meantime null -> runCallback(handlerCallback) else -> runCallback( handlerCallback, cancelled = true diff --git a/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt b/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt index f79f0c90c5..0d0b52a402 100644 --- a/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt +++ b/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt @@ -84,7 +84,7 @@ public class OkHttpWebsocketClient( this.webSocket = webSocket response.header(protocolSelectionHeader)?.let { selectedProtocol -> - runIfNotClosing { + runIfObserverNotClosed { observer.onConnected(selectedProtocol) } } @@ -92,9 +92,9 @@ public class OkHttpWebsocketClient( override fun onMessage(webSocket: WebSocket, bytes: ByteString) { super.onMessage(webSocket, bytes) - logger.debug("onMessage: ${bytes.toByteArray().decodeToString()} isClosed = ${isClosed.get()} observerIsClosed = ${observerIsClosed.get()}") + logger.trace("onMessage: ${bytes.toByteArray().decodeToString()} isClosed = ${isClosed.get()} observerIsClosed = ${observerIsClosed.get()}") - runIfNotClosing { + runIfObserverNotClosed { val shouldClose: Boolean = observer.onNewMessage(bytes.toByteArray()) if (shouldClose) { webSocket.close( @@ -116,7 +116,7 @@ public class OkHttpWebsocketClient( isClosed.set(true) - runIfNotClosing { + runIfObserverNotClosed { // It's important to rely properly the error code from the server. // The server will report auth errors (and a few other error types) // as websocket application-level errors after establishing the socket, rather than failing at the HTTP layer. @@ -143,13 +143,13 @@ public class OkHttpWebsocketClient( super.onFailure(webSocket, t, response) logger.debug("onFailure throwable '${t.message}' isClosed = ${isClosed.get()} observerIsClosed = ${observerIsClosed.get()}") - runIfNotClosing { + runIfObserverNotClosed { observer.onError() } } override fun send(message: ByteArray, handlerCallback: RealmWebsocketHandlerCallbackPointer) { - logger.debug("send: ${message.decodeToString()} isClosed = ${isClosed.get()} observerIsClosed = ${observerIsClosed.get()}") + logger.trace("send: ${message.decodeToString()} isClosed = ${isClosed.get()} observerIsClosed = ${observerIsClosed.get()}") // send any queued Frames even if the Core observer is closed, but only if the websocket is still open, this can be a message like 'unbind' // which instruct the Sync server to terminate the Sync Session (server will respond by 'unbound'). @@ -211,7 +211,7 @@ public class OkHttpWebsocketClient( /** * Runs the [block] inside the transport [scope] only if Core didn't initiate the Websocket closure. */ - private fun runIfNotClosing(block: () -> Unit) { + private fun runIfObserverNotClosed(block: () -> Unit) { if (!observerIsClosed.get()) { // if Core has already closed the websocket there's no point in scheduling this coroutine. scope.launch { // The session could have been paused/closed in the meantime which will cause the WebSocket to be destroyed, as well as the 'observer', diff --git a/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt b/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt index 6dc3c149a2..5b74109910 100644 --- a/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt +++ b/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt @@ -43,11 +43,7 @@ class RealmTests { val app = TestApp("cleanupAllRealmThreadsOnClose") val user = app.login(Credentials.anonymous()) val configuration = SyncConfiguration.create(user, TestHelper.randomPartitionValue(), setOf(ParentPk::class, ChildPk::class)) - Realm.open(configuration).use { - // we make sure Schema is exchanged correctly - it.syncSession.uploadAllLocalChanges() - it.syncSession.downloadAllServerChanges() - } + Realm.open(configuration).close() app.close() // Wait max 30 seconds for threads to settle From 0c8dca82943f9492593fb0124863142041260417 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 11 Dec 2023 15:19:13 +0000 Subject: [PATCH 45/45] - Unused imports - Changelog --- CHANGELOG.md | 3 +-- .../kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33b32ab66d..a93526b20f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ * None. ### Enhancements -* None. +* [Sync] Added option to use managed WebSockets via OkHttp instead of Realm's built-in WebSocket client for Sync traffic (Only Android and JVM targets for now). Managed WebSockets offer improved support for proxies and firewalls that require authentication. This feature is currently opt-in and can be enabled by using `AppConfiguration.usePlatformNetworking()`. Managed WebSockets will become the default in a future version. (PR [#1528](https://github.com/realm/realm-kotlin/pull/1528)). ### Fixed * None. @@ -74,7 +74,6 @@ typically improve performance. The behavior can be controlled through [AppConfiguration.Builder.enableSessionMultiplexing]. It will be made the default in a future release. (Issue [#1578](https://github.com/realm/realm-kotlin/pull/1578)) * [Sync] Various sync timeout options can now be configured through `AppConfiguration.Builder.syncTimeouts()`. (Issue [#971](https://github.com/realm/realm-kotlin/issues/971)). -* [Sync] Added option to use managed WebSockets via OkHttp instead of Realm's built-in WebSocket client for Sync traffic (Only Android and JVM targets for now). Managed WebSockets offer improved support for proxies and firewalls that require authentication. This feature is currently opt-in and can be enabled by using `AppConfiguration.usePlatformNetworking()`. Managed WebSockets will become the default in a future version. (PR [#1528](https://github.com/realm/realm-kotlin/pull/1528)). ### Fixed * `RealmInstant.now` used an API (`java.time.Clock.systemUTC().instant()`) introduced in API 26, current minSDK is 16. (Issue [#1564](https://github.com/realm/realm-kotlin/issues/1564)) diff --git a/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt b/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt index 5b74109910..8c0d0d7f94 100644 --- a/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt +++ b/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt @@ -21,11 +21,9 @@ import io.realm.kotlin.entities.sync.ChildPk import io.realm.kotlin.entities.sync.ParentPk import io.realm.kotlin.mongodb.Credentials import io.realm.kotlin.mongodb.sync.SyncConfiguration -import io.realm.kotlin.mongodb.syncSession import io.realm.kotlin.test.mongodb.TestApp import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.TestHelper -import io.realm.kotlin.test.util.use import kotlinx.coroutines.runBlocking import kotlin.test.Test import kotlin.test.assertEquals