diff --git a/src/Discord.Net.Core/Cache/ICached.cs b/src/Discord.Net.Core/Cache/ICached.cs index 6f7a99bfc..19445598f 100644 --- a/src/Discord.Net.Core/Cache/ICached.cs +++ b/src/Discord.Net.Core/Cache/ICached.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace Discord { - internal interface ICached + internal interface ICached : ICached, IDisposable { void Update(TType model); @@ -14,4 +14,9 @@ namespace Discord TResult ToModel() where TResult : TType, new(); } + + public interface ICached + { + bool IsFreed { get; } + } } diff --git a/src/Discord.Net.WebSocket/Cache/LazyCached.cs b/src/Discord.Net.WebSocket/Cache/LazyCached.cs new file mode 100644 index 000000000..ca568b5ee --- /dev/null +++ b/src/Discord.Net.WebSocket/Cache/LazyCached.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + /// + /// Represents a lazily-loaded cached value that can be loaded synchronously or asynchronously. + /// + /// The type of the entity. + /// The primary id type of the entity. + public class LazyCached + where TEntity : class, ICached + where TId : IEquatable + { + /// + /// Gets or loads the cached value synchronously. + /// + public TEntity Value + => GetOrLoad(); + + /// + /// Gets whether or not the has been loaded and is still alive. + /// + public bool IsValueCreated + => _loadedValue != null && _loadedValue.IsFreed; + + private TEntity _loadedValue; + private readonly ILookupReferenceStore _store; + private readonly TId _id; + private readonly object _lock = new(); + + internal LazyCached(TEntity value) + { + _loadedValue = value; + } + + internal LazyCached(TId id, ILookupReferenceStore store) + { + _store = store; + _id = id; + } + + private TEntity GetOrLoad() + { + lock (_lock) + { + if(!IsValueCreated) + _loadedValue = _store.Get(_id); + return _loadedValue; + } + } + + /// + /// Gets or loads the value from the cache asynchronously. + /// + /// The loaded or fetched entity. + public async ValueTask GetAsync() + { + if (!IsValueCreated) + _loadedValue = await _store.GetAsync(_id).ConfigureAwait(false); + return _loadedValue; + } + } + + public class LazyCached : LazyCached + where TEntity : class, ICached + { + internal LazyCached(ulong id, ILookupReferenceStore store) + : base(id, store) { } + internal LazyCached(TEntity entity) + : base(entity) { } + } +} diff --git a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs index f657e526d..ccbcc2f77 100644 --- a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs +++ b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs @@ -19,8 +19,6 @@ namespace Discord.WebSocket private int _referenceCount; - private readonly object _lock = new object(); - public CacheReference(TType value) { Reference = new(value); @@ -39,28 +37,31 @@ namespace Discord.WebSocket public void ReleaseReference() { - lock (_lock) - { - if (_referenceCount > 0) - _referenceCount--; - } + Interlocked.Decrement(ref _referenceCount); } } - internal class ReferenceStore - where TEntity : class, ICached, ISharedEntity + + internal interface ILookupReferenceStore + { + TEntity Get(TId id); + ValueTask GetAsync(TId id); + } + + internal class ReferenceStore : ILookupReferenceStore + where TEntity : class, ICached, TSharedEntity where TModel : IEntityModel where TId : IEquatable - where ISharedEntity : class + where TSharedEntity : class { private readonly ICacheProvider _cacheProvider; private readonly ConcurrentDictionary> _references = new(); private IEntityStore _store; private Func _entityBuilder; - private Func> _restLookup; + private Func> _restLookup; private readonly bool _allowSyncWaits; private readonly object _lock = new(); - public ReferenceStore(ICacheProvider cacheProvider, Func entityBuilder, Func> restLookup, bool allowSyncWaits) + public ReferenceStore(ICacheProvider cacheProvider, Func entityBuilder, Func> restLookup, bool allowSyncWaits) { _allowSyncWaits = allowSyncWaits; _cacheProvider = cacheProvider; @@ -68,6 +69,19 @@ namespace Discord.WebSocket _restLookup = restLookup; } + internal bool RemoveReference(TId id) + { + if(_references.TryGetValue(id, out var rf)) + { + rf.ReleaseReference(); + + if (rf.CanRelease) + return _references.TryRemove(id, out _); + } + + return false; + } + internal void ClearDeadReferences() { lock (_lock) @@ -135,7 +149,7 @@ namespace Discord.WebSocket return null; } - public async ValueTask GetAsync(TId id, CacheMode mode, RequestOptions options = null) + public async ValueTask GetAsync(TId id, CacheMode mode, RequestOptions options = null) { if (TryGetReference(id, out var entity)) { @@ -216,6 +230,28 @@ namespace Discord.WebSocket return _store.AddOrUpdateAsync(model, CacheRunMode.Async); } + public void BulkAddOrUpdate(IEnumerable models) + { + RunOrThrowValueTask(_store.AddOrUpdateBatchAsync(models, CacheRunMode.Sync)); + + foreach(var model in models) + { + if (_references.TryGetValue(model.Id, out var rf) && rf.Reference.TryGetTarget(out var entity)) + entity.Update(model); + } + } + + public async ValueTask BulkAddOrUpdateAsync(IEnumerable models) + { + await _store.AddOrUpdateBatchAsync(models, CacheRunMode.Async).ConfigureAwait(false); + + foreach (var model in models) + { + if (_references.TryGetValue(model.Id, out var rf) && rf.Reference.TryGetTarget(out var entity)) + entity.Update(model); + } + } + public void Remove(TId id) { RunOrThrowValueTask(_store.RemoveAsync(id, CacheRunMode.Sync)); @@ -239,6 +275,9 @@ namespace Discord.WebSocket _references.Clear(); return _store.PurgeAllAsync(CacheRunMode.Async); } + + TEntity ILookupReferenceStore.Get(TId id) => Get(id); + async ValueTask ILookupReferenceStore.GetAsync(TId id) => (TEntity)await GetAsync(id, CacheMode.CacheOnly).ConfigureAwait(false); } internal partial class ClientStateManager @@ -261,7 +300,7 @@ namespace Discord.WebSocket PresenceStore = new ReferenceStore( _cacheProvider, - m => SocketPresence.Create(m), + m => SocketPresence.Create(_client, m), (id, options) => Task.FromResult(null), AllowSyncWaits); @@ -284,6 +323,9 @@ namespace Discord.WebSocket await PresenceStore.InitializeAsync(); } + public ReferenceStore GetMemberStore(ulong guildId) + => TryGetMemberStore(guildId, out var store) ? store : null; + public bool TryGetMemberStore(ulong guildId, out ReferenceStore store) => _memberStores.TryGetValue(guildId, out store); diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index f482e3afd..a7a037243 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -214,7 +214,7 @@ namespace Discord.WebSocket { if (StateManager.TryGetMemberStore(guildId, out var store)) return store.GetAsync(userId, cacheMode, options); - return ValueTask.FromResult(null); + return new ValueTask((IGuildUser)null); } #endregion @@ -690,7 +690,7 @@ namespace Discord.WebSocket if (CurrentUser == null) return; var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; - await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(Status, null, activities).ToModel()).ConfigureAwait(false); + await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(this, Status, null, activities).ToModel()).ConfigureAwait(false); var presence = BuildCurrentStatus() ?? (UserStatus.Online, false, null, null); @@ -870,7 +870,7 @@ namespace Discord.WebSocket Rest.CreateRestSelfUser(data.User); var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; - await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(Status, null, activities).ToModel()).ConfigureAwait(false); + await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(this, Status, null, activities).ToModel()).ConfigureAwait(false); ApiClient.CurrentUserId = currentUser.Id; ApiClient.CurrentApplicationId = data.Application.Id; @@ -1345,7 +1345,7 @@ namespace Discord.WebSocket if (user != null) user.Update(data.User); else - user = StateManager.GetOrAddUser(data.User.Id, (x) => data.User); + user = await StateManager.UserStore.GetOrAddAsync(data.User.Id, _ => data.User).ConfigureAwait(false); await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user).ConfigureAwait(false); } @@ -1560,7 +1560,7 @@ namespace Discord.WebSocket SocketUser user = guild.GetUser(data.User.Id); if (user == null) - user = SocketUnknownUser.Create(this, StateManager, data.User); + user = SocketUnknownUser.Create(this, data.User); await TimedInvokeAsync(_userBannedEvent, nameof(UserBanned), user, guild).ConfigureAwait(false); } else @@ -1584,9 +1584,9 @@ namespace Discord.WebSocket return; } - SocketUser user = StateManager.GetUser(data.User.Id); + SocketUser user = (SocketUser)await StateManager.UserStore.GetAsync(data.User.Id, CacheMode.CacheOnly).ConfigureAwait(false); if (user == null) - user = SocketUnknownUser.Create(this, StateManager, data.User); + user = SocketUnknownUser.Create(this, data.User); await TimedInvokeAsync(_userUnbannedEvent, nameof(UserUnbanned), user, guild).ConfigureAwait(false); } else @@ -1630,7 +1630,7 @@ namespace Discord.WebSocket if (guild != null) { if (data.WebhookId.IsSpecified) - author = SocketWebhookUser.Create(guild, StateManager, data.Author.Value, data.WebhookId.Value); + author = SocketWebhookUser.Create(guild, data.Author.Value, data.WebhookId.Value); else author = guild.GetUser(data.Author.Value.Id); } @@ -1695,7 +1695,7 @@ namespace Discord.WebSocket if (guild != null) { if (data.WebhookId.IsSpecified) - author = SocketWebhookUser.Create(guild, StateManager, data.Author.Value, data.WebhookId.Value); + author = SocketWebhookUser.Create(guild, data.Author.Value, data.WebhookId.Value); else author = guild.GetUser(data.Author.Value.Id); } @@ -1966,7 +1966,7 @@ namespace Discord.WebSocket else { var globalBefore = user.GlobalUser.Value.Clone(); - if (user.GlobalUser.Value.Update(StateManager, data.User)) + if (user.GlobalUser.Value.Update(data.User)) { //Global data was updated, trigger UserUpdated await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); @@ -1975,7 +1975,7 @@ namespace Discord.WebSocket } else { - user = StateManager.GetUser(data.User.Id); + user = (SocketUser)await StateManager.UserStore.GetAsync(data.User.Id, CacheMode.CacheOnly).ConfigureAwait(false); if (user == null) { await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); @@ -1984,9 +1984,9 @@ namespace Discord.WebSocket } var before = user.Presence?.Value?.Clone(); - user.Update(StateManager, data.User); - var after = SocketPresence.Create(data); - StateManager.AddOrUpdatePresence(data); + user.Update(data.User); + var after = SocketPresence.Create(this, data); + await StateManager.PresenceStore.AddOrUpdateAsync(data).ConfigureAwait(false); await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, after).ConfigureAwait(false); } break; @@ -2114,7 +2114,7 @@ namespace Discord.WebSocket if (data.Id == CurrentUser.Id) { var before = CurrentUser.Clone(); - CurrentUser.Update(StateManager, data); + CurrentUser.Update(data); await TimedInvokeAsync(_selfUpdatedEvent, nameof(CurrentUserUpdated), before, CurrentUser).ConfigureAwait(false); } else @@ -2277,7 +2277,7 @@ namespace Discord.WebSocket : null; SocketUser target = data.TargetUser.IsSpecified - ? (guild.GetUser(data.TargetUser.Value.Id) ?? (SocketUser)SocketUnknownUser.Create(this, StateManager, data.TargetUser.Value)) + ? (guild.GetUser(data.TargetUser.Value.Id) ?? (SocketUser)SocketUnknownUser.Create(this, data.TargetUser.Value)) : null; var invite = SocketInvite.Create(this, guild, channel, inviter, target, data); @@ -2332,7 +2332,7 @@ namespace Discord.WebSocket } SocketUser user = data.User.IsSpecified - ? StateManager.GetOrAddUser(data.User.Value.Id, (_) => data.User.Value) + ? await StateManager.UserStore.GetOrAddAsync(data.User.Value.Id, (_) => data.User.Value).ConfigureAwait(false) : guild?.AddOrUpdateUser(data.Member.Value); // null if the bot scope isn't set, so the guild cannot be retrieved. SocketChannel channel = null; @@ -2821,7 +2821,7 @@ namespace Discord.WebSocket return; } - var user = (SocketUser)guild.GetUser(data.UserId) ?? StateManager.GetUser(data.UserId); + var user = (SocketUser)guild.GetUser(data.UserId) ?? StateManager.UserStore.Get(data.UserId); var cacheableUser = new Cacheable(user, data.UserId, user != null, () => Rest.GetUserAsync(data.UserId)); @@ -2958,7 +2958,7 @@ namespace Discord.WebSocket internal async Task AddGuildAsync(ExtendedGuild model) { - await StateManager.InitializeGuildStoreAsync(model.Id).ConfigureAwait(false); + await StateManager.GetMemberStoreAsync(model.Id).ConfigureAwait(false); var guild = SocketGuild.Create(this, StateManager, model); StateManager.AddGuild(guild); if (model.Large) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 755fa7ab3..e3c56b987 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -43,7 +43,7 @@ namespace Discord.WebSocket } internal override void Update(ClientStateManager state, Model model) { - Recipient.Update(state, model.Recipients.Value[0]); + Recipient.Update(model.Recipients.Value[0]); } internal static SocketDMChannel Create(DiscordSocketClient discord, ClientStateManager state, ulong channelId, API.User recipient) { @@ -53,7 +53,7 @@ namespace Discord.WebSocket } internal void Update(ClientStateManager state, API.User recipient) { - Recipient.Update(state, recipient); + Recipient.Update(recipient); } /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index f6736245d..b5885c1d9 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -77,7 +77,7 @@ namespace Discord.WebSocket { var users = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(models.Length * 1.05)); for (int i = 0; i < models.Length; i++) - users[models[i].Id] = SocketGroupUser.Create(this, state, models[i]); + users[models[i].Id] = SocketGroupUser.Create(this, models[i]); _users = users; } @@ -265,8 +265,7 @@ namespace Discord.WebSocket return user; else { - var privateUser = SocketGroupUser.Create(this, Discord.StateManager, model); - privateUser.GlobalUser.AddRef(); + var privateUser = SocketGroupUser.Create(this, model); _users[privateUser.Id] = privateUser; return privateUser; } @@ -275,7 +274,6 @@ namespace Discord.WebSocket { if (_users.TryRemove(id, out SocketGroupUser user)) { - user.GlobalUser.RemoveRef(Discord); return user; } return null; diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs index 4ff39e5e5..3fbf9d6e4 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs @@ -171,7 +171,6 @@ namespace Discord.WebSocket else { member = SocketThreadUser.Create(Guild, this, model, guildMember); - member.GlobalUser.AddRef(); _members[member.Id] = member; } return member; diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 5330f0826..13da7cce7 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -305,7 +305,7 @@ namespace Discord.WebSocket /// /// Gets the current logged-in user. /// - public SocketGuildUser CurrentUser => Discord.StateManager.GetMember(Discord.CurrentUser.Id, Id); + public SocketGuildUser CurrentUser => Discord.StateManager.TryGetMemberStore(Id, out var store) ? store.Get(Discord.CurrentUser.Id) : null; /// /// Gets the built-in role containing all users in this guild. /// @@ -356,7 +356,7 @@ namespace Discord.WebSocket /// /// A collection of guild users found within this guild. /// - public IReadOnlyCollection Users => Discord.StateManager.GetMembers(Id).Cast().ToImmutableArray(); + public IReadOnlyCollection Users => Discord.StateManager.TryGetMemberStore(Id, out var store) ? store.GetAll().ToImmutableArray() : ImmutableArray.Empty; /// /// Gets a collection of all roles in this guild. /// @@ -547,8 +547,12 @@ namespace Discord.WebSocket internal async ValueTask UpdateCacheAsync(ExtendedModel model) { - await Discord.StateManager.BulkAddOrUpdatePresenceAsync(model.Presences).ConfigureAwait(false); - await Discord.StateManager.BulkAddOrUpdateMembersAsync(Id, model.Members).ConfigureAwait(false); + await Discord.StateManager.PresenceStore.BulkAddOrUpdateAsync(model.Presences); + + await Discord.StateManager.UserStore.BulkAddOrUpdateAsync(model.Members.Select(x => x.User)); + + if(Discord.StateManager.TryGetMemberStore(Id, out var store)) + store.BulkAddOrUpdate(model.Members); } internal void Update(ClientStateManager state, EmojiUpdateModel model) @@ -1055,7 +1059,7 @@ namespace Discord.WebSocket /// A guild user associated with the specified ; if none is found. /// public SocketGuildUser GetUser(ulong id) - => Discord.StateManager.GetMember(id, Id); + => Discord.StateManager.TryGetMemberStore(Id, out var store) ? store.Get(id) : null; /// public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable includeRoleIds = null) => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options, includeRoleIds); @@ -1064,11 +1068,10 @@ namespace Discord.WebSocket { SocketGuildUser member; if ((member = GetUser(model.Id)) != null) - member.GlobalUser?.Update(Discord.StateManager, model); + member.Update(model); else { member = SocketGuildUser.Create(Id, Discord, model); - member.GlobalUser.AddRef(); DownloadedMemberCount++; } return member; @@ -1076,12 +1079,11 @@ namespace Discord.WebSocket internal SocketGuildUser AddOrUpdateUser(MemberModel model) { SocketGuildUser member; - if ((member = GetUser(model.User.Id)) != null) - member.Update(Discord.StateManager, model); + if ((member = GetUser(model.Id)) != null) + member.Update(model); else { member = SocketGuildUser.Create(Id, Discord, model); - member.GlobalUser.AddRef(); DownloadedMemberCount++; } return member; @@ -1092,8 +1094,8 @@ namespace Discord.WebSocket if ((member = GetUser(id)) != null) { DownloadedMemberCount--; - member.GlobalUser.RemoveRef(Discord); - Discord.StateManager.RemoveMember(id, Id); + if (Discord.StateManager.TryGetMemberStore(Id, out var store)) + store.Remove(id); return member; } return null; @@ -1114,8 +1116,9 @@ namespace Discord.WebSocket var membersToPurge = users.Where(x => predicate.Invoke(x) && x?.Id != Discord.CurrentUser.Id); var membersToKeep = users.Where(x => !predicate.Invoke(x) || x?.Id == Discord.CurrentUser.Id); - foreach (var member in membersToPurge) - Discord.StateManager.RemoveMember(member.Id, Id); + if(Discord.StateManager.TryGetMemberStore(Id, out var store)) + foreach (var member in membersToPurge) + store.Remove(member.Id); _downloaderPromise = new TaskCompletionSource(); DownloadedMemberCount = membersToKeep.Count(); @@ -1240,7 +1243,6 @@ namespace Discord.WebSocket /// in order to use this property. /// /// - /// A collection of speakers for the event. /// The location of the event; links are supported /// The optional banner image for the event. /// The options to be used when sending the request. diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs index 9f019cdb1..bdf89d1ab 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs @@ -89,7 +89,7 @@ namespace Discord.WebSocket if(guildUser != null) { if(model.Creator.IsSpecified) - guildUser.Update(Discord.StateManager, model.Creator.Value); + guildUser.Update(model.Creator.Value); Creator = guildUser; } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs index 28a922e65..d019a87a9 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs @@ -56,7 +56,7 @@ namespace Discord.WebSocket if (Channel is SocketGuildChannel channel) { if (model.Message.Value.WebhookId.IsSpecified) - author = SocketWebhookUser.Create(channel.Guild, Discord.StateManager, model.Message.Value.Author.Value, model.Message.Value.WebhookId.Value); + author = SocketWebhookUser.Create(channel.Guild, model.Message.Value.Author.Value, model.Message.Value.WebhookId.Value); else if (model.Message.Value.Author.IsSpecified) author = channel.Guild.GetUser(model.Message.Value.Author.Value.Id); } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs index d36960749..e8687dab3 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs @@ -88,7 +88,7 @@ namespace Discord.WebSocket if (guild != null) { if (msg.Value.WebhookId.IsSpecified) - author = SocketWebhookUser.Create(guild, discord.StateManager, msg.Value.Author.Value, msg.Value.WebhookId.Value); + author = SocketWebhookUser.Create(guild, msg.Value.Author.Value, msg.Value.WebhookId.Value); else author = guild.GetUser(msg.Value.Author.Value.Id); } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 51a691b6f..c7bc5e873 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -251,7 +251,7 @@ namespace Discord.WebSocket if (user != null) newMentions.Add(user); else - newMentions.Add(SocketUnknownUser.Create(Discord, state, val)); + newMentions.Add(SocketUnknownUser.Create(Discord, val)); } } _userMentions = newMentions.ToImmutable(); @@ -263,7 +263,7 @@ namespace Discord.WebSocket Interaction = new MessageInteraction(model.Interaction.Value.Id, model.Interaction.Value.Type, model.Interaction.Value.Name, - SocketGlobalUser.Create(Discord, state, model.Interaction.Value.User)); + SocketGlobalUser.Create(Discord, model.Interaction.Value.User)); } if (model.Flags.IsSpecified) diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index 94c081d75..4c8e94432 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -122,14 +122,14 @@ namespace Discord.WebSocket if (guild != null) { if (webhookId != null) - refMsgAuthor = SocketWebhookUser.Create(guild, state, refMsg.Author.Value, webhookId.Value); + refMsgAuthor = SocketWebhookUser.Create(guild, refMsg.Author.Value, webhookId.Value); else refMsgAuthor = guild.GetUser(refMsg.Author.Value.Id); } else refMsgAuthor = (Channel as SocketChannel).GetUser(refMsg.Author.Value.Id); if (refMsgAuthor == null) - refMsgAuthor = SocketUnknownUser.Create(Discord, state, refMsg.Author.Value); + refMsgAuthor = SocketUnknownUser.Create(Discord, refMsg.Author.Value); } else // Message author wasn't specified in the payload, so create a completely anonymous unknown user diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index 80dce6170..1187a235a 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -26,11 +26,11 @@ namespace Discord.WebSocket return entity; } - ~SocketGlobalUser() => Discord.StateManager.RemoveReferencedGlobalUser(Id); + ~SocketGlobalUser() => Dispose(); public override void Dispose() { GC.SuppressFinalize(this); - Discord.StateManager.RemoveReferencedGlobalUser(Id); + Discord.StateManager.UserStore.RemoveReference(Id); } private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 00593d8ba..42169f5ad 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -28,7 +28,7 @@ namespace Discord.WebSocket /// /// Gets the guild the user is in. /// - public Lazy Guild { get; } + public Lazy Guild { get; } // TODO: convert to LazyCached once guilds are cached. /// /// Gets the guilds id that the user is in. /// @@ -146,7 +146,7 @@ namespace Discord.WebSocket { var entity = new SocketGuildUser(guildId, model.Id, client); if (entity.Update(model)) - client.StateManager.AddOrUpdateMember(guildId, entity.ToModel()); + client.StateManager.GetMemberStore(guildId)?.AddOrUpdate(entity.ToModel()); entity.UpdateRoles(Array.Empty()); return entity; } @@ -154,7 +154,7 @@ namespace Discord.WebSocket { var entity = new SocketGuildUser(guildId, model.Id, client); entity.Update(model); - client.StateManager.AddOrUpdateMember(guildId, model); + client.StateManager.GetMemberStore(guildId)?.AddOrUpdate(model); return entity; } internal void Update(MemberModel model) @@ -301,9 +301,9 @@ namespace Discord.WebSocket public override void Dispose() { GC.SuppressFinalize(this); - Discord.StateManager.RemovedReferencedMember(Id, _guildId); + Discord.StateManager.GetMemberStore(_guildId)?.RemoveReference(Id); } - ~SocketGuildUser() => Discord.StateManager.RemovedReferencedMember(Id, _guildId); + ~SocketGuildUser() => Dispose(); #endregion } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index 8e2464a22..aa0220453 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -15,6 +15,8 @@ namespace Discord.WebSocket { internal ulong UserId; internal ulong? GuildId; + internal bool IsFreed; + internal DiscordSocketClient Discord; /// public UserStatus Status { get; private set; } @@ -23,17 +25,24 @@ namespace Discord.WebSocket /// public IReadOnlyCollection Activities { get; private set; } - internal SocketPresence() { } - internal SocketPresence(UserStatus status, IImmutableSet activeClients, IImmutableList activities) + public static SocketPresence Default + => new SocketPresence(null, UserStatus.Offline, null, null); + + internal SocketPresence(DiscordSocketClient discord) + { + Discord = discord; + } + internal SocketPresence(DiscordSocketClient discord, UserStatus status, IImmutableSet activeClients, IImmutableList activities) + : this(discord) { Status = status; ActiveClients = activeClients ?? ImmutableHashSet.Empty; Activities = activities ?? ImmutableList.Empty; } - internal static SocketPresence Create(Model model) + internal static SocketPresence Create(DiscordSocketClient client, Model model) { - var entity = new SocketPresence(); + var entity = new SocketPresence(client); entity.Update(model); return entity; } @@ -102,6 +111,22 @@ namespace Discord.WebSocket internal SocketPresence Clone() => MemberwiseClone() as SocketPresence; + ~SocketPresence() => Dispose(); + + public void Dispose() + { + if (IsFreed) + return; + + GC.SuppressFinalize(this); + + if(Discord != null) + { + Discord.StateManager.PresenceStore.RemoveReference(UserId); + IsFreed = true; + } + } + #region Cache private struct CacheModel : Model { @@ -205,6 +230,7 @@ namespace Discord.WebSocket Model ICached.ToModel() => ToModel(); TResult ICached.ToModel() => ToModel(); void ICached.Update(Model model) => Update(model); + bool ICached.IsFreed => IsFreed; #endregion } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs index cc6bcd912..1d1c3bb6c 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -19,17 +19,6 @@ namespace Discord.WebSocket public bool IsVerified { get; private set; } /// public bool IsMfaEnabled { get; private set; } - - /// - public override bool IsBot { get { return GlobalUser.Value.IsBot; } internal set { GlobalUser.Value.IsBot = value; } } - /// - public override string Username { get { return GlobalUser.Value.Username; } internal set { GlobalUser.Value.Username = value; } } - /// - public override ushort DiscriminatorValue { get { return GlobalUser.Value.DiscriminatorValue; } internal set { GlobalUser.Value.DiscriminatorValue = value; } } - /// - public override string AvatarId { get { return GlobalUser.Value.AvatarId; } internal set { GlobalUser.Value.AvatarId = value; } } - /// - internal override Lazy Presence { get { return GlobalUser.Value.Presence; } set { GlobalUser.Value.Presence = value; } } /// public UserProperties Flags { get; internal set; } /// @@ -99,8 +88,12 @@ namespace Discord.WebSocket internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; public override void Dispose() { + if (IsFreed) + return; + GC.SuppressFinalize(this); - Discord.StateManager.RemoveReferencedGlobalUser(Id); + Discord.StateManager.UserStore.RemoveReference(Id); + IsFreed = true; } #region Cache diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs index a00a78a4a..48aebc92e 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs @@ -238,7 +238,7 @@ namespace Discord.WebSocket /// 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 LazyCached Presence { get => GuildUser.Value.Presence; set => GuildUser.Value.Presence = value; } public override void Dispose() { diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs index 151f00b72..6da6160fd 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs @@ -26,8 +26,8 @@ namespace Discord.WebSocket /// public override bool IsWebhook => false; /// - internal override Lazy Presence { get { return new Lazy(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } } - internal override Lazy GlobalUser { get => new Lazy(() => null); set { } } + internal override LazyCached Presence { get { return new(SocketPresence.Default); } set { } } + internal override LazyCached GlobalUser { get => new(null); set { } } internal SocketUnknownUser(DiscordSocketClient discord, ulong id) : base(discord, id) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 0495c5118..43b3b0e54 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -15,7 +15,7 @@ namespace Discord.WebSocket /// Represents a WebSocket-based user. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public abstract class SocketUser : SocketEntity, IUser, ICached, IDisposable + public abstract class SocketUser : SocketEntity, IUser, ICached { /// public virtual bool IsBot { get; internal set; } @@ -29,9 +29,9 @@ namespace Discord.WebSocket public virtual bool IsWebhook { get; } /// public UserProperties? PublicFlags { get; private set; } - internal virtual Lazy GlobalUser { get; set; } - internal virtual Lazy Presence { get; set; } - + internal virtual LazyCached GlobalUser { get; set; } + internal virtual LazyCached Presence { get; set; } + internal bool IsFreed { get; set; } /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); /// @@ -56,11 +56,11 @@ namespace Discord.WebSocket internal SocketUser(DiscordSocketClient discord, ulong id) : base(discord, id) { + Presence = new LazyCached(id, discord.StateManager.PresenceStore); + GlobalUser = new LazyCached(id, discord.StateManager.UserStore); } internal virtual bool Update(Model model) { - 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) { @@ -124,7 +124,7 @@ namespace Discord.WebSocket internal SocketUser Clone() => MemberwiseClone() as SocketUser; #region Cache - private struct CacheModel : Model + private class CacheModel : Model { public string Username { get; set; } @@ -160,6 +160,8 @@ namespace Discord.WebSocket void ICached.Update(Model model) => Update(model); + bool ICached.IsFreed => IsFreed; + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index bd3c9fd5e..d1867ddbf 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -33,8 +33,8 @@ 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 Lazy GlobalUser { get => new Lazy(() => null); set { } } + internal override LazyCached Presence { get { return new(SocketPresence.Default); } set { } } + internal override LazyCached GlobalUser { get => new(null); set { } } internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) : base(guild.Discord, id)