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

Keypath filtering #3179

Merged
merged 34 commits into from
Jan 20, 2023
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6a8d8a1
add tests to outline how we want notifications to work for objects
Dec 8, 2022
ef78f98
move GetPropertyIndex to Metadata
Dec 8, 2022
c470a09
add propertyIndices to managedAccessor
Dec 12, 2022
e995770
Add logic in wrapper to deal with propertyindices for object_add_noti…
Dec 12, 2022
d65e755
renaming, removed unecessary comment
Dec 12, 2022
7803682
make tests more elaborate
Dec 12, 2022
7a6b9e8
add empty-check for array
Dec 12, 2022
30ca8b7
use linq expression instead of loop
Dec 12, 2022
b27e492
make propertyIndices an array rather than list
Dec 13, 2022
9af03ce
make getpropertyIndex an instance method of Metadata
Dec 13, 2022
17f9cd6
cache propertyIndices
Dec 13, 2022
abab7f0
elaborate tests
Dec 13, 2022
905d164
add tests for collectionChanged
Dec 14, 2022
e63ddb3
fix warnings
Dec 14, 2022
6a7093f
disambiguate using, collectionchanged += or SubscribeForNotifications
Dec 14, 2022
21890f7
update submodule
Dec 14, 2022
2996f3d
fix some pr comments
Dec 16, 2022
bd973ae
update core submodule
Dec 16, 2022
a28aa5b
share common logic in wrappers for add_notification_callback
Dec 16, 2022
112405a
fixed bug, now disambiguates if collection-subscribtions should be sh…
Dec 16, 2022
ccce9ba
add test for backlink
Dec 19, 2022
0da4cc7
rename property indices and make it lazy
Dec 19, 2022
c30f3fc
update submodule
Dec 19, 2022
54263d4
add changelog
Dec 19, 2022
1437d0f
make shallow an optional parameter
Dec 20, 2022
e68cca6
check if collection contains primitives or objects when adding a noti…
Dec 20, 2022
14cee92
Merge branch 'main' into jg/keypath_filtering_internal
nirinchev Jan 13, 2023
e2be969
Some simplifications
nirinchev Jan 13, 2023
9e49775
Wire more things up
nirinchev Jan 13, 2023
1d0222c
Add a few more tests
nirinchev Jan 16, 2023
627eb3f
Map Paused session state to Inactive
nirinchev Jan 16, 2023
54d5527
Relax test requirements
nirinchev Jan 17, 2023
4da5908
add more comments
nirinchev Jan 20, 2023
c69609e
Add changelog
nirinchev Jan 20, 2023
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
## 10.19.0 (2023-01-06)

### Enhancements
* PropertyChanged will no longer fire notifications for changes to collection-properties. CollectionChanged will no longer fire notifications for modifications to objects within the collection itself. (Issue [#3112](https://github.com/realm/realm-dotnet/issues/3112))
* Removed redundant serialization/deserialization of arguments in CallAsync. (Issue [#3079](https://github.com/realm/realm-dotnet/issues/3079))
* Added a field `Transaction.State` which describes the current state of the transaction. (Issue [#2551](https://github.com/realm/realm-dotnet/issues/2551))
* Improved error message when null is passed as argument to params for EmailPasswordAuth.CallResetPasswordFunctionAsync. (Issue [#3011](https://github.com/realm/realm-dotnet/issues/3011))
Expand Down
2 changes: 1 addition & 1 deletion Realm/Realm/DatabaseTypes/Accessors/ManagedAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ public void UnsubscribeFromNotifications()
}

/// <inheritdoc/>
void INotifiable<NotifiableObjectHandleBase.CollectionChangeSet>.NotifyCallbacks(NotifiableObjectHandleBase.CollectionChangeSet? changes)
void INotifiable<NotifiableObjectHandleBase.CollectionChangeSet>.NotifyCallbacks(NotifiableObjectHandleBase.CollectionChangeSet? changes, bool shallow)
{
if (changes.HasValue)
{
Expand Down
3 changes: 2 additions & 1 deletion Realm/Realm/DatabaseTypes/INotifiable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ internal interface INotifiable<TChangeset>
/// Method called when there are changes to report for that object.
/// </summary>
/// <param name="changes">The changes that occurred.</param>
void NotifyCallbacks(TChangeset? changes);
/// <param name="shallow">Whether the changes are coming from a shallow notifier or not.</param>
void NotifyCallbacks(TChangeset? changes, bool shallow);
}

internal class NotificationToken<TCallback> : IDisposable
Expand Down
10 changes: 10 additions & 0 deletions Realm/Realm/DatabaseTypes/Metadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,15 @@ public Metadata(TableKey tableKey, IRealmObjectHelper helper, IDictionary<string
PropertyIndices = new ReadOnlyDictionary<string, IntPtr>(propertyIndices);
Schema = schema;
}

public IntPtr GetPropertyIndex(string propertyName)
{
if (PropertyIndices.TryGetValue(propertyName, out var result))
{
return result;
}

throw new MissingMemberException(Schema.Name, propertyName);
}
}
}
163 changes: 109 additions & 54 deletions Realm/Realm/DatabaseTypes/RealmCollectionBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,7 @@ public abstract class RealmCollectionBase<T>
IThreadConfined,
IMetadataObject
{
private readonly List<NotificationCallbackDelegate<T>> _callbacks = new();

private NotificationTokenHandle _notificationToken;

private bool _deliveredInitialNotification;
private readonly Lazy<NotificationCallbacks<T>> _notificationCallbacks;

internal readonly bool _isEmbedded;

Expand Down Expand Up @@ -149,11 +145,15 @@ internal RealmCollectionBase(Realm realm, Metadata metadata)
Handle = new Lazy<CollectionHandleBase>(GetOrCreateHandle);
Metadata = metadata;
_isEmbedded = metadata?.Schema.BaseType == ObjectSchema.ObjectType.EmbeddedObject;
_notificationCallbacks = new(() => new(this));
}

~RealmCollectionBase()
{
UnsubscribeFromNotifications();
if (_notificationCallbacks.IsValueCreated)
{
_notificationCallbacks.Value.RemoveAll();
}
}

internal abstract CollectionHandleBase GetOrCreateHandle();
Expand All @@ -179,22 +179,12 @@ public IRealmCollection<T> Freeze()
return CreateCollection(frozenRealm, frozenHandle);
}

public IDisposable SubscribeForNotifications(NotificationCallbackDelegate<T> callback)
public IDisposable SubscribeForNotifications(NotificationCallbackDelegate<T> callback, bool shallow)
{
Argument.NotNull(callback, nameof(callback));
_notificationCallbacks.Value.Add(callback, shallow);

if (_callbacks.Count == 0)
{
SubscribeForNotifications();
}
else if (_deliveredInitialNotification)
{
callback(this, null, null);
}

_callbacks.Add(callback);

return NotificationToken.Create(callback, UnsubscribeFromNotifications);
return NotificationToken.Create(callback, (c) => UnsubscribeFromNotifications(c, shallow));
}

protected abstract T GetValueAtIndex(int index);
Expand Down Expand Up @@ -254,31 +244,9 @@ protected static IEmbeddedObject EnsureUnmanagedEmbedded(in RealmValue value)
return result;
}

private void UnsubscribeFromNotifications(NotificationCallbackDelegate<T> callback)
{
if (_callbacks.Remove(callback) &&
_callbacks.Count == 0)
{
UnsubscribeFromNotifications();
}
}

private void SubscribeForNotifications()
{
Debug.Assert(_notificationToken == null, "_notificationToken must be null before subscribing.");

Realm.ExecuteOutsideTransaction(() =>
{
var managedResultsHandle = GCHandle.Alloc(this, GCHandleType.Weak);
_notificationToken = Handle.Value.AddNotificationCallback(GCHandle.ToIntPtr(managedResultsHandle));
});
}

private void UnsubscribeFromNotifications()
private void UnsubscribeFromNotifications(NotificationCallbackDelegate<T> callback, bool shallow)
{
_notificationToken?.Dispose();
_notificationToken = null;
_deliveredInitialNotification = false;
_notificationCallbacks.Value.Remove(callback, shallow);
}

#region INotifyCollectionChanged
Expand Down Expand Up @@ -410,18 +378,18 @@ private void UpdateCollectionChangedSubscriptionIfNecessary(bool isSubscribed)
{
if (isSubscribed)
{
SubscribeForNotifications(OnChange);
SubscribeForNotifications(OnChange, shallow: true);
}
else
{
UnsubscribeFromNotifications(OnChange);
UnsubscribeFromNotifications(OnChange, shallow: true);
}
}
}

#endregion INotifyCollectionChanged

void INotifiable<NotifiableObjectHandleBase.CollectionChangeSet>.NotifyCallbacks(NotifiableObjectHandleBase.CollectionChangeSet? changes)
void INotifiable<NotifiableObjectHandleBase.CollectionChangeSet>.NotifyCallbacks(NotifiableObjectHandleBase.CollectionChangeSet? changes, bool shallow)
{
ChangeSet changeset = null;
if (changes != null)
Expand All @@ -435,15 +403,8 @@ private void UpdateCollectionChangedSubscriptionIfNecessary(bool isSubscribed)
moves: actualChanges.Moves.AsEnumerable().Select(m => new ChangeSet.Move((int)m.From, (int)m.To)).ToArray(),
cleared: actualChanges.Cleared);
}
else
{
_deliveredInitialNotification = true;
}

foreach (var callback in _callbacks.ToArray())
{
callback(this, changeset, null);
}
_notificationCallbacks.Value.Notify(changeset, shallow);
}

public IEnumerator<T> GetEnumerator() => new Enumerator(this);
Expand Down Expand Up @@ -624,6 +585,100 @@ internal interface IRealmCollectionBase<THandle> : IMetadataObject
THandle NativeHandle { get; }
}

[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "NotificationCallbacks are tighly coupled with the collection and it's easier to reason about when they're in the same file.")]
internal class NotificationCallbacks<T>
{
private readonly RealmCollectionBase<T> _parent;

private readonly Dictionary<KeyPath, (NotificationTokenHandle Token, bool DeliveredInitialNotification, List<NotificationCallbackDelegate<T>> Callbacks)> _subscriptions = new();

public NotificationCallbacks(RealmCollectionBase<T> parent)
{
_parent = parent;
}

// Shallow here and everywhere else is a bit of a shortcut we're taking until we have proper keypath filtering.
// Once we do, we probably want this to be some keypath identifier that we can pass between managed and native.
public void Add(NotificationCallbackDelegate<T> callback, bool shallow)
{
var keyPath = shallow ? KeyPath.Empty : KeyPath.Full;
if (_subscriptions.TryGetValue(keyPath, out var subscription))
{
if (subscription.DeliveredInitialNotification)
{
callback(_parent, null, null);
}

// If we have a subscription already, we just add the callback to the list we're managing
subscription.Callbacks.Add(callback);
}
else
{
// If this is a new subscription, we store it in the backing dictionary, then we subscribe outside of transaction
_subscriptions[keyPath] = (null, false, new() { callback });
_parent.Realm.ExecuteOutsideTransaction(() =>
{
// It's possible that we unsubscribed in the meantime, so only add a notification callback if we still have callbacks
if (_subscriptions.TryGetValue(keyPath, out var subscription) && subscription.Callbacks.Count > 0)
{
var managedResultsHandle = GCHandle.Alloc(_parent, GCHandleType.Weak);
_subscriptions[keyPath] = (_parent.Handle.Value.AddNotificationCallback(GCHandle.ToIntPtr(managedResultsHandle), shallow), subscription.DeliveredInitialNotification, subscription.Callbacks);
}
});
}
}

public bool Remove(NotificationCallbackDelegate<T> callback, bool shallow)
{
var keyPath = shallow ? KeyPath.Empty : KeyPath.Full;
if (_subscriptions.TryGetValue(keyPath, out var subscription))
{
subscription.Callbacks.Remove(callback);
if (subscription.Callbacks.Count == 0)
{
subscription.Token?.Dispose();
_subscriptions.Remove(keyPath);
}

return true;
}

return false;
}

public void Notify(ChangeSet changes, bool shallow)
{
var keyPath = shallow ? KeyPath.Empty : KeyPath.Full;
if (_subscriptions.TryGetValue(keyPath, out var subscription))
{
if (changes == null)
{
_subscriptions[keyPath] = (subscription.Token, true, subscription.Callbacks);
}

foreach (var callback in subscription.Callbacks.ToArray())
{
callback(_parent, changes, null);
}
}
}

public void RemoveAll()
{
foreach (var token in _subscriptions.Values.Select(c => c.Token))
{
token.Dispose();
}
}
}

// Eventually this should be a proper class holding the keypaths
internal enum KeyPath
{
Full,
Empty
}

/// <summary>
/// Special invalid object that is used to avoid an exception in WPF
/// when deleting an element from a collection bound to UI (<see href="https://github.com/realm/realm-dotnet/issues/1903">#1903</see>).
Expand Down
6 changes: 4 additions & 2 deletions Realm/Realm/DatabaseTypes/RealmDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public class RealmDictionary<TValue>
IRealmCollectionBase<DictionaryHandle>,
INotifiable<DictionaryHandle.DictionaryChangeSet>
{
private readonly List<DictionaryNotificationCallbackDelegate<TValue>> _keyCallbacks = new List<DictionaryNotificationCallbackDelegate<TValue>>();
private readonly List<DictionaryNotificationCallbackDelegate<TValue>> _keyCallbacks = new();
private readonly DictionaryHandle _dictionaryHandle;

private bool _deliveredInitialKeyNotification;
Expand Down Expand Up @@ -249,8 +249,10 @@ private static string ValidateKey(string key)

protected override KeyValuePair<string, TValue> GetValueAtIndex(int index) => _dictionaryHandle.GetValueAtIndex<TValue>(index, Realm);

void INotifiable<DictionaryHandle.DictionaryChangeSet>.NotifyCallbacks(DictionaryHandle.DictionaryChangeSet? changes)
void INotifiable<DictionaryHandle.DictionaryChangeSet>.NotifyCallbacks(DictionaryHandle.DictionaryChangeSet? changes, bool shallow)
{
Debug.Assert(!shallow, "Shallow should always be false here as we don't expose a way to configure it.");

DictionaryChangeSet changeset = null;
if (changes != null)
{
Expand Down
2 changes: 2 additions & 0 deletions Realm/Realm/Handles/CollectionHandleBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,7 @@ public ResultsHandle GetFilteredResults(string query, RealmValue[] arguments)
protected abstract IntPtr GetFilteredResultsCore(string query, PrimitiveValue[] arguments, out NativeException ex);

protected virtual IntPtr SnapshotCore(out NativeException ex) => throw new NotSupportedException("Snapshotting this collection is not supported.");

public abstract NotificationTokenHandle AddNotificationCallback(IntPtr managedObjectHandle, bool shallow);
}
}
12 changes: 6 additions & 6 deletions Realm/Realm/Handles/DictionaryHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ internal struct DictionaryChangeSet
}

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void KeyNotificationCallback(IntPtr managedHandle, IntPtr changes);
public delegate void KeyNotificationCallback(IntPtr managedHandle, IntPtr changes, [MarshalAs(UnmanagedType.U1)] bool shallow);

private static class NativeMethods
{
Expand All @@ -48,7 +48,7 @@ private static class NativeMethods
public static extern void destroy(IntPtr listInternalHandle);

[DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_dictionary_add_notification_callback", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr add_notification_callback(DictionaryHandle handle, IntPtr managedDictionaryHandle, out NativeException ex);
public static extern IntPtr add_notification_callback(DictionaryHandle handle, IntPtr managedDictionaryHandle, [MarshalAs(UnmanagedType.U1)] bool shallow, out NativeException ex);

[DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_dictionary_add_key_notification_callback", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr add_key_notification_callback(DictionaryHandle handle, IntPtr managedDictionaryHandle, out NativeException ex);
Expand Down Expand Up @@ -136,11 +136,11 @@ public override void Clear()
nativeException.ThrowIfNecessary();
}

public override NotificationTokenHandle AddNotificationCallback(IntPtr managedObjectHandle)
public override NotificationTokenHandle AddNotificationCallback(IntPtr managedObjectHandle, bool shallow)
{
EnsureIsOpen();

var result = NativeMethods.add_notification_callback(this, managedObjectHandle, out var nativeException);
var result = NativeMethods.add_notification_callback(this, managedObjectHandle, shallow, out var nativeException);

Check notice

Code scanning / CodeQL

Calls to unmanaged code

Replace this call with a call to managed code if possible.
nativeException.ThrowIfNecessary();
return new NotificationTokenHandle(Root, result);
}
Expand Down Expand Up @@ -338,11 +338,11 @@ public ResultsHandle GetKeys()
}

[MonoPInvokeCallback(typeof(KeyNotificationCallback))]
public static void NotifyDictionaryChanged(IntPtr managedHandle, IntPtr changes)
public static void NotifyDictionaryChanged(IntPtr managedHandle, IntPtr changes, bool shallow)
{
if (GCHandle.FromIntPtr(managedHandle).Target is INotifiable<DictionaryChangeSet> notifiable)
{
notifiable.NotifyCallbacks(new PtrTo<DictionaryChangeSet>(changes).Value);
notifiable.NotifyCallbacks(new PtrTo<DictionaryChangeSet>(changes).Value, shallow);
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions Realm/Realm/Handles/ListHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
////////////////////////////////////////////////////////////////////////////

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Realms.Native;

Expand Down Expand Up @@ -63,7 +64,7 @@ private static class NativeMethods
public static extern void destroy(IntPtr listInternalHandle);

[DllImport(InteropConfig.DLL_NAME, EntryPoint = "list_add_notification_callback", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr add_notification_callback(ListHandle listHandle, IntPtr managedListHandle, out NativeException ex);
public static extern IntPtr add_notification_callback(ListHandle listHandle, IntPtr managedListHandle, [MarshalAs(UnmanagedType.U1)] bool shallow, out NativeException ex);

[DllImport(InteropConfig.DLL_NAME, EntryPoint = "list_move", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr move(ListHandle listHandle, IntPtr sourceIndex, IntPtr targetIndex, out NativeException ex);
Expand Down Expand Up @@ -213,11 +214,11 @@ public void Move(IntPtr sourceIndex, IntPtr targetIndex)
nativeException.ThrowIfNecessary();
}

public override NotificationTokenHandle AddNotificationCallback(IntPtr managedObjectHandle)
public override NotificationTokenHandle AddNotificationCallback(IntPtr managedObjectHandle, bool shallow)
{
EnsureIsOpen();

var result = NativeMethods.add_notification_callback(this, managedObjectHandle, out var nativeException);
var result = NativeMethods.add_notification_callback(this, managedObjectHandle, shallow, out var nativeException);

Check notice

Code scanning / CodeQL

Calls to unmanaged code

Replace this call with a call to managed code if possible.
nativeException.ThrowIfNecessary();
return new NotificationTokenHandle(Root, result);
}
Expand Down
Loading