Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Keypath filtering in notifications #1547

Merged
merged 27 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
* Minimum R8: 8.0.34.

### Internal
* None.
* Updated to Realm Core 13.24.0, commit e593a5f19d0dc205db931ec5618a8c10c95cac90.


## 1.12.1-SNAPSHOT (YYYY-MM-DD)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ const val UUID_BYTES_SIZE = 16
interface CapiT
interface RealmConfigT : CapiT
interface RealmSchemaT : CapiT
interface RealmObjectSchemaT : CapiT
interface RealmT : CapiT
interface LiveRealmT : RealmT
interface FrozenRealmT : RealmT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ import org.mongodb.kbson.ObjectId
actual val INVALID_CLASS_KEY: ClassKey by lazy { ClassKey(realmc.getRLM_INVALID_CLASS_KEY()) }
actual val INVALID_PROPERTY_KEY: PropertyKey by lazy { PropertyKey(realmc.getRLM_INVALID_PROPERTY_KEY()) }

// The value to pass to JNI functions that accept longs as replacements for pointers and need
// to represent null.
const val NULL_POINTER_VALUE = 0L

/**
* JVM/Android interop implementation.
*
Expand Down Expand Up @@ -814,7 +818,7 @@ actual object RealmInterop {
}

actual fun realm_create_key_paths_array(realm: RealmPointer, clazz: ClassKey, keyPaths: List<String>): RealmKeyPathArrayPointer {
val ptr = realmc.jni_realm_create_key_path_array(realm.cptr(), clazz.key, keyPaths.size, keyPaths.toTypedArray())
val ptr = realmc.realm_create_key_path_array(realm.cptr(), clazz.key, keyPaths.size.toLong(), keyPaths.toTypedArray())
return LongPointerWrapper(ptr)
}

Expand All @@ -828,7 +832,7 @@ actual object RealmInterop {
realmc.register_notification_cb(
obj.cptr(),
CollectionType.RLM_COLLECTION_TYPE_NONE.nativeValue,
keyPaths?.cptr() ?: 0,
keyPaths?.cptr() ?: NULL_POINTER_VALUE,
object : NotificationCallback {
override fun onChange(pointer: Long) {
callback.onChange(LongPointerWrapper(realmc.realm_clone(pointer), true))
Expand All @@ -847,7 +851,7 @@ actual object RealmInterop {
return LongPointerWrapper(
realmc.register_results_notification_cb(
results.cptr(),
keyPaths?.cptr() ?: 0,
keyPaths?.cptr() ?: NULL_POINTER_VALUE,
object : NotificationCallback {
override fun onChange(pointer: Long) {
callback.onChange(LongPointerWrapper(realmc.realm_clone(pointer), true))
Expand All @@ -867,7 +871,7 @@ actual object RealmInterop {
realmc.register_notification_cb(
list.cptr(),
CollectionType.RLM_COLLECTION_TYPE_LIST.nativeValue,
keyPaths?.cptr() ?: 0,
keyPaths?.cptr() ?: NULL_POINTER_VALUE,
object : NotificationCallback {
override fun onChange(pointer: Long) {
callback.onChange(LongPointerWrapper(realmc.realm_clone(pointer), true))
Expand All @@ -887,7 +891,7 @@ actual object RealmInterop {
realmc.register_notification_cb(
set.cptr(),
CollectionType.RLM_COLLECTION_TYPE_SET.nativeValue,
keyPaths?.cptr() ?: 0,
keyPaths?.cptr() ?: NULL_POINTER_VALUE,
object : NotificationCallback {
override fun onChange(pointer: Long) {
callback.onChange(LongPointerWrapper(realmc.realm_clone(pointer), true))
Expand All @@ -907,7 +911,7 @@ actual object RealmInterop {
realmc.register_notification_cb(
map.cptr(),
CollectionType.RLM_COLLECTION_TYPE_DICTIONARY.nativeValue,
keyPaths?.cptr() ?: 0,
keyPaths?.cptr() ?: NULL_POINTER_VALUE,
object : NotificationCallback {
override fun onChange(pointer: Long) {
callback.onChange(LongPointerWrapper(realmc.realm_clone(pointer), true))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1658,7 +1658,7 @@ actual object RealmInterop {
actual fun realm_create_key_paths_array(realm: RealmPointer, clazz: ClassKey, keyPaths: List<String>): RealmKeyPathArrayPointer {
memScoped {
val userKeyPaths: CPointer<CPointerVarOf<CPointer<ByteVarOf<Byte>>>> = keyPaths.toCStringArray(this)
val keyPathPointer = realm_wrapper.realm_create_key_path_array(realm.cptr(), clazz.key.toUInt(), keyPaths.size, userKeyPaths)
val keyPathPointer = realm_wrapper.realm_create_key_path_array(realm.cptr(), clazz.key.toUInt(), keyPaths.size.toULong(), userKeyPaths)
return CPointerWrapper(keyPathPointer)
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/external/core
rorbech marked this conversation as resolved.
Show resolved Hide resolved
Submodule core updated 71 files
+28 −0 CHANGELOG.md
+10 −20 Jenkinsfile
+1 −1 Package.swift
+3 −1 bindgen/spec.yml
+2 −2 dependencies.list
+6 −4 evergreen/config.yml
+1 −1 evergreen/install_baas.sh
+1 −1 src/realm.h
+8 −0 src/realm/CMakeLists.txt
+2 −0 src/realm/collection.cpp
+5 −0 src/realm/collection.hpp
+0 −1 src/realm/group.cpp
+0 −1 src/realm/group.hpp
+3 −0 src/realm/keys.hpp
+2 −11 src/realm/object-store/CMakeLists.txt
+1 −1 src/realm/object-store/c_api/conversion.hpp
+2 −44 src/realm/object-store/c_api/notifications.cpp
+0 −2 src/realm/object-store/impl/deep_change_checker.hpp
+0 −7 src/realm/object-store/object_store.cpp
+230 −2 src/realm/object-store/shared_realm.cpp
+6 −0 src/realm/object-store/shared_realm.hpp
+1 −1 src/realm/object-store/sync/app_credentials.cpp
+1 −1 src/realm/object-store/sync/app_service_client.hpp
+2 −1 src/realm/object-store/sync/impl/emscripten/network_transport.cpp
+14 −9 src/realm/object-store/sync/impl/sync_file.cpp
+1 −1 src/realm/object-store/sync/mongo_collection.hpp
+1 −1 src/realm/object-store/sync/push_client.cpp
+6 −2 src/realm/object-store/sync/sync_manager.cpp
+1 −1 src/realm/object-store/sync/sync_user.hpp
+53 −22 src/realm/object_converter.cpp
+16 −0 src/realm/set.hpp
+1 −1 src/realm/sync/config.cpp
+7 −6 src/realm/sync/noinst/client_reset.cpp
+21 −18 src/realm/sync/noinst/client_reset_recovery.cpp
+3 −4 src/realm/sync/noinst/client_reset_recovery.hpp
+3 −2 src/realm/sync/noinst/migration_store.cpp
+39 −26 src/realm/sync/noinst/sync_metadata_schema.cpp
+15 −11 src/realm/sync/subscriptions.cpp
+1 −1 src/realm/util/bson/bson.cpp
+5 −5 src/realm/util/bson/bson.hpp
+0 −0 src/realm/util/bson/indexed_map.hpp
+0 −0 src/realm/util/bson/max_key.hpp
+0 −0 src/realm/util/bson/min_key.hpp
+0 −0 src/realm/util/bson/mongo_timestamp.hpp
+1 −1 src/realm/util/bson/regular_expression.cpp
+0 −0 src/realm/util/bson/regular_expression.hpp
+48 −26 test/CMakeLists.txt
+38 −0 test/combined_tests.cpp
+27 −21 test/object-store/CMakeLists.txt
+2 −3 test/object-store/benchmarks/CMakeLists.txt
+1 −1 test/object-store/benchmarks/client_reset.cpp
+1 −1 test/object-store/bson.cpp
+47 −3 test/object-store/c_api/c_api.cpp
+0 −0 test/object-store/c_api/c_api_file_tests.c
+17 −0 test/object-store/collection_fixtures.hpp
+4 −223 test/object-store/main.cpp
+114 −71 test/object-store/object.cpp
+240 −37 test/object-store/sync/client_reset.cpp
+1 −1 test/object-store/sync/flx_sync.cpp
+3 −2 test/object-store/sync/session/session.cpp
+245 −0 test/object-store/test_runner.cpp
+2 −0 test/object-store/util/event_loop.cpp
+73 −5 test/test_client_reset.cpp
+4 −0 test/test_crypto.cpp
+325 −323 test/test_sync.cpp
+4 −0 test/test_sync_auth.cpp
+3 −0 test/test_sync_history_migration.cpp
+1 −1 test/test_sync_subscriptions.cpp
+17 −0 test/test_unresolved_links.cpp
+3 −1 test/test_util_network_ssl.cpp
+36 −122 test/util/compare_groups.cpp
16 changes: 0 additions & 16 deletions packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1067,22 +1067,6 @@ realm_create_generic_scheduler() {
return new realm_scheduler_t { realm::util::Scheduler::make_dummy() };
}

realm_key_path_array_t*
jni_realm_create_key_path_array(const realm_t* realm,
const realm_class_key_t object_class_key,
int user_key_paths_count,
const char** user_key_paths)
{
realm_key_path_array_t* result = realm_create_key_path_array(realm, object_class_key, user_key_paths_count, user_key_paths);
if (result) {
return result ;
} else {
auto env = get_env();
throw_last_error_as_java_exception(env);
return nullptr;
}
}

void
realm_property_info_t_cleanup(realm_property_info_t* value) {
delete[] value->link_origin_property_name;
Expand Down
6 changes: 0 additions & 6 deletions packages/jni-swig-stub/src/main/jni/realm_api_helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,6 @@ realm_sync_thread_error(realm_userdata_t userdata, const char* error);
realm_scheduler_t*
realm_create_generic_scheduler();

realm_key_path_array_t*
jni_realm_create_key_path_array(const realm_t* realm,
const realm_class_key_t object_class_key,
int user_key_paths_count,
const char** user_key_paths);

void
realm_property_info_t_cleanup(realm_property_info_t* value);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ public fun BaseRealmObject.isValid(): Boolean = runIfManaged {
*
* @param keyPaths An optional list of properties that defines when a change to the object will
* result in a change being emitted. Nested properties can be defined using a dotted
* syntax, e.g. `parent.child.name`. If no keypaths are provided, changes to all top-level
* syntax, e.g. `parent.child.name`. Wildcards `*` can be be used to capture all properties at a
* given level, e.g. `child.*` or `*.*`. If no keypaths are provided, changes to all top-level
* properties and nested properties 4 levels down will trigger a change.
* @return a flow representing changes to the object.
* @throws UnsupportedOperationException if called on a live [RealmObject] or [EmbeddedRealmObject]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,3 @@ import kotlinx.coroutines.flow.Flow
internal interface KeyPathFlowable<T> {
fun asFlow(keyPaths: List<String>? = null): Flow<T>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason for using List and not vararg?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the internal interface that just mirrors the public API which uses List. I was considering whether to use vararg or List in the public API but ended up with List as it makes it easier to extend the API later. I.e. there is the potential that projections will end up adding arguments to the asFlow method as well.

}

internal interface Flowable<T> {
fun asFlow(): Flow<T>
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ internal class ObjectBoundRealmResults<E : BaseRealmObject>(
*/

override fun asFlow(keyPaths: List<String>?): Flow<ResultsChange<E>> {
return realmResults.asFlow(keyPaths).bind(targetObject, keyPaths)
return realmResults.asFlow(keyPaths).bind(targetObject)
}

override fun delete() {
Expand All @@ -79,7 +79,6 @@ internal class ObjectBoundRealmResults<E : BaseRealmObject>(
* deleted. It is used on sub-queries and backlinks.
*/
internal fun <T> Flow<T>.bind(
reference: RealmObjectReference<out BaseRealmObject>,
keyPaths: List<String>?
reference: RealmObjectReference<out BaseRealmObject>
): Flow<T> =
this.terminateWhen(reference.asFlow(keyPaths)) { it is DeletedObject }
this.terminateWhen(reference.asFlow()) { it is DeletedObject }
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import kotlin.reflect.KClass
// TODO Public due to being accessed from `SyncedRealmContext`
public class RealmImpl private constructor(
configuration: InternalConfiguration,
) : BaseRealmImpl(configuration), Realm, InternalTypedRealm, Flowable<RealmChange<Realm>> {
) : BaseRealmImpl(configuration), Realm, InternalTypedRealm {

public val notificationScheduler: LiveRealmContext =
configuration.notificationDispatcherFactory.createLiveRealmContext()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import io.realm.kotlin.internal.interop.getterScope
import io.realm.kotlin.internal.interop.inputScope
import io.realm.kotlin.internal.query.ObjectBoundQuery
import io.realm.kotlin.internal.query.ObjectQuery
import io.realm.kotlin.internal.util.Validation
import io.realm.kotlin.notifications.ListChange
import io.realm.kotlin.notifications.internal.DeletedListImpl
import io.realm.kotlin.notifications.internal.InitialListImpl
Expand Down Expand Up @@ -117,11 +118,8 @@ internal class ManagedRealmList<E>(
override fun asFlow(keyPaths: List<String>?): Flow<ListChange<E>> {
operator.realmReference.checkClosed()
val keyPathInfo = keyPaths?.let {
cmelchior marked this conversation as resolved.
Show resolved Hide resolved
val classKey = if (operator is RealmObjectListOperator) {
operator.classKey
} else {
throw IllegalArgumentException("Keypaths are only supported for lists of objects.")
}
Validation.isType<RealmObjectListOperator<*>>(operator, "Keypaths are only supported for lists of objects.")
val classKey = operator.classKey
cmelchior marked this conversation as resolved.
Show resolved Hide resolved
Pair(classKey, keyPaths)
}
return operator.realmReference.owner.registerObserver(this, keyPathInfo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import io.realm.kotlin.internal.interop.getterScope
import io.realm.kotlin.internal.interop.inputScope
import io.realm.kotlin.internal.query.ObjectBoundQuery
import io.realm.kotlin.internal.query.ObjectQuery
import io.realm.kotlin.internal.util.Validation
import io.realm.kotlin.notifications.MapChange
import io.realm.kotlin.notifications.MapChangeSet
import io.realm.kotlin.notifications.internal.DeletedDictionaryImpl
Expand Down Expand Up @@ -103,11 +104,8 @@ internal abstract class ManagedRealmMap<K, V> constructor(
override fun asFlow(keyPaths: List<String>?): Flow<MapChange<K, V>> {
operator.realmReference.checkClosed()
val keyPathInfo = keyPaths?.let {
val classKey = if (operator is RealmObjectMapOperator) {
operator.classKey
} else {
throw IllegalArgumentException("Keypaths are only supported for maps of objects.")
}
Validation.isType<RealmObjectMapOperator<*, *>>(operator, "Keypaths are only supported for maps of objects.")
val classKey = operator.classKey // if (operator is RealmObjectMapOperator) {
cmelchior marked this conversation as resolved.
Show resolved Hide resolved
Pair(classKey, keyPaths)
}
return operator.realmReference.owner.registerObserver(this, keyPathInfo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import io.realm.kotlin.internal.interop.getterScope
import io.realm.kotlin.internal.interop.inputScope
import io.realm.kotlin.internal.query.ObjectBoundQuery
import io.realm.kotlin.internal.query.ObjectQuery
import io.realm.kotlin.internal.util.Validation
import io.realm.kotlin.notifications.SetChange
import io.realm.kotlin.notifications.internal.DeletedSetImpl
import io.realm.kotlin.notifications.internal.InitialSetImpl
Expand Down Expand Up @@ -162,11 +163,8 @@ internal class ManagedRealmSet<E> constructor(

override fun asFlow(keyPaths: List<String>?): Flow<SetChange<E>> {
val keyPathInfo = keyPaths?.let {
val classKey = if (operator is RealmObjectSetOperator) {
operator.classKey
} else {
throw IllegalArgumentException("Keypaths are only supported for sets of objects.")
}
Validation.isType<RealmObjectSetOperator<*>>(operator, "Keypaths are only supported for sets of objects.")
val classKey = operator.classKey
Pair(classKey, keyPaths)
}
return operator.realmReference.owner.registerObserver(this, keyPathInfo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,8 @@ internal class ObjectBoundQuery<E : BaseRealmObject>(
realmQuery.query(filter, *arguments)
)

override fun asFlow(keyPaths: List<String>?): Flow<ResultsChange<E>> = realmQuery.asFlow().bind(
targetObject,
keyPaths
override fun asFlow(keyPaths: List<String>?): Flow<ResultsChange<E>> = realmQuery.asFlow(keyPaths).bind(
targetObject
)

override fun sort(property: String, sortOrder: Sort): RealmQuery<E> = ObjectBoundQuery(
Expand Down Expand Up @@ -110,19 +109,19 @@ internal class ObjectBoundRealmSingleQuery<E : BaseRealmObject>(
val targetObject: RealmObjectReference<*>,
val realmQuery: RealmSingleQuery<E>
) : RealmSingleQuery<E> by realmQuery {
override fun asFlow(keyPaths: List<String>?): Flow<SingleQueryChange<E>> = realmQuery.asFlow(keyPaths).bind(targetObject, keyPaths)
override fun asFlow(keyPaths: List<String>?): Flow<SingleQueryChange<E>> = realmQuery.asFlow(keyPaths).bind(targetObject)
}

internal class ObjectBoundRealmScalarNullableQuery<E>(
val targetObject: RealmObjectReference<*>,
val realmQuery: RealmScalarNullableQuery<E>
) : RealmScalarNullableQuery<E> by realmQuery {
override fun asFlow(): Flow<E?> = realmQuery.asFlow().bind(targetObject, null)
override fun asFlow(): Flow<E?> = realmQuery.asFlow().bind(targetObject)
}

internal class ObjectBoundRealmScalarQuery<E>(
val targetObject: RealmObjectReference<*>,
val realmQuery: RealmScalarQuery<E>
) : RealmScalarQuery<E> by realmQuery {
override fun asFlow(): Flow<E> = realmQuery.asFlow().bind(targetObject, null)
override fun asFlow(): Flow<E> = realmQuery.asFlow().bind(targetObject)
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ internal class CountQuery<E : BaseRealmObject> constructor(
override fun asFlow(): Flow<Long> {
realmReference.checkClosed()
return realmReference.owner
// TODO
.registerObserver(this, null)
.map {
it.list.size.toLong()
Expand Down Expand Up @@ -217,7 +216,6 @@ internal class SumQuery<E : BaseRealmObject, T : Any> constructor(
override fun asFlow(): Flow<T> {
realmReference.checkClosed()
return realmReference.owner
// TODO
.registerObserver(this, null)
.map { findFromResults((it.list as RealmResultsImpl<*>).nativePointer) }
.distinctUntilChanged()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

package io.realm.kotlin.internal.util

import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract

/**
* Collection of validation methods to ensure uniform input validation.
*/
Expand All @@ -30,6 +33,20 @@ public object Validation {
return value
}

/**
* Verifies that a given argument has a given type. If yes, it will be implicitly cast
* to that type, otherwise an IllegalArgumentException is thrown with the provided error message.
*/
@OptIn(ExperimentalContracts::class)
public inline fun <reified T : Any?> isType(arg: Any?, errorMessage: String) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥇

contract {
returns() implies (arg is T)
}
if (arg !is T) {
throw IllegalArgumentException(errorMessage)
}
}

public fun isEmptyString(str: String?): Boolean {
return str == null || str.length == 0
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,14 @@ public interface RealmResults<T : BaseRealmObject> : List<T>, Deleteable, Versio
* the elements in a timely manner the coroutine scope will be cancelled with a
* [CancellationException].
*
* @param keyPath TODO
* @param keyPaths An optional list of model class properties that defines when a change to
* objects inside the RealmResults will result in a change being emitted. Nested properties can
* be defined using a dotted syntax, e.g. `parent.child.name`. Wildcards `*` can be be used
* to capture all properties at a given level, e.g. `child.*` or `*.*`. If no keypaths are
* provided, changes to all top-level properties and nested properties up to 4 levels down
* will trigger a change.
* @return a flow representing changes to the list.
* @throws IllegalArgumentException if an invalid keypath is provided.
* @return a flow representing changes to the RealmResults.
*/
public fun asFlow(keyPaths: List<String>? = null): Flow<ResultsChange<T>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,15 @@ public interface RealmList<E> : MutableList<E>, Deleteable {
* the elements in a timely manner the coroutine scope will be cancelled with a
* [CancellationException].
*
* @param keyPaths An optional list of properties that defines when a change to the object will
* result in a change being emitted. Nested properties can be defined using a dotted
* syntax, e.g. `parent.child.name`. If no keypaths are provided, changes to all top-level
* properties and nested properties 4 levels down will trigger a change.
* @param keyPaths An optional list of model class properties that defines when a change to
* objects inside the list will result in a change being emitted. Nested properties can be
* defined using a dotted syntax, e.g. `parent.child.name`. Wildcards `*` can be be used
* to capture all properties at a given level, e.g. `child.*` or `*.*`.If no keypaths are
* provided, changes to all top-level properties and nested properties up to 4 levels down
* will trigger a change. Keypaths are only supported for lists of objects.
* @return a flow representing changes to the list.
* @throws IllegalArgumentException if an invalid keypath is provided.
* @throws IllegalArgumentException if an invalid keypath is provided or the RealmList does not
* contain realm objects.
* @throws CancellationException if the stream produces changes faster than the consumer can
* consume them and results in a buffer overflow.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,16 @@ public interface RealmMap<K, V> : MutableMap<K, V> {
* the elements in a timely manner the coroutine scope will be cancelled with a
* [CancellationException].
*
* @param keyPaths An optional list of properties that defines when a change to the object will
* result in a change being emitted. Nested properties can be defined using a dotted
* syntax, e.g. `parent.child.name`. If no keypaths are provided, changes to all top-level
* properties and nested properties 4 levels down will trigger a change.
* @param keyPaths An optional list of model class properties that defines when a change to
* objects inside the map will result in a change being emitted. For maps, keypaths are
* evaluted based on the values of the map. This means that keypaths are only supported
* for maps containing realm objects. Nested properties can be defined using a dotted syntax,
* e.g. `parent.child.name`. Wildcards `*` can be be used to capture all properties at a given
* level, e.g. `child.*` or `*.*`. If no keypaths are provided, changes to all top-level
* properties and nested properties up to 4 levels down will trigger a change
* @return a flow representing changes to the dictionary.
* @throws IllegalArgumentException if keypaths are provided for maps not containing objects.
* @throws IllegalArgumentException if keypaths are invalid or the map does not contain realm
* objects.
* @throws CancellationException if the stream produces changes faster than the consumer can
* consume them and results in a buffer overflow.
*/
Expand Down
Loading