@@ -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) | |||