Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
acfddee
Implement basics
KubaZ2 Aug 23, 2024
93d40ec
Implement more
KubaZ2 Aug 25, 2024
c1f15c5
Remove commented code
KubaZ2 Aug 25, 2024
11d5618
Merge alpha
KubaZ2 Aug 25, 2024
09706e5
Implement basics
KubaZ2 Aug 23, 2024
7396c41
Implement more
KubaZ2 Aug 25, 2024
4070cf6
Remove commented code
KubaZ2 Aug 25, 2024
9083e01
Merge branch 'feature/gateway-retries-and-rate-limiter' of https://gi…
KubaZ2 Aug 25, 2024
81fcb62
Use auto property
KubaZ2 Aug 25, 2024
6864f6d
Improve the WebSocketClient
KubaZ2 Aug 26, 2024
b035bc2
Simplify null check
KubaZ2 Aug 26, 2024
8371f4b
Refactor connection handling in WebSocketClient
KubaZ2 Aug 26, 2024
39b6c5b
Update tests and guides
KubaZ2 Aug 26, 2024
e29cf00
Fix guide
KubaZ2 Aug 26, 2024
ea57869
Complete tasks in dispose
KubaZ2 Aug 26, 2024
d1ea3bb
Remove cancellation token from reading task
KubaZ2 Aug 26, 2024
753f331
Fix voice client
KubaZ2 Aug 26, 2024
8ef83be
Optimize
KubaZ2 Aug 26, 2024
9eed3d5
Improve payload sending
KubaZ2 Aug 28, 2024
3a6876e
Resolve possible concurrency issues
KubaZ2 Aug 28, 2024
5a665e5
Refactor
KubaZ2 Aug 28, 2024
27e2127
Invoke closed on abort when reconnecting and improve Abort method
KubaZ2 Aug 28, 2024
d59d1d2
Normalize naming
KubaZ2 Aug 28, 2024
7db7c81
Delete `ReadTask`
KubaZ2 Aug 28, 2024
de187d1
Improve gateway rate limits
KubaZ2 Aug 29, 2024
3876cc2
Normalize rest rate limits
KubaZ2 Aug 29, 2024
ae39d41
Cleanup test project
KubaZ2 Aug 29, 2024
a14bd3f
Code cleanup
KubaZ2 Aug 29, 2024
a648e8e
Improve send cancellation and remove `WebSocketMessageFlags.BypassRea…
KubaZ2 Aug 29, 2024
f9b2210
Fix test
KubaZ2 Aug 29, 2024
defd84d
Add rate limiter support for voice client
KubaZ2 Aug 29, 2024
bd72ecc
Improve disposing
KubaZ2 Aug 29, 2024
34a52e2
Remove connected task
KubaZ2 Aug 30, 2024
da7e654
Refactor payload sending
KubaZ2 Aug 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions Documentation/guides/advanced/Voice/VoiceModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ public async Task PlayAsync(string track)
// Connect
await voiceClient.StartAsync();

// Wait for ready
await voiceClient.ReadyAsync;

// Enter speaking state, to be able to send voice
await voiceClient.EnterSpeakingStateAsync(SpeakingFlags.Microphone);

Expand Down Expand Up @@ -124,9 +121,6 @@ public async Task EchoAsync()
// Connect
await voiceClient.StartAsync();

// Wait for ready
await voiceClient.ReadyAsync;

// Enter speaking state, to be able to send voice
await voiceClient.EnterSpeakingStateAsync(SpeakingFlags.Microphone);

Expand Down
4 changes: 2 additions & 2 deletions Documentation/guides/advanced/voice.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Follow the [installation guide](installing-native-dependencies.md) to install th
> In the following examples streams and @NetCord.Gateway.Voice.VoiceClient instances are not disposed because they should be stored somewhere and disposed later.

### Sending Voice
[!code-cs[VoiceModule.cs](Voice/VoiceModule.cs#L12-L102)]
[!code-cs[VoiceModule.cs](Voice/VoiceModule.cs#L12-L99)]

### Receiving Voice
[!code-cs[VoiceModule.cs](Voice/VoiceModule.cs#L104-L146)]
[!code-cs[VoiceModule.cs](Voice/VoiceModule.cs#L101-L140)]
2 changes: 2 additions & 0 deletions NetCord/Gateway/CancellationTokenProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ internal sealed class CancellationTokenProvider : IDisposable
{
private readonly CancellationTokenSource _source;

public bool IsCancellationRequested => _source.IsCancellationRequested;

public CancellationToken Token { get; }

public CancellationTokenProvider()
Expand Down
59 changes: 30 additions & 29 deletions NetCord/Gateway/GatewayClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
namespace NetCord.Gateway;

/// <summary>
/// The GatewayClient class allows applications to send and receive data from the Discord Gateway, such as events and resource requests, via a WebSocket client.
/// The <see cref="GatewayClient"/> class allows applications to send and receive data from the Discord Gateway, such as events and resource requests.
/// </summary>
public partial class GatewayClient : WebSocketClient, IEntity
{
Expand Down Expand Up @@ -370,7 +370,7 @@ public partial class GatewayClient : WebSocketClient, IEntity
public event Func<GuildUser, ValueTask>? GuildUserUpdate;

/// <summary>
/// Sent in response to <see cref="RequestGuildUsersAsync(GuildUsersRequestProperties, CancellationToken)"/>. You can use the <see cref="GuildUserChunkEventArgs.ChunkIndex"/> and <see cref="GuildUserChunkEventArgs.ChunkCount"/> to calculate how many chunks are left for your request.<br/>
/// Sent in response to <see cref="RequestGuildUsersAsync(GuildUsersRequestProperties, WebSocketPayloadProperties, CancellationToken)"/>. You can use the <see cref="GuildUserChunkEventArgs.ChunkIndex"/> and <see cref="GuildUserChunkEventArgs.ChunkCount"/> to calculate how many chunks are left for your request.<br/>
/// </summary>
/// <remarks>
/// <br/> Required Intents: None
Expand Down Expand Up @@ -836,7 +836,7 @@ private protected override void OnConnected()
_compression.Initialize();
}

private ValueTask SendIdentifyAsync(PresenceProperties? presence = null, CancellationToken cancellationToken = default)
private ValueTask SendIdentifyAsync(ConnectionState connectionState, PresenceProperties? presence = null, CancellationToken cancellationToken = default)
{
var serializedPayload = new GatewayPayloadProperties<GatewayIdentifyProperties>(GatewayOpcode.Identify, new(Token.RawToken)
{
Expand All @@ -847,7 +847,7 @@ private ValueTask SendIdentifyAsync(PresenceProperties? presence = null, Cancell
Intents = _configuration.Intents,
}).Serialize(Serialization.Default.GatewayPayloadPropertiesGatewayIdentifyProperties);
_latencyTimer.Start();
return SendPayloadAsync(serializedPayload, cancellationToken);
return SendConnectionPayloadAsync(connectionState, serializedPayload, _internalPayloadProperties, cancellationToken);
}

/// <summary>
Expand All @@ -858,8 +858,8 @@ private ValueTask SendIdentifyAsync(PresenceProperties? presence = null, Cancell
/// <returns></returns>
public async Task StartAsync(PresenceProperties? presence = null, CancellationToken cancellationToken = default)
{
await StartAsync(cancellationToken).ConfigureAwait(false);
await SendIdentifyAsync(presence, cancellationToken).ConfigureAwait(false);
var connectionState = await StartAsync(cancellationToken).ConfigureAwait(false);
await SendIdentifyAsync(connectionState, presence, cancellationToken).ConfigureAwait(false);
}

/// <summary>
Expand All @@ -871,43 +871,43 @@ public async Task StartAsync(PresenceProperties? presence = null, CancellationTo
/// <returns></returns>
public async Task ResumeAsync(string sessionId, int sequenceNumber, CancellationToken cancellationToken = default)
{
await ConnectAsync(cancellationToken).ConfigureAwait(false);
await TryResumeAsync(SessionId = sessionId, SequenceNumber = sequenceNumber, cancellationToken).ConfigureAwait(false);
var connectionState = await StartAsync(cancellationToken).ConfigureAwait(false);
await TryResumeAsync(connectionState, SessionId = sessionId, SequenceNumber = sequenceNumber, cancellationToken).ConfigureAwait(false);
}

private protected override bool Reconnect(WebSocketCloseStatus? status, string? description)
=> status is not ((WebSocketCloseStatus)4004 or (WebSocketCloseStatus)4010 or (WebSocketCloseStatus)4011 or (WebSocketCloseStatus)4012 or (WebSocketCloseStatus)4013 or (WebSocketCloseStatus)4014);

private protected override ValueTask TryResumeAsync(CancellationToken cancellationToken = default)
private protected override ValueTask TryResumeAsync(ConnectionState connectionState, CancellationToken cancellationToken = default)
{
return TryResumeAsync(SessionId!, SequenceNumber, cancellationToken);
return TryResumeAsync(connectionState, SessionId!, SequenceNumber, cancellationToken);
}

private ValueTask TryResumeAsync(string sessionId, int sequenceNumber, CancellationToken cancellationToken = default)
private ValueTask TryResumeAsync(ConnectionState connectionState, string sessionId, int sequenceNumber, CancellationToken cancellationToken = default)
{
var serializedPayload = new GatewayPayloadProperties<GatewayResumeProperties>(GatewayOpcode.Resume, new(Token.RawToken, sessionId, sequenceNumber)).Serialize(Serialization.Default.GatewayPayloadPropertiesGatewayResumeProperties);
_latencyTimer.Start();
return SendPayloadAsync(serializedPayload, cancellationToken);
return SendConnectionPayloadAsync(connectionState, serializedPayload, _internalPayloadProperties, cancellationToken);
}

private protected override ValueTask HeartbeatAsync(CancellationToken cancellationToken = default)
private protected override ValueTask HeartbeatAsync(ConnectionState connectionState, CancellationToken cancellationToken = default)
{
var serializedPayload = new GatewayPayloadProperties<int>(GatewayOpcode.Heartbeat, SequenceNumber).Serialize(Serialization.Default.GatewayPayloadPropertiesInt32);
_latencyTimer.Start();
return SendPayloadAsync(serializedPayload, cancellationToken);
return SendConnectionPayloadAsync(connectionState, serializedPayload, _internalPayloadProperties, cancellationToken);
}

private protected override JsonPayload CreatePayload(ReadOnlyMemory<byte> payload) => JsonSerializer.Deserialize(_compression.Decompress(payload).Span, Serialization.Default.JsonPayload)!;

private protected override async Task ProcessPayloadAsync(JsonPayload payload)
private protected override async Task ProcessPayloadAsync(State state, ConnectionState connectionState, JsonPayload payload)
{
switch ((GatewayOpcode)payload.Opcode)
{
case GatewayOpcode.Dispatch:
SequenceNumber = payload.SequenceNumber.GetValueOrDefault();
try
{
await ProcessEventAsync(payload).ConfigureAwait(false);
await ProcessEventAsync(state, connectionState, payload).ConfigureAwait(false);
}
catch (Exception ex)
{
Expand All @@ -918,21 +918,21 @@ private protected override async Task ProcessPayloadAsync(JsonPayload payload)
break;
case GatewayOpcode.Reconnect:
InvokeLog(LogMessage.Info("Reconnect request"));
await AbortAndReconnectAsync().ConfigureAwait(false);
await AbortAndReconnectAsync(state, connectionState).ConfigureAwait(false);
break;
case GatewayOpcode.InvalidSession:
InvokeLog(LogMessage.Info("Invalid session"));
try
{
await SendIdentifyAsync().ConfigureAwait(false);
await SendIdentifyAsync(connectionState).ConfigureAwait(false);
}
catch (Exception ex)
{
InvokeLog(LogMessage.Error(ex));
}
break;
case GatewayOpcode.Hello:
StartHeartbeating(payload.Data.GetValueOrDefault().ToObject(Serialization.Default.JsonHello).HeartbeatInterval);
StartHeartbeating(connectionState, payload.Data.GetValueOrDefault().ToObject(Serialization.Default.JsonHello).HeartbeatInterval);
break;
case GatewayOpcode.HeartbeatACK:
await UpdateLatencyAsync(_latencyTimer.Elapsed).ConfigureAwait(false);
Expand All @@ -943,33 +943,34 @@ private protected override async Task ProcessPayloadAsync(JsonPayload payload)
/// <summary>
/// Joins, moves, or disconnects the app from a voice channel.
/// </summary>
public ValueTask UpdateVoiceStateAsync(VoiceStateProperties voiceState, CancellationToken cancellationToken = default)
public ValueTask UpdateVoiceStateAsync(VoiceStateProperties voiceState, WebSocketPayloadProperties? properties = null, CancellationToken cancellationToken = default)
{
GatewayPayloadProperties<VoiceStateProperties> payload = new(GatewayOpcode.VoiceStateUpdate, voiceState);
return SendPayloadAsync(payload.Serialize(Serialization.Default.GatewayPayloadPropertiesVoiceStateProperties), cancellationToken);
return SendPayloadAsync(payload.Serialize(Serialization.Default.GatewayPayloadPropertiesVoiceStateProperties), properties, cancellationToken);
}

/// <summary>
/// Updates an app's presence.
/// </summary>
/// <param name="presence">The presence to set.</param>
/// <param name="properties"></param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
public ValueTask UpdatePresenceAsync(PresenceProperties presence, CancellationToken cancellationToken = default)
public ValueTask UpdatePresenceAsync(PresenceProperties presence, WebSocketPayloadProperties? properties = null, CancellationToken cancellationToken = default)
{
GatewayPayloadProperties<PresenceProperties> payload = new(GatewayOpcode.PresenceUpdate, presence);
return SendPayloadAsync(payload.Serialize(Serialization.Default.GatewayPayloadPropertiesPresenceProperties), cancellationToken);
return SendPayloadAsync(payload.Serialize(Serialization.Default.GatewayPayloadPropertiesPresenceProperties), properties, cancellationToken);
}

/// <summary>
/// Requests user for a guild.
/// Requests users for a guild.
/// </summary>
public ValueTask RequestGuildUsersAsync(GuildUsersRequestProperties requestProperties, CancellationToken cancellationToken = default)
public ValueTask RequestGuildUsersAsync(GuildUsersRequestProperties requestProperties, WebSocketPayloadProperties? properties = null, CancellationToken cancellationToken = default)
{
GatewayPayloadProperties<GuildUsersRequestProperties> payload = new(GatewayOpcode.RequestGuildUsers, requestProperties);
return SendPayloadAsync(payload.Serialize(Serialization.Default.GatewayPayloadPropertiesGuildUsersRequestProperties), cancellationToken);
return SendPayloadAsync(payload.Serialize(Serialization.Default.GatewayPayloadPropertiesGuildUsersRequestProperties), properties, cancellationToken);
}

private async Task ProcessEventAsync(JsonPayload payload)
private async Task ProcessEventAsync(State state, ConnectionState connectionState, JsonPayload payload)
{
var data = payload.Data.GetValueOrDefault();
var name = payload.Event!;
Expand All @@ -991,7 +992,7 @@ await InvokeEventAsync(Ready, args, data =>
SessionId = args.SessionId;
ApplicationFlags = args.ApplicationFlags;

_readyCompletionSource.TrySetResult();
state.IndicateReady(connectionState);
}).ConfigureAwait(false);
await updateLatencyTask.ConfigureAwait(false);
}
Expand All @@ -1003,7 +1004,7 @@ await InvokeEventAsync(Ready, args, data =>
var updateLatencyTask = UpdateLatencyAsync(latency);
var resumeTask = InvokeResumeEventAsync();

_readyCompletionSource.TrySetResult();
state.IndicateReady(connectionState);

await updateLatencyTask.ConfigureAwait(false);
await resumeTask.ConfigureAwait(false);
Expand Down
6 changes: 5 additions & 1 deletion NetCord/Gateway/GatewayClientConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ namespace NetCord.Gateway;

public class GatewayClientConfiguration : IWebSocketClientConfiguration
{
public IWebSocket? WebSocket { get; init; }
public IWebSocketConnectionProvider? WebSocketConnectionProvider { get; init; }
public IRateLimiterProvider? RateLimiterProvider { get; init; }
public WebSocketPayloadProperties? DefaultPayloadProperties { get; init; }
public IReconnectStrategy? ReconnectStrategy { get; init; }
public ILatencyTimer? LatencyTimer { get; init; }
public ApiVersion Version { get; init; } = ApiVersion.V10;
Expand All @@ -21,4 +23,6 @@ public class GatewayClientConfiguration : IWebSocketClientConfiguration
public Shard? Shard { get; init; }
public bool CacheDMChannels { get; init; } = true;
public Rest.RestClientConfiguration? RestClientConfiguration { get; init; }

IRateLimiterProvider? IWebSocketClientConfiguration.RateLimiterProvider => RateLimiterProvider is { } rateLimiter ? rateLimiter : new GatewayRateLimiterProvider(120, 60_000);
}
59 changes: 59 additions & 0 deletions NetCord/Gateway/GatewayRateLimiterProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
namespace NetCord.Gateway;

public class GatewayRateLimiterProvider(int limit, long duration) : IRateLimiterProvider
{
public IRateLimiter CreateRateLimiter() => new GatewayRateLimiter(limit, duration);

private sealed class GatewayRateLimiter(int limit, long duration) : IRateLimiter
{
private readonly object _lock = new();
private readonly int _limit = limit;
private int _remaining = limit;
private long _reset;

public ValueTask<RateLimitAcquisitionResult> TryAcquireAsync(CancellationToken cancellationToken = default)
{
var timestamp = Environment.TickCount64;
lock (_lock)
{
var diff = _reset - timestamp;
if (diff <= 0)
{
_remaining = _limit - 1;
_reset = timestamp + duration;
}
else
{
if (_remaining == 0)
return new(RateLimitAcquisitionResult.RateLimit((int)diff));
else
_remaining--;
}
}

return new(RateLimitAcquisitionResult.NoRateLimit);
}

public ValueTask CancelAcquireAsync(long acquisitionTimestamp, CancellationToken cancellationToken = default)
{
var currentTimestamp = Environment.TickCount64;
lock (_lock)
{
var reset = _reset;
var start = reset - duration;
if (acquisitionTimestamp <= reset
&& acquisitionTimestamp >= start
&& currentTimestamp <= reset
&& currentTimestamp >= start
&& _remaining < _limit)
_remaining++;
}

return default;
}

public void Dispose()
{
}
}
}
8 changes: 8 additions & 0 deletions NetCord/Gateway/IRateLimiter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace NetCord.Gateway;

public interface IRateLimiter : IDisposable
{
public ValueTask<RateLimitAcquisitionResult> TryAcquireAsync(CancellationToken cancellationToken = default);

public ValueTask CancelAcquireAsync(long acquisitionTimestamp, CancellationToken cancellationToken = default);
}
6 changes: 6 additions & 0 deletions NetCord/Gateway/IRateLimiterProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace NetCord.Gateway;

public interface IRateLimiterProvider
{
public IRateLimiter CreateRateLimiter();
}
4 changes: 3 additions & 1 deletion NetCord/Gateway/IWebSocketClientConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ namespace NetCord.Gateway;

internal interface IWebSocketClientConfiguration
{
public IWebSocket? WebSocket { get; }
public IWebSocketConnectionProvider? WebSocketConnectionProvider { get; }
public IReconnectStrategy? ReconnectStrategy { get; }
public ILatencyTimer? LatencyTimer { get; }
public IRateLimiterProvider? RateLimiterProvider { get; }
public WebSocketPayloadProperties? DefaultPayloadProperties { get; }
}
25 changes: 25 additions & 0 deletions NetCord/Gateway/NullRateLimiterProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace NetCord.Gateway;

public class NullRateLimiterProvider : IRateLimiterProvider
{
public static NullRateLimiterProvider Instance { get; } = new();

public IRateLimiter CreateRateLimiter() => NullRateLimiter.Instance;

private sealed class NullRateLimiter : IRateLimiter
{
public static NullRateLimiter Instance { get; } = new();

private NullRateLimiter()
{
}

public ValueTask<RateLimitAcquisitionResult> TryAcquireAsync(CancellationToken cancellationToken = default) => new(RateLimitAcquisitionResult.NoRateLimit);

public ValueTask CancelAcquireAsync(long acquisitionTimestamp, CancellationToken cancellationToken = default) => default;

public void Dispose()
{
}
}
}
18 changes: 18 additions & 0 deletions NetCord/Gateway/RateLimitAcquisitionResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace NetCord.Gateway;

public readonly struct RateLimitAcquisitionResult
{
private RateLimitAcquisitionResult(int resetAfter, bool rateLimited)
{
ResetAfter = resetAfter;
RateLimited = rateLimited;
}

public static RateLimitAcquisitionResult NoRateLimit { get; } = new(0, false);

public static RateLimitAcquisitionResult RateLimit(int resetAfter) => new(resetAfter, true);

public int ResetAfter { get; }

public bool RateLimited { get; }
}
2 changes: 1 addition & 1 deletion NetCord/Gateway/RentedArrayBufferWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private void ResizeBuffer(int sizeHint)
{
var pool = ArrayPool<T>.Shared;
var newBuffer = pool.Rent(sum);
Array.Copy(buffer, newBuffer, index);
buffer.AsSpan(0, index).CopyTo(newBuffer);
_buffer = newBuffer;
pool.Return(buffer);
}
Expand Down
Loading