| @@ -6,7 +6,7 @@ using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| internal interface ICached<TType> | |||
| internal interface ICached<TType> : ICached, IDisposable | |||
| { | |||
| void Update(TType model); | |||
| @@ -14,4 +14,9 @@ namespace Discord | |||
| TResult ToModel<TResult>() where TResult : TType, new(); | |||
| } | |||
| public interface ICached | |||
| { | |||
| bool IsFreed { get; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,76 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.WebSocket | |||
| { | |||
| /// <summary> | |||
| /// Represents a lazily-loaded cached value that can be loaded synchronously or asynchronously. | |||
| /// </summary> | |||
| /// <typeparam name="TEntity">The type of the entity.</typeparam> | |||
| /// <typeparam name="TId">The primary id type of the entity.</typeparam> | |||
| public class LazyCached<TEntity, TId> | |||
| where TEntity : class, ICached | |||
| where TId : IEquatable<TId> | |||
| { | |||
| /// <summary> | |||
| /// Gets or loads the cached value synchronously. | |||
| /// </summary> | |||
| public TEntity Value | |||
| => GetOrLoad(); | |||
| /// <summary> | |||
| /// Gets whether or not the <see cref="Value"/> has been loaded and is still alive. | |||
| /// </summary> | |||
| public bool IsValueCreated | |||
| => _loadedValue != null && _loadedValue.IsFreed; | |||
| private TEntity _loadedValue; | |||
| private readonly ILookupReferenceStore<TEntity, TId> _store; | |||
| private readonly TId _id; | |||
| private readonly object _lock = new(); | |||
| internal LazyCached(TEntity value) | |||
| { | |||
| _loadedValue = value; | |||
| } | |||
| internal LazyCached(TId id, ILookupReferenceStore<TEntity, TId> store) | |||
| { | |||
| _store = store; | |||
| _id = id; | |||
| } | |||
| private TEntity GetOrLoad() | |||
| { | |||
| lock (_lock) | |||
| { | |||
| if(!IsValueCreated) | |||
| _loadedValue = _store.Get(_id); | |||
| return _loadedValue; | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// Gets or loads the value from the cache asynchronously. | |||
| /// </summary> | |||
| /// <returns>The loaded or fetched entity.</returns> | |||
| public async ValueTask<TEntity> GetAsync() | |||
| { | |||
| if (!IsValueCreated) | |||
| _loadedValue = await _store.GetAsync(_id).ConfigureAwait(false); | |||
| return _loadedValue; | |||
| } | |||
| } | |||
| public class LazyCached<TEntity> : LazyCached<TEntity, ulong> | |||
| where TEntity : class, ICached | |||
| { | |||
| internal LazyCached(ulong id, ILookupReferenceStore<TEntity, ulong> store) | |||
| : base(id, store) { } | |||
| internal LazyCached(TEntity entity) | |||
| : base(entity) { } | |||
| } | |||
| } | |||
| @@ -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<TEntity, TModel, TId, ISharedEntity> | |||
| where TEntity : class, ICached<TModel>, ISharedEntity | |||
| internal interface ILookupReferenceStore<TEntity, TId> | |||
| { | |||
| TEntity Get(TId id); | |||
| ValueTask<TEntity> GetAsync(TId id); | |||
| } | |||
| internal class ReferenceStore<TEntity, TModel, TId, TSharedEntity> : ILookupReferenceStore<TEntity, TId> | |||
| where TEntity : class, ICached<TModel>, TSharedEntity | |||
| where TModel : IEntityModel<TId> | |||
| where TId : IEquatable<TId> | |||
| where ISharedEntity : class | |||
| where TSharedEntity : class | |||
| { | |||
| private readonly ICacheProvider _cacheProvider; | |||
| private readonly ConcurrentDictionary<TId, CacheReference<TEntity>> _references = new(); | |||
| private IEntityStore<TModel, TId> _store; | |||
| private Func<TModel, TEntity> _entityBuilder; | |||
| private Func<TId, RequestOptions, Task<ISharedEntity>> _restLookup; | |||
| private Func<TId, RequestOptions, Task<TSharedEntity>> _restLookup; | |||
| private readonly bool _allowSyncWaits; | |||
| private readonly object _lock = new(); | |||
| public ReferenceStore(ICacheProvider cacheProvider, Func<TModel, TEntity> entityBuilder, Func<TId, RequestOptions, Task<ISharedEntity>> restLookup, bool allowSyncWaits) | |||
| public ReferenceStore(ICacheProvider cacheProvider, Func<TModel, TEntity> entityBuilder, Func<TId, RequestOptions, Task<TSharedEntity>> 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<ISharedEntity> GetAsync(TId id, CacheMode mode, RequestOptions options = null) | |||
| public async ValueTask<TSharedEntity> 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<TModel> 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<TModel> 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<TEntity, TId>.Get(TId id) => Get(id); | |||
| async ValueTask<TEntity> ILookupReferenceStore<TEntity, TId>.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<SocketPresence, IPresenceModel, ulong, IPresence>( | |||
| _cacheProvider, | |||
| m => SocketPresence.Create(m), | |||
| m => SocketPresence.Create(_client, m), | |||
| (id, options) => Task.FromResult<IPresence>(null), | |||
| AllowSyncWaits); | |||
| @@ -284,6 +323,9 @@ namespace Discord.WebSocket | |||
| await PresenceStore.InitializeAsync(); | |||
| } | |||
| public ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser> GetMemberStore(ulong guildId) | |||
| => TryGetMemberStore(guildId, out var store) ? store : null; | |||
| public bool TryGetMemberStore(ulong guildId, out ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser> store) | |||
| => _memberStores.TryGetValue(guildId, out store); | |||
| @@ -214,7 +214,7 @@ namespace Discord.WebSocket | |||
| { | |||
| if (StateManager.TryGetMemberStore(guildId, out var store)) | |||
| return store.GetAsync(userId, cacheMode, options); | |||
| return ValueTask.FromResult<IGuildUser>(null); | |||
| return new ValueTask<IGuildUser>((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<SocketUser, RestUser, IUser, ulong>(user, data.UserId, user != null, () => Rest.GetUserAsync(data.UserId)); | |||
| @@ -2958,7 +2958,7 @@ namespace Discord.WebSocket | |||
| internal async Task<SocketGuild> 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) | |||
| @@ -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); | |||
| } | |||
| /// <inheritdoc /> | |||
| @@ -77,7 +77,7 @@ namespace Discord.WebSocket | |||
| { | |||
| var users = new ConcurrentDictionary<ulong, SocketGroupUser>(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; | |||
| @@ -171,7 +171,6 @@ namespace Discord.WebSocket | |||
| else | |||
| { | |||
| member = SocketThreadUser.Create(Guild, this, model, guildMember); | |||
| member.GlobalUser.AddRef(); | |||
| _members[member.Id] = member; | |||
| } | |||
| return member; | |||
| @@ -305,7 +305,7 @@ namespace Discord.WebSocket | |||
| /// <summary> | |||
| /// Gets the current logged-in user. | |||
| /// </summary> | |||
| 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; | |||
| /// <summary> | |||
| /// Gets the built-in role containing all users in this guild. | |||
| /// </summary> | |||
| @@ -356,7 +356,7 @@ namespace Discord.WebSocket | |||
| /// <returns> | |||
| /// A collection of guild users found within this guild. | |||
| /// </returns> | |||
| public IReadOnlyCollection<SocketGuildUser> Users => Discord.StateManager.GetMembers(Id).Cast<SocketGuildUser>().ToImmutableArray(); | |||
| public IReadOnlyCollection<SocketGuildUser> Users => Discord.StateManager.TryGetMemberStore(Id, out var store) ? store.GetAll().ToImmutableArray() : ImmutableArray<SocketGuildUser>.Empty; | |||
| /// <summary> | |||
| /// Gets a collection of all roles in this guild. | |||
| /// </summary> | |||
| @@ -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 <paramref name="id"/>; <see langword="null"/> if none is found. | |||
| /// </returns> | |||
| public SocketGuildUser GetUser(ulong id) | |||
| => Discord.StateManager.GetMember(id, Id); | |||
| => Discord.StateManager.TryGetMemberStore(Id, out var store) ? store.Get(id) : null; | |||
| /// <inheritdoc /> | |||
| public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable<ulong> 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<bool>(); | |||
| DownloadedMemberCount = membersToKeep.Count(); | |||
| @@ -1240,7 +1243,6 @@ namespace Discord.WebSocket | |||
| /// in order to use this property. | |||
| /// </remarks> | |||
| /// </param> | |||
| /// <param name="speakers">A collection of speakers for the event.</param> | |||
| /// <param name="location">The location of the event; links are supported</param> | |||
| /// <param name="coverImage">The optional banner image for the event.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| @@ -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; | |||
| } | |||
| @@ -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); | |||
| } | |||
| @@ -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); | |||
| } | |||
| @@ -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<SocketUser>(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) | |||
| @@ -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 | |||
| @@ -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)"; | |||
| @@ -28,7 +28,7 @@ namespace Discord.WebSocket | |||
| /// <summary> | |||
| /// Gets the guild the user is in. | |||
| /// </summary> | |||
| public Lazy<SocketGuild> Guild { get; } | |||
| public Lazy<SocketGuild> Guild { get; } // TODO: convert to LazyCached once guilds are cached. | |||
| /// <summary> | |||
| /// Gets the guilds id that the user is in. | |||
| /// </summary> | |||
| @@ -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<ulong>()); | |||
| 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 | |||
| } | |||
| @@ -15,6 +15,8 @@ namespace Discord.WebSocket | |||
| { | |||
| internal ulong UserId; | |||
| internal ulong? GuildId; | |||
| internal bool IsFreed; | |||
| internal DiscordSocketClient Discord; | |||
| /// <inheritdoc /> | |||
| public UserStatus Status { get; private set; } | |||
| @@ -23,17 +25,24 @@ namespace Discord.WebSocket | |||
| /// <inheritdoc /> | |||
| public IReadOnlyCollection<IActivity> Activities { get; private set; } | |||
| internal SocketPresence() { } | |||
| internal SocketPresence(UserStatus status, IImmutableSet<ClientType> activeClients, IImmutableList<IActivity> 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<ClientType> activeClients, IImmutableList<IActivity> activities) | |||
| : this(discord) | |||
| { | |||
| Status = status; | |||
| ActiveClients = activeClients ?? ImmutableHashSet<ClientType>.Empty; | |||
| Activities = activities ?? ImmutableList<IActivity>.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<Model>.ToModel() => ToModel(); | |||
| TResult ICached<Model>.ToModel<TResult>() => ToModel<TResult>(); | |||
| void ICached<Model>.Update(Model model) => Update(model); | |||
| bool ICached.IsFreed => IsFreed; | |||
| #endregion | |||
| } | |||
| @@ -19,17 +19,6 @@ namespace Discord.WebSocket | |||
| public bool IsVerified { get; private set; } | |||
| /// <inheritdoc /> | |||
| public bool IsMfaEnabled { get; private set; } | |||
| /// <inheritdoc /> | |||
| public override bool IsBot { get { return GlobalUser.Value.IsBot; } internal set { GlobalUser.Value.IsBot = value; } } | |||
| /// <inheritdoc /> | |||
| public override string Username { get { return GlobalUser.Value.Username; } internal set { GlobalUser.Value.Username = value; } } | |||
| /// <inheritdoc /> | |||
| public override ushort DiscriminatorValue { get { return GlobalUser.Value.DiscriminatorValue; } internal set { GlobalUser.Value.DiscriminatorValue = value; } } | |||
| /// <inheritdoc /> | |||
| public override string AvatarId { get { return GlobalUser.Value.AvatarId; } internal set { GlobalUser.Value.AvatarId = value; } } | |||
| /// <inheritdoc /> | |||
| internal override Lazy<SocketPresence> Presence { get { return GlobalUser.Value.Presence; } set { GlobalUser.Value.Presence = value; } } | |||
| /// <inheritdoc /> | |||
| public UserProperties Flags { get; internal set; } | |||
| /// <inheritdoc /> | |||
| @@ -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 | |||
| @@ -238,7 +238,7 @@ namespace Discord.WebSocket | |||
| /// <inheritdoc /> | |||
| string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.Value.GetGuildAvatarUrl(format, size); | |||
| internal override Lazy<SocketPresence> Presence { get => GuildUser.Value.Presence; set => GuildUser.Value.Presence = value; } | |||
| internal override LazyCached<SocketPresence> Presence { get => GuildUser.Value.Presence; set => GuildUser.Value.Presence = value; } | |||
| public override void Dispose() | |||
| { | |||
| @@ -26,8 +26,8 @@ namespace Discord.WebSocket | |||
| /// <inheritdoc /> | |||
| public override bool IsWebhook => false; | |||
| /// <inheritdoc /> | |||
| internal override Lazy<SocketPresence> Presence { get { return new Lazy<SocketPresence>(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } } | |||
| internal override Lazy<SocketGlobalUser> GlobalUser { get => new Lazy<SocketGlobalUser>(() => null); set { } } | |||
| internal override LazyCached<SocketPresence> Presence { get { return new(SocketPresence.Default); } set { } } | |||
| internal override LazyCached<SocketGlobalUser> GlobalUser { get => new(null); set { } } | |||
| internal SocketUnknownUser(DiscordSocketClient discord, ulong id) | |||
| : base(discord, id) | |||
| @@ -15,7 +15,7 @@ namespace Discord.WebSocket | |||
| /// Represents a WebSocket-based user. | |||
| /// </summary> | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public abstract class SocketUser : SocketEntity<ulong>, IUser, ICached<Model>, IDisposable | |||
| public abstract class SocketUser : SocketEntity<ulong>, IUser, ICached<Model> | |||
| { | |||
| /// <inheritdoc /> | |||
| public virtual bool IsBot { get; internal set; } | |||
| @@ -29,9 +29,9 @@ namespace Discord.WebSocket | |||
| public virtual bool IsWebhook { get; } | |||
| /// <inheritdoc /> | |||
| public UserProperties? PublicFlags { get; private set; } | |||
| internal virtual Lazy<SocketGlobalUser> GlobalUser { get; set; } | |||
| internal virtual Lazy<SocketPresence> Presence { get; set; } | |||
| internal virtual LazyCached<SocketGlobalUser> GlobalUser { get; set; } | |||
| internal virtual LazyCached<SocketPresence> Presence { get; set; } | |||
| internal bool IsFreed { get; set; } | |||
| /// <inheritdoc /> | |||
| public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | |||
| /// <inheritdoc /> | |||
| @@ -56,11 +56,11 @@ namespace Discord.WebSocket | |||
| internal SocketUser(DiscordSocketClient discord, ulong id) | |||
| : base(discord, id) | |||
| { | |||
| Presence = new LazyCached<SocketPresence>(id, discord.StateManager.PresenceStore); | |||
| GlobalUser = new LazyCached<SocketGlobalUser>(id, discord.StateManager.UserStore); | |||
| } | |||
| internal virtual bool Update(Model model) | |||
| { | |||
| Presence ??= new Lazy<SocketPresence>(() => Discord.StateManager.GetPresence(Id), System.Threading.LazyThreadSafetyMode.PublicationOnly); | |||
| GlobalUser ??= new Lazy<SocketGlobalUser>(() => 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<Model>.Update(Model model) => Update(model); | |||
| bool ICached.IsFreed => IsFreed; | |||
| #endregion | |||
| } | |||
| } | |||
| @@ -33,8 +33,8 @@ namespace Discord.WebSocket | |||
| /// <inheritdoc /> | |||
| public override bool IsWebhook => true; | |||
| /// <inheritdoc /> | |||
| internal override Lazy<SocketPresence> Presence { get { return new Lazy<SocketPresence>(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } } | |||
| internal override Lazy<SocketGlobalUser> GlobalUser { get => new Lazy<SocketGlobalUser>(() => null); set { } } | |||
| internal override LazyCached<SocketPresence> Presence { get { return new(SocketPresence.Default); } set { } } | |||
| internal override LazyCached<SocketGlobalUser> GlobalUser { get => new(null); set { } } | |||
| internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) | |||
| : base(guild.Discord, id) | |||