// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using AutoMapper; using osu.Framework.Development; using osu.Game.Input.Bindings; using Realms; #nullable enable namespace osu.Game.Database { public static class RealmObjectExtensions { private static readonly IMapper mapper = new MapperConfiguration(c => { c.ShouldMapField = fi => false; c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic; c.CreateMap(); }).CreateMapper(); /// /// Create a detached copy of the each item in the collection. /// /// A list of managed s to detach. /// The type of object. /// A list containing non-managed copies of provided items. public static List Detach(this IEnumerable items) where T : RealmObject { var list = new List(); foreach (var obj in items) list.Add(obj.Detach()); return list; } /// /// Create a detached copy of the item. /// /// The managed to detach. /// The type of object. /// A non-managed copy of provided item. Will return the provided item if already detached. public static T Detach(this T item) where T : RealmObject { if (!item.IsManaged) return item; return mapper.Map(item); } public static List> ToLiveUnmanaged(this IEnumerable realmList) where T : RealmObject, IHasGuidPrimaryKey { return realmList.Select(l => new RealmLiveUnmanaged(l)).Cast>().ToList(); } public static ILive ToLiveUnmanaged(this T realmObject) where T : RealmObject, IHasGuidPrimaryKey { return new RealmLiveUnmanaged(realmObject); } public static List> ToLive(this IEnumerable realmList, RealmContextFactory realmContextFactory) where T : RealmObject, IHasGuidPrimaryKey { return realmList.Select(l => new RealmLive(l, realmContextFactory)).Cast>().ToList(); } public static ILive ToLive(this T realmObject, RealmContextFactory realmContextFactory) where T : RealmObject, IHasGuidPrimaryKey { return new RealmLive(realmObject, realmContextFactory); } /// /// Register a callback to be invoked each time this changes. /// /// /// /// This adds osu! specific thread and managed state safety checks on top of . /// /// /// The first callback will be invoked with the initial after the asynchronous query completes, /// and then called again after each write transaction which changes either any of the objects in the collection, or /// which objects are in the collection. The changes parameter will /// be null the first time the callback is invoked with the initial results. For each call after that, /// it will contain information about which rows in the results were added, removed or modified. /// /// /// If a write transaction did not modify any objects in this , the callback is not invoked at all. /// If an error occurs the callback will be invoked with null for the sender parameter and a non-null error. /// Currently the only errors that can occur are when opening the on the background worker thread. /// /// /// At the time when the block is called, the object will be fully evaluated /// and up-to-date, and as long as you do not perform a write transaction on the same thread /// or explicitly call , accessing it will never perform blocking work. /// /// /// Notifications are delivered via the standard event loop, and so can't be delivered while the event loop is blocked by other activity. /// When notifications can't be delivered instantly, multiple notifications may be coalesced into a single notification. /// This can include the notification with the initial collection. /// /// /// The to observe for changes. /// The callback to be invoked with the updated . /// /// A subscription token. It must be kept alive for as long as you want to receive change notifications. /// To stop receiving notifications, call . /// /// May be null in the case the provided collection is not managed. /// /// /// public static IDisposable? QueryAsyncWithNotifications(this IRealmCollection collection, NotificationCallbackDelegate callback) where T : RealmObjectBase { // Subscriptions can only work on the main thread. if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException("Cannot subscribe for realm notifications from a non-update thread."); return collection.SubscribeForNotifications(callback); } /// /// A convenience method that casts to and subscribes for change notifications. /// /// /// This adds osu! specific thread and managed state safety checks on top of . /// /// The to observe for changes. /// Type of the elements in the list. /// /// The callback to be invoked with the updated . /// /// A subscription token. It must be kept alive for as long as you want to receive change notifications. /// To stop receiving notifications, call . /// /// May be null in the case the provided collection is not managed. /// public static IDisposable? QueryAsyncWithNotifications(this IQueryable list, NotificationCallbackDelegate callback) where T : RealmObjectBase { // Subscribing to non-managed instances doesn't work. // In this usage, the instance may be non-managed in tests. if (!(list is IRealmCollection realmCollection)) return null; return QueryAsyncWithNotifications(realmCollection, callback); } /// /// A convenience method that casts to and subscribes for change notifications. /// /// /// This adds osu! specific thread and managed state safety checks on top of . /// /// The to observe for changes. /// Type of the elements in the list. /// /// The callback to be invoked with the updated . /// /// A subscription token. It must be kept alive for as long as you want to receive change notifications. /// To stop receiving notifications, call . /// /// May be null in the case the provided collection is not managed. /// public static IDisposable? QueryAsyncWithNotifications(this IList list, NotificationCallbackDelegate callback) where T : RealmObjectBase { // Subscribing to non-managed instances doesn't work. // In this usage, the instance may be non-managed in tests. if (!(list is IRealmCollection realmCollection)) return null; return QueryAsyncWithNotifications(realmCollection, callback); } } }