Browse Source

updates

v4/state-cache-providers
Quin Lynch 3 years ago
parent
commit
17306d5139
30 changed files with 697 additions and 804 deletions
  1. +4
    -0
      src/Discord.Net.Core/Cache/ICached.cs
  2. +13
    -0
      src/Discord.Net.Core/Cache/Models/IEntityModel.cs
  3. +1
    -1
      src/Discord.Net.Core/Cache/Models/Presense/IPresenceModel.cs
  4. +2
    -3
      src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs
  5. +15
    -0
      src/Discord.Net.Core/Cache/Models/Users/IThreadMemberModel.cs
  6. +1
    -2
      src/Discord.Net.Core/Cache/Models/Users/IUserModel.cs
  7. +2
    -2
      src/Discord.Net.Rest/API/Common/GuildMember.cs
  8. +3
    -0
      src/Discord.Net.Rest/API/Common/Presence.cs
  9. +6
    -4
      src/Discord.Net.Rest/API/Common/ThreadMember.cs
  10. +2
    -2
      src/Discord.Net.Rest/API/Common/User.cs
  11. +59
    -59
      src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs
  12. +17
    -23
      src/Discord.Net.WebSocket/Cache/ICacheProvider.cs
  13. +267
    -88
      src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs
  14. +11
    -3
      src/Discord.Net.WebSocket/ClientStateManager.cs
  15. +46
    -44
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  16. +6
    -6
      src/Discord.Net.WebSocket/DiscordSocketConfig.cs
  17. +5
    -31
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  18. +6
    -24
      src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs
  19. +14
    -19
      src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs
  20. +33
    -32
      src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs
  21. +12
    -1
      src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs
  22. +26
    -17
      src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs
  23. +112
    -68
      src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs
  24. +5
    -5
      src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs
  25. +24
    -15
      src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs
  26. +5
    -4
      src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs
  27. +0
    -21
      src/Discord.Net.WebSocket/Extensions/StateExtensions.cs
  28. +0
    -252
      src/Discord.Net.WebSocket/State/DefaultStateProvider.cs
  29. +0
    -25
      src/Discord.Net.WebSocket/State/IStateProvider.cs
  30. +0
    -53
      src/Discord.Net.WebSocket/State/StateBehavior.cs

+ 4
- 0
src/Discord.Net.Core/Cache/ICached.cs View File

@@ -8,6 +8,10 @@ namespace Discord
{ {
internal interface ICached<TType> internal interface ICached<TType>
{ {
void Update(TType model);

TType ToModel(); TType ToModel();

TResult ToModel<TResult>() where TResult : TType, new();
} }
} }

+ 13
- 0
src/Discord.Net.Core/Cache/Models/IEntityModel.cs View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
public interface IEntityModel<TId> where TId : IEquatable<TId>
{
TId Id { get; set; }
}
}

+ 1
- 1
src/Discord.Net.Core/Cache/Models/Presense/IPresenceModel.cs View File

@@ -6,7 +6,7 @@ using System.Threading.Tasks;


namespace Discord namespace Discord
{ {
public interface IPresenceModel
public interface IPresenceModel : IEntityModel<ulong>
{ {
ulong UserId { get; set; } ulong UserId { get; set; }
ulong? GuildId { get; set; } ulong? GuildId { get; set; }


+ 2
- 3
src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs View File

@@ -6,10 +6,9 @@ using System.Threading.Tasks;


namespace Discord namespace Discord
{ {
public interface IMemberModel
public interface IMemberModel : IEntityModel<ulong>
{ {
IUserModel User { get; set; }

//IUserModel User { get; set; }
string Nickname { get; set; } string Nickname { get; set; }
string GuildAvatar { get; set; } string GuildAvatar { get; set; }
ulong[] Roles { get; set; } ulong[] Roles { get; set; }


+ 15
- 0
src/Discord.Net.Core/Cache/Models/Users/IThreadMemberModel.cs View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
public interface IThreadMemberModel : IEntityModel<ulong>
{
ulong? ThreadId { get; set; }
ulong? UserId { get; set; }
DateTimeOffset JoinedAt { get; set; }
}
}

+ 1
- 2
src/Discord.Net.Core/Cache/Models/Users/IUserModel.cs View File

@@ -6,9 +6,8 @@ using System.Threading.Tasks;


namespace Discord namespace Discord
{ {
public interface IUserModel
public interface IUserModel : IEntityModel<ulong>
{ {
ulong Id { get; set; }
string Username { get; set; } string Username { get; set; }
string Discriminator { get; set; } string Discriminator { get; set; }
bool? IsBot { get; set; } bool? IsBot { get; set; }


+ 2
- 2
src/Discord.Net.Rest/API/Common/GuildMember.cs View File

@@ -63,8 +63,8 @@ namespace Discord.API
get => TimedOutUntil.GetValueOrDefault(); set => throw new NotSupportedException(); get => TimedOutUntil.GetValueOrDefault(); set => throw new NotSupportedException();
} }


IUserModel IMemberModel.User {
get => User; set => throw new NotSupportedException();
ulong IEntityModel<ulong>.Id {
get => User.Id; set => throw new NotSupportedException();
} }
} }
} }

+ 3
- 0
src/Discord.Net.Rest/API/Common/Presence.cs View File

@@ -49,5 +49,8 @@ namespace Discord.API
IActivityModel[] IPresenceModel.Activities { IActivityModel[] IPresenceModel.Activities {
get => Activities.ToArray(); set => throw new NotSupportedException(); get => Activities.ToArray(); set => throw new NotSupportedException();
} }
ulong IEntityModel<ulong>.Id {
get => User.Id; set => throw new NotSupportedException();
}
} }
} }

+ 6
- 4
src/Discord.Net.Rest/API/Common/ThreadMember.cs View File

@@ -3,10 +3,10 @@ using System;


namespace Discord.API namespace Discord.API
{ {
internal class ThreadMember
internal class ThreadMember : IThreadMemberModel
{ {
[JsonProperty("id")] [JsonProperty("id")]
public Optional<ulong> Id { get; set; }
public Optional<ulong> ThreadId { get; set; }


[JsonProperty("user_id")] [JsonProperty("user_id")]
public Optional<ulong> UserId { get; set; } public Optional<ulong> UserId { get; set; }
@@ -14,7 +14,9 @@ namespace Discord.API
[JsonProperty("join_timestamp")] [JsonProperty("join_timestamp")]
public DateTimeOffset JoinTimestamp { get; set; } public DateTimeOffset JoinTimestamp { get; set; }


[JsonProperty("flags")]
public int Flags { get; set; } // No enum type (yet?)
ulong? IThreadMemberModel.ThreadId { get => ThreadId.ToNullable(); set => throw new NotSupportedException(); }
ulong? IThreadMemberModel.UserId { get => UserId.ToNullable(); set => throw new NotSupportedException(); }
DateTimeOffset IThreadMemberModel.JoinedAt { get => JoinTimestamp; set => throw new NotSupportedException(); }
ulong IEntityModel<ulong>.Id { get => UserId.GetValueOrDefault(0); set => throw new NotSupportedException(); }
} }
} }

+ 2
- 2
src/Discord.Net.Rest/API/Common/User.cs View File

@@ -43,10 +43,10 @@ namespace Discord.API
get => Avatar.GetValueOrDefault(); set => throw new NotSupportedException(); get => Avatar.GetValueOrDefault(); set => throw new NotSupportedException();
} }


ulong IUserModel.Id
ulong IEntityModel<ulong>.Id
{ {
get => Id; get => Id;
set => throw new NotSupportedException(); set => throw new NotSupportedException();
}
}
} }
} }

+ 59
- 59
src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs View File

@@ -9,74 +9,74 @@ namespace Discord.WebSocket
{ {
public class DefaultConcurrentCacheProvider : ICacheProvider public class DefaultConcurrentCacheProvider : ICacheProvider
{ {
private readonly ConcurrentDictionary<ulong, IUserModel> _users;
private readonly ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, IMemberModel>> _members;
private readonly ConcurrentDictionary<ulong, IPresenceModel> _presense;
private readonly ConcurrentDictionary<Type, object> _storeCache = new();
private readonly ConcurrentDictionary<object, object> _subStoreCache = new();


private ValueTask CompletedValueTask => new ValueTask(Task.CompletedTask).Preserve();
public DefaultConcurrentCacheProvider(int defaultConcurrency, int defaultCapacity)
private class DefaultEntityStore<TModel, TId> : IEntityStore<TModel, TId>
where TModel : IEntityModel<TId>
where TId : IEquatable<TId>
{ {
_users = new(defaultConcurrency, defaultCapacity);
_members = new(defaultConcurrency, defaultCapacity);
_presense = new(defaultConcurrency, defaultCapacity);
}
private ConcurrentDictionary<TId, TModel> _cache;


public ValueTask AddOrUpdateUserAsync(IUserModel model, CacheRunMode mode)
{
_users.AddOrUpdate(model.Id, model, (_, __) => model);
return CompletedValueTask;
}
public ValueTask AddOrUpdateMemberAsync(IMemberModel model, ulong guildId, CacheRunMode mode)
{
var guildMemberCache = _members.GetOrAdd(guildId, (_) => new ConcurrentDictionary<ulong, IMemberModel>());
guildMemberCache.AddOrUpdate(model.User.Id, model, (_, __) => model);
return CompletedValueTask;
}
public ValueTask<IMemberModel> GetMemberAsync(ulong id, ulong guildId, CacheRunMode mode)
=> new ValueTask<IMemberModel>(_members.FirstOrDefault(x => x.Key == guildId).Value?.FirstOrDefault(x => x.Key == id).Value);
public DefaultEntityStore(ConcurrentDictionary<TId, TModel> cache)
{
_cache = cache;
}


public ValueTask<IEnumerable<IMemberModel>> GetMembersAsync(ulong guildId, CacheRunMode mode)
{
if(_members.TryGetValue(guildId, out var inner))
return new ValueTask<IEnumerable<IMemberModel>>(inner.ToArray().Select(x => x.Value)); // ToArray here is important before .Select due to concurrency
return new ValueTask<IEnumerable<IMemberModel>>(Array.Empty<IMemberModel>());
}
public ValueTask<IUserModel> GetUserAsync(ulong id, CacheRunMode mode)
{
if (_users.TryGetValue(id, out var result))
return new ValueTask<IUserModel>(result);
return new ValueTask<IUserModel>((IUserModel)null);
}
public ValueTask<IEnumerable<IUserModel>> GetUsersAsync(CacheRunMode mode)
=> new ValueTask<IEnumerable<IUserModel>>(_users.ToArray().Select(x => x.Value));
public ValueTask RemoveMemberAsync(ulong id, ulong guildId, CacheRunMode mode)
{
if (_members.TryGetValue(guildId, out var inner))
inner.TryRemove(id, out var _);
return CompletedValueTask;
}
public ValueTask RemoveUserAsync(ulong id, CacheRunMode mode)
{
_members.TryRemove(id, out var _);
return CompletedValueTask;
}
public ValueTask AddOrUpdateAsync(TModel model, CacheRunMode runmode)
{
_cache.AddOrUpdate(model.Id, model, (_, __) => model);
return default;
}


public ValueTask<IPresenceModel> GetPresenceAsync(ulong userId, CacheRunMode runmode)
{
if (_presense.TryGetValue(userId, out var presense))
return new ValueTask<IPresenceModel>(presense);
return new ValueTask<IPresenceModel>((IPresenceModel)null);
public ValueTask AddOrUpdateBatchAsync(IEnumerable<TModel> models, CacheRunMode runmode)
{
foreach (var model in models)
_cache.AddOrUpdate(model.Id, model, (_, __) => model);
return default;
}

public IAsyncEnumerable<TModel> GetAllAsync(CacheRunMode runmode)
{
var coll = _cache.Select(x => x.Value).GetEnumerator();
return AsyncEnumerable.Create((_) => AsyncEnumerator.Create(
() => new ValueTask<bool>(coll.MoveNext()),
() => coll.Current,
() => new ValueTask()));
}
public ValueTask<TModel> GetAsync(TId id, CacheRunMode runmode)
{
if (_cache.TryGetValue(id, out var model))
return new ValueTask<TModel>(model);
return default;
}
public ValueTask RemoveAsync(TId id, CacheRunMode runmode)
{
_cache.TryRemove(id, out _);
return default;
}

public ValueTask PurgeAllAsync(CacheRunMode runmode)
{
_cache.Clear();
return default;
}
} }
public ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresenceModel presense, CacheRunMode runmode)

public virtual ValueTask<IEntityStore<TModel, TId>> GetStoreAsync<TModel, TId>()
where TModel : IEntityModel<TId>
where TId : IEquatable<TId>
{ {
_presense.AddOrUpdate(userId, presense, (_, __) => presense);
return CompletedValueTask;
var store = _storeCache.GetOrAdd(typeof(TModel), (_) => new DefaultEntityStore<TModel, TId>(new ConcurrentDictionary<TId, TModel>()));
return new ValueTask<IEntityStore<TModel, TId>>((IEntityStore<TModel, TId>)store);
} }
public ValueTask RemovePresenseAsync(ulong userId, CacheRunMode runmode)

public virtual ValueTask<IEntityStore<TModel, TId>> GetSubStoreAsync<TModel, TId>(TId parentId)
where TModel : IEntityModel<TId>
where TId : IEquatable<TId>
{ {
_presense.TryRemove(userId, out var _);
return CompletedValueTask;
var store = _subStoreCache.GetOrAdd(parentId, (_) => new DefaultEntityStore<TModel, TId>(new ConcurrentDictionary<TId, TModel>()));
return new ValueTask<IEntityStore<TModel, TId>>((IEntityStore<TModel, TId>)store);
} }
} }
} }

+ 17
- 23
src/Discord.Net.WebSocket/Cache/ICacheProvider.cs View File

@@ -8,30 +8,24 @@ namespace Discord.WebSocket
{ {
public interface ICacheProvider public interface ICacheProvider
{ {
#region Users
ValueTask<IEntityStore<TModel, TId>> GetStoreAsync<TModel, TId>()
where TModel : IEntityModel<TId>
where TId : IEquatable<TId>;


ValueTask<IUserModel> GetUserAsync(ulong id, CacheRunMode runmode);
ValueTask<IEnumerable<IUserModel>> GetUsersAsync(CacheRunMode runmode);
ValueTask AddOrUpdateUserAsync(IUserModel model, CacheRunMode runmode);
ValueTask RemoveUserAsync(ulong id, CacheRunMode runmode);

#endregion

#region Members

ValueTask<IMemberModel> GetMemberAsync(ulong id, ulong guildId, CacheRunMode runmode);
ValueTask<IEnumerable<IMemberModel>> GetMembersAsync(ulong guildId, CacheRunMode runmode);
ValueTask AddOrUpdateMemberAsync(IMemberModel model, ulong guildId, CacheRunMode runmode);
ValueTask RemoveMemberAsync(ulong id, ulong guildId, CacheRunMode runmode);

#endregion

#region Presence

ValueTask<IPresenceModel> GetPresenceAsync(ulong userId, CacheRunMode runmode);
ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresenceModel model, CacheRunMode runmode);
ValueTask RemovePresenseAsync(ulong userId, CacheRunMode runmode);
ValueTask<IEntityStore<TModel, TId>> GetSubStoreAsync<TModel, TId>(TId parentId)
where TModel : IEntityModel<TId>
where TId : IEquatable<TId>;
}


#endregion
public interface IEntityStore<TModel, TId>
where TModel : IEntityModel<TId>
where TId : IEquatable<TId>
{
ValueTask<TModel> GetAsync(TId id, CacheRunMode runmode);
IAsyncEnumerable<TModel> GetAllAsync(CacheRunMode runmode);
ValueTask AddOrUpdateAsync(TModel model, CacheRunMode runmode);
ValueTask AddOrUpdateBatchAsync(IEnumerable<TModel> models, CacheRunMode runmode);
ValueTask RemoveAsync(TId id, CacheRunMode runmode);
ValueTask PurgeAllAsync(CacheRunMode runmode);
} }
} }

+ 267
- 88
src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs View File

@@ -1,163 +1,342 @@
using Discord.Rest;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;


namespace Discord.WebSocket namespace Discord.WebSocket
{ {
internal class CacheWeakReference<T> : WeakReference
internal class CacheReference<TType> where TType : class
{ {
public new T Target { get => (T)base.Target; set => base.Target = value; }
public CacheWeakReference(T target)
: base(target, false)
public WeakReference<TType> Reference { get; }

public bool CanRelease
=> !Reference.TryGetTarget(out _) || _referenceCount <= 0;

private int _referenceCount;

private readonly object _lock = new object();

public CacheReference(TType value)
{ {
Reference = new(value);
_referenceCount = 1;
}


public bool TryObtainReference(out TType reference)
{
if (Reference.TryGetTarget(out reference))
{
Interlocked.Increment(ref _referenceCount);
return true;
}
return false;
} }


public bool TryGetTarget(out T target)
public void ReleaseReference()
{ {
target = Target;
return IsAlive;
lock (_lock)
{
if (_referenceCount > 0)
_referenceCount--;
}
} }
} }

internal partial class ClientStateManager
internal class ReferenceStore<TEntity, TModel, TId, ISharedEntity>
where TEntity : class, ICached<TModel>, ISharedEntity
where TModel : IEntityModel<TId>
where TId : IEquatable<TId>
where ISharedEntity : class
{ {
private readonly ConcurrentDictionary<ulong, CacheWeakReference<SocketGlobalUser>> _userReferences = new();
private readonly ConcurrentDictionary<(ulong GuildId, ulong UserId), CacheWeakReference<SocketGuildUser>> _memberReferences = new();


#region Helpers

private void EnsureSync(ValueTask vt)
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 readonly bool _allowSyncWaits;
private readonly object _lock = new();

public ReferenceStore(ICacheProvider cacheProvider, Func<TModel, TEntity> entityBuilder, Func<TId, RequestOptions, Task<ISharedEntity>> restLookup, bool allowSyncWaits)
{ {
if (!vt.IsCompleted)
throw new NotSupportedException($"Cannot use async context for value task lookup");
_allowSyncWaits = allowSyncWaits;
_cacheProvider = cacheProvider;
_entityBuilder = entityBuilder;
_restLookup = restLookup;
} }


#endregion
internal void ClearDeadReferences()
{
lock (_lock)
{
var references = _references.Where(x => x.Value.CanRelease).ToArray();
foreach (var reference in references)
_references.TryRemove(reference.Key, out _);
}
}


#region Global users
internal void RemoveReferencedGlobalUser(ulong id)
private TResult RunOrThrowValueTask<TResult>(ValueTask<TResult> t)
{ {
Console.WriteLine("Global user untracked");
_userReferences.TryRemove(id, out _);
if (_allowSyncWaits)
{
return t.GetAwaiter().GetResult();
}
else if (t.IsCompleted)
return t.Result;
else
throw new InvalidOperationException("Cannot run asynchronous value task in synchronous context");
} }


private void TrackGlobalUser(ulong id, SocketGlobalUser user)
private void RunOrThrowValueTask(ValueTask t)
{ {
if (user != null)
if (_allowSyncWaits)
{ {
_userReferences.TryAdd(id, new CacheWeakReference<SocketGlobalUser>(user));
t.GetAwaiter().GetResult();
} }
else if (!t.IsCompleted)
throw new InvalidOperationException("Cannot run asynchronous value task in synchronous context");
} }


internal ValueTask<IUser> GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null)
=> _state.GetUserAsync(id, mode.ToBehavior(), options);
public async ValueTask InitializeAsync()
{
_store ??= await _cacheProvider.GetStoreAsync<TModel, TId>().ConfigureAwait(false);
}

public async ValueTask InitializeAsync(TId parentId)
{
_store ??= await _cacheProvider.GetSubStoreAsync<TModel, TId>(parentId).ConfigureAwait(false);
}


internal SocketGlobalUser GetUser(ulong id)
private bool TryGetReference(TId id, out TEntity entity)
{ {
if (_userReferences.TryGetValue(id, out var userRef) && userRef.TryGetTarget(out var user))
return user;
entity = null;
return _references.TryGetValue(id, out var reference) && reference.TryObtainReference(out entity);
}

public TEntity Get(TId id)
{
if(TryGetReference(id, out var entity))
{
return entity;
}


user = (SocketGlobalUser)_state.GetUserAsync(id, StateBehavior.SyncOnly).Result;
var model = RunOrThrowValueTask(_store.GetAsync(id, CacheRunMode.Sync));


if(user != null)
TrackGlobalUser(id, user);
if (model != null)
{
entity = _entityBuilder(model);
_references.TryAdd(id, new CacheReference<TEntity>(entity));
return entity;
}


return user;
return null;
} }


internal SocketGlobalUser GetOrAddUser(ulong id, Func<ulong, SocketGlobalUser> userFactory)
public async ValueTask<ISharedEntity> GetAsync(TId id, CacheMode mode, RequestOptions options = null)
{ {
if (_userReferences.TryGetValue(id, out var userRef) && userRef.TryGetTarget(out var user))
return user;
if (TryGetReference(id, out var entity))
{
return entity;
}

var model = await _store.GetAsync(id, CacheRunMode.Async).ConfigureAwait(false);


user = GetUser(id);
if (model != null)
{
entity = _entityBuilder(model);
_references.TryAdd(id, new CacheReference<TEntity>(entity));
return entity;
}


if (user == null)
if(mode == CacheMode.AllowDownload)
{ {
user ??= userFactory(id);
_state.AddOrUpdateUserAsync(user);
TrackGlobalUser(id, user);
return await _restLookup(id, options).ConfigureAwait(false);
} }


return user;
return null;
} }


internal void RemoveUser(ulong id)
public IEnumerable<TEntity> GetAll()
{ {
_state.RemoveUserAsync(id);
var models = RunOrThrowValueTask(_store.GetAllAsync(CacheRunMode.Sync).ToArrayAsync());
return models.Select(x =>
{
var entity = _entityBuilder(x);
_references.TryAdd(x.Id, new CacheReference<TEntity>(entity));
return entity;
});
} }
#endregion


#region GuildUsers
private void TrackMember(ulong userId, ulong guildId, SocketGuildUser user)
public async IAsyncEnumerable<TEntity> GetAllAsync()
{ {
if(user != null)
await foreach(var model in _store.GetAllAsync(CacheRunMode.Async))
{ {
_memberReferences.TryAdd((guildId, userId), new CacheWeakReference<SocketGuildUser>(user));
var entity = _entityBuilder(model);
_references.TryAdd(model.Id, new CacheReference<TEntity>(entity));
yield return entity;
} }
} }
internal void RemovedReferencedMember(ulong userId, ulong guildId)
=> _memberReferences.TryRemove((guildId, userId), out _);


internal ValueTask<IGuildUser> GetMemberAsync(ulong userId, ulong guildId, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null)
=> _state.GetMemberAsync(guildId, userId, mode.ToBehavior(), options);
public TEntity GetOrAdd(TId id, Func<TId, TModel> valueFactory)
{
var entity = Get(id);
if (entity != null)
return entity;

var model = valueFactory(id);
AddOrUpdate(model);
return _entityBuilder(model);
}

public async ValueTask<TEntity> GetOrAddAsync(TId id, Func<TId, TModel> valueFactory)
{
var entity = await GetAsync(id, CacheMode.CacheOnly).ConfigureAwait(false);
if (entity != null)
return (TEntity)entity;

var model = valueFactory(id);
await AddOrUpdateAsync(model);
return _entityBuilder(model);
}


internal SocketGuildUser GetMember(ulong userId, ulong guildId)
public void AddOrUpdate(TModel model)
{ {
if (_memberReferences.TryGetValue((guildId, userId), out var memberRef) && memberRef.TryGetTarget(out var member))
return member;
member = (SocketGuildUser)_state.GetMemberAsync(guildId, userId, StateBehavior.SyncOnly).Result;
if(member != null)
TrackMember(userId, guildId, member);
return member;
RunOrThrowValueTask(_store.AddOrUpdateAsync(model, CacheRunMode.Sync));
if (TryGetReference(model.Id, out var reference))
reference.Update(model);
} }


internal SocketGuildUser GetOrAddMember(ulong userId, ulong guildId, Func<ulong, ulong, SocketGuildUser> memberFactory)
public ValueTask AddOrUpdateAsync(TModel model)
{ {
if (_memberReferences.TryGetValue((guildId, userId), out var memberRef) && memberRef.TryGetTarget(out var member))
return member;
if (TryGetReference(model.Id, out var reference))
reference.Update(model);
return _store.AddOrUpdateAsync(model, CacheRunMode.Async);
}


member = GetMember(userId, guildId);
public void Remove(TId id)
{
RunOrThrowValueTask(_store.RemoveAsync(id, CacheRunMode.Sync));
_references.TryRemove(id, out _);
}


if (member == null)
{
member ??= memberFactory(userId, guildId);
TrackMember(userId, guildId, member);
Task.Run(async () => await _state.AddOrUpdateMemberAsync(guildId, member)); // can run async, think of this as fire and forget.
}
public ValueTask RemoveAsync(TId id)
{
_references.TryRemove(id, out _);
return _store.RemoveAsync(id, CacheRunMode.Async);
}


return member;
public void Purge()
{
RunOrThrowValueTask(_store.PurgeAllAsync(CacheRunMode.Sync));
_references.Clear();
} }


internal IEnumerable<IGuildUser> GetMembers(ulong guildId)
=> _state.GetMembersAsync(guildId, StateBehavior.SyncOnly).Result;
public ValueTask PurgeAsync()
{
_references.Clear();
return _store.PurgeAllAsync(CacheRunMode.Async);
}
}


internal void AddOrUpdateMember(ulong guildId, SocketGuildUser user)
=> EnsureSync(_state.AddOrUpdateMemberAsync(guildId, user));
internal partial class ClientStateManager
{
public ReferenceStore<SocketGlobalUser, IUserModel, ulong, IUser> UserStore;
public ReferenceStore<SocketPresence, IPresenceModel, ulong, IPresence> PresenceStore;
private ConcurrentDictionary<ulong, ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser>> _memberStores;
private ConcurrentDictionary<ulong, ReferenceStore<SocketThreadUser, IThreadMemberModel, ulong, IThreadUser>> _threadMemberStores;


internal void RemoveMember(ulong userId, ulong guildId)
=> EnsureSync(_state.RemoveMemberAsync(guildId, userId));
private SemaphoreSlim _memberStoreLock;
private SemaphoreSlim _threadMemberLock;


#endregion
private void CreateStores()
{
UserStore = new ReferenceStore<SocketGlobalUser, IUserModel, ulong, IUser>(
_cacheProvider,
m => SocketGlobalUser.Create(_client, m),
async (id, options) => await _client.Rest.GetUserAsync(id, options).ConfigureAwait(false),
AllowSyncWaits);

PresenceStore = new ReferenceStore<SocketPresence, IPresenceModel, ulong, IPresence>(
_cacheProvider,
m => SocketPresence.Create(m),
(id, options) => Task.FromResult<IPresence>(null),
AllowSyncWaits);

_memberStores = new();
_threadMemberStores = new();

_threadMemberLock = new(1, 1);
_memberStoreLock = new(1,1);
}


#region Presence
internal void AddOrUpdatePresence(SocketPresence presence)
public void ClearDeadReferences()
{ {
EnsureSync(_state.AddOrUpdatePresenseAsync(presence.UserId, presence, StateBehavior.SyncOnly));
UserStore.ClearDeadReferences();
PresenceStore.ClearDeadReferences();
} }


internal SocketPresence GetPresence(ulong userId)
public async ValueTask InitializeAsync()
{ {
if (_state.GetPresenceAsync(userId, StateBehavior.SyncOnly).Result is not SocketPresence socketPresence)
throw new NotSupportedException("Cannot use non-socket entity for presence");
await UserStore.InitializeAsync();
await PresenceStore.InitializeAsync();
}

public bool TryGetMemberStore(ulong guildId, out ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser> store)
=> _memberStores.TryGetValue(guildId, out store);


return socketPresence;
public async ValueTask<ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser>> GetMemberStoreAsync(ulong guildId)
{
if (_memberStores.TryGetValue(guildId, out var store))
return store;

await _memberStoreLock.WaitAsync().ConfigureAwait(false);

try
{
store = new ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser>(
_cacheProvider,
m => SocketGuildUser.Create(guildId, _client, m),
async (id, options) => await _client.Rest.GetGuildUserAsync(guildId, id, options).ConfigureAwait(false),
AllowSyncWaits);

await store.InitializeAsync(guildId).ConfigureAwait(false);

_memberStores.TryAdd(guildId, store);
return store;
}
finally
{
_memberStoreLock.Release();
}
}

public async Task<ReferenceStore<SocketThreadUser, IThreadMemberModel, ulong, IThreadUser>> GetThreadMemberStoreAsync(ulong threadId, ulong guildId)
{
if (_threadMemberStores.TryGetValue(threadId, out var store))
return store;

await _threadMemberLock.WaitAsync().ConfigureAwait(false);

try
{
store = new ReferenceStore<SocketThreadUser, IThreadMemberModel, ulong, IThreadUser>(
_cacheProvider,
m => SocketThreadUser.Create(_client, guildId, threadId, m),
async (id, options) => await ThreadHelper.GetUserAsync(id, _client.GetChannel(threadId) as SocketThreadChannel, _client, options).ConfigureAwait(false),
AllowSyncWaits);

await store.InitializeAsync().ConfigureAwait(false);

_threadMemberStores.TryAdd(threadId, store);
return store;
}
finally
{
_threadMemberLock.Release();
}
} }
#endregion
} }
} }

+ 11
- 3
src/Discord.Net.WebSocket/ClientStateManager.cs View File

@@ -30,11 +30,17 @@ namespace Discord.WebSocket
_groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel)) _groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel))
.ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count); .ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count);


private readonly IStateProvider _state;
internal bool AllowSyncWaits
=> _client.AllowSynchronousWaiting;


public ClientStateManager(IStateProvider state, int guildCount, int dmChannelCount)
private readonly ICacheProvider _cacheProvider;
private readonly DiscordSocketClient _client;


public ClientStateManager(DiscordSocketClient client, int guildCount, int dmChannelCount)
{ {
_state = state;
_client = client;
_cacheProvider = client.CacheProvider;
double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount;
double estimatedUsersCount = guildCount * AverageUsersPerGuild; double estimatedUsersCount = guildCount * AverageUsersPerGuild;
_channels = new ConcurrentDictionary<ulong, SocketChannel>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); _channels = new ConcurrentDictionary<ulong, SocketChannel>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier));
@@ -43,6 +49,8 @@ namespace Discord.WebSocket
_users = new ConcurrentDictionary<ulong, SocketGlobalUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); _users = new ConcurrentDictionary<ulong, SocketGlobalUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier));
_groupChannels = new ConcurrentHashSet<ulong>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(10 * CollectionMultiplier)); _groupChannels = new ConcurrentHashSet<ulong>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(10 * CollectionMultiplier));
_commands = new ConcurrentDictionary<ulong, SocketApplicationCommand>(); _commands = new ConcurrentDictionary<ulong, SocketApplicationCommand>();

CreateStores();
} }


internal SocketChannel GetChannel(ulong id) internal SocketChannel GetChannel(ulong id)


+ 46
- 44
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -70,16 +70,17 @@ namespace Discord.WebSocket
internal int TotalShards { get; private set; } internal int TotalShards { get; private set; }
internal int MessageCacheSize { get; private set; } internal int MessageCacheSize { get; private set; }
internal int LargeThreshold { get; private set; } internal int LargeThreshold { get; private set; }
internal ICacheProvider CacheProvider { get; private set; }
internal ClientStateManager StateManager { get; private set; } internal ClientStateManager StateManager { get; private set; }
internal UdpSocketProvider UdpSocketProvider { get; private set; } internal UdpSocketProvider UdpSocketProvider { get; private set; }
internal WebSocketProvider WebSocketProvider { get; private set; } internal WebSocketProvider WebSocketProvider { get; private set; }
internal IStateProvider StateProvider { get; private set; }
internal bool AlwaysDownloadUsers { get; private set; } internal bool AlwaysDownloadUsers { get; private set; }
internal int? HandlerTimeout { get; private set; } internal int? HandlerTimeout { get; private set; }
internal bool AlwaysDownloadDefaultStickers { get; private set; } internal bool AlwaysDownloadDefaultStickers { get; private set; }
internal bool AlwaysResolveStickers { get; private set; } internal bool AlwaysResolveStickers { get; private set; }
internal bool LogGatewayIntentWarnings { get; private set; } internal bool LogGatewayIntentWarnings { get; private set; }
internal bool SuppressUnknownDispatchWarnings { get; private set; } internal bool SuppressUnknownDispatchWarnings { get; private set; }
internal bool AllowSynchronousWaiting { get; private set; }
internal new DiscordSocketApiClient ApiClient => base.ApiClient; internal new DiscordSocketApiClient ApiClient => base.ApiClient;
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<SocketGuild> Guilds => StateManager.Guilds; public override IReadOnlyCollection<SocketGuild> Guilds => StateManager.Guilds;
@@ -155,6 +156,8 @@ namespace Discord.WebSocket
LogGatewayIntentWarnings = config.LogGatewayIntentWarnings; LogGatewayIntentWarnings = config.LogGatewayIntentWarnings;
SuppressUnknownDispatchWarnings = config.SuppressUnknownDispatchWarnings; SuppressUnknownDispatchWarnings = config.SuppressUnknownDispatchWarnings;
HandlerTimeout = config.HandlerTimeout; HandlerTimeout = config.HandlerTimeout;
CacheProvider = config.CacheProvider ?? new DefaultConcurrentCacheProvider();
AllowSynchronousWaiting = config.AllowSynchronousWaiting;
Rest = new DiscordSocketRestClient(config, ApiClient); Rest = new DiscordSocketRestClient(config, ApiClient);
_heartbeatTimes = new ConcurrentQueue<long>(); _heartbeatTimes = new ConcurrentQueue<long>();
_gatewayIntents = config.GatewayIntents; _gatewayIntents = config.GatewayIntents;
@@ -166,7 +169,6 @@ namespace Discord.WebSocket
OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x);
_connection.Connected += () => TimedInvokeAsync(_connectedEvent, nameof(Connected)); _connection.Connected += () => TimedInvokeAsync(_connectedEvent, nameof(Connected));
_connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex); _connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex);
StateProvider = config.StateProvider ?? new DefaultStateProvider(_gatewayLogger, config.CacheProvider ?? new DefaultConcurrentCacheProvider(5, 50), this, config.DefaultStateBehavior);


_nextAudioId = 1; _nextAudioId = 1;
_shardedClient = shardedClient; _shardedClient = shardedClient;
@@ -206,10 +208,14 @@ namespace Discord.WebSocket
#region State #region State


public ValueTask<IUser> GetUserAsync(ulong id, CacheMode cacheMode = CacheMode.AllowDownload, RequestOptions options = null) public ValueTask<IUser> GetUserAsync(ulong id, CacheMode cacheMode = CacheMode.AllowDownload, RequestOptions options = null)
=> StateManager.GetUserAsync(id, cacheMode, options);
=> StateManager.UserStore.GetAsync(id, cacheMode, options);


public ValueTask<IGuildUser> GetGuildUserAsync(ulong userId, ulong guildId, CacheMode cacheMode = CacheMode.AllowDownload, RequestOptions options = null) public ValueTask<IGuildUser> GetGuildUserAsync(ulong userId, ulong guildId, CacheMode cacheMode = CacheMode.AllowDownload, RequestOptions options = null)
=> StateManager.GetMemberAsync(userId, guildId, cacheMode, options);
{
if (StateManager.TryGetMemberStore(guildId, out var store))
return store.GetAsync(userId, cacheMode, options);
return ValueTask.FromResult<IGuildUser>(null);
}


#endregion #endregion


@@ -409,7 +415,7 @@ namespace Discord.WebSocket


/// <inheritdoc /> /// <inheritdoc />
public override SocketUser GetUser(ulong id) public override SocketUser GetUser(ulong id)
=> StateManager.GetUser(id);
=> StateManager.UserStore.Get(id);
/// <inheritdoc /> /// <inheritdoc />
public override SocketUser GetUser(string username, string discriminator) public override SocketUser GetUser(string username, string discriminator)
=> StateManager.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); => StateManager.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username);
@@ -496,23 +502,18 @@ namespace Discord.WebSocket
public void PurgeUserCache() => StateManager.PurgeUsers(); public void PurgeUserCache() => StateManager.PurgeUsers();
internal SocketGlobalUser GetOrCreateUser(ClientStateManager state, IUserModel model) internal SocketGlobalUser GetOrCreateUser(ClientStateManager state, IUserModel model)
{ {
return state.GetOrAddUser(model.Id, x => SocketGlobalUser.Create(this, state, model));
return state.UserStore.GetOrAdd(model.Id, x => model);
} }
internal SocketUser GetOrCreateTemporaryUser(ClientStateManager state, Discord.API.User model) internal SocketUser GetOrCreateTemporaryUser(ClientStateManager state, Discord.API.User model)
{ {
return state.GetUser(model.Id) ?? (SocketUser)SocketUnknownUser.Create(this, state, model);
return state.UserStore.Get(model.Id) ?? (SocketUser)SocketUnknownUser.Create(this, model);
} }
internal SocketGlobalUser GetOrCreateSelfUser(ClientStateManager state, ICurrentUserModel model) internal SocketGlobalUser GetOrCreateSelfUser(ClientStateManager state, ICurrentUserModel model)
{ {
return state.GetOrAddUser(model.Id, x =>
{
var user = SocketGlobalUser.Create(this, state, model);
user.GlobalUser.AddRef();
return user;
});
return state.UserStore.GetOrAdd(model.Id, x => model);
} }
internal void RemoveUser(ulong id) internal void RemoveUser(ulong id)
=> StateManager.RemoveUser(id);
=> StateManager.UserStore.Remove(id);


/// <inheritdoc/> /// <inheritdoc/>
public override async Task<SocketSticker> GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) public override async Task<SocketSticker> GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null)
@@ -689,7 +690,7 @@ namespace Discord.WebSocket
if (CurrentUser == null) if (CurrentUser == null)
return; return;
var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null;
StateManager.AddOrUpdatePresence(new SocketPresence(Status, null, activities));
await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(Status, null, activities).ToModel()).ConfigureAwait(false);


var presence = BuildCurrentStatus() ?? (UserStatus.Online, false, null, null); var presence = BuildCurrentStatus() ?? (UserStatus.Online, false, null, null);


@@ -813,6 +814,7 @@ namespace Discord.WebSocket
int latency = (int)(Environment.TickCount - time); int latency = (int)(Environment.TickCount - time);
int before = Latency; int before = Latency;
Latency = latency; Latency = latency;
StateManager?.ClearDeadReferences();


await TimedInvokeAsync(_latencyUpdatedEvent, nameof(LatencyUpdated), before, latency).ConfigureAwait(false); await TimedInvokeAsync(_latencyUpdatedEvent, nameof(LatencyUpdated), before, latency).ConfigureAwait(false);
} }
@@ -859,21 +861,26 @@ namespace Discord.WebSocket
await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false);


var data = (payload as JToken).ToObject<ReadyEvent>(_serializer); var data = (payload as JToken).ToObject<ReadyEvent>(_serializer);
var state = new ClientStateManager(StateProvider, data.Guilds.Length, data.PrivateChannels.Length);

var state = new ClientStateManager(this, data.Guilds.Length, data.PrivateChannels.Length);
StateManager = state; StateManager = state;
await StateManager.InitializeAsync().ConfigureAwait(false);


var currentUser = SocketSelfUser.Create(this, state, data.User);
var currentUser = SocketSelfUser.Create(this, data.User);
Rest.CreateRestSelfUser(data.User); Rest.CreateRestSelfUser(data.User);

var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null;
StateManager.AddOrUpdatePresence(new SocketPresence(Status, null, activities));
await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(Status, null, activities).ToModel()).ConfigureAwait(false);

ApiClient.CurrentUserId = currentUser.Id; ApiClient.CurrentUserId = currentUser.Id;
ApiClient.CurrentApplicationId = data.Application.Id; ApiClient.CurrentApplicationId = data.Application.Id;
Rest.CurrentUser = RestSelfUser.Create(this, data.User); Rest.CurrentUser = RestSelfUser.Create(this, data.User);

int unavailableGuilds = 0; int unavailableGuilds = 0;
for (int i = 0; i < data.Guilds.Length; i++) for (int i = 0; i < data.Guilds.Length; i++)
{ {
var model = data.Guilds[i]; var model = data.Guilds[i];
var guild = AddGuild(model, state);
var guild = await AddGuildAsync(model).ConfigureAwait(false);
if (!guild.IsAvailable) if (!guild.IsAvailable)
unavailableGuilds++; unavailableGuilds++;
else else
@@ -950,6 +957,7 @@ namespace Discord.WebSocket
if (guild != null) if (guild != null)
{ {
guild.Update(StateManager, data); guild.Update(StateManager, data);
await guild.UpdateCacheAsync(data).ConfigureAwait(false);


if (_unavailableGuildCount != 0) if (_unavailableGuildCount != 0)
_unavailableGuildCount--; _unavailableGuildCount--;
@@ -971,7 +979,7 @@ namespace Discord.WebSocket
{ {
await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_CREATE)").ConfigureAwait(false); await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_CREATE)").ConfigureAwait(false);


var guild = AddGuild(data, StateManager);
var guild = await AddGuildAsync(data).ConfigureAwait(false);
if (guild != null) if (guild != null)
{ {
await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false);
@@ -1290,13 +1298,13 @@ namespace Discord.WebSocket
if (user != null) if (user != null)
{ {
var before = user.Clone(); var before = user.Clone();
if (user.GlobalUser.Update(StateManager, data.User))
if (user.GlobalUser.Value.Update(data.User)) // TODO: update cache only and have lazy like support for events.
{ {
//Global data was updated, trigger UserUpdated //Global data was updated, trigger UserUpdated
await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before.GlobalUser, user).ConfigureAwait(false);
await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before.GlobalUser.Value, user).ConfigureAwait(false);
} }


user.Update(StateManager, data);
user.Update(data);


var cacheableBefore = new Cacheable<SocketGuildUser, ulong>(before, user.Id, true, () => null); var cacheableBefore = new Cacheable<SocketGuildUser, ulong>(before, user.Id, true, () => null);
await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false);
@@ -1332,12 +1340,12 @@ namespace Discord.WebSocket
return; return;
} }


user ??= StateManager.GetUser(data.User.Id);
user ??= (SocketUser)await StateManager.UserStore.GetAsync(data.User.Id, CacheMode.CacheOnly).ConfigureAwait(false);


if (user != null) if (user != null)
user.Update(StateManager, data.User);
user.Update(data.User);
else else
user = StateManager.GetOrAddUser(data.User.Id, (x) => SocketGlobalUser.Create(this, StateManager, data.User));
user = StateManager.GetOrAddUser(data.User.Id, (x) => data.User);


await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user).ConfigureAwait(false); await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user).ConfigureAwait(false);
} }
@@ -1957,8 +1965,8 @@ namespace Discord.WebSocket
} }
else else
{ {
var globalBefore = user.GlobalUser.Clone();
if (user.GlobalUser.Update(StateManager, data.User))
var globalBefore = user.GlobalUser.Value.Clone();
if (user.GlobalUser.Value.Update(StateManager, data.User))
{ {
//Global data was updated, trigger UserUpdated //Global data was updated, trigger UserUpdated
await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false);
@@ -1978,7 +1986,7 @@ namespace Discord.WebSocket
var before = user.Presence?.Value?.Clone(); var before = user.Presence?.Value?.Clone();
user.Update(StateManager, data.User); user.Update(StateManager, data.User);
var after = SocketPresence.Create(data); var after = SocketPresence.Create(data);
StateManager.AddOrUpdatePresence(after);
StateManager.AddOrUpdatePresence(data);
await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, after).ConfigureAwait(false); await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, after).ConfigureAwait(false);
} }
break; break;
@@ -2324,7 +2332,7 @@ namespace Discord.WebSocket
} }


SocketUser user = data.User.IsSpecified SocketUser user = data.User.IsSpecified
? StateManager.GetOrAddUser(data.User.Value.Id, (_) => SocketGlobalUser.Create(this, StateManager, data.User.Value))
? StateManager.GetOrAddUser(data.User.Value.Id, (_) => data.User.Value)
: guild?.AddOrUpdateUser(data.Member.Value); // null if the bot scope isn't set, so the guild cannot be retrieved. : guild?.AddOrUpdateUser(data.Member.Value); // null if the bot scope isn't set, so the guild cannot be retrieved.


SocketChannel channel = null; SocketChannel channel = null;
@@ -2579,9 +2587,9 @@ namespace Discord.WebSocket
entity.Update(StateManager, thread); entity.Update(StateManager, thread);
} }


foreach(var member in data.Members.Where(x => x.Id.Value == entity.Id))
foreach(var member in data.Members.Where(x => x.ThreadId.Value == entity.Id))
{ {
var guildMember = guild.GetUser(member.Id.Value);
var guildMember = guild.GetUser(member.ThreadId.Value);


entity.AddOrUpdateThreadMember(member, guildMember); entity.AddOrUpdateThreadMember(member, guildMember);
} }
@@ -2594,11 +2602,11 @@ namespace Discord.WebSocket


var data = (payload as JToken).ToObject<ThreadMember>(_serializer); var data = (payload as JToken).ToObject<ThreadMember>(_serializer);


var thread = (SocketThreadChannel)StateManager.GetChannel(data.Id.Value);
var thread = (SocketThreadChannel)StateManager.GetChannel(data.ThreadId.Value);


if (thread == null) if (thread == null)
{ {
await UnknownChannelAsync(type, data.Id.Value);
await UnknownChannelAsync(type, data.ThreadId.Value);
return; return;
} }


@@ -2948,10 +2956,11 @@ namespace Discord.WebSocket
await ApiClient.SendGuildSyncAsync(guildIds).ConfigureAwait(false); await ApiClient.SendGuildSyncAsync(guildIds).ConfigureAwait(false);
} }


internal SocketGuild AddGuild(ExtendedGuild model, ClientStateManager state)
internal async Task<SocketGuild> AddGuildAsync(ExtendedGuild model)
{ {
var guild = SocketGuild.Create(this, state, model);
state.AddGuild(guild);
await StateManager.InitializeGuildStoreAsync(model.Id).ConfigureAwait(false);
var guild = SocketGuild.Create(this, StateManager, model);
StateManager.AddGuild(guild);
if (model.Large) if (model.Large)
_largeGuilds.Enqueue(model.Id); _largeGuilds.Enqueue(model.Id);
return guild; return guild;
@@ -2977,19 +2986,12 @@ namespace Discord.WebSocket
internal ISocketPrivateChannel RemovePrivateChannel(ulong id) internal ISocketPrivateChannel RemovePrivateChannel(ulong id)
{ {
var channel = StateManager.RemoveChannel(id) as ISocketPrivateChannel; var channel = StateManager.RemoveChannel(id) as ISocketPrivateChannel;
if (channel != null)
{
foreach (var recipient in channel.Recipients)
recipient.GlobalUser.RemoveRef(this);
}
return channel; return channel;
} }
internal void RemoveDMChannels() internal void RemoveDMChannels()
{ {
var channels = StateManager.DMChannels; var channels = StateManager.DMChannels;
StateManager.PurgeDMChannels(); StateManager.PurgeDMChannels();
foreach (var channel in channels)
channel.Recipient.GlobalUser.RemoveRef(this);
} }


internal void EnsureGatewayIntent(GatewayIntents intents) internal void EnsureGatewayIntent(GatewayIntents intents)


+ 6
- 6
src/Discord.Net.WebSocket/DiscordSocketConfig.cs View File

@@ -29,7 +29,12 @@ namespace Discord.WebSocket
/// Gets or sets the cache provider to use /// Gets or sets the cache provider to use
/// </summary> /// </summary>
public ICacheProvider CacheProvider { get; set; } public ICacheProvider CacheProvider { get; set; }
public IStateProvider StateProvider { get; set; }

/// <summary>
/// Gets or sets whether or not non-async cache lookups would wait for the task to complete
/// synchronously or to throw.
/// </summary>
public bool AllowSynchronousWaiting { get; set; } = false;


/// <summary> /// <summary>
/// Returns the encoding gateway should use. /// Returns the encoding gateway should use.
@@ -199,11 +204,6 @@ namespace Discord.WebSocket
/// </summary> /// </summary>
public bool SuppressUnknownDispatchWarnings { get; set; } = true; public bool SuppressUnknownDispatchWarnings { get; set; } = true;


/// <summary>
/// Gets or sets the default state behavior clients will use.
/// </summary>
public StateBehavior DefaultStateBehavior { get; set; } = StateBehavior.Default;

/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DiscordSocketConfig"/> class with the default configuration. /// Initializes a new instance of the <see cref="DiscordSocketConfig"/> class with the default configuration.
/// </summary> /// </summary>


+ 5
- 31
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -452,17 +452,8 @@ namespace Discord.WebSocket
} }
_events = events; _events = events;


for (int i = 0; i < model.Members.Length; i++)
{
Discord.StateManager.AddOrUpdateMember(Id, SocketGuildUser.Create(Id, Discord, model.Members[i]));
}
DownloadedMemberCount = model.Members.Length; DownloadedMemberCount = model.Members.Length;


for (int i = 0; i < model.Presences.Length; i++)
{
Discord.StateManager.AddOrUpdatePresence(SocketPresence.Create(model.Presences[i]));
}

MemberCount = model.MemberCount; MemberCount = model.MemberCount;




@@ -553,29 +544,12 @@ namespace Discord.WebSocket
else else
_stickers = new ConcurrentDictionary<ulong, SocketCustomSticker>(ConcurrentHashSet.DefaultConcurrencyLevel, 7); _stickers = new ConcurrentDictionary<ulong, SocketCustomSticker>(ConcurrentHashSet.DefaultConcurrencyLevel, 7);
} }
/*internal void Update(ClientStateManager state, GuildSyncModel model) //TODO remove? userbot related
{
var members = new ConcurrentDictionary<ulong, SocketGuildUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05));
{
for (int i = 0; i < model.Members.Length; i++)
{
var member = SocketGuildUser.Create(this, state, model.Members[i]);
members.TryAdd(member.Id, member);
}
DownloadedMemberCount = members.Count;

for (int i = 0; i < model.Presences.Length; i++)
{
if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member))
member.Update(state, model.Presences[i], true);
}
}
_members = members;


var _ = _syncPromise.TrySetResultAsync(true);
//if (!model.Large)
// _ = _downloaderPromise.TrySetResultAsync(true);
}*/
internal async ValueTask UpdateCacheAsync(ExtendedModel model)
{
await Discord.StateManager.BulkAddOrUpdatePresenceAsync(model.Presences).ConfigureAwait(false);
await Discord.StateManager.BulkAddOrUpdateMembersAsync(Id, model.Members).ConfigureAwait(false);
}


internal void Update(ClientStateManager state, EmojiUpdateModel model) internal void Update(ClientStateManager state, EmojiUpdateModel model)
{ {


+ 6
- 24
src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs View File

@@ -12,45 +12,27 @@ namespace Discord.WebSocket
public override string Username { get; internal set; } public override string Username { get; internal set; }
public override ushort DiscriminatorValue { get; internal set; } public override ushort DiscriminatorValue { get; internal set; }
public override string AvatarId { get; internal set; } public override string AvatarId { get; internal set; }

public override bool IsWebhook => false; public override bool IsWebhook => false;
internal override SocketGlobalUser GlobalUser { get => this; set => throw new NotImplementedException(); }

private readonly object _lockObj = new object();
private ushort _references;


private SocketGlobalUser(DiscordSocketClient discord, ulong id) private SocketGlobalUser(DiscordSocketClient discord, ulong id)
: base(discord, id) : base(discord, id)
{ {


} }
internal static SocketGlobalUser Create(DiscordSocketClient discord, ClientStateManager state, Model model)
internal static SocketGlobalUser Create(DiscordSocketClient discord, Model model)
{ {
var entity = new SocketGlobalUser(discord, model.Id); var entity = new SocketGlobalUser(discord, model.Id);
entity.Update(state, model);
entity.Update(model);
return entity; return entity;
} }


internal void AddRef()
{
checked
{
lock (_lockObj)
_references++;
}
}
internal void RemoveRef(DiscordSocketClient discord)
~SocketGlobalUser() => Discord.StateManager.RemoveReferencedGlobalUser(Id);
public override void Dispose()
{ {
lock (_lockObj)
{
if (--_references <= 0)
discord.RemoveUser(Id);
}
GC.SuppressFinalize(this);
Discord.StateManager.RemoveReferencedGlobalUser(Id);
} }


~SocketGlobalUser() => Discord.StateManager.RemoveReferencedGlobalUser(Id);
public void Dispose() => Discord.StateManager.RemoveReferencedGlobalUser(Id);

private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)";
internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser;
} }


+ 14
- 19
src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs View File

@@ -18,38 +18,33 @@ namespace Discord.WebSocket
/// A <see cref="SocketGroupChannel" /> representing the channel of which the user belongs to. /// A <see cref="SocketGroupChannel" /> representing the channel of which the user belongs to.
/// </returns> /// </returns>
public SocketGroupChannel Channel { get; } public SocketGroupChannel Channel { get; }
/// <inheritdoc />
internal override SocketGlobalUser GlobalUser { get; set; }

/// <inheritdoc />
public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } }
/// <inheritdoc />
public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } }
/// <inheritdoc />
public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } }
/// <inheritdoc />
public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } }
/// <inheritdoc />
internal override Lazy<SocketPresence> Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } }


/// <inheritdoc /> /// <inheritdoc />
public override bool IsWebhook => false; public override bool IsWebhook => false;


internal SocketGroupUser(SocketGroupChannel channel, SocketGlobalUser globalUser)
: base(channel.Discord, globalUser.Id)
internal SocketGroupUser(SocketGroupChannel channel, ulong userId)
: base(channel.Discord, userId)
{ {
Channel = channel; Channel = channel;
GlobalUser = globalUser;
} }
internal static SocketGroupUser Create(SocketGroupChannel channel, ClientStateManager state, Model model)
internal static SocketGroupUser Create(SocketGroupChannel channel, Model model)
{ {
var entity = new SocketGroupUser(channel, channel.Discord.GetOrCreateUser(state, model));
entity.Update(state, model);
var entity = new SocketGroupUser(channel, model.Id);
entity.Update(model);
return entity; return entity;
} }


private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Group)"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Group)";
internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser; internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser;
public override void Dispose()
{
GC.SuppressFinalize(this);

if (GlobalUser.IsValueCreated)
GlobalUser.Value.Dispose();
}
~SocketGroupUser() => Dispose();

#endregion #endregion


#region IVoiceState #region IVoiceState


+ 33
- 32
src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs View File

@@ -25,7 +25,6 @@ namespace Discord.WebSocket
private ImmutableArray<ulong> _roleIds; private ImmutableArray<ulong> _roleIds;
private ulong _guildId; private ulong _guildId;


internal override SocketGlobalUser GlobalUser { get; set; }
/// <summary> /// <summary>
/// Gets the guild the user is in. /// Gets the guild the user is in.
/// </summary> /// </summary>
@@ -43,13 +42,13 @@ namespace Discord.WebSocket
/// <inheritdoc/> /// <inheritdoc/>
public string GuildAvatarId { get; private set; } public string GuildAvatarId { get; private set; }
/// <inheritdoc /> /// <inheritdoc />
public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } }
public override bool IsBot { get { return GlobalUser.Value.IsBot; } internal set { GlobalUser.Value.IsBot = value; } }
/// <inheritdoc /> /// <inheritdoc />
public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } }
public override string Username { get { return GlobalUser.Value.Username; } internal set { GlobalUser.Value.Username = value; } }
/// <inheritdoc /> /// <inheritdoc />
public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } }
public override ushort DiscriminatorValue { get { return GlobalUser.Value.DiscriminatorValue; } internal set { GlobalUser.Value.DiscriminatorValue = value; } }
/// <inheritdoc /> /// <inheritdoc />
public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } }
public override string AvatarId { get { return GlobalUser.Value.AvatarId; } internal set { GlobalUser.Value.AvatarId = value; } }


/// <inheritdoc /> /// <inheritdoc />
public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild.Value, this)); public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild.Value, this));
@@ -137,32 +136,29 @@ namespace Discord.WebSocket
} }
} }


internal SocketGuildUser(ulong guildId, SocketGlobalUser globalUser, DiscordSocketClient client)
: base(client, globalUser.Id)
internal SocketGuildUser(ulong guildId, ulong userId, DiscordSocketClient client)
: base(client, userId)
{ {
_guildId = guildId; _guildId = guildId;
Guild = new Lazy<SocketGuild>(() => client.StateManager.GetGuild(_guildId), System.Threading.LazyThreadSafetyMode.PublicationOnly); Guild = new Lazy<SocketGuild>(() => client.StateManager.GetGuild(_guildId), System.Threading.LazyThreadSafetyMode.PublicationOnly);
GlobalUser = globalUser;
} }
internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, UserModel model) internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, UserModel model)
{ {
var entity = new SocketGuildUser(guildId, client.GetOrCreateUser(client.StateManager, (Discord.API.User)model), client);
if (entity.Update(client.StateManager, model))
client.StateManager.AddOrUpdateMember(guildId, entity);
var entity = new SocketGuildUser(guildId, model.Id, client);
if (entity.Update(model))
client.StateManager.AddOrUpdateMember(guildId, entity.ToModel());
entity.UpdateRoles(Array.Empty<ulong>()); entity.UpdateRoles(Array.Empty<ulong>());
return entity; return entity;
} }
internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, MemberModel model) internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, MemberModel model)
{ {
var entity = new SocketGuildUser(guildId, client.GetOrCreateUser(client.StateManager, model.User), client);
entity.Update(client.StateManager, model);
client.StateManager.AddOrUpdateMember(guildId, entity);
var entity = new SocketGuildUser(guildId, model.Id, client);
entity.Update(model);
client.StateManager.AddOrUpdateMember(guildId, model);
return entity; return entity;
} }
internal void Update(ClientStateManager state, MemberModel model)
internal void Update(MemberModel model)
{ {
base.Update(state, model.User);

_joinedAtTicks = model.JoinedAt.UtcTicks; _joinedAtTicks = model.JoinedAt.UtcTicks;
Nickname = model.Nickname; Nickname = model.Nickname;
GuildAvatarId = model.GuildAvatar; GuildAvatarId = model.GuildAvatar;
@@ -234,12 +230,7 @@ namespace Discord.WebSocket


private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)";


internal new SocketGuildUser Clone()
{
var clone = MemberwiseClone() as SocketGuildUser;
clone.GlobalUser = GlobalUser.Clone();
return clone;
}
internal new SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser;


#endregion #endregion


@@ -260,8 +251,7 @@ namespace Discord.WebSocket


private struct CacheModel : MemberModel private struct CacheModel : MemberModel
{ {
public UserModel User { get; set; }

public ulong Id { get; set; }
public string Nickname { get; set; } public string Nickname { get; set; }


public string GuildAvatar { get; set; } public string GuildAvatar { get; set; }
@@ -280,15 +270,14 @@ namespace Discord.WebSocket


public DateTimeOffset? CommunicationsDisabledUntil { get; set; } public DateTimeOffset? CommunicationsDisabledUntil { get; set; }
} }
internal new MemberModel ToModel()
=> ToModel<CacheModel>();


MemberModel ICached<MemberModel>.ToModel()
=> ToMemberModel();

internal MemberModel ToMemberModel()
internal new TModel ToModel<TModel>() where TModel : MemberModel, new()
{ {
return new CacheModel
return new TModel
{ {
User = ((ICached<UserModel>)this).ToModel(),
Id = Id,
CommunicationsDisabledUntil = TimedOutUntil, CommunicationsDisabledUntil = TimedOutUntil,
GuildAvatar = GuildAvatarId, GuildAvatar = GuildAvatarId,
IsDeaf = IsDeafened, IsDeaf = IsDeafened,
@@ -301,7 +290,19 @@ namespace Discord.WebSocket
}; };
} }


public void Dispose() => Discord.StateManager.RemovedReferencedMember(Id, _guildId);
MemberModel ICached<MemberModel>.ToModel()
=> ToModel();

TResult ICached<MemberModel>.ToModel<TResult>()
=> ToModel<TResult>();

void ICached<MemberModel>.Update(MemberModel model) => Update(model);

public override void Dispose()
{
GC.SuppressFinalize(this);
Discord.StateManager.RemovedReferencedMember(Id, _guildId);
}
~SocketGuildUser() => Discord.StateManager.RemovedReferencedMember(Id, _guildId); ~SocketGuildUser() => Discord.StateManager.RemovedReferencedMember(Id, _guildId);


#endregion #endregion


+ 12
- 1
src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs View File

@@ -114,6 +114,12 @@ namespace Discord.WebSocket
public ulong UserId { get; set; } public ulong UserId { get; set; }


public ulong? GuildId { get; set; } public ulong? GuildId { get; set; }

ulong IEntityModel<ulong>.Id
{
get => UserId;
set => throw new NotSupportedException();
}
} }


private struct ActivityCacheModel : IActivityModel private struct ActivityCacheModel : IActivityModel
@@ -156,8 +162,11 @@ namespace Discord.WebSocket
} }


internal Model ToModel() internal Model ToModel()
=> ToModel<CacheModel>();

internal TModel ToModel<TModel>() where TModel : Model, new()
{ {
return new CacheModel
return new TModel
{ {
Status = Status, Status = Status,
ActiveClients = ActiveClients.ToArray(), ActiveClients = ActiveClients.ToArray(),
@@ -194,6 +203,8 @@ namespace Discord.WebSocket
} }


Model ICached<Model>.ToModel() => ToModel(); Model ICached<Model>.ToModel() => ToModel();
TResult ICached<Model>.ToModel<TResult>() => ToModel<TResult>();
void ICached<Model>.Update(Model model) => Update(model);


#endregion #endregion
} }


+ 26
- 17
src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs View File

@@ -19,18 +19,17 @@ namespace Discord.WebSocket
public bool IsVerified { get; private set; } public bool IsVerified { get; private set; }
/// <inheritdoc /> /// <inheritdoc />
public bool IsMfaEnabled { get; private set; } public bool IsMfaEnabled { get; private set; }
internal override SocketGlobalUser GlobalUser { get; set; }


/// <inheritdoc /> /// <inheritdoc />
public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } }
public override bool IsBot { get { return GlobalUser.Value.IsBot; } internal set { GlobalUser.Value.IsBot = value; } }
/// <inheritdoc /> /// <inheritdoc />
public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } }
public override string Username { get { return GlobalUser.Value.Username; } internal set { GlobalUser.Value.Username = value; } }
/// <inheritdoc /> /// <inheritdoc />
public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } }
public override ushort DiscriminatorValue { get { return GlobalUser.Value.DiscriminatorValue; } internal set { GlobalUser.Value.DiscriminatorValue = value; } }
/// <inheritdoc /> /// <inheritdoc />
public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } }
public override string AvatarId { get { return GlobalUser.Value.AvatarId; } internal set { GlobalUser.Value.AvatarId = value; } }
/// <inheritdoc /> /// <inheritdoc />
internal override Lazy<SocketPresence> Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } }
internal override Lazy<SocketPresence> Presence { get { return GlobalUser.Value.Presence; } set { GlobalUser.Value.Presence = value; } }
/// <inheritdoc /> /// <inheritdoc />
public UserProperties Flags { get; internal set; } public UserProperties Flags { get; internal set; }
/// <inheritdoc /> /// <inheritdoc />
@@ -41,20 +40,20 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
public override bool IsWebhook => false; public override bool IsWebhook => false;


internal SocketSelfUser(DiscordSocketClient discord, SocketGlobalUser globalUser)
: base(discord, globalUser.Id)
internal SocketSelfUser(DiscordSocketClient discord, ulong userId)
: base(discord, userId)
{ {
GlobalUser = globalUser;
} }
internal static SocketSelfUser Create(DiscordSocketClient discord, ClientStateManager state, Model model)
internal static SocketSelfUser Create(DiscordSocketClient discord, Model model)
{ {
var entity = new SocketSelfUser(discord, discord.GetOrCreateSelfUser(state, model));
entity.Update(state, model);
var entity = new SocketSelfUser(discord, model.Id);
entity.Update(model);
return entity; return entity;
} }
internal override bool Update(ClientStateManager state, UserModel model)
internal override bool Update(UserModel model)
{ {
bool hasGlobalChanges = base.Update(state, model);
bool hasGlobalChanges = base.Update(model);


if (model is not Model currentUserModel) if (model is not Model currentUserModel)
throw new ArgumentException($"Got unexpected model type \"{model?.GetType()}\""); throw new ArgumentException($"Got unexpected model type \"{model?.GetType()}\"");
@@ -98,9 +97,13 @@ namespace Discord.WebSocket


private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)";
internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser;
public override void Dispose()
{
GC.SuppressFinalize(this);
Discord.StateManager.RemoveReferencedGlobalUser(Id);
}


#region Cache #region Cache

private struct CacheModel : Model private struct CacheModel : Model
{ {
public bool? IsVerified { get; set; } public bool? IsVerified { get; set; }
@@ -128,9 +131,12 @@ namespace Discord.WebSocket
public ulong Id { get; set; } public ulong Id { get; set; }
} }


Model ICached<Model>.ToModel()
internal new Model ToModel()
=> ToModel<CacheModel>();

internal new TModel ToModel<TModel>() where TModel : Model, new()
{ {
return new CacheModel
return new TModel
{ {
Avatar = AvatarId, Avatar = AvatarId,
Discriminator = Discriminator, Discriminator = Discriminator,
@@ -147,6 +153,9 @@ namespace Discord.WebSocket
}; };
} }


Model ICached<Model>.ToModel() => ToModel();
TResult ICached<Model>.ToModel<TResult>() => ToModel<TResult>();
void ICached<Model>.Update(Model model) => Update(model);
#endregion #endregion
} }
} }

+ 112
- 68
src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs View File

@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Model = Discord.API.ThreadMember;
using Model = Discord.IThreadMemberModel;
using System.Collections.Immutable; using System.Collections.Immutable;


namespace Discord.WebSocket namespace Discord.WebSocket
@@ -10,12 +10,12 @@ namespace Discord.WebSocket
/// <summary> /// <summary>
/// Represents a thread user received over the gateway. /// Represents a thread user received over the gateway.
/// </summary> /// </summary>
public class SocketThreadUser : SocketUser, IThreadUser, IGuildUser
public class SocketThreadUser : SocketUser, IThreadUser, IGuildUser, ICached<Model>
{ {
/// <summary> /// <summary>
/// Gets the <see cref="SocketThreadChannel"/> this user is in. /// Gets the <see cref="SocketThreadChannel"/> this user is in.
/// </summary> /// </summary>
public SocketThreadChannel Thread { get; private set; }
public Lazy<SocketThreadChannel> Thread { get; private set; }


/// <inheritdoc/> /// <inheritdoc/>
public DateTimeOffset ThreadJoinedAt { get; private set; } public DateTimeOffset ThreadJoinedAt { get; private set; }
@@ -23,126 +23,142 @@ namespace Discord.WebSocket
/// <summary> /// <summary>
/// Gets the guild this user is in. /// Gets the guild this user is in.
/// </summary> /// </summary>
public SocketGuild Guild { get; private set; }
public Lazy<SocketGuild> Guild { get; private set; }


/// <inheritdoc/> /// <inheritdoc/>
public DateTimeOffset? JoinedAt public DateTimeOffset? JoinedAt
=> GuildUser.JoinedAt;
=> GuildUser.Value.JoinedAt;


/// <inheritdoc/> /// <inheritdoc/>
public string DisplayName public string DisplayName
=> GuildUser.Nickname ?? GuildUser.Username;
=> GuildUser.Value.Nickname ?? GuildUser.Value.Username;


/// <inheritdoc/> /// <inheritdoc/>
public string Nickname public string Nickname
=> GuildUser.Nickname;
=> GuildUser.Value.Nickname;


/// <inheritdoc/> /// <inheritdoc/>
public DateTimeOffset? PremiumSince public DateTimeOffset? PremiumSince
=> GuildUser.PremiumSince;
=> GuildUser.Value.PremiumSince;


/// <inheritdoc/> /// <inheritdoc/>
public DateTimeOffset? TimedOutUntil public DateTimeOffset? TimedOutUntil
=> GuildUser.TimedOutUntil;
=> GuildUser.Value.TimedOutUntil;


/// <inheritdoc/> /// <inheritdoc/>
public bool? IsPending public bool? IsPending
=> GuildUser.IsPending;
=> GuildUser.Value.IsPending;

/// <inheritdoc /> /// <inheritdoc />
public int Hierarchy public int Hierarchy
=> GuildUser.Hierarchy;
=> GuildUser.Value.Hierarchy;


/// <inheritdoc/> /// <inheritdoc/>
public override string AvatarId public override string AvatarId
{ {
get => GuildUser.AvatarId;
internal set => GuildUser.AvatarId = value;
get => GuildUser.Value.AvatarId;
internal set => GuildUser.Value.AvatarId = value;
} }

/// <inheritdoc/> /// <inheritdoc/>
public string DisplayAvatarId => GuildAvatarId ?? AvatarId; public string DisplayAvatarId => GuildAvatarId ?? AvatarId;


/// <inheritdoc/> /// <inheritdoc/>
public string GuildAvatarId public string GuildAvatarId
=> GuildUser.GuildAvatarId;
=> GuildUser.Value.GuildAvatarId;


/// <inheritdoc/> /// <inheritdoc/>
public override ushort DiscriminatorValue public override ushort DiscriminatorValue
{ {
get => GuildUser.DiscriminatorValue;
internal set => GuildUser.DiscriminatorValue = value;
get => GuildUser.Value.DiscriminatorValue;
internal set => GuildUser.Value.DiscriminatorValue = value;
} }


/// <inheritdoc/> /// <inheritdoc/>
public override bool IsBot public override bool IsBot
{ {
get => GuildUser.IsBot;
internal set => GuildUser.IsBot = value;
get => GuildUser.Value.IsBot;
internal set => GuildUser.Value.IsBot = value;
} }


/// <inheritdoc/> /// <inheritdoc/>
public override bool IsWebhook public override bool IsWebhook
=> GuildUser.IsWebhook;
=> GuildUser.Value.IsWebhook;


/// <inheritdoc/> /// <inheritdoc/>
public override string Username public override string Username
{ {
get => GuildUser.Username;
internal set => GuildUser.Username = value;
get => GuildUser.Value.Username;
internal set => GuildUser.Value.Username = value;
} }


/// <inheritdoc/> /// <inheritdoc/>
public bool IsDeafened public bool IsDeafened
=> GuildUser.IsDeafened;
=> GuildUser.Value.IsDeafened;


/// <inheritdoc/> /// <inheritdoc/>
public bool IsMuted public bool IsMuted
=> GuildUser.IsMuted;
=> GuildUser.Value.IsMuted;


/// <inheritdoc/> /// <inheritdoc/>
public bool IsSelfDeafened public bool IsSelfDeafened
=> GuildUser.IsSelfDeafened;
=> GuildUser.Value.IsSelfDeafened;


/// <inheritdoc/> /// <inheritdoc/>
public bool IsSelfMuted public bool IsSelfMuted
=> GuildUser.IsSelfMuted;
=> GuildUser.Value.IsSelfMuted;


/// <inheritdoc/> /// <inheritdoc/>
public bool IsSuppressed public bool IsSuppressed
=> GuildUser.IsSuppressed;
=> GuildUser.Value.IsSuppressed;


/// <inheritdoc/> /// <inheritdoc/>
public IVoiceChannel VoiceChannel public IVoiceChannel VoiceChannel
=> GuildUser.VoiceChannel;
=> GuildUser.Value.VoiceChannel;


/// <inheritdoc/> /// <inheritdoc/>
public string VoiceSessionId public string VoiceSessionId
=> GuildUser.VoiceSessionId;
=> GuildUser.Value.VoiceSessionId;


/// <inheritdoc/> /// <inheritdoc/>
public bool IsStreaming public bool IsStreaming
=> GuildUser.IsStreaming;
=> GuildUser.Value.IsStreaming;


/// <inheritdoc/> /// <inheritdoc/>
public bool IsVideoing public bool IsVideoing
=> GuildUser.IsVideoing;
=> GuildUser.Value.IsVideoing;


/// <inheritdoc/> /// <inheritdoc/>
public DateTimeOffset? RequestToSpeakTimestamp public DateTimeOffset? RequestToSpeakTimestamp
=> GuildUser.RequestToSpeakTimestamp;
=> GuildUser.Value.RequestToSpeakTimestamp;

private Lazy<SocketGuildUser> GuildUser { get; set; }


private SocketGuildUser GuildUser { get; set; }
private ulong _threadId;
private ulong _guildId;


internal SocketThreadUser(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser member, ulong userId)
: base(guild.Discord, userId)

internal SocketThreadUser(DiscordSocketClient client, ulong guildId, ulong threadId, ulong userId)
: base(client, userId)
{ {
Thread = thread;
Guild = guild;
GuildUser = member;
_guildId = guildId;
_threadId = threadId;

GuildUser = new(() => client.StateManager.TryGetMemberStore(guildId, out var store) ? store.Get(userId) : null);
Thread = new(() => client.GetChannel(threadId) as SocketThreadChannel);
Guild = new(() => client.GetGuild(guildId));
} }


internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, Model model, SocketGuildUser member) internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, Model model, SocketGuildUser member)
{ {
var entity = new SocketThreadUser(guild, thread, member, model.UserId.Value);
var entity = new SocketThreadUser(guild.Discord, guild.Id, thread.Id, model.UserId.Value);
entity.Update(model);
return entity;
}

internal static SocketThreadUser Create(DiscordSocketClient client, ulong guildId, ulong threadId, Model model)
{
var entity = new SocketThreadUser(client, guildId, threadId, model.UserId.Value);
entity.Update(model); entity.Update(model);
return entity; return entity;
} }
@@ -150,89 +166,117 @@ namespace Discord.WebSocket
internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser owner) internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser owner)
{ {
// this is used for creating the owner of the thread. // this is used for creating the owner of the thread.
var entity = new SocketThreadUser(guild, thread, owner, owner.Id);
entity.Update(new Model
{
JoinTimestamp = thread.CreatedAt,
});
var entity = new SocketThreadUser(guild.Discord, guild.Id, thread.Id, owner.Id);
entity.ThreadJoinedAt = thread.CreatedAt;
return entity; return entity;
} }


internal void Update(Model model) internal void Update(Model model)
{ {
ThreadJoinedAt = model.JoinTimestamp;
ThreadJoinedAt = model.JoinedAt;
} }


/// <inheritdoc/> /// <inheritdoc/>
public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.GetPermissions(channel);
public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.Value.GetPermissions(channel);


/// <inheritdoc/> /// <inheritdoc/>
public Task KickAsync(string reason = null, RequestOptions options = null) => GuildUser.KickAsync(reason, options);
public Task KickAsync(string reason = null, RequestOptions options = null) => GuildUser.Value.KickAsync(reason, options);


/// <inheritdoc/> /// <inheritdoc/>
public Task ModifyAsync(Action<GuildUserProperties> func, RequestOptions options = null) => GuildUser.ModifyAsync(func, options);
public Task ModifyAsync(Action<GuildUserProperties> func, RequestOptions options = null) => GuildUser.Value.ModifyAsync(func, options);


/// <inheritdoc/> /// <inheritdoc/>
public Task AddRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.AddRoleAsync(roleId, options);
public Task AddRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.Value.AddRoleAsync(roleId, options);


/// <inheritdoc/> /// <inheritdoc/>
public Task AddRoleAsync(IRole role, RequestOptions options = null) => GuildUser.AddRoleAsync(role, options);
public Task AddRoleAsync(IRole role, RequestOptions options = null) => GuildUser.Value.AddRoleAsync(role, options);


/// <inheritdoc/> /// <inheritdoc/>
public Task AddRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null) => GuildUser.AddRolesAsync(roleIds, options);
public Task AddRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null) => GuildUser.Value.AddRolesAsync(roleIds, options);


/// <inheritdoc/> /// <inheritdoc/>
public Task AddRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null) => GuildUser.AddRolesAsync(roles, options);
public Task AddRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null) => GuildUser.Value.AddRolesAsync(roles, options);


/// <inheritdoc/> /// <inheritdoc/>
public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.RemoveRoleAsync(roleId, options);
public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.Value.RemoveRoleAsync(roleId, options);


/// <inheritdoc/> /// <inheritdoc/>
public Task RemoveRoleAsync(IRole role, RequestOptions options = null) => GuildUser.RemoveRoleAsync(role, options);
public Task RemoveRoleAsync(IRole role, RequestOptions options = null) => GuildUser.Value.RemoveRoleAsync(role, options);


/// <inheritdoc/> /// <inheritdoc/>
public Task RemoveRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roleIds, options);
public Task RemoveRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null) => GuildUser.Value.RemoveRolesAsync(roleIds, options);


/// <inheritdoc/> /// <inheritdoc/>
public Task RemoveRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roles, options);
public Task RemoveRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null) => GuildUser.Value.RemoveRolesAsync(roles, options);
/// <inheritdoc/> /// <inheritdoc/>
public Task SetTimeOutAsync(TimeSpan span, RequestOptions options = null) => GuildUser.SetTimeOutAsync(span, options);
public Task SetTimeOutAsync(TimeSpan span, RequestOptions options = null) => GuildUser.Value.SetTimeOutAsync(span, options);


/// <inheritdoc/> /// <inheritdoc/>
public Task RemoveTimeOutAsync(RequestOptions options = null) => GuildUser.RemoveTimeOutAsync(options);
public Task RemoveTimeOutAsync(RequestOptions options = null) => GuildUser.Value.RemoveTimeOutAsync(options);


/// <inheritdoc/> /// <inheritdoc/>
IThreadChannel IThreadUser.Thread => Thread;
IThreadChannel IThreadUser.Thread => Thread.Value;


/// <inheritdoc/> /// <inheritdoc/>
IGuild IThreadUser.Guild => Guild;
IGuild IThreadUser.Guild => Guild.Value;


/// <inheritdoc/> /// <inheritdoc/>
IGuild IGuildUser.Guild => Guild;
IGuild IGuildUser.Guild => Guild.Value;


/// <inheritdoc/> /// <inheritdoc/>
ulong IGuildUser.GuildId => Guild.Id;
ulong IGuildUser.GuildId => Guild.Value.Id;


/// <inheritdoc/> /// <inheritdoc/>
GuildPermissions IGuildUser.GuildPermissions => GuildUser.GuildPermissions;
GuildPermissions IGuildUser.GuildPermissions => GuildUser.Value.GuildPermissions;


/// <inheritdoc/> /// <inheritdoc/>
IReadOnlyCollection<ulong> IGuildUser.RoleIds => GuildUser.Roles.Select(x => x.Id).ToImmutableArray();
IReadOnlyCollection<ulong> IGuildUser.RoleIds => GuildUser.Value.Roles.Select(x => x.Id).ToImmutableArray();


/// <inheritdoc /> /// <inheritdoc />
string IGuildUser.GetDisplayAvatarUrl(ImageFormat format, ushort size) => GuildUser.GetDisplayAvatarUrl(format, size);
string IGuildUser.GetDisplayAvatarUrl(ImageFormat format, ushort size) => GuildUser.Value.GetDisplayAvatarUrl(format, size);


/// <inheritdoc /> /// <inheritdoc />
string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.GetGuildAvatarUrl(format, size);
string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.Value.GetGuildAvatarUrl(format, size);

internal override Lazy<SocketPresence> Presence { get => GuildUser.Value.Presence; set => GuildUser.Value.Presence = value; }


internal override SocketGlobalUser GlobalUser { get => GuildUser.GlobalUser; set => GuildUser.GlobalUser = value; }
public override void Dispose()
{
GC.SuppressFinalize(this);
}


internal override Lazy<SocketPresence> Presence { get => GuildUser.Presence; set => GuildUser.Presence = value; }


/// <summary> /// <summary>
/// Gets the guild user of this thread user. /// Gets the guild user of this thread user.
/// </summary> /// </summary>
/// <param name="user"></param> /// <param name="user"></param>
public static explicit operator SocketGuildUser(SocketThreadUser user) => user.GuildUser;
public static explicit operator SocketGuildUser(SocketThreadUser user) => user.GuildUser.Value;

#region Cache
private class CacheModel : Model
{
public ulong? ThreadId { get; set; }
public ulong? UserId { get; set; }
public DateTimeOffset JoinedAt { get; set; }

ulong IEntityModel<ulong>.Id { get => UserId.GetValueOrDefault(); set => throw new NotSupportedException(); }
}

internal new Model ToModel() => ToModel<CacheModel>();

internal new TModel ToModel<TModel>() where TModel : Model, new()
{
return new TModel
{
JoinedAt = ThreadJoinedAt,
ThreadId = _threadId,
UserId = Id
};
}

Model ICached<Model>.ToModel() => ToModel();
TResult ICached<Model>.ToModel<TResult>() => ToModel<TResult>();
void ICached<Model>.Update(Model model) => Update(model);
#endregion
} }
} }

+ 5
- 5
src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs View File

@@ -27,21 +27,21 @@ namespace Discord.WebSocket
public override bool IsWebhook => false; public override bool IsWebhook => false;
/// <inheritdoc /> /// <inheritdoc />
internal override Lazy<SocketPresence> Presence { get { return new Lazy<SocketPresence>(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } } internal override Lazy<SocketPresence> Presence { get { return new Lazy<SocketPresence>(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } }
/// <inheritdoc />
/// <exception cref="NotSupportedException">This field is not supported for an unknown user.</exception>
internal override SocketGlobalUser GlobalUser { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
internal override Lazy<SocketGlobalUser> GlobalUser { get => new Lazy<SocketGlobalUser>(() => null); set { } }


internal SocketUnknownUser(DiscordSocketClient discord, ulong id) internal SocketUnknownUser(DiscordSocketClient discord, ulong id)
: base(discord, id) : base(discord, id)
{ {
} }
internal static SocketUnknownUser Create(DiscordSocketClient discord, ClientStateManager state, Model model)
internal static SocketUnknownUser Create(DiscordSocketClient discord, Model model)
{ {
var entity = new SocketUnknownUser(discord, model.Id); var entity = new SocketUnknownUser(discord, model.Id);
entity.Update(state, model);
entity.Update(model);
return entity; return entity;
} }


public override void Dispose() { }

private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Unknown)"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Unknown)";
internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser; internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser;
} }


+ 24
- 15
src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs View File

@@ -18,18 +18,18 @@ namespace Discord.WebSocket
public abstract class SocketUser : SocketEntity<ulong>, IUser, ICached<Model>, IDisposable public abstract class SocketUser : SocketEntity<ulong>, IUser, ICached<Model>, IDisposable
{ {
/// <inheritdoc /> /// <inheritdoc />
public abstract bool IsBot { get; internal set; }
public virtual bool IsBot { get; internal set; }
/// <inheritdoc /> /// <inheritdoc />
public abstract string Username { get; internal set; }
public virtual string Username { get; internal set; }
/// <inheritdoc /> /// <inheritdoc />
public abstract ushort DiscriminatorValue { get; internal set; }
public virtual ushort DiscriminatorValue { get; internal set; }
/// <inheritdoc /> /// <inheritdoc />
public abstract string AvatarId { get; internal set; }
public virtual string AvatarId { get; internal set; }
/// <inheritdoc /> /// <inheritdoc />
public abstract bool IsWebhook { get; }
public virtual bool IsWebhook { get; }
/// <inheritdoc /> /// <inheritdoc />
public UserProperties? PublicFlags { get; private set; } public UserProperties? PublicFlags { get; private set; }
internal abstract SocketGlobalUser GlobalUser { get; set; }
internal virtual Lazy<SocketGlobalUser> GlobalUser { get; set; }
internal virtual Lazy<SocketPresence> Presence { get; set; } internal virtual Lazy<SocketPresence> Presence { get; set; }


/// <inheritdoc /> /// <inheritdoc />
@@ -57,9 +57,10 @@ namespace Discord.WebSocket
: base(discord, id) : base(discord, id)
{ {
} }
internal virtual bool Update(ClientStateManager state, Model model)
internal virtual bool Update(Model model)
{ {
Presence ??= new Lazy<SocketPresence>(() => state.GetPresence(Id), System.Threading.LazyThreadSafetyMode.PublicationOnly);
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; bool hasChanges = false;
if (model.Avatar != AvatarId) if (model.Avatar != AvatarId)
{ {
@@ -98,6 +99,8 @@ namespace Discord.WebSocket
return hasChanges; return hasChanges;
} }


public abstract void Dispose();

/// <inheritdoc /> /// <inheritdoc />
public async Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null) public async Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null)
=> await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false); => await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false);
@@ -117,8 +120,6 @@ namespace Discord.WebSocket
/// The full name of the user. /// The full name of the user.
/// </returns> /// </returns>
public override string ToString() => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode); public override string ToString() => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode);
~SocketUser() => GlobalUser?.Dispose();
public void Dispose() => GlobalUser?.Dispose();
private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})"; private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})";
internal SocketUser Clone() => MemberwiseClone() as SocketUser; internal SocketUser Clone() => MemberwiseClone() as SocketUser;


@@ -136,12 +137,9 @@ namespace Discord.WebSocket
public ulong Id { get; set; } public ulong Id { get; set; }
} }


Model ICached<Model>.ToModel()
=> ToModel();

internal Model ToModel()
internal TModel ToModel<TModel>() where TModel : Model, new()
{ {
return new CacheModel
return new TModel
{ {
Avatar = AvatarId, Avatar = AvatarId,
Discriminator = Discriminator, Discriminator = Discriminator,
@@ -151,6 +149,17 @@ namespace Discord.WebSocket
}; };
} }


internal Model ToModel()
=> ToModel<CacheModel>();

Model ICached<Model>.ToModel()
=> ToModel();

TResult ICached<Model>.ToModel<TResult>()
=> ToModel<TResult>();

void ICached<Model>.Update(Model model) => Update(model);

#endregion #endregion
} }
} }

+ 5
- 4
src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs View File

@@ -34,7 +34,7 @@ namespace Discord.WebSocket
public override bool IsWebhook => true; public override bool IsWebhook => true;
/// <inheritdoc /> /// <inheritdoc />
internal override Lazy<SocketPresence> Presence { get { return new Lazy<SocketPresence>(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } } internal override Lazy<SocketPresence> Presence { get { return new Lazy<SocketPresence>(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } }
internal override SocketGlobalUser GlobalUser { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
internal override Lazy<SocketGlobalUser> GlobalUser { get => new Lazy<SocketGlobalUser>(() => null); set { } }


internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId)
: base(guild.Discord, id) : base(guild.Discord, id)
@@ -42,16 +42,17 @@ namespace Discord.WebSocket
Guild = guild; Guild = guild;
WebhookId = webhookId; WebhookId = webhookId;
} }
internal static SocketWebhookUser Create(SocketGuild guild, ClientStateManager state, Model model, ulong webhookId)
internal static SocketWebhookUser Create(SocketGuild guild, Model model, ulong webhookId)
{ {
var entity = new SocketWebhookUser(guild, model.Id, webhookId); var entity = new SocketWebhookUser(guild, model.Id, webhookId);
entity.Update(state, model);
entity.Update(model);
return entity; return entity;
} }


private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Webhook)"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Webhook)";
internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser; internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser;
#endregion
public override void Dispose() { }
#endregion


#region IGuildUser #region IGuildUser
/// <inheritdoc /> /// <inheritdoc />


+ 0
- 21
src/Discord.Net.WebSocket/Extensions/StateExtensions.cs View File

@@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.WebSocket
{
internal static class StateExtensions
{
public static StateBehavior ToBehavior(this CacheMode mode)
{
return mode switch
{
CacheMode.AllowDownload => StateBehavior.AllowDownload,
CacheMode.CacheOnly => StateBehavior.CacheOnly,
_ => StateBehavior.AllowDownload
};
}
}
}

+ 0
- 252
src/Discord.Net.WebSocket/State/DefaultStateProvider.cs View File

@@ -1,252 +0,0 @@
using Discord.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.WebSocket
{
internal class DefaultStateProvider : IStateProvider
{
private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149
private const double AverageUsersPerGuild = 47.78; //Source: Googie2149
private const double CollectionMultiplier = 1.05; //Add 5% buffer to handle growth

private readonly ICacheProvider _cache;
private readonly StateBehavior _defaultBehavior;
private readonly DiscordSocketClient _client;
private readonly Logger _logger;
public DefaultStateProvider(Logger logger, ICacheProvider cacheProvider, DiscordSocketClient client, StateBehavior stateBehavior)
{
_cache = cacheProvider;
_client = client;
_logger = logger;

if (stateBehavior == StateBehavior.Default)
throw new ArgumentException("Cannot use \"default\" as the default state behavior");

_defaultBehavior = stateBehavior;
}

private void RunAsyncWithLogs(ValueTask task)
{
_ = Task.Run(async () =>
{
try
{
await task.ConfigureAwait(false);
}
catch (Exception x)
{
await _logger.ErrorAsync("Cache provider failed", x).ConfigureAwait(false);
}
});
}

private TType ValidateAsSocketEntity<TType>(ISnowflakeEntity entity) where TType : SocketEntity<ulong>
{
if(entity is not TType val)
throw new NotSupportedException("Cannot cache non-socket entities");
return val;
}

private StateBehavior ResolveBehavior(StateBehavior behavior)
=> behavior == StateBehavior.Default ? _defaultBehavior : behavior;

public ValueTask AddOrUpdateMemberAsync(ulong guildId, IGuildUser user)
{
var socketGuildUser = ValidateAsSocketEntity<SocketGuildUser>(user);
var model = socketGuildUser.ToMemberModel();
RunAsyncWithLogs(_cache.AddOrUpdateMemberAsync(model, guildId, CacheRunMode.Async));
return default;
}
public ValueTask AddOrUpdateUserAsync(IUser user)
{
var socketUser = ValidateAsSocketEntity<SocketUser>(user);
var model = socketUser.ToModel();
RunAsyncWithLogs(_cache.AddOrUpdateUserAsync(model, CacheRunMode.Async));
return default;
}
public ValueTask<IGuildUser> GetMemberAsync(ulong guildId, ulong id, StateBehavior stateBehavior, RequestOptions options = null)
{
var behavior = ResolveBehavior(stateBehavior);

var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async;

if(behavior != StateBehavior.DownloadOnly)
{
var memberLookupTask = _cache.GetMemberAsync(id, guildId, cacheMode);

if (memberLookupTask.IsCompleted)
{
var model = memberLookupTask.Result;
if(model != null)
return new ValueTask<IGuildUser>(SocketGuildUser.Create(guildId, _client, model));
}
else
{
return new ValueTask<IGuildUser>(Task.Run(async () => // review: task.run here?
{
var result = await memberLookupTask;

if (result != null)
return (IGuildUser)SocketGuildUser.Create(guildId, _client, result);
else if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly)
return await _client.Rest.GetGuildUserAsync(guildId, id, options).ConfigureAwait(false);
return null;
}));
}
}

if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly)
return new ValueTask<IGuildUser>(_client.Rest.GetGuildUserAsync(guildId, id, options).ContinueWith(x => (IGuildUser)x.Result));

return default;
}

public ValueTask<IEnumerable<IGuildUser>> GetMembersAsync(ulong guildId, StateBehavior stateBehavior, RequestOptions options = null)
{
var behavior = ResolveBehavior(stateBehavior);

var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async;

if(behavior != StateBehavior.DownloadOnly)
{
var memberLookupTask = _cache.GetMembersAsync(guildId, cacheMode);

if (memberLookupTask.IsCompleted)
return new ValueTask<IEnumerable<IGuildUser>>(memberLookupTask.Result?.Select(x => SocketGuildUser.Create(guildId, _client, x)));
else
{
return new ValueTask<IEnumerable<IGuildUser>>(Task.Run(async () =>
{
var result = await memberLookupTask;

if (result != null && result.Any())
return result.Select(x => (IGuildUser)SocketGuildUser.Create(guildId, _client, x));

if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly)
return await _client.Rest.GetGuildUsersAsync(guildId, options);

return null;
}));
}
}

if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly)
return new ValueTask<IEnumerable<IGuildUser>>(_client.Rest.GetGuildUsersAsync(guildId, options).ContinueWith(x => x.Result.Cast<IGuildUser>()));

return default;
}

public ValueTask<IUser> GetUserAsync(ulong id, StateBehavior stateBehavior, RequestOptions options = null)
{
var behavior = ResolveBehavior(stateBehavior);

var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async;

if (behavior != StateBehavior.DownloadOnly)
{
var userLookupTask = _cache.GetUserAsync(id, cacheMode);

if (userLookupTask.IsCompleted)
{
var model = userLookupTask.Result;
if(model != null)
return new ValueTask<IUser>(SocketGlobalUser.Create(_client, null, model));
}
else
{
return new ValueTask<IUser>(Task.Run<IUser>(async () =>
{
var result = await userLookupTask;

if (result != null)
return SocketGlobalUser.Create(_client, null, result);

if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly)
return await _client.Rest.GetUserAsync(id, options);

return null;
}));
}
}

if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly)
return new ValueTask<IUser>(_client.Rest.GetUserAsync(id, options).ContinueWith(x => (IUser)x.Result));

return default;
}

public ValueTask<IEnumerable<IUser>> GetUsersAsync(StateBehavior stateBehavior, RequestOptions options = null)
{
var behavior = ResolveBehavior(stateBehavior);

var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async;

if(behavior != StateBehavior.DownloadOnly)
{
var usersTask = _cache.GetUsersAsync(cacheMode);

if (usersTask.IsCompleted)
return new ValueTask<IEnumerable<IUser>>(usersTask.Result.Select(x => (IUser)SocketGlobalUser.Create(_client, null, x)));
else
{
return new ValueTask<IEnumerable<IUser>>(usersTask.AsTask().ContinueWith(x => x.Result.Select(x => (IUser)SocketGlobalUser.Create(_client, null, x))));
}
}

// no download path
return default;
}

public ValueTask RemoveMemberAsync(ulong id, ulong guildId)
=> _cache.RemoveMemberAsync(id, guildId, CacheRunMode.Async);
public ValueTask RemoveUserAsync(ulong id)
=> _cache.RemoveUserAsync(id, CacheRunMode.Async);

public ValueTask<IPresence> GetPresenceAsync(ulong userId, StateBehavior stateBehavior)
{
var behavior = ResolveBehavior(stateBehavior);

var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async;

if(stateBehavior != StateBehavior.DownloadOnly)
{
var fetchTask = _cache.GetPresenceAsync(userId, cacheMode);

if (fetchTask.IsCompleted)
return new ValueTask<IPresence>(SocketPresence.Create(fetchTask.Result));
else
{
return new ValueTask<IPresence>(Task.Run(async () =>
{
var result = await fetchTask;

if(result != null)
return (IPresence)SocketPresence.Create(result);
return null;
}));
}
}

// no download path
return new ValueTask<IPresence>((IPresence)null);
}

public ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresence presense, StateBehavior stateBehavior)
{
if (presense is not SocketPresence socketPresense)
throw new ArgumentException($"Expected socket entity but got {presense?.GetType()}");

var model = socketPresense.ToModel();

RunAsyncWithLogs(_cache.AddOrUpdatePresenseAsync(userId, model, CacheRunMode.Async));
return default;
}
public ValueTask RemovePresenseAsync(ulong userId)
=> _cache.RemovePresenseAsync(userId, CacheRunMode.Async);
}
}

+ 0
- 25
src/Discord.Net.WebSocket/State/IStateProvider.cs View File

@@ -1,25 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.WebSocket
{
public interface IStateProvider
{
ValueTask<IPresence> GetPresenceAsync(ulong userId, StateBehavior stateBehavior);
ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresence presense, StateBehavior stateBehavior);
ValueTask RemovePresenseAsync(ulong userId);

ValueTask<IUser> GetUserAsync(ulong id, StateBehavior stateBehavior, RequestOptions options = null);
ValueTask<IEnumerable<IUser>> GetUsersAsync(StateBehavior stateBehavior, RequestOptions options = null);
ValueTask AddOrUpdateUserAsync(IUser user);
ValueTask RemoveUserAsync(ulong id);

ValueTask<IGuildUser> GetMemberAsync(ulong guildId, ulong id, StateBehavior stateBehavior, RequestOptions options = null);
ValueTask<IEnumerable<IGuildUser>> GetMembersAsync(ulong guildId, StateBehavior stateBehavior, RequestOptions options = null);
ValueTask AddOrUpdateMemberAsync(ulong guildId, IGuildUser user);
ValueTask RemoveMemberAsync(ulong guildId, ulong id);
}
}

+ 0
- 53
src/Discord.Net.WebSocket/State/StateBehavior.cs View File

@@ -1,53 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.WebSocket
{
public enum StateBehavior
{
/// <summary>
/// Use the default Cache Behavior of the client.
/// </summary>
/// <seealso cref="DiscordSocketConfig.DefaultStateBehavior"/>
Default = 0,
/// <summary>
/// The entity will only be retrieved via a synchronous cache lookup.
///
/// For the default <see cref="IStateProvider"/>, this is equivalent to using <see cref="CacheOnly"/>
/// </summary>
/// <remarks>
/// This flag is used to indicate that the retrieval of this entity should not leave the
/// synchronous path of the <see cref="System.Threading.Tasks.ValueTask"/>. When true,
/// the calling method *should* not ever leave the calling task, and never generate an async
/// state machine.
///
/// Bear in mind that the true behavior of this flag depends entirely on the <see cref="IStateProvider"/> to
/// abide by design implications of this flag. Once Discord.Net has called out to the state provider with this
/// flag, it is out of our control whether or not an async method is evaluated.
/// </remarks>
SyncOnly = 1,
/// <summary>
/// The entity will only be retrieved via a cache lookup - the Discord API will not be contacted to retrieve the entity.
/// </summary>
/// <remarks>
/// When using an alternative <see cref="IStateProvider"/>, usage of this flag implies that it is
/// okay for the state provider to make an external call if the local cache missed the entity.
///
/// Note that when designing an <see cref="IStateProvider"/>, this flag does not imply that the state
/// provider itself should contact Discord for the entity; rather that if using a dual-layer caching system,
/// it would be okay to contact an external layer, e.g. Redis, for the entity.
/// </remarks>
CacheOnly = 2,
/// <summary>
/// The entity will be downloaded from the Discord REST API if the <see cref="ICacheProvider"/> on hand cannot locate it.
/// </summary>
AllowDownload = 3,
/// <summary>
/// The entity will be downloaded from the Discord REST API. The local <see cref="ICacheProvider"/> will not be contacted to find the entity.
/// </summary>
DownloadOnly = 4
}
}

Loading…
Cancel
Save