diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs index 363a189f6e..d99bcc092d 100644 --- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs +++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs @@ -1,25 +1,80 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Tests.Resources; using Realms; -#nullable enable - namespace osu.Game.Tests.Database { [TestFixture] public class RealmSubscriptionRegistrationTests : RealmTest { + [Test] + public void TestSubscriptionCollectionAndPropertyChanges() + { + int collectionChanges = 0; + int propertyChanges = 0; + + ChangeSet? lastChanges = null; + + RunTestWithRealm((realm, _) => + { + var registration = realm.RegisterForNotifications(r => r.All(), onChanged); + + realm.Run(r => r.Refresh()); + + realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo())); + realm.Run(r => r.Refresh()); + + Assert.That(collectionChanges, Is.EqualTo(1)); + Assert.That(propertyChanges, Is.EqualTo(0)); + Assert.That(lastChanges?.InsertedIndices, Has.One.Items); + Assert.That(lastChanges?.ModifiedIndices, Is.Empty); + Assert.That(lastChanges?.NewModifiedIndices, Is.Empty); + + realm.Write(r => r.All().First().Beatmaps.First().CountdownOffset = 5); + realm.Run(r => r.Refresh()); + + Assert.That(collectionChanges, Is.EqualTo(1)); + Assert.That(propertyChanges, Is.EqualTo(1)); + Assert.That(lastChanges?.InsertedIndices, Is.Empty); + Assert.That(lastChanges?.ModifiedIndices, Has.One.Items); + Assert.That(lastChanges?.NewModifiedIndices, Has.One.Items); + + registration.Dispose(); + }); + + void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error) + { + lastChanges = changes; + + if (changes == null) + return; + + if (changes.HasCollectionChanges()) + { + Interlocked.Increment(ref collectionChanges); + } + else + { + Interlocked.Increment(ref propertyChanges); + } + } + } + [Test] public void TestSubscriptionWithAsyncWrite() { diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index e6f3dba39f..551b84f7b6 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -4,6 +4,8 @@ using System; using Realms; +#nullable enable + namespace osu.Game.Database { public static class RealmExtensions @@ -22,5 +24,14 @@ namespace osu.Game.Database transaction.Commit(); return result; } + + /// + /// Whether the provided change set has changes to the top level collection. + /// + /// + /// Realm subscriptions fire on both collection and property changes (including *all* nested properties). + /// Quite often we only care about changes at a collection level. This can be used to guard and early-return when no such changes are in a callback. + /// + public static bool HasCollectionChanges(this ChangeSet changes) => changes.InsertedIndices.Length > 0 || changes.DeletedIndices.Length > 0 || changes.Moves.Length > 0; } } diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index eb0addd377..8d1654eb1d 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -191,6 +191,11 @@ namespace osu.Game.Screens.Select.Leaderboards if (cancellationToken.IsCancellationRequested) return; + // This subscription may fire from changes to linked beatmaps, which we don't care about. + // It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications. + if (changes?.HasCollectionChanges() == false) + return; + var scores = sender.AsEnumerable(); if (filterMods && !mods.Value.Any())