From 17306d51397316e731f73f857b0c510973cde564 Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Sat, 23 Apr 2022 14:07:17 -0300 Subject: [PATCH] updates --- src/Discord.Net.Core/Cache/ICached.cs | 4 + .../Cache/Models/IEntityModel.cs | 13 + .../Cache/Models/Presense/IPresenceModel.cs | 2 +- .../Cache/Models/Users/IMemberModel.cs | 5 +- .../Cache/Models/Users/IThreadMemberModel.cs | 15 + .../Cache/Models/Users/IUserModel.cs | 3 +- .../API/Common/GuildMember.cs | 4 +- src/Discord.Net.Rest/API/Common/Presence.cs | 3 + .../API/Common/ThreadMember.cs | 10 +- src/Discord.Net.Rest/API/Common/User.cs | 4 +- .../Cache/DefaultConcurrentCacheProvider.cs | 118 +++--- .../Cache/ICacheProvider.cs | 40 +- .../ClientStateManager.Experiment.cs | 355 +++++++++++++----- .../ClientStateManager.cs | 14 +- .../DiscordSocketClient.cs | 90 ++--- .../DiscordSocketConfig.cs | 12 +- .../Entities/Guilds/SocketGuild.cs | 36 +- .../Entities/Users/SocketGlobalUser.cs | 30 +- .../Entities/Users/SocketGroupUser.cs | 33 +- .../Entities/Users/SocketGuildUser.cs | 65 ++-- .../Entities/Users/SocketPresence.cs | 13 +- .../Entities/Users/SocketSelfUser.cs | 43 ++- .../Entities/Users/SocketThreadUser.cs | 180 +++++---- .../Entities/Users/SocketUnknownUser.cs | 10 +- .../Entities/Users/SocketUser.cs | 39 +- .../Entities/Users/SocketWebhookUser.cs | 9 +- .../Extensions/StateExtensions.cs | 21 -- .../State/DefaultStateProvider.cs | 252 ------------- .../State/IStateProvider.cs | 25 -- .../State/StateBehavior.cs | 53 --- 30 files changed, 697 insertions(+), 804 deletions(-) create mode 100644 src/Discord.Net.Core/Cache/Models/IEntityModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Users/IThreadMemberModel.cs delete mode 100644 src/Discord.Net.WebSocket/Extensions/StateExtensions.cs delete mode 100644 src/Discord.Net.WebSocket/State/DefaultStateProvider.cs delete mode 100644 src/Discord.Net.WebSocket/State/IStateProvider.cs delete mode 100644 src/Discord.Net.WebSocket/State/StateBehavior.cs diff --git a/src/Discord.Net.Core/Cache/ICached.cs b/src/Discord.Net.Core/Cache/ICached.cs index 3146741bb..6f7a99bfc 100644 --- a/src/Discord.Net.Core/Cache/ICached.cs +++ b/src/Discord.Net.Core/Cache/ICached.cs @@ -8,6 +8,10 @@ namespace Discord { internal interface ICached { + void Update(TType model); + TType ToModel(); + + TResult ToModel() where TResult : TType, new(); } } diff --git a/src/Discord.Net.Core/Cache/Models/IEntityModel.cs b/src/Discord.Net.Core/Cache/Models/IEntityModel.cs new file mode 100644 index 000000000..c48fc05d5 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/IEntityModel.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IEntityModel where TId : IEquatable + { + TId Id { get; set; } + } +} diff --git a/src/Discord.Net.Core/Cache/Models/Presense/IPresenceModel.cs b/src/Discord.Net.Core/Cache/Models/Presense/IPresenceModel.cs index 887aa858b..b1b45f21e 100644 --- a/src/Discord.Net.Core/Cache/Models/Presense/IPresenceModel.cs +++ b/src/Discord.Net.Core/Cache/Models/Presense/IPresenceModel.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace Discord { - public interface IPresenceModel + public interface IPresenceModel : IEntityModel { ulong UserId { get; set; } ulong? GuildId { get; set; } diff --git a/src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs b/src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs index 972fa7354..a6daa66d0 100644 --- a/src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs +++ b/src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs @@ -6,10 +6,9 @@ using System.Threading.Tasks; namespace Discord { - public interface IMemberModel + public interface IMemberModel : IEntityModel { - IUserModel User { get; set; } - + //IUserModel User { get; set; } string Nickname { get; set; } string GuildAvatar { get; set; } ulong[] Roles { get; set; } diff --git a/src/Discord.Net.Core/Cache/Models/Users/IThreadMemberModel.cs b/src/Discord.Net.Core/Cache/Models/Users/IThreadMemberModel.cs new file mode 100644 index 000000000..b8ad311fb --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Users/IThreadMemberModel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IThreadMemberModel : IEntityModel + { + ulong? ThreadId { get; set; } + ulong? UserId { get; set; } + DateTimeOffset JoinedAt { get; set; } + } +} diff --git a/src/Discord.Net.Core/Cache/Models/Users/IUserModel.cs b/src/Discord.Net.Core/Cache/Models/Users/IUserModel.cs index 6dcf1d9a7..88b050520 100644 --- a/src/Discord.Net.Core/Cache/Models/Users/IUserModel.cs +++ b/src/Discord.Net.Core/Cache/Models/Users/IUserModel.cs @@ -6,9 +6,8 @@ using System.Threading.Tasks; namespace Discord { - public interface IUserModel + public interface IUserModel : IEntityModel { - ulong Id { get; set; } string Username { get; set; } string Discriminator { get; set; } bool? IsBot { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/GuildMember.cs b/src/Discord.Net.Rest/API/Common/GuildMember.cs index 18aac3822..8a373fbab 100644 --- a/src/Discord.Net.Rest/API/Common/GuildMember.cs +++ b/src/Discord.Net.Rest/API/Common/GuildMember.cs @@ -63,8 +63,8 @@ namespace Discord.API get => TimedOutUntil.GetValueOrDefault(); set => throw new NotSupportedException(); } - IUserModel IMemberModel.User { - get => User; set => throw new NotSupportedException(); + ulong IEntityModel.Id { + get => User.Id; set => throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.Rest/API/Common/Presence.cs b/src/Discord.Net.Rest/API/Common/Presence.cs index 4269074c0..de450172b 100644 --- a/src/Discord.Net.Rest/API/Common/Presence.cs +++ b/src/Discord.Net.Rest/API/Common/Presence.cs @@ -49,5 +49,8 @@ namespace Discord.API IActivityModel[] IPresenceModel.Activities { get => Activities.ToArray(); set => throw new NotSupportedException(); } + ulong IEntityModel.Id { + get => User.Id; set => throw new NotSupportedException(); + } } } diff --git a/src/Discord.Net.Rest/API/Common/ThreadMember.cs b/src/Discord.Net.Rest/API/Common/ThreadMember.cs index 30249ee44..11531c77f 100644 --- a/src/Discord.Net.Rest/API/Common/ThreadMember.cs +++ b/src/Discord.Net.Rest/API/Common/ThreadMember.cs @@ -3,10 +3,10 @@ using System; namespace Discord.API { - internal class ThreadMember + internal class ThreadMember : IThreadMemberModel { [JsonProperty("id")] - public Optional Id { get; set; } + public Optional ThreadId { get; set; } [JsonProperty("user_id")] public Optional UserId { get; set; } @@ -14,7 +14,9 @@ namespace Discord.API [JsonProperty("join_timestamp")] public DateTimeOffset JoinTimestamp { get; set; } - [JsonProperty("flags")] - public int Flags { get; set; } // No enum type (yet?) + ulong? IThreadMemberModel.ThreadId { get => ThreadId.ToNullable(); set => throw new NotSupportedException(); } + ulong? IThreadMemberModel.UserId { get => UserId.ToNullable(); set => throw new NotSupportedException(); } + DateTimeOffset IThreadMemberModel.JoinedAt { get => JoinTimestamp; set => throw new NotSupportedException(); } + ulong IEntityModel.Id { get => UserId.GetValueOrDefault(0); set => throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.Rest/API/Common/User.cs b/src/Discord.Net.Rest/API/Common/User.cs index 03d23374f..5c8a5b240 100644 --- a/src/Discord.Net.Rest/API/Common/User.cs +++ b/src/Discord.Net.Rest/API/Common/User.cs @@ -43,10 +43,10 @@ namespace Discord.API get => Avatar.GetValueOrDefault(); set => throw new NotSupportedException(); } - ulong IUserModel.Id + ulong IEntityModel.Id { get => Id; set => throw new NotSupportedException(); - } + } } } diff --git a/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs b/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs index f0131759d..136d88a75 100644 --- a/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs +++ b/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs @@ -9,74 +9,74 @@ namespace Discord.WebSocket { public class DefaultConcurrentCacheProvider : ICacheProvider { - private readonly ConcurrentDictionary _users; - private readonly ConcurrentDictionary> _members; - private readonly ConcurrentDictionary _presense; + private readonly ConcurrentDictionary _storeCache = new(); + private readonly ConcurrentDictionary _subStoreCache = new(); - private ValueTask CompletedValueTask => new ValueTask(Task.CompletedTask).Preserve(); - - public DefaultConcurrentCacheProvider(int defaultConcurrency, int defaultCapacity) + private class DefaultEntityStore : IEntityStore + where TModel : IEntityModel + where TId : IEquatable { - _users = new(defaultConcurrency, defaultCapacity); - _members = new(defaultConcurrency, defaultCapacity); - _presense = new(defaultConcurrency, defaultCapacity); - } + private ConcurrentDictionary _cache; - public ValueTask AddOrUpdateUserAsync(IUserModel model, CacheRunMode mode) - { - _users.AddOrUpdate(model.Id, model, (_, __) => model); - return CompletedValueTask; - } - public ValueTask AddOrUpdateMemberAsync(IMemberModel model, ulong guildId, CacheRunMode mode) - { - var guildMemberCache = _members.GetOrAdd(guildId, (_) => new ConcurrentDictionary()); - guildMemberCache.AddOrUpdate(model.User.Id, model, (_, __) => model); - return CompletedValueTask; - } - public ValueTask GetMemberAsync(ulong id, ulong guildId, CacheRunMode mode) - => new ValueTask(_members.FirstOrDefault(x => x.Key == guildId).Value?.FirstOrDefault(x => x.Key == id).Value); + public DefaultEntityStore(ConcurrentDictionary cache) + { + _cache = cache; + } - public ValueTask> GetMembersAsync(ulong guildId, CacheRunMode mode) - { - if(_members.TryGetValue(guildId, out var inner)) - return new ValueTask>(inner.ToArray().Select(x => x.Value)); // ToArray here is important before .Select due to concurrency - return new ValueTask>(Array.Empty()); - } - public ValueTask GetUserAsync(ulong id, CacheRunMode mode) - { - if (_users.TryGetValue(id, out var result)) - return new ValueTask(result); - return new ValueTask((IUserModel)null); - } - public ValueTask> GetUsersAsync(CacheRunMode mode) - => new ValueTask>(_users.ToArray().Select(x => x.Value)); - public ValueTask RemoveMemberAsync(ulong id, ulong guildId, CacheRunMode mode) - { - if (_members.TryGetValue(guildId, out var inner)) - inner.TryRemove(id, out var _); - return CompletedValueTask; - } - public ValueTask RemoveUserAsync(ulong id, CacheRunMode mode) - { - _members.TryRemove(id, out var _); - return CompletedValueTask; - } + public ValueTask AddOrUpdateAsync(TModel model, CacheRunMode runmode) + { + _cache.AddOrUpdate(model.Id, model, (_, __) => model); + return default; + } - public ValueTask GetPresenceAsync(ulong userId, CacheRunMode runmode) - { - if (_presense.TryGetValue(userId, out var presense)) - return new ValueTask(presense); - return new ValueTask((IPresenceModel)null); + public ValueTask AddOrUpdateBatchAsync(IEnumerable models, CacheRunMode runmode) + { + foreach (var model in models) + _cache.AddOrUpdate(model.Id, model, (_, __) => model); + return default; + } + + public IAsyncEnumerable GetAllAsync(CacheRunMode runmode) + { + var coll = _cache.Select(x => x.Value).GetEnumerator(); + return AsyncEnumerable.Create((_) => AsyncEnumerator.Create( + () => new ValueTask(coll.MoveNext()), + () => coll.Current, + () => new ValueTask())); + } + public ValueTask GetAsync(TId id, CacheRunMode runmode) + { + if (_cache.TryGetValue(id, out var model)) + return new ValueTask(model); + return default; + } + public ValueTask RemoveAsync(TId id, CacheRunMode runmode) + { + _cache.TryRemove(id, out _); + return default; + } + + public ValueTask PurgeAllAsync(CacheRunMode runmode) + { + _cache.Clear(); + return default; + } } - public ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresenceModel presense, CacheRunMode runmode) + + public virtual ValueTask> GetStoreAsync() + where TModel : IEntityModel + where TId : IEquatable { - _presense.AddOrUpdate(userId, presense, (_, __) => presense); - return CompletedValueTask; + var store = _storeCache.GetOrAdd(typeof(TModel), (_) => new DefaultEntityStore(new ConcurrentDictionary())); + return new ValueTask>((IEntityStore)store); } - public ValueTask RemovePresenseAsync(ulong userId, CacheRunMode runmode) + + public virtual ValueTask> GetSubStoreAsync(TId parentId) + where TModel : IEntityModel + where TId : IEquatable { - _presense.TryRemove(userId, out var _); - return CompletedValueTask; + var store = _subStoreCache.GetOrAdd(parentId, (_) => new DefaultEntityStore(new ConcurrentDictionary())); + return new ValueTask>((IEntityStore)store); } } } diff --git a/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs b/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs index 5d67b892e..f4ae3b994 100644 --- a/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs +++ b/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs @@ -8,30 +8,24 @@ namespace Discord.WebSocket { public interface ICacheProvider { - #region Users + ValueTask> GetStoreAsync() + where TModel : IEntityModel + where TId : IEquatable; - ValueTask GetUserAsync(ulong id, CacheRunMode runmode); - ValueTask> GetUsersAsync(CacheRunMode runmode); - ValueTask AddOrUpdateUserAsync(IUserModel model, CacheRunMode runmode); - ValueTask RemoveUserAsync(ulong id, CacheRunMode runmode); - - #endregion - - #region Members - - ValueTask GetMemberAsync(ulong id, ulong guildId, CacheRunMode runmode); - ValueTask> GetMembersAsync(ulong guildId, CacheRunMode runmode); - ValueTask AddOrUpdateMemberAsync(IMemberModel model, ulong guildId, CacheRunMode runmode); - ValueTask RemoveMemberAsync(ulong id, ulong guildId, CacheRunMode runmode); - - #endregion - - #region Presence - - ValueTask GetPresenceAsync(ulong userId, CacheRunMode runmode); - ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresenceModel model, CacheRunMode runmode); - ValueTask RemovePresenseAsync(ulong userId, CacheRunMode runmode); + ValueTask> GetSubStoreAsync(TId parentId) + where TModel : IEntityModel + where TId : IEquatable; + } - #endregion + public interface IEntityStore + where TModel : IEntityModel + where TId : IEquatable + { + ValueTask GetAsync(TId id, CacheRunMode runmode); + IAsyncEnumerable GetAllAsync(CacheRunMode runmode); + ValueTask AddOrUpdateAsync(TModel model, CacheRunMode runmode); + ValueTask AddOrUpdateBatchAsync(IEnumerable models, CacheRunMode runmode); + ValueTask RemoveAsync(TId id, CacheRunMode runmode); + ValueTask PurgeAllAsync(CacheRunMode runmode); } } diff --git a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs index 4ad2460a3..f657e526d 100644 --- a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs +++ b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs @@ -1,163 +1,342 @@ +using Discord.Rest; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Discord.WebSocket { - internal class CacheWeakReference : WeakReference + internal class CacheReference where TType : class { - public new T Target { get => (T)base.Target; set => base.Target = value; } - public CacheWeakReference(T target) - : base(target, false) + public WeakReference Reference { get; } + + public bool CanRelease + => !Reference.TryGetTarget(out _) || _referenceCount <= 0; + + private int _referenceCount; + + private readonly object _lock = new object(); + + public CacheReference(TType value) { + Reference = new(value); + _referenceCount = 1; + } + public bool TryObtainReference(out TType reference) + { + if (Reference.TryGetTarget(out reference)) + { + Interlocked.Increment(ref _referenceCount); + return true; + } + return false; } - public bool TryGetTarget(out T target) + public void ReleaseReference() { - target = Target; - return IsAlive; + lock (_lock) + { + if (_referenceCount > 0) + _referenceCount--; + } } } - - internal partial class ClientStateManager + internal class ReferenceStore + where TEntity : class, ICached, ISharedEntity + where TModel : IEntityModel + where TId : IEquatable + where ISharedEntity : class { - private readonly ConcurrentDictionary> _userReferences = new(); - private readonly ConcurrentDictionary<(ulong GuildId, ulong UserId), CacheWeakReference> _memberReferences = new(); - - - #region Helpers - - private void EnsureSync(ValueTask vt) + private readonly ICacheProvider _cacheProvider; + private readonly ConcurrentDictionary> _references = new(); + private IEntityStore _store; + private Func _entityBuilder; + private Func> _restLookup; + private readonly bool _allowSyncWaits; + private readonly object _lock = new(); + + public ReferenceStore(ICacheProvider cacheProvider, Func entityBuilder, Func> restLookup, bool allowSyncWaits) { - if (!vt.IsCompleted) - throw new NotSupportedException($"Cannot use async context for value task lookup"); + _allowSyncWaits = allowSyncWaits; + _cacheProvider = cacheProvider; + _entityBuilder = entityBuilder; + _restLookup = restLookup; } - #endregion + internal void ClearDeadReferences() + { + lock (_lock) + { + var references = _references.Where(x => x.Value.CanRelease).ToArray(); + foreach (var reference in references) + _references.TryRemove(reference.Key, out _); + } + } - #region Global users - internal void RemoveReferencedGlobalUser(ulong id) + private TResult RunOrThrowValueTask(ValueTask t) { - Console.WriteLine("Global user untracked"); - _userReferences.TryRemove(id, out _); + if (_allowSyncWaits) + { + return t.GetAwaiter().GetResult(); + } + else if (t.IsCompleted) + return t.Result; + else + throw new InvalidOperationException("Cannot run asynchronous value task in synchronous context"); } - private void TrackGlobalUser(ulong id, SocketGlobalUser user) + private void RunOrThrowValueTask(ValueTask t) { - if (user != null) + if (_allowSyncWaits) { - _userReferences.TryAdd(id, new CacheWeakReference(user)); + t.GetAwaiter().GetResult(); } + else if (!t.IsCompleted) + throw new InvalidOperationException("Cannot run asynchronous value task in synchronous context"); } - internal ValueTask GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) - => _state.GetUserAsync(id, mode.ToBehavior(), options); + public async ValueTask InitializeAsync() + { + _store ??= await _cacheProvider.GetStoreAsync().ConfigureAwait(false); + } + + public async ValueTask InitializeAsync(TId parentId) + { + _store ??= await _cacheProvider.GetSubStoreAsync(parentId).ConfigureAwait(false); + } - internal SocketGlobalUser GetUser(ulong id) + private bool TryGetReference(TId id, out TEntity entity) { - if (_userReferences.TryGetValue(id, out var userRef) && userRef.TryGetTarget(out var user)) - return user; + entity = null; + return _references.TryGetValue(id, out var reference) && reference.TryObtainReference(out entity); + } + + public TEntity Get(TId id) + { + if(TryGetReference(id, out var entity)) + { + return entity; + } - user = (SocketGlobalUser)_state.GetUserAsync(id, StateBehavior.SyncOnly).Result; + var model = RunOrThrowValueTask(_store.GetAsync(id, CacheRunMode.Sync)); - if(user != null) - TrackGlobalUser(id, user); + if (model != null) + { + entity = _entityBuilder(model); + _references.TryAdd(id, new CacheReference(entity)); + return entity; + } - return user; + return null; } - internal SocketGlobalUser GetOrAddUser(ulong id, Func userFactory) + public async ValueTask GetAsync(TId id, CacheMode mode, RequestOptions options = null) { - if (_userReferences.TryGetValue(id, out var userRef) && userRef.TryGetTarget(out var user)) - return user; + if (TryGetReference(id, out var entity)) + { + return entity; + } + + var model = await _store.GetAsync(id, CacheRunMode.Async).ConfigureAwait(false); - user = GetUser(id); + if (model != null) + { + entity = _entityBuilder(model); + _references.TryAdd(id, new CacheReference(entity)); + return entity; + } - if (user == null) + if(mode == CacheMode.AllowDownload) { - user ??= userFactory(id); - _state.AddOrUpdateUserAsync(user); - TrackGlobalUser(id, user); + return await _restLookup(id, options).ConfigureAwait(false); } - return user; + return null; } - internal void RemoveUser(ulong id) + public IEnumerable GetAll() { - _state.RemoveUserAsync(id); + var models = RunOrThrowValueTask(_store.GetAllAsync(CacheRunMode.Sync).ToArrayAsync()); + return models.Select(x => + { + var entity = _entityBuilder(x); + _references.TryAdd(x.Id, new CacheReference(entity)); + return entity; + }); } - #endregion - #region GuildUsers - private void TrackMember(ulong userId, ulong guildId, SocketGuildUser user) + public async IAsyncEnumerable GetAllAsync() { - if(user != null) + await foreach(var model in _store.GetAllAsync(CacheRunMode.Async)) { - _memberReferences.TryAdd((guildId, userId), new CacheWeakReference(user)); + var entity = _entityBuilder(model); + _references.TryAdd(model.Id, new CacheReference(entity)); + yield return entity; } } - internal void RemovedReferencedMember(ulong userId, ulong guildId) - => _memberReferences.TryRemove((guildId, userId), out _); - internal ValueTask GetMemberAsync(ulong userId, ulong guildId, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) - => _state.GetMemberAsync(guildId, userId, mode.ToBehavior(), options); + public TEntity GetOrAdd(TId id, Func valueFactory) + { + var entity = Get(id); + if (entity != null) + return entity; + + var model = valueFactory(id); + AddOrUpdate(model); + return _entityBuilder(model); + } + + public async ValueTask GetOrAddAsync(TId id, Func valueFactory) + { + var entity = await GetAsync(id, CacheMode.CacheOnly).ConfigureAwait(false); + if (entity != null) + return (TEntity)entity; + + var model = valueFactory(id); + await AddOrUpdateAsync(model); + return _entityBuilder(model); + } - internal SocketGuildUser GetMember(ulong userId, ulong guildId) + public void AddOrUpdate(TModel model) { - if (_memberReferences.TryGetValue((guildId, userId), out var memberRef) && memberRef.TryGetTarget(out var member)) - return member; - member = (SocketGuildUser)_state.GetMemberAsync(guildId, userId, StateBehavior.SyncOnly).Result; - if(member != null) - TrackMember(userId, guildId, member); - return member; + RunOrThrowValueTask(_store.AddOrUpdateAsync(model, CacheRunMode.Sync)); + if (TryGetReference(model.Id, out var reference)) + reference.Update(model); } - internal SocketGuildUser GetOrAddMember(ulong userId, ulong guildId, Func memberFactory) + public ValueTask AddOrUpdateAsync(TModel model) { - if (_memberReferences.TryGetValue((guildId, userId), out var memberRef) && memberRef.TryGetTarget(out var member)) - return member; + if (TryGetReference(model.Id, out var reference)) + reference.Update(model); + return _store.AddOrUpdateAsync(model, CacheRunMode.Async); + } - member = GetMember(userId, guildId); + public void Remove(TId id) + { + RunOrThrowValueTask(_store.RemoveAsync(id, CacheRunMode.Sync)); + _references.TryRemove(id, out _); + } - if (member == null) - { - member ??= memberFactory(userId, guildId); - TrackMember(userId, guildId, member); - Task.Run(async () => await _state.AddOrUpdateMemberAsync(guildId, member)); // can run async, think of this as fire and forget. - } + public ValueTask RemoveAsync(TId id) + { + _references.TryRemove(id, out _); + return _store.RemoveAsync(id, CacheRunMode.Async); + } - return member; + public void Purge() + { + RunOrThrowValueTask(_store.PurgeAllAsync(CacheRunMode.Sync)); + _references.Clear(); } - internal IEnumerable GetMembers(ulong guildId) - => _state.GetMembersAsync(guildId, StateBehavior.SyncOnly).Result; + public ValueTask PurgeAsync() + { + _references.Clear(); + return _store.PurgeAllAsync(CacheRunMode.Async); + } + } - internal void AddOrUpdateMember(ulong guildId, SocketGuildUser user) - => EnsureSync(_state.AddOrUpdateMemberAsync(guildId, user)); + internal partial class ClientStateManager + { + public ReferenceStore UserStore; + public ReferenceStore PresenceStore; + private ConcurrentDictionary> _memberStores; + private ConcurrentDictionary> _threadMemberStores; - internal void RemoveMember(ulong userId, ulong guildId) - => EnsureSync(_state.RemoveMemberAsync(guildId, userId)); + private SemaphoreSlim _memberStoreLock; + private SemaphoreSlim _threadMemberLock; - #endregion + private void CreateStores() + { + UserStore = new ReferenceStore( + _cacheProvider, + m => SocketGlobalUser.Create(_client, m), + async (id, options) => await _client.Rest.GetUserAsync(id, options).ConfigureAwait(false), + AllowSyncWaits); + + PresenceStore = new ReferenceStore( + _cacheProvider, + m => SocketPresence.Create(m), + (id, options) => Task.FromResult(null), + AllowSyncWaits); + + _memberStores = new(); + _threadMemberStores = new(); + + _threadMemberLock = new(1, 1); + _memberStoreLock = new(1,1); + } - #region Presence - internal void AddOrUpdatePresence(SocketPresence presence) + public void ClearDeadReferences() { - EnsureSync(_state.AddOrUpdatePresenseAsync(presence.UserId, presence, StateBehavior.SyncOnly)); + UserStore.ClearDeadReferences(); + PresenceStore.ClearDeadReferences(); } - internal SocketPresence GetPresence(ulong userId) + public async ValueTask InitializeAsync() { - if (_state.GetPresenceAsync(userId, StateBehavior.SyncOnly).Result is not SocketPresence socketPresence) - throw new NotSupportedException("Cannot use non-socket entity for presence"); + await UserStore.InitializeAsync(); + await PresenceStore.InitializeAsync(); + } + + public bool TryGetMemberStore(ulong guildId, out ReferenceStore store) + => _memberStores.TryGetValue(guildId, out store); - return socketPresence; + public async ValueTask> GetMemberStoreAsync(ulong guildId) + { + if (_memberStores.TryGetValue(guildId, out var store)) + return store; + + await _memberStoreLock.WaitAsync().ConfigureAwait(false); + + try + { + store = new ReferenceStore( + _cacheProvider, + m => SocketGuildUser.Create(guildId, _client, m), + async (id, options) => await _client.Rest.GetGuildUserAsync(guildId, id, options).ConfigureAwait(false), + AllowSyncWaits); + + await store.InitializeAsync(guildId).ConfigureAwait(false); + + _memberStores.TryAdd(guildId, store); + return store; + } + finally + { + _memberStoreLock.Release(); + } + } + + public async Task> GetThreadMemberStoreAsync(ulong threadId, ulong guildId) + { + if (_threadMemberStores.TryGetValue(threadId, out var store)) + return store; + + await _threadMemberLock.WaitAsync().ConfigureAwait(false); + + try + { + store = new ReferenceStore( + _cacheProvider, + m => SocketThreadUser.Create(_client, guildId, threadId, m), + async (id, options) => await ThreadHelper.GetUserAsync(id, _client.GetChannel(threadId) as SocketThreadChannel, _client, options).ConfigureAwait(false), + AllowSyncWaits); + + await store.InitializeAsync().ConfigureAwait(false); + + _threadMemberStores.TryAdd(threadId, store); + return store; + } + finally + { + _threadMemberLock.Release(); + } } - #endregion } } diff --git a/src/Discord.Net.WebSocket/ClientStateManager.cs b/src/Discord.Net.WebSocket/ClientStateManager.cs index 1416e9cf9..d506387a7 100644 --- a/src/Discord.Net.WebSocket/ClientStateManager.cs +++ b/src/Discord.Net.WebSocket/ClientStateManager.cs @@ -30,11 +30,17 @@ namespace Discord.WebSocket _groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel)) .ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count); - private readonly IStateProvider _state; + internal bool AllowSyncWaits + => _client.AllowSynchronousWaiting; - public ClientStateManager(IStateProvider state, int guildCount, int dmChannelCount) + private readonly ICacheProvider _cacheProvider; + private readonly DiscordSocketClient _client; + + + public ClientStateManager(DiscordSocketClient client, int guildCount, int dmChannelCount) { - _state = state; + _client = client; + _cacheProvider = client.CacheProvider; double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; double estimatedUsersCount = guildCount * AverageUsersPerGuild; _channels = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); @@ -43,6 +49,8 @@ namespace Discord.WebSocket _users = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); _groupChannels = new ConcurrentHashSet(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(10 * CollectionMultiplier)); _commands = new ConcurrentDictionary(); + + CreateStores(); } internal SocketChannel GetChannel(ulong id) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 355dec006..f482e3afd 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -70,16 +70,17 @@ namespace Discord.WebSocket internal int TotalShards { get; private set; } internal int MessageCacheSize { get; private set; } internal int LargeThreshold { get; private set; } + internal ICacheProvider CacheProvider { get; private set; } internal ClientStateManager StateManager { get; private set; } internal UdpSocketProvider UdpSocketProvider { get; private set; } internal WebSocketProvider WebSocketProvider { get; private set; } - internal IStateProvider StateProvider { get; private set; } internal bool AlwaysDownloadUsers { get; private set; } internal int? HandlerTimeout { get; private set; } internal bool AlwaysDownloadDefaultStickers { get; private set; } internal bool AlwaysResolveStickers { get; private set; } internal bool LogGatewayIntentWarnings { get; private set; } internal bool SuppressUnknownDispatchWarnings { get; private set; } + internal bool AllowSynchronousWaiting { get; private set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient; /// public override IReadOnlyCollection Guilds => StateManager.Guilds; @@ -155,6 +156,8 @@ namespace Discord.WebSocket LogGatewayIntentWarnings = config.LogGatewayIntentWarnings; SuppressUnknownDispatchWarnings = config.SuppressUnknownDispatchWarnings; HandlerTimeout = config.HandlerTimeout; + CacheProvider = config.CacheProvider ?? new DefaultConcurrentCacheProvider(); + AllowSynchronousWaiting = config.AllowSynchronousWaiting; Rest = new DiscordSocketRestClient(config, ApiClient); _heartbeatTimes = new ConcurrentQueue(); _gatewayIntents = config.GatewayIntents; @@ -166,7 +169,6 @@ namespace Discord.WebSocket OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); _connection.Connected += () => TimedInvokeAsync(_connectedEvent, nameof(Connected)); _connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex); - StateProvider = config.StateProvider ?? new DefaultStateProvider(_gatewayLogger, config.CacheProvider ?? new DefaultConcurrentCacheProvider(5, 50), this, config.DefaultStateBehavior); _nextAudioId = 1; _shardedClient = shardedClient; @@ -206,10 +208,14 @@ namespace Discord.WebSocket #region State public ValueTask GetUserAsync(ulong id, CacheMode cacheMode = CacheMode.AllowDownload, RequestOptions options = null) - => StateManager.GetUserAsync(id, cacheMode, options); + => StateManager.UserStore.GetAsync(id, cacheMode, options); public ValueTask GetGuildUserAsync(ulong userId, ulong guildId, CacheMode cacheMode = CacheMode.AllowDownload, RequestOptions options = null) - => StateManager.GetMemberAsync(userId, guildId, cacheMode, options); + { + if (StateManager.TryGetMemberStore(guildId, out var store)) + return store.GetAsync(userId, cacheMode, options); + return ValueTask.FromResult(null); + } #endregion @@ -409,7 +415,7 @@ namespace Discord.WebSocket /// public override SocketUser GetUser(ulong id) - => StateManager.GetUser(id); + => StateManager.UserStore.Get(id); /// public override SocketUser GetUser(string username, string discriminator) => StateManager.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); @@ -496,23 +502,18 @@ namespace Discord.WebSocket public void PurgeUserCache() => StateManager.PurgeUsers(); internal SocketGlobalUser GetOrCreateUser(ClientStateManager state, IUserModel model) { - return state.GetOrAddUser(model.Id, x => SocketGlobalUser.Create(this, state, model)); + return state.UserStore.GetOrAdd(model.Id, x => model); } internal SocketUser GetOrCreateTemporaryUser(ClientStateManager state, Discord.API.User model) { - return state.GetUser(model.Id) ?? (SocketUser)SocketUnknownUser.Create(this, state, model); + return state.UserStore.Get(model.Id) ?? (SocketUser)SocketUnknownUser.Create(this, model); } internal SocketGlobalUser GetOrCreateSelfUser(ClientStateManager state, ICurrentUserModel model) { - return state.GetOrAddUser(model.Id, x => - { - var user = SocketGlobalUser.Create(this, state, model); - user.GlobalUser.AddRef(); - return user; - }); + return state.UserStore.GetOrAdd(model.Id, x => model); } internal void RemoveUser(ulong id) - => StateManager.RemoveUser(id); + => StateManager.UserStore.Remove(id); /// public override async Task GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) @@ -689,7 +690,7 @@ namespace Discord.WebSocket if (CurrentUser == null) return; var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; - StateManager.AddOrUpdatePresence(new SocketPresence(Status, null, activities)); + await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(Status, null, activities).ToModel()).ConfigureAwait(false); var presence = BuildCurrentStatus() ?? (UserStatus.Online, false, null, null); @@ -813,6 +814,7 @@ namespace Discord.WebSocket int latency = (int)(Environment.TickCount - time); int before = Latency; Latency = latency; + StateManager?.ClearDeadReferences(); await TimedInvokeAsync(_latencyUpdatedEvent, nameof(LatencyUpdated), before, latency).ConfigureAwait(false); } @@ -859,21 +861,26 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var state = new ClientStateManager(StateProvider, data.Guilds.Length, data.PrivateChannels.Length); + + var state = new ClientStateManager(this, data.Guilds.Length, data.PrivateChannels.Length); StateManager = state; + await StateManager.InitializeAsync().ConfigureAwait(false); - var currentUser = SocketSelfUser.Create(this, state, data.User); + var currentUser = SocketSelfUser.Create(this, data.User); Rest.CreateRestSelfUser(data.User); + var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; - StateManager.AddOrUpdatePresence(new SocketPresence(Status, null, activities)); + await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(Status, null, activities).ToModel()).ConfigureAwait(false); + ApiClient.CurrentUserId = currentUser.Id; ApiClient.CurrentApplicationId = data.Application.Id; Rest.CurrentUser = RestSelfUser.Create(this, data.User); + int unavailableGuilds = 0; for (int i = 0; i < data.Guilds.Length; i++) { var model = data.Guilds[i]; - var guild = AddGuild(model, state); + var guild = await AddGuildAsync(model).ConfigureAwait(false); if (!guild.IsAvailable) unavailableGuilds++; else @@ -950,6 +957,7 @@ namespace Discord.WebSocket if (guild != null) { guild.Update(StateManager, data); + await guild.UpdateCacheAsync(data).ConfigureAwait(false); if (_unavailableGuildCount != 0) _unavailableGuildCount--; @@ -971,7 +979,7 @@ namespace Discord.WebSocket { await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_CREATE)").ConfigureAwait(false); - var guild = AddGuild(data, StateManager); + var guild = await AddGuildAsync(data).ConfigureAwait(false); if (guild != null) { await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); @@ -1290,13 +1298,13 @@ namespace Discord.WebSocket if (user != null) { var before = user.Clone(); - if (user.GlobalUser.Update(StateManager, data.User)) + if (user.GlobalUser.Value.Update(data.User)) // TODO: update cache only and have lazy like support for events. { //Global data was updated, trigger UserUpdated - await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before.GlobalUser, user).ConfigureAwait(false); + await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before.GlobalUser.Value, user).ConfigureAwait(false); } - user.Update(StateManager, data); + user.Update(data); var cacheableBefore = new Cacheable(before, user.Id, true, () => null); await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); @@ -1332,12 +1340,12 @@ namespace Discord.WebSocket return; } - user ??= StateManager.GetUser(data.User.Id); + user ??= (SocketUser)await StateManager.UserStore.GetAsync(data.User.Id, CacheMode.CacheOnly).ConfigureAwait(false); if (user != null) - user.Update(StateManager, data.User); + user.Update(data.User); else - user = StateManager.GetOrAddUser(data.User.Id, (x) => SocketGlobalUser.Create(this, StateManager, data.User)); + user = StateManager.GetOrAddUser(data.User.Id, (x) => data.User); await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user).ConfigureAwait(false); } @@ -1957,8 +1965,8 @@ namespace Discord.WebSocket } else { - var globalBefore = user.GlobalUser.Clone(); - if (user.GlobalUser.Update(StateManager, data.User)) + var globalBefore = user.GlobalUser.Value.Clone(); + if (user.GlobalUser.Value.Update(StateManager, data.User)) { //Global data was updated, trigger UserUpdated await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); @@ -1978,7 +1986,7 @@ namespace Discord.WebSocket var before = user.Presence?.Value?.Clone(); user.Update(StateManager, data.User); var after = SocketPresence.Create(data); - StateManager.AddOrUpdatePresence(after); + StateManager.AddOrUpdatePresence(data); await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, after).ConfigureAwait(false); } break; @@ -2324,7 +2332,7 @@ namespace Discord.WebSocket } SocketUser user = data.User.IsSpecified - ? StateManager.GetOrAddUser(data.User.Value.Id, (_) => SocketGlobalUser.Create(this, StateManager, data.User.Value)) + ? StateManager.GetOrAddUser(data.User.Value.Id, (_) => data.User.Value) : guild?.AddOrUpdateUser(data.Member.Value); // null if the bot scope isn't set, so the guild cannot be retrieved. SocketChannel channel = null; @@ -2579,9 +2587,9 @@ namespace Discord.WebSocket entity.Update(StateManager, thread); } - foreach(var member in data.Members.Where(x => x.Id.Value == entity.Id)) + foreach(var member in data.Members.Where(x => x.ThreadId.Value == entity.Id)) { - var guildMember = guild.GetUser(member.Id.Value); + var guildMember = guild.GetUser(member.ThreadId.Value); entity.AddOrUpdateThreadMember(member, guildMember); } @@ -2594,11 +2602,11 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); - var thread = (SocketThreadChannel)StateManager.GetChannel(data.Id.Value); + var thread = (SocketThreadChannel)StateManager.GetChannel(data.ThreadId.Value); if (thread == null) { - await UnknownChannelAsync(type, data.Id.Value); + await UnknownChannelAsync(type, data.ThreadId.Value); return; } @@ -2948,10 +2956,11 @@ namespace Discord.WebSocket await ApiClient.SendGuildSyncAsync(guildIds).ConfigureAwait(false); } - internal SocketGuild AddGuild(ExtendedGuild model, ClientStateManager state) + internal async Task AddGuildAsync(ExtendedGuild model) { - var guild = SocketGuild.Create(this, state, model); - state.AddGuild(guild); + await StateManager.InitializeGuildStoreAsync(model.Id).ConfigureAwait(false); + var guild = SocketGuild.Create(this, StateManager, model); + StateManager.AddGuild(guild); if (model.Large) _largeGuilds.Enqueue(model.Id); return guild; @@ -2977,19 +2986,12 @@ namespace Discord.WebSocket internal ISocketPrivateChannel RemovePrivateChannel(ulong id) { var channel = StateManager.RemoveChannel(id) as ISocketPrivateChannel; - if (channel != null) - { - foreach (var recipient in channel.Recipients) - recipient.GlobalUser.RemoveRef(this); - } return channel; } internal void RemoveDMChannels() { var channels = StateManager.DMChannels; StateManager.PurgeDMChannels(); - foreach (var channel in channels) - channel.Recipient.GlobalUser.RemoveRef(this); } internal void EnsureGatewayIntent(GatewayIntents intents) diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index 21d84ba3b..d4d10719b 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -29,7 +29,12 @@ namespace Discord.WebSocket /// Gets or sets the cache provider to use /// public ICacheProvider CacheProvider { get; set; } - public IStateProvider StateProvider { get; set; } + + /// + /// Gets or sets whether or not non-async cache lookups would wait for the task to complete + /// synchronously or to throw. + /// + public bool AllowSynchronousWaiting { get; set; } = false; /// /// Returns the encoding gateway should use. @@ -199,11 +204,6 @@ namespace Discord.WebSocket /// public bool SuppressUnknownDispatchWarnings { get; set; } = true; - /// - /// Gets or sets the default state behavior clients will use. - /// - public StateBehavior DefaultStateBehavior { get; set; } = StateBehavior.Default; - /// /// Initializes a new instance of the class with the default configuration. /// diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 1a16e8a25..5330f0826 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -452,17 +452,8 @@ namespace Discord.WebSocket } _events = events; - for (int i = 0; i < model.Members.Length; i++) - { - Discord.StateManager.AddOrUpdateMember(Id, SocketGuildUser.Create(Id, Discord, model.Members[i])); - } DownloadedMemberCount = model.Members.Length; - for (int i = 0; i < model.Presences.Length; i++) - { - Discord.StateManager.AddOrUpdatePresence(SocketPresence.Create(model.Presences[i])); - } - MemberCount = model.MemberCount; @@ -553,29 +544,12 @@ namespace Discord.WebSocket else _stickers = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, 7); } - /*internal void Update(ClientStateManager state, GuildSyncModel model) //TODO remove? userbot related - { - var members = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); - { - for (int i = 0; i < model.Members.Length; i++) - { - var member = SocketGuildUser.Create(this, state, model.Members[i]); - members.TryAdd(member.Id, member); - } - DownloadedMemberCount = members.Count; - - for (int i = 0; i < model.Presences.Length; i++) - { - if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) - member.Update(state, model.Presences[i], true); - } - } - _members = members; - var _ = _syncPromise.TrySetResultAsync(true); - //if (!model.Large) - // _ = _downloaderPromise.TrySetResultAsync(true); - }*/ + internal async ValueTask UpdateCacheAsync(ExtendedModel model) + { + await Discord.StateManager.BulkAddOrUpdatePresenceAsync(model.Presences).ConfigureAwait(false); + await Discord.StateManager.BulkAddOrUpdateMembersAsync(Id, model.Members).ConfigureAwait(false); + } internal void Update(ClientStateManager state, EmojiUpdateModel model) { diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index 41eadcc4c..80dce6170 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -12,45 +12,27 @@ namespace Discord.WebSocket public override string Username { get; internal set; } public override ushort DiscriminatorValue { get; internal set; } public override string AvatarId { get; internal set; } - public override bool IsWebhook => false; - internal override SocketGlobalUser GlobalUser { get => this; set => throw new NotImplementedException(); } - - private readonly object _lockObj = new object(); - private ushort _references; private SocketGlobalUser(DiscordSocketClient discord, ulong id) : base(discord, id) { } - internal static SocketGlobalUser Create(DiscordSocketClient discord, ClientStateManager state, Model model) + internal static SocketGlobalUser Create(DiscordSocketClient discord, Model model) { var entity = new SocketGlobalUser(discord, model.Id); - entity.Update(state, model); + entity.Update(model); return entity; } - internal void AddRef() - { - checked - { - lock (_lockObj) - _references++; - } - } - internal void RemoveRef(DiscordSocketClient discord) + ~SocketGlobalUser() => Discord.StateManager.RemoveReferencedGlobalUser(Id); + public override void Dispose() { - lock (_lockObj) - { - if (--_references <= 0) - discord.RemoveUser(Id); - } + GC.SuppressFinalize(this); + Discord.StateManager.RemoveReferencedGlobalUser(Id); } - ~SocketGlobalUser() => Discord.StateManager.RemoveReferencedGlobalUser(Id); - public void Dispose() => Discord.StateManager.RemoveReferencedGlobalUser(Id); - private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs index 9d5fb0ef8..ed13f6314 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs @@ -18,38 +18,33 @@ namespace Discord.WebSocket /// A representing the channel of which the user belongs to. /// public SocketGroupChannel Channel { get; } - /// - internal override SocketGlobalUser GlobalUser { get; set; } - - /// - public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } - /// - public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } - /// - public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } - /// - public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } - /// - internal override Lazy Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } /// public override bool IsWebhook => false; - internal SocketGroupUser(SocketGroupChannel channel, SocketGlobalUser globalUser) - : base(channel.Discord, globalUser.Id) + internal SocketGroupUser(SocketGroupChannel channel, ulong userId) + : base(channel.Discord, userId) { Channel = channel; - GlobalUser = globalUser; } - internal static SocketGroupUser Create(SocketGroupChannel channel, ClientStateManager state, Model model) + internal static SocketGroupUser Create(SocketGroupChannel channel, Model model) { - var entity = new SocketGroupUser(channel, channel.Discord.GetOrCreateUser(state, model)); - entity.Update(state, model); + var entity = new SocketGroupUser(channel, model.Id); + entity.Update(model); return entity; } private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Group)"; internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser; + public override void Dispose() + { + GC.SuppressFinalize(this); + + if (GlobalUser.IsValueCreated) + GlobalUser.Value.Dispose(); + } + ~SocketGroupUser() => Dispose(); + #endregion #region IVoiceState diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 293013938..00593d8ba 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -25,7 +25,6 @@ namespace Discord.WebSocket private ImmutableArray _roleIds; private ulong _guildId; - internal override SocketGlobalUser GlobalUser { get; set; } /// /// Gets the guild the user is in. /// @@ -43,13 +42,13 @@ namespace Discord.WebSocket /// public string GuildAvatarId { get; private set; } /// - public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } + public override bool IsBot { get { return GlobalUser.Value.IsBot; } internal set { GlobalUser.Value.IsBot = value; } } /// - public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } + public override string Username { get { return GlobalUser.Value.Username; } internal set { GlobalUser.Value.Username = value; } } /// - public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } + public override ushort DiscriminatorValue { get { return GlobalUser.Value.DiscriminatorValue; } internal set { GlobalUser.Value.DiscriminatorValue = value; } } /// - public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } + public override string AvatarId { get { return GlobalUser.Value.AvatarId; } internal set { GlobalUser.Value.AvatarId = value; } } /// public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild.Value, this)); @@ -137,32 +136,29 @@ namespace Discord.WebSocket } } - internal SocketGuildUser(ulong guildId, SocketGlobalUser globalUser, DiscordSocketClient client) - : base(client, globalUser.Id) + internal SocketGuildUser(ulong guildId, ulong userId, DiscordSocketClient client) + : base(client, userId) { _guildId = guildId; Guild = new Lazy(() => client.StateManager.GetGuild(_guildId), System.Threading.LazyThreadSafetyMode.PublicationOnly); - GlobalUser = globalUser; } internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, UserModel model) { - var entity = new SocketGuildUser(guildId, client.GetOrCreateUser(client.StateManager, (Discord.API.User)model), client); - if (entity.Update(client.StateManager, model)) - client.StateManager.AddOrUpdateMember(guildId, entity); + var entity = new SocketGuildUser(guildId, model.Id, client); + if (entity.Update(model)) + client.StateManager.AddOrUpdateMember(guildId, entity.ToModel()); entity.UpdateRoles(Array.Empty()); return entity; } internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, MemberModel model) { - var entity = new SocketGuildUser(guildId, client.GetOrCreateUser(client.StateManager, model.User), client); - entity.Update(client.StateManager, model); - client.StateManager.AddOrUpdateMember(guildId, entity); + var entity = new SocketGuildUser(guildId, model.Id, client); + entity.Update(model); + client.StateManager.AddOrUpdateMember(guildId, model); return entity; } - internal void Update(ClientStateManager state, MemberModel model) + internal void Update(MemberModel model) { - base.Update(state, model.User); - _joinedAtTicks = model.JoinedAt.UtcTicks; Nickname = model.Nickname; GuildAvatarId = model.GuildAvatar; @@ -234,12 +230,7 @@ namespace Discord.WebSocket private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)"; - internal new SocketGuildUser Clone() - { - var clone = MemberwiseClone() as SocketGuildUser; - clone.GlobalUser = GlobalUser.Clone(); - return clone; - } + internal new SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; #endregion @@ -260,8 +251,7 @@ namespace Discord.WebSocket private struct CacheModel : MemberModel { - public UserModel User { get; set; } - + public ulong Id { get; set; } public string Nickname { get; set; } public string GuildAvatar { get; set; } @@ -280,15 +270,14 @@ namespace Discord.WebSocket public DateTimeOffset? CommunicationsDisabledUntil { get; set; } } + internal new MemberModel ToModel() + => ToModel(); - MemberModel ICached.ToModel() - => ToMemberModel(); - - internal MemberModel ToMemberModel() + internal new TModel ToModel() where TModel : MemberModel, new() { - return new CacheModel + return new TModel { - User = ((ICached)this).ToModel(), + Id = Id, CommunicationsDisabledUntil = TimedOutUntil, GuildAvatar = GuildAvatarId, IsDeaf = IsDeafened, @@ -301,7 +290,19 @@ namespace Discord.WebSocket }; } - public void Dispose() => Discord.StateManager.RemovedReferencedMember(Id, _guildId); + MemberModel ICached.ToModel() + => ToModel(); + + TResult ICached.ToModel() + => ToModel(); + + void ICached.Update(MemberModel model) => Update(model); + + public override void Dispose() + { + GC.SuppressFinalize(this); + Discord.StateManager.RemovedReferencedMember(Id, _guildId); + } ~SocketGuildUser() => Discord.StateManager.RemovedReferencedMember(Id, _guildId); #endregion diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index 9e64bc2bb..8e2464a22 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -114,6 +114,12 @@ namespace Discord.WebSocket public ulong UserId { get; set; } public ulong? GuildId { get; set; } + + ulong IEntityModel.Id + { + get => UserId; + set => throw new NotSupportedException(); + } } private struct ActivityCacheModel : IActivityModel @@ -156,8 +162,11 @@ namespace Discord.WebSocket } internal Model ToModel() + => ToModel(); + + internal TModel ToModel() where TModel : Model, new() { - return new CacheModel + return new TModel { Status = Status, ActiveClients = ActiveClients.ToArray(), @@ -194,6 +203,8 @@ namespace Discord.WebSocket } Model ICached.ToModel() => ToModel(); + TResult ICached.ToModel() => ToModel(); + void ICached.Update(Model model) => Update(model); #endregion } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs index 45b3ebc4f..cc6bcd912 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -19,18 +19,17 @@ namespace Discord.WebSocket public bool IsVerified { get; private set; } /// public bool IsMfaEnabled { get; private set; } - internal override SocketGlobalUser GlobalUser { get; set; } /// - public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } + public override bool IsBot { get { return GlobalUser.Value.IsBot; } internal set { GlobalUser.Value.IsBot = value; } } /// - public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } + public override string Username { get { return GlobalUser.Value.Username; } internal set { GlobalUser.Value.Username = value; } } /// - public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } + public override ushort DiscriminatorValue { get { return GlobalUser.Value.DiscriminatorValue; } internal set { GlobalUser.Value.DiscriminatorValue = value; } } /// - public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } + public override string AvatarId { get { return GlobalUser.Value.AvatarId; } internal set { GlobalUser.Value.AvatarId = value; } } /// - internal override Lazy Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + internal override Lazy Presence { get { return GlobalUser.Value.Presence; } set { GlobalUser.Value.Presence = value; } } /// public UserProperties Flags { get; internal set; } /// @@ -41,20 +40,20 @@ namespace Discord.WebSocket /// public override bool IsWebhook => false; - internal SocketSelfUser(DiscordSocketClient discord, SocketGlobalUser globalUser) - : base(discord, globalUser.Id) + internal SocketSelfUser(DiscordSocketClient discord, ulong userId) + : base(discord, userId) { - GlobalUser = globalUser; + } - internal static SocketSelfUser Create(DiscordSocketClient discord, ClientStateManager state, Model model) + internal static SocketSelfUser Create(DiscordSocketClient discord, Model model) { - var entity = new SocketSelfUser(discord, discord.GetOrCreateSelfUser(state, model)); - entity.Update(state, model); + var entity = new SocketSelfUser(discord, model.Id); + entity.Update(model); return entity; } - internal override bool Update(ClientStateManager state, UserModel model) + internal override bool Update(UserModel model) { - bool hasGlobalChanges = base.Update(state, model); + bool hasGlobalChanges = base.Update(model); if (model is not Model currentUserModel) throw new ArgumentException($"Got unexpected model type \"{model?.GetType()}\""); @@ -98,9 +97,13 @@ namespace Discord.WebSocket private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)"; internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; + public override void Dispose() + { + GC.SuppressFinalize(this); + Discord.StateManager.RemoveReferencedGlobalUser(Id); + } #region Cache - private struct CacheModel : Model { public bool? IsVerified { get; set; } @@ -128,9 +131,12 @@ namespace Discord.WebSocket public ulong Id { get; set; } } - Model ICached.ToModel() + internal new Model ToModel() + => ToModel(); + + internal new TModel ToModel() where TModel : Model, new() { - return new CacheModel + return new TModel { Avatar = AvatarId, Discriminator = Discriminator, @@ -147,6 +153,9 @@ namespace Discord.WebSocket }; } + Model ICached.ToModel() => ToModel(); + TResult ICached.ToModel() => ToModel(); + void ICached.Update(Model model) => Update(model); #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs index e42805e4e..a00a78a4a 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Model = Discord.API.ThreadMember; +using Model = Discord.IThreadMemberModel; using System.Collections.Immutable; namespace Discord.WebSocket @@ -10,12 +10,12 @@ namespace Discord.WebSocket /// /// Represents a thread user received over the gateway. /// - public class SocketThreadUser : SocketUser, IThreadUser, IGuildUser + public class SocketThreadUser : SocketUser, IThreadUser, IGuildUser, ICached { /// /// Gets the this user is in. /// - public SocketThreadChannel Thread { get; private set; } + public Lazy Thread { get; private set; } /// public DateTimeOffset ThreadJoinedAt { get; private set; } @@ -23,126 +23,142 @@ namespace Discord.WebSocket /// /// Gets the guild this user is in. /// - public SocketGuild Guild { get; private set; } + public Lazy Guild { get; private set; } /// public DateTimeOffset? JoinedAt - => GuildUser.JoinedAt; + => GuildUser.Value.JoinedAt; /// public string DisplayName - => GuildUser.Nickname ?? GuildUser.Username; + => GuildUser.Value.Nickname ?? GuildUser.Value.Username; /// public string Nickname - => GuildUser.Nickname; + => GuildUser.Value.Nickname; /// public DateTimeOffset? PremiumSince - => GuildUser.PremiumSince; + => GuildUser.Value.PremiumSince; /// public DateTimeOffset? TimedOutUntil - => GuildUser.TimedOutUntil; + => GuildUser.Value.TimedOutUntil; /// public bool? IsPending - => GuildUser.IsPending; + => GuildUser.Value.IsPending; + /// public int Hierarchy - => GuildUser.Hierarchy; + => GuildUser.Value.Hierarchy; /// public override string AvatarId { - get => GuildUser.AvatarId; - internal set => GuildUser.AvatarId = value; + get => GuildUser.Value.AvatarId; + internal set => GuildUser.Value.AvatarId = value; } + /// public string DisplayAvatarId => GuildAvatarId ?? AvatarId; /// public string GuildAvatarId - => GuildUser.GuildAvatarId; + => GuildUser.Value.GuildAvatarId; /// public override ushort DiscriminatorValue { - get => GuildUser.DiscriminatorValue; - internal set => GuildUser.DiscriminatorValue = value; + get => GuildUser.Value.DiscriminatorValue; + internal set => GuildUser.Value.DiscriminatorValue = value; } /// public override bool IsBot { - get => GuildUser.IsBot; - internal set => GuildUser.IsBot = value; + get => GuildUser.Value.IsBot; + internal set => GuildUser.Value.IsBot = value; } /// public override bool IsWebhook - => GuildUser.IsWebhook; + => GuildUser.Value.IsWebhook; /// public override string Username { - get => GuildUser.Username; - internal set => GuildUser.Username = value; + get => GuildUser.Value.Username; + internal set => GuildUser.Value.Username = value; } /// public bool IsDeafened - => GuildUser.IsDeafened; + => GuildUser.Value.IsDeafened; /// public bool IsMuted - => GuildUser.IsMuted; + => GuildUser.Value.IsMuted; /// public bool IsSelfDeafened - => GuildUser.IsSelfDeafened; + => GuildUser.Value.IsSelfDeafened; /// public bool IsSelfMuted - => GuildUser.IsSelfMuted; + => GuildUser.Value.IsSelfMuted; /// public bool IsSuppressed - => GuildUser.IsSuppressed; + => GuildUser.Value.IsSuppressed; /// public IVoiceChannel VoiceChannel - => GuildUser.VoiceChannel; + => GuildUser.Value.VoiceChannel; /// public string VoiceSessionId - => GuildUser.VoiceSessionId; + => GuildUser.Value.VoiceSessionId; /// public bool IsStreaming - => GuildUser.IsStreaming; + => GuildUser.Value.IsStreaming; /// public bool IsVideoing - => GuildUser.IsVideoing; + => GuildUser.Value.IsVideoing; /// public DateTimeOffset? RequestToSpeakTimestamp - => GuildUser.RequestToSpeakTimestamp; + => GuildUser.Value.RequestToSpeakTimestamp; + + private Lazy GuildUser { get; set; } - private SocketGuildUser GuildUser { get; set; } + private ulong _threadId; + private ulong _guildId; - internal SocketThreadUser(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser member, ulong userId) - : base(guild.Discord, userId) + + internal SocketThreadUser(DiscordSocketClient client, ulong guildId, ulong threadId, ulong userId) + : base(client, userId) { - Thread = thread; - Guild = guild; - GuildUser = member; + _guildId = guildId; + _threadId = threadId; + + GuildUser = new(() => client.StateManager.TryGetMemberStore(guildId, out var store) ? store.Get(userId) : null); + Thread = new(() => client.GetChannel(threadId) as SocketThreadChannel); + Guild = new(() => client.GetGuild(guildId)); } internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, Model model, SocketGuildUser member) { - var entity = new SocketThreadUser(guild, thread, member, model.UserId.Value); + var entity = new SocketThreadUser(guild.Discord, guild.Id, thread.Id, model.UserId.Value); + entity.Update(model); + return entity; + } + + internal static SocketThreadUser Create(DiscordSocketClient client, ulong guildId, ulong threadId, Model model) + { + var entity = new SocketThreadUser(client, guildId, threadId, model.UserId.Value); entity.Update(model); return entity; } @@ -150,89 +166,117 @@ namespace Discord.WebSocket internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser owner) { // this is used for creating the owner of the thread. - var entity = new SocketThreadUser(guild, thread, owner, owner.Id); - entity.Update(new Model - { - JoinTimestamp = thread.CreatedAt, - }); + var entity = new SocketThreadUser(guild.Discord, guild.Id, thread.Id, owner.Id); + entity.ThreadJoinedAt = thread.CreatedAt; return entity; } internal void Update(Model model) { - ThreadJoinedAt = model.JoinTimestamp; + ThreadJoinedAt = model.JoinedAt; } /// - public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.GetPermissions(channel); + public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.Value.GetPermissions(channel); /// - public Task KickAsync(string reason = null, RequestOptions options = null) => GuildUser.KickAsync(reason, options); + public Task KickAsync(string reason = null, RequestOptions options = null) => GuildUser.Value.KickAsync(reason, options); /// - public Task ModifyAsync(Action func, RequestOptions options = null) => GuildUser.ModifyAsync(func, options); + public Task ModifyAsync(Action func, RequestOptions options = null) => GuildUser.Value.ModifyAsync(func, options); /// - public Task AddRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.AddRoleAsync(roleId, options); + public Task AddRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.Value.AddRoleAsync(roleId, options); /// - public Task AddRoleAsync(IRole role, RequestOptions options = null) => GuildUser.AddRoleAsync(role, options); + public Task AddRoleAsync(IRole role, RequestOptions options = null) => GuildUser.Value.AddRoleAsync(role, options); /// - public Task AddRolesAsync(IEnumerable roleIds, RequestOptions options = null) => GuildUser.AddRolesAsync(roleIds, options); + public Task AddRolesAsync(IEnumerable roleIds, RequestOptions options = null) => GuildUser.Value.AddRolesAsync(roleIds, options); /// - public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) => GuildUser.AddRolesAsync(roles, options); + public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) => GuildUser.Value.AddRolesAsync(roles, options); /// - public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.RemoveRoleAsync(roleId, options); + public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.Value.RemoveRoleAsync(roleId, options); /// - public Task RemoveRoleAsync(IRole role, RequestOptions options = null) => GuildUser.RemoveRoleAsync(role, options); + public Task RemoveRoleAsync(IRole role, RequestOptions options = null) => GuildUser.Value.RemoveRoleAsync(role, options); /// - public Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roleIds, options); + public Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions options = null) => GuildUser.Value.RemoveRolesAsync(roleIds, options); /// - public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roles, options); + public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) => GuildUser.Value.RemoveRolesAsync(roles, options); /// - public Task SetTimeOutAsync(TimeSpan span, RequestOptions options = null) => GuildUser.SetTimeOutAsync(span, options); + public Task SetTimeOutAsync(TimeSpan span, RequestOptions options = null) => GuildUser.Value.SetTimeOutAsync(span, options); /// - public Task RemoveTimeOutAsync(RequestOptions options = null) => GuildUser.RemoveTimeOutAsync(options); + public Task RemoveTimeOutAsync(RequestOptions options = null) => GuildUser.Value.RemoveTimeOutAsync(options); /// - IThreadChannel IThreadUser.Thread => Thread; + IThreadChannel IThreadUser.Thread => Thread.Value; /// - IGuild IThreadUser.Guild => Guild; + IGuild IThreadUser.Guild => Guild.Value; /// - IGuild IGuildUser.Guild => Guild; + IGuild IGuildUser.Guild => Guild.Value; /// - ulong IGuildUser.GuildId => Guild.Id; + ulong IGuildUser.GuildId => Guild.Value.Id; /// - GuildPermissions IGuildUser.GuildPermissions => GuildUser.GuildPermissions; + GuildPermissions IGuildUser.GuildPermissions => GuildUser.Value.GuildPermissions; /// - IReadOnlyCollection IGuildUser.RoleIds => GuildUser.Roles.Select(x => x.Id).ToImmutableArray(); + IReadOnlyCollection IGuildUser.RoleIds => GuildUser.Value.Roles.Select(x => x.Id).ToImmutableArray(); /// - string IGuildUser.GetDisplayAvatarUrl(ImageFormat format, ushort size) => GuildUser.GetDisplayAvatarUrl(format, size); + string IGuildUser.GetDisplayAvatarUrl(ImageFormat format, ushort size) => GuildUser.Value.GetDisplayAvatarUrl(format, size); /// - string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.GetGuildAvatarUrl(format, size); + string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.Value.GetGuildAvatarUrl(format, size); + + internal override Lazy Presence { get => GuildUser.Value.Presence; set => GuildUser.Value.Presence = value; } - internal override SocketGlobalUser GlobalUser { get => GuildUser.GlobalUser; set => GuildUser.GlobalUser = value; } + public override void Dispose() + { + GC.SuppressFinalize(this); + } - internal override Lazy Presence { get => GuildUser.Presence; set => GuildUser.Presence = value; } /// /// Gets the guild user of this thread user. /// /// - public static explicit operator SocketGuildUser(SocketThreadUser user) => user.GuildUser; + public static explicit operator SocketGuildUser(SocketThreadUser user) => user.GuildUser.Value; + + #region Cache + private class CacheModel : Model + { + public ulong? ThreadId { get; set; } + public ulong? UserId { get; set; } + public DateTimeOffset JoinedAt { get; set; } + + ulong IEntityModel.Id { get => UserId.GetValueOrDefault(); set => throw new NotSupportedException(); } + } + + internal new Model ToModel() => ToModel(); + + internal new TModel ToModel() where TModel : Model, new() + { + return new TModel + { + JoinedAt = ThreadJoinedAt, + ThreadId = _threadId, + UserId = Id + }; + } + + Model ICached.ToModel() => ToModel(); + TResult ICached.ToModel() => ToModel(); + void ICached.Update(Model model) => Update(model); + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs index 5d2ddef32..151f00b72 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs @@ -27,21 +27,21 @@ namespace Discord.WebSocket public override bool IsWebhook => false; /// internal override Lazy Presence { get { return new Lazy(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } } - /// - /// This field is not supported for an unknown user. - internal override SocketGlobalUser GlobalUser { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + internal override Lazy GlobalUser { get => new Lazy(() => null); set { } } internal SocketUnknownUser(DiscordSocketClient discord, ulong id) : base(discord, id) { } - internal static SocketUnknownUser Create(DiscordSocketClient discord, ClientStateManager state, Model model) + internal static SocketUnknownUser Create(DiscordSocketClient discord, Model model) { var entity = new SocketUnknownUser(discord, model.Id); - entity.Update(state, model); + entity.Update(model); return entity; } + public override void Dispose() { } + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Unknown)"; internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index d61fe8ea7..0495c5118 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -18,18 +18,18 @@ namespace Discord.WebSocket public abstract class SocketUser : SocketEntity, IUser, ICached, IDisposable { /// - public abstract bool IsBot { get; internal set; } + public virtual bool IsBot { get; internal set; } /// - public abstract string Username { get; internal set; } + public virtual string Username { get; internal set; } /// - public abstract ushort DiscriminatorValue { get; internal set; } + public virtual ushort DiscriminatorValue { get; internal set; } /// - public abstract string AvatarId { get; internal set; } + public virtual string AvatarId { get; internal set; } /// - public abstract bool IsWebhook { get; } + public virtual bool IsWebhook { get; } /// public UserProperties? PublicFlags { get; private set; } - internal abstract SocketGlobalUser GlobalUser { get; set; } + internal virtual Lazy GlobalUser { get; set; } internal virtual Lazy Presence { get; set; } /// @@ -57,9 +57,10 @@ namespace Discord.WebSocket : base(discord, id) { } - internal virtual bool Update(ClientStateManager state, Model model) + internal virtual bool Update(Model model) { - Presence ??= new Lazy(() => state.GetPresence(Id), System.Threading.LazyThreadSafetyMode.PublicationOnly); + Presence ??= new Lazy(() => Discord.StateManager.GetPresence(Id), System.Threading.LazyThreadSafetyMode.PublicationOnly); + GlobalUser ??= new Lazy(() => Discord.StateManager.GetUser(Id), System.Threading.LazyThreadSafetyMode.PublicationOnly); bool hasChanges = false; if (model.Avatar != AvatarId) { @@ -98,6 +99,8 @@ namespace Discord.WebSocket return hasChanges; } + public abstract void Dispose(); + /// public async Task CreateDMChannelAsync(RequestOptions options = null) => await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false); @@ -117,8 +120,6 @@ namespace Discord.WebSocket /// The full name of the user. /// public override string ToString() => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode); - ~SocketUser() => GlobalUser?.Dispose(); - public void Dispose() => GlobalUser?.Dispose(); private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})"; internal SocketUser Clone() => MemberwiseClone() as SocketUser; @@ -136,12 +137,9 @@ namespace Discord.WebSocket public ulong Id { get; set; } } - Model ICached.ToModel() - => ToModel(); - - internal Model ToModel() + internal TModel ToModel() where TModel : Model, new() { - return new CacheModel + return new TModel { Avatar = AvatarId, Discriminator = Discriminator, @@ -151,6 +149,17 @@ namespace Discord.WebSocket }; } + internal Model ToModel() + => ToModel(); + + Model ICached.ToModel() + => ToModel(); + + TResult ICached.ToModel() + => ToModel(); + + void ICached.Update(Model model) => Update(model); + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index 06f9a8ab5..bd3c9fd5e 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -34,7 +34,7 @@ namespace Discord.WebSocket public override bool IsWebhook => true; /// internal override Lazy Presence { get { return new Lazy(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } } - internal override SocketGlobalUser GlobalUser { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + internal override Lazy GlobalUser { get => new Lazy(() => null); set { } } internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) : base(guild.Discord, id) @@ -42,16 +42,17 @@ namespace Discord.WebSocket Guild = guild; WebhookId = webhookId; } - internal static SocketWebhookUser Create(SocketGuild guild, ClientStateManager state, Model model, ulong webhookId) + internal static SocketWebhookUser Create(SocketGuild guild, Model model, ulong webhookId) { var entity = new SocketWebhookUser(guild, model.Id, webhookId); - entity.Update(state, model); + entity.Update(model); return entity; } private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Webhook)"; internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser; -#endregion + public override void Dispose() { } + #endregion #region IGuildUser /// diff --git a/src/Discord.Net.WebSocket/Extensions/StateExtensions.cs b/src/Discord.Net.WebSocket/Extensions/StateExtensions.cs deleted file mode 100644 index 7719b26c1..000000000 --- a/src/Discord.Net.WebSocket/Extensions/StateExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Discord.WebSocket -{ - internal static class StateExtensions - { - public static StateBehavior ToBehavior(this CacheMode mode) - { - return mode switch - { - CacheMode.AllowDownload => StateBehavior.AllowDownload, - CacheMode.CacheOnly => StateBehavior.CacheOnly, - _ => StateBehavior.AllowDownload - }; - } - } -} diff --git a/src/Discord.Net.WebSocket/State/DefaultStateProvider.cs b/src/Discord.Net.WebSocket/State/DefaultStateProvider.cs deleted file mode 100644 index a95227d34..000000000 --- a/src/Discord.Net.WebSocket/State/DefaultStateProvider.cs +++ /dev/null @@ -1,252 +0,0 @@ -using Discord.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.WebSocket -{ - internal class DefaultStateProvider : IStateProvider - { - private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149 - private const double AverageUsersPerGuild = 47.78; //Source: Googie2149 - private const double CollectionMultiplier = 1.05; //Add 5% buffer to handle growth - - private readonly ICacheProvider _cache; - private readonly StateBehavior _defaultBehavior; - private readonly DiscordSocketClient _client; - private readonly Logger _logger; - public DefaultStateProvider(Logger logger, ICacheProvider cacheProvider, DiscordSocketClient client, StateBehavior stateBehavior) - { - _cache = cacheProvider; - _client = client; - _logger = logger; - - if (stateBehavior == StateBehavior.Default) - throw new ArgumentException("Cannot use \"default\" as the default state behavior"); - - _defaultBehavior = stateBehavior; - } - - private void RunAsyncWithLogs(ValueTask task) - { - _ = Task.Run(async () => - { - try - { - await task.ConfigureAwait(false); - } - catch (Exception x) - { - await _logger.ErrorAsync("Cache provider failed", x).ConfigureAwait(false); - } - }); - } - - private TType ValidateAsSocketEntity(ISnowflakeEntity entity) where TType : SocketEntity - { - if(entity is not TType val) - throw new NotSupportedException("Cannot cache non-socket entities"); - return val; - } - - private StateBehavior ResolveBehavior(StateBehavior behavior) - => behavior == StateBehavior.Default ? _defaultBehavior : behavior; - - public ValueTask AddOrUpdateMemberAsync(ulong guildId, IGuildUser user) - { - var socketGuildUser = ValidateAsSocketEntity(user); - var model = socketGuildUser.ToMemberModel(); - RunAsyncWithLogs(_cache.AddOrUpdateMemberAsync(model, guildId, CacheRunMode.Async)); - return default; - } - public ValueTask AddOrUpdateUserAsync(IUser user) - { - var socketUser = ValidateAsSocketEntity(user); - var model = socketUser.ToModel(); - RunAsyncWithLogs(_cache.AddOrUpdateUserAsync(model, CacheRunMode.Async)); - return default; - } - public ValueTask GetMemberAsync(ulong guildId, ulong id, StateBehavior stateBehavior, RequestOptions options = null) - { - var behavior = ResolveBehavior(stateBehavior); - - var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; - - if(behavior != StateBehavior.DownloadOnly) - { - var memberLookupTask = _cache.GetMemberAsync(id, guildId, cacheMode); - - if (memberLookupTask.IsCompleted) - { - var model = memberLookupTask.Result; - if(model != null) - return new ValueTask(SocketGuildUser.Create(guildId, _client, model)); - } - else - { - return new ValueTask(Task.Run(async () => // review: task.run here? - { - var result = await memberLookupTask; - - if (result != null) - return (IGuildUser)SocketGuildUser.Create(guildId, _client, result); - else if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) - return await _client.Rest.GetGuildUserAsync(guildId, id, options).ConfigureAwait(false); - return null; - })); - } - } - - if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) - return new ValueTask(_client.Rest.GetGuildUserAsync(guildId, id, options).ContinueWith(x => (IGuildUser)x.Result)); - - return default; - } - - public ValueTask> GetMembersAsync(ulong guildId, StateBehavior stateBehavior, RequestOptions options = null) - { - var behavior = ResolveBehavior(stateBehavior); - - var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; - - if(behavior != StateBehavior.DownloadOnly) - { - var memberLookupTask = _cache.GetMembersAsync(guildId, cacheMode); - - if (memberLookupTask.IsCompleted) - return new ValueTask>(memberLookupTask.Result?.Select(x => SocketGuildUser.Create(guildId, _client, x))); - else - { - return new ValueTask>(Task.Run(async () => - { - var result = await memberLookupTask; - - if (result != null && result.Any()) - return result.Select(x => (IGuildUser)SocketGuildUser.Create(guildId, _client, x)); - - if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) - return await _client.Rest.GetGuildUsersAsync(guildId, options); - - return null; - })); - } - } - - if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) - return new ValueTask>(_client.Rest.GetGuildUsersAsync(guildId, options).ContinueWith(x => x.Result.Cast())); - - return default; - } - - public ValueTask GetUserAsync(ulong id, StateBehavior stateBehavior, RequestOptions options = null) - { - var behavior = ResolveBehavior(stateBehavior); - - var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; - - if (behavior != StateBehavior.DownloadOnly) - { - var userLookupTask = _cache.GetUserAsync(id, cacheMode); - - if (userLookupTask.IsCompleted) - { - var model = userLookupTask.Result; - if(model != null) - return new ValueTask(SocketGlobalUser.Create(_client, null, model)); - } - else - { - return new ValueTask(Task.Run(async () => - { - var result = await userLookupTask; - - if (result != null) - return SocketGlobalUser.Create(_client, null, result); - - if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) - return await _client.Rest.GetUserAsync(id, options); - - return null; - })); - } - } - - if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) - return new ValueTask(_client.Rest.GetUserAsync(id, options).ContinueWith(x => (IUser)x.Result)); - - return default; - } - - public ValueTask> GetUsersAsync(StateBehavior stateBehavior, RequestOptions options = null) - { - var behavior = ResolveBehavior(stateBehavior); - - var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; - - if(behavior != StateBehavior.DownloadOnly) - { - var usersTask = _cache.GetUsersAsync(cacheMode); - - if (usersTask.IsCompleted) - return new ValueTask>(usersTask.Result.Select(x => (IUser)SocketGlobalUser.Create(_client, null, x))); - else - { - return new ValueTask>(usersTask.AsTask().ContinueWith(x => x.Result.Select(x => (IUser)SocketGlobalUser.Create(_client, null, x)))); - } - } - - // no download path - return default; - } - - public ValueTask RemoveMemberAsync(ulong id, ulong guildId) - => _cache.RemoveMemberAsync(id, guildId, CacheRunMode.Async); - public ValueTask RemoveUserAsync(ulong id) - => _cache.RemoveUserAsync(id, CacheRunMode.Async); - - public ValueTask GetPresenceAsync(ulong userId, StateBehavior stateBehavior) - { - var behavior = ResolveBehavior(stateBehavior); - - var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; - - if(stateBehavior != StateBehavior.DownloadOnly) - { - var fetchTask = _cache.GetPresenceAsync(userId, cacheMode); - - if (fetchTask.IsCompleted) - return new ValueTask(SocketPresence.Create(fetchTask.Result)); - else - { - return new ValueTask(Task.Run(async () => - { - var result = await fetchTask; - - if(result != null) - return (IPresence)SocketPresence.Create(result); - return null; - })); - } - } - - // no download path - return new ValueTask((IPresence)null); - } - - public ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresence presense, StateBehavior stateBehavior) - { - if (presense is not SocketPresence socketPresense) - throw new ArgumentException($"Expected socket entity but got {presense?.GetType()}"); - - var model = socketPresense.ToModel(); - - RunAsyncWithLogs(_cache.AddOrUpdatePresenseAsync(userId, model, CacheRunMode.Async)); - return default; - } - public ValueTask RemovePresenseAsync(ulong userId) - => _cache.RemovePresenseAsync(userId, CacheRunMode.Async); - } -} diff --git a/src/Discord.Net.WebSocket/State/IStateProvider.cs b/src/Discord.Net.WebSocket/State/IStateProvider.cs deleted file mode 100644 index c944d9f19..000000000 --- a/src/Discord.Net.WebSocket/State/IStateProvider.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Discord.WebSocket -{ - public interface IStateProvider - { - ValueTask GetPresenceAsync(ulong userId, StateBehavior stateBehavior); - ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresence presense, StateBehavior stateBehavior); - ValueTask RemovePresenseAsync(ulong userId); - - ValueTask GetUserAsync(ulong id, StateBehavior stateBehavior, RequestOptions options = null); - ValueTask> GetUsersAsync(StateBehavior stateBehavior, RequestOptions options = null); - ValueTask AddOrUpdateUserAsync(IUser user); - ValueTask RemoveUserAsync(ulong id); - - ValueTask GetMemberAsync(ulong guildId, ulong id, StateBehavior stateBehavior, RequestOptions options = null); - ValueTask> GetMembersAsync(ulong guildId, StateBehavior stateBehavior, RequestOptions options = null); - ValueTask AddOrUpdateMemberAsync(ulong guildId, IGuildUser user); - ValueTask RemoveMemberAsync(ulong guildId, ulong id); - } -} diff --git a/src/Discord.Net.WebSocket/State/StateBehavior.cs b/src/Discord.Net.WebSocket/State/StateBehavior.cs deleted file mode 100644 index 4a387d5a9..000000000 --- a/src/Discord.Net.WebSocket/State/StateBehavior.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Discord.WebSocket -{ - public enum StateBehavior - { - /// - /// Use the default Cache Behavior of the client. - /// - /// - Default = 0, - /// - /// The entity will only be retrieved via a synchronous cache lookup. - /// - /// For the default , this is equivalent to using - /// - /// - /// This flag is used to indicate that the retrieval of this entity should not leave the - /// synchronous path of the . When true, - /// the calling method *should* not ever leave the calling task, and never generate an async - /// state machine. - /// - /// Bear in mind that the true behavior of this flag depends entirely on the to - /// abide by design implications of this flag. Once Discord.Net has called out to the state provider with this - /// flag, it is out of our control whether or not an async method is evaluated. - /// - SyncOnly = 1, - /// - /// The entity will only be retrieved via a cache lookup - the Discord API will not be contacted to retrieve the entity. - /// - /// - /// When using an alternative , usage of this flag implies that it is - /// okay for the state provider to make an external call if the local cache missed the entity. - /// - /// Note that when designing an , this flag does not imply that the state - /// provider itself should contact Discord for the entity; rather that if using a dual-layer caching system, - /// it would be okay to contact an external layer, e.g. Redis, for the entity. - /// - CacheOnly = 2, - /// - /// The entity will be downloaded from the Discord REST API if the on hand cannot locate it. - /// - AllowDownload = 3, - /// - /// The entity will be downloaded from the Discord REST API. The local will not be contacted to find the entity. - /// - DownloadOnly = 4 - } -}