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
Changes from 5 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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -4,7 +4,8 @@
* None.

### Enhancements
* Added support for keypaths in `asFlow()` methods on objects. This makes it possible to control which properties will trigger change events, including properties on objects below the default nested limit of 4. (Issue [#661](https://github.com/realm/realm-kotlin/issues/661))
* Added support for keypaths in `asFlow()` methods on objects and queries. This makes it possible to control which properties will trigger change events, including properties on objects below the default nested limit of 4. (Issue [#661](https://github.com/realm/realm-kotlin/issues/661))
* Support for experimental K2-compilation with `kotlin.experimental.tryK2=true`. (Issue [#1483](https://github.com/realm/realm-kotlin/issues/1483))

### Fixed
* None.
Original file line number Diff line number Diff line change
@@ -438,7 +438,7 @@ expect object RealmInterop {
): RealmObjectPointer?
fun realm_object_delete(obj: RealmObjectPointer)

fun create_key_paths_array(realm: RealmPointer, clazz: ClassKey, keyPaths: List<String>): RealmKeyPathArrayPointer
fun realm_create_key_paths_array(realm: RealmPointer, clazz: ClassKey, keyPaths: List<String>): RealmKeyPathArrayPointer
fun realm_object_add_notification_callback(
obj: RealmObjectPointer,
keyPaths: RealmKeyPathArrayPointer?,
Original file line number Diff line number Diff line change
@@ -813,7 +813,7 @@ actual object RealmInterop {
return realmc.realm_dictionary_is_valid(dictionary.cptr())
}

actual fun create_key_paths_array(realm: RealmPointer, clazz: ClassKey, keyPaths: List<String>): RealmKeyPathArrayPointer {
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())
return LongPointerWrapper(ptr)
}
Original file line number Diff line number Diff line change
@@ -105,7 +105,6 @@ import realm_wrapper.realm_http_header_t
import realm_wrapper.realm_http_request_method
import realm_wrapper.realm_http_request_t
import realm_wrapper.realm_http_response_t
import realm_wrapper.realm_key_path_array_t
import realm_wrapper.realm_link_t
import realm_wrapper.realm_list_t
import realm_wrapper.realm_object_id_t
@@ -1656,12 +1655,11 @@ actual object RealmInterop {
checkedBooleanResult(realm_wrapper.realm_object_delete(obj.cptr()))
}

actual fun create_key_paths_array(realm: RealmPointer, clazz: ClassKey, keyPaths: List<String>): RealmKeyPathArrayPointer {
actual fun realm_create_key_paths_array(realm: RealmPointer, clazz: ClassKey, keyPaths: List<String>): RealmKeyPathArrayPointer {
memScoped {
val kps: CPointer<CPointerVarOf<CPointer<ByteVarOf<Byte>>>> = keyPaths.toCStringArray(this)
val kp = allocArray<CPointerVar<realm_key_path_array_t>>(1)
checkedBooleanResult(realm_wrapper.realm_create_key_path_array(realm.cptr(), clazz.key.toUInt(), keyPaths.size, kps, kp))
return CPointerWrapper(kp[0])
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)
return CPointerWrapper(keyPathPointer)
}
}

2 changes: 1 addition & 1 deletion packages/external/core
Submodule core updated 65 files
+2 −1 .github/pull_request_template.md
+4 −0 .gitignore
+27 −1 CHANGELOG.md
+1 −1 Package.swift
+1 −0 bindgen/spec.yml
+2 −2 dependencies.list
+295 −0 doc/development/how-to-use-remote-baas-host.md
+182 −23 evergreen/config.yml
+0 −16 evergreen/config_overrides.json
+237 −0 evergreen/configure_baas_proxy.sh
+304 −214 evergreen/install_baas.sh
+4 −0 evergreen/proxy-network-faults.toxics
+5 −0 evergreen/proxy-nonideal-transfer.toxics
+160 −30 evergreen/setup_baas_host.sh
+149 −55 evergreen/setup_baas_host_local.sh
+263 −0 evergreen/setup_baas_proxy.sh
+21 −11 evergreen/wait_for_baas.sh
+7 −25 src/realm.h
+0 −1 src/realm/CMakeLists.txt
+0 −1 src/realm/alloc_slab.cpp
+11 −11 src/realm/collection.hpp
+0 −1 src/realm/group.cpp
+0 −1 src/realm/group_writer.cpp
+0 −1 src/realm/object-store/CMakeLists.txt
+52 −20 src/realm/object-store/c_api/notifications.cpp
+5 −5 src/realm/object-store/c_api/types.hpp
+0 −73 src/realm/object-store/keypath_helpers.cpp
+0 −16 src/realm/object-store/keypath_helpers.hpp
+7 −0 src/realm/object-store/object_store.cpp
+2 −0 src/realm/object-store/shared_realm.cpp
+3 −4 src/realm/object-store/sync/sync_session.cpp
+0 −1 src/realm/query_engine.hpp
+137 −129 src/realm/set.cpp
+0 −22 src/realm/set.hpp
+12 −7 src/realm/sync/client.cpp
+90 −111 src/realm/sync/noinst/client_impl_base.cpp
+29 −48 src/realm/sync/noinst/client_impl_base.hpp
+3 −7 src/realm/sync/noinst/client_reset.cpp
+1 −1 src/realm/sync/noinst/client_reset.hpp
+41 −83 src/realm/sync/noinst/client_reset_operation.cpp
+12 −50 src/realm/sync/noinst/client_reset_operation.hpp
+47 −53 src/realm/sync/subscriptions.cpp
+26 −23 src/realm/sync/subscriptions.hpp
+5 −1 src/realm/table.cpp
+2 −0 src/realm/table.hpp
+0 −49 src/realm/util/miscellaneous.hpp
+8 −0 src/realm/utilities.hpp
+16 −0 test/object-store/CMakeLists.txt
+1 −1 test/object-store/audit.cpp
+69 −31 test/object-store/c_api/c_api.cpp
+12 −0 test/object-store/main.cpp
+22 −28 test/object-store/sync/app.cpp
+16 −18 test/object-store/sync/client_reset.cpp
+24 −31 test/object-store/sync/flx_migration.cpp
+116 −58 test/object-store/sync/flx_sync.cpp
+98 −68 test/object-store/util/sync/baas_admin_api.cpp
+21 −12 test/object-store/util/sync/baas_admin_api.hpp
+1 −1 test/object-store/util/sync/flx_sync_harness.hpp
+58 −16 test/object-store/util/sync/sync_test_utils.cpp
+10 −5 test/object-store/util/sync/sync_test_utils.hpp
+4 −3 test/object-store/util/test_file.cpp
+3 −0 test/object-store/util/test_file.hpp
+387 −6 test/test_client_reset.cpp
+144 −34 test/test_set.cpp
+9 −0 test/util/test_path.hpp
7 changes: 4 additions & 3 deletions packages/jni-swig-stub/realm.i
Original file line number Diff line number Diff line change
@@ -423,8 +423,9 @@ import static io.realm.kotlin.internal.interop.realm_errno_e.*;
/* This cleans up the memory we malloc'd before the function call */
%typemap(freearg) char ** {
int i;
for (i=0; i<size$argnum-1; i++)
free($1[i]);
for (i=0; i<size$argnum-1; i++) {
free($1[i]);
}
free($1);
}

@@ -454,7 +455,7 @@ import static io.realm.kotlin.internal.interop.realm_errno_e.*;
and vice versa */
%typemap(javain) char ** "$javainput"
%typemap(javaout) char ** {
return $jnicall;
return $jnicall;
}
// -- End

5 changes: 2 additions & 3 deletions packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp
Original file line number Diff line number Diff line change
@@ -1073,10 +1073,9 @@ jni_realm_create_key_path_array(const realm_t* realm,
int user_key_paths_count,
const char** user_key_paths)
{
realm_key_path_array_t* out = nullptr;
bool result = realm_create_key_path_array(realm, object_class_key, user_key_paths_count, user_key_paths, &out);
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 out;
return result ;
} else {
auto env = get_env();
throw_last_error_as_java_exception(env);
Original file line number Diff line number Diff line change
@@ -85,12 +85,13 @@ 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
* syntex, e.g. `parent.child.name`. If no keypaths are provided, changes to all top-level
* 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.
* @return a flow representing changes to the object.
* @throws UnsupportedOperationException if called on a live [RealmObject] or [EmbeddedRealmObject]
* from a write transaction ([Realm.write]) or on a [DynamicRealmObject] inside a migration
* ([AutomaticSchemaMigration.migrate]).
* @throws IllegalArgumentException if an invalid keypath is provided.
*/
public fun <T : BaseRealmObject> T.asFlow(keyPaths: List<String>? = null): Flow<ObjectChange<T>> = runIfManaged {
checkNotificationsAvailable()
Original file line number Diff line number Diff line change
@@ -96,7 +96,7 @@ internal class SuspendableNotifier(
}

internal fun <T : CoreNotifiable<T, C>, C> registerObserver(flowable: Observable<T, C>, keyPaths: Pair<ClassKey, List<String>>?): Flow<C> {
val keypathsPtr: RealmKeyPathArrayPointer? = keyPaths?.let { RealmInterop.create_key_paths_array(realm.owner.realmReference.dbPointer, keyPaths.first, keyPaths.second) }
val keypathsPtr: RealmKeyPathArrayPointer? = keyPaths?.let { RealmInterop.realm_create_key_paths_array(realm.owner.realmReference.dbPointer, keyPaths.first, keyPaths.second) }
return callbackFlow {
val token: AtomicRef<Cancellable> =
kotlinx.atomicfu.atomic(NO_OP_NOTIFICATION_TOKEN)
Original file line number Diff line number Diff line change
@@ -87,9 +87,9 @@ internal class SingleQuery<E : BaseRealmObject> constructor(
} else {
oldHead = newHead
if (!oldHeadDeleted) {
newHead.asFlow()
newHead.asFlow(keyPaths)
} else {
newHead.asFlow().onStart { emit(DeletedObjectImpl()) }
newHead.asFlow(keyPaths).onStart { emit(DeletedObjectImpl()) }
}
}
}
Original file line number Diff line number Diff line change
@@ -60,8 +60,12 @@ public interface RealmElementQuery<T : BaseRealmObject> : Deleteable {
*
* **It is not allowed to call [asFlow] on queries generated from a [MutableRealm].**
*
* @param keyPath TODO
* @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.
* @return a flow representing changes to the [RealmResults] resulting from running this query.
* @throws IllegalArgumentException if an invalid keypath is provided.
*/
public fun asFlow(keyPath: List<String>? = null): Flow<ResultsChange<T>>
}
Original file line number Diff line number Diff line change
@@ -72,9 +72,13 @@ public interface RealmSingleQuery<T : BaseRealmObject> : Deleteable {
* the elements in a timely manner the coroutine scope will be cancelled with a
* [CancellationException].
*
* @param keyPaths TODO
* @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
* syntex, 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.
* @return a flow representing changes to the [RealmObject] or [EmbeddedRealmObject] resulting from
* running this query.
* @throws IllegalArgumentException if an invalid keypath is provided.
*/
public fun asFlow(keyPaths: List<String>? = null): Flow<SingleQueryChange<T>>
}
Original file line number Diff line number Diff line change
@@ -74,10 +74,10 @@ public interface RealmList<E> : MutableList<E>, Deleteable {
*
* @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
* syntex, e.g. `parent.child.name`. If no keypaths are provided, changes to all top-level
* 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.
* @return a flow representing changes to the list.
* @throws IllegalArgumentException if keypaths are provided for lists not containing objects.
* @throws IllegalArgumentException if an invalid keypath is provided.
* @throws CancellationException if the stream produces changes faster than the consumer can
* consume them and results in a buffer overflow.
*/
Original file line number Diff line number Diff line change
@@ -57,7 +57,7 @@ public interface RealmMap<K, V> : MutableMap<K, V> {
*
* @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
* syntex, e.g. `parent.child.name`. If no keypaths are provided, changes to all top-level
* 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.
* @return a flow representing changes to the dictionary.
* @throws IllegalArgumentException if keypaths are provided for maps not containing objects.
Original file line number Diff line number Diff line change
@@ -54,10 +54,10 @@ public interface RealmSet<E> : MutableSet<E>, Deleteable {
*
* @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
* syntex, e.g. `parent.child.name`. If no keypaths are provided, changes to all top-level
* 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.
* @return a flow representing changes to the set.
* @throws IllegalArgumentException if keypaths are provided for sets not containing objects.
* @throws IllegalArgumentException if an invalid keypath is provided.
* @throws CancellationException if the stream produces changes faster than the consumer can
* consume them and results in a buffer overflow.
*/
Original file line number Diff line number Diff line change
@@ -2946,6 +2946,86 @@ class QueryTests {
}
}

@Test
fun asFlow_results_withKeyPath() {
val channel = Channel<ResultsChange<QuerySample>>(1)
runBlocking {
val observer = async {
realm.query<QuerySample>()
.asFlow(listOf("stringField"))
.collect { results ->
assertNotNull(results)
channel.send(results)
}
}
channel.receiveOrFail().let { resultsChange ->
assertIs<InitialResults<*>>(resultsChange)
assertTrue(resultsChange.list.isEmpty())
}
val obj = realm.writeBlocking {
copyToRealm(QuerySample())
}
channel.receiveOrFail().let { resultsChange ->
assertIs<UpdatedResults<*>>(resultsChange)
assertEquals(1, resultsChange.list.size)
}
realm.writeBlocking {
// Should not trigger notification
findLatest(obj)!!.intField = 42
}
realm.writeBlocking {
// Should not trigger notification
findLatest(obj)!!.stringField = "update"
}
channel.receiveOrFail().let { resultsChange ->
assertIs<UpdatedResults<*>>(resultsChange)
assertEquals(1, resultsChange.list.size)
}
observer.cancel()
channel.close()
}
}

@Test
fun asFlow_objectBound_withKeyPath() {
val channel = Channel<SingleQueryChange<QuerySample>>(1)
runBlocking {
val observer = async {
realm.query<QuerySample>()
.first()
.asFlow(listOf("stringField"))
.collect { change ->
assertNotNull(change)
channel.send(change)
}
}
channel.receiveOrFail().let { objChange ->
assertIs<PendingObject<*>>(objChange)
}
val obj = realm.writeBlocking {
copyToRealm(QuerySample())
}
channel.receiveOrFail().let { objChange ->
assertIs<InitialObject<*>>(objChange)
}
realm.writeBlocking {
// Should not trigger notification
findLatest(obj)!!.intField = 42
}
realm.writeBlocking {
// Should trigger notification
findLatest(obj)!!.stringField = "update"
}
channel.receiveOrFail().let { objChange ->
assertIs<UpdatedObject<*>>(objChange)
assertEquals(1, objChange.changedFields.size)
assertEquals("stringField", objChange.changedFields.first())
}
observer.cancel()
channel.close()
}
}

// ----------------
// Coercion helpers
// ----------------
Loading