diff --git a/src/Discord.Net/Class1.cs b/src/Discord.Net/Class1.cs
deleted file mode 100644
index 9a50619c7..000000000
--- a/src/Discord.Net/Class1.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using System;
-
-namespace Discord.Net
-{
- public class Class1
- {
- }
-}
diff --git a/src/Discord.Net/Discord.Net.csproj b/src/Discord.Net/Discord.Net.csproj
index d4c395e8c..260238f05 100644
--- a/src/Discord.Net/Discord.Net.csproj
+++ b/src/Discord.Net/Discord.Net.csproj
@@ -2,6 +2,8 @@
netstandard2.1
+ 8.0
+ enable
diff --git a/src/Discord.Net/Socket/Gateway.cs b/src/Discord.Net/Socket/Gateway.cs
new file mode 100644
index 000000000..df4e1ab6d
--- /dev/null
+++ b/src/Discord.Net/Socket/Gateway.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Discord.Net.Socket
+{
+ public class Gateway
+ {
+ static readonly Uri DefaultGatewayUri = new Uri("wss://gateway.discord.gg");
+
+ ISocket Socket { get; set; }
+
+ public Gateway(SocketFactory socketFactory)
+ {
+ Socket = socketFactory(OnAborted, OnPacket);
+ }
+
+ public async Task ConnectAsync(Uri? gatewayUri)
+ {
+ await Socket.ConnectAsync(gatewayUri ?? DefaultGatewayUri, CancellationToken.None).ConfigureAwait(false);
+ }
+
+ public void OnAborted(Exception error)
+ {
+ // todo: log
+ }
+ public async Task OnPacket(object packet)
+ {
+ await Task.CompletedTask;
+ }
+ }
+}
diff --git a/src/Discord.Net/Socket/ISocket.cs b/src/Discord.Net/Socket/ISocket.cs
new file mode 100644
index 000000000..0c65e0828
--- /dev/null
+++ b/src/Discord.Net/Socket/ISocket.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Discord.Net.Socket
+{
+ public delegate ISocket SocketFactory(OnAbortionHandler abortionHandler, OnPacketHandler packetHandler);
+
+ // A socket should only have one parent, so these do not need to be decoupled events.
+ public delegate Task OnPacketHandler(object packet);
+ public delegate void OnAbortionHandler(Exception error);
+
+ public enum SocketState
+ {
+ Closed = default,
+ AcquiringOpenLock,
+ Opening,
+ Open,
+ AcquiringClosingLock,
+ Closing,
+ Aborted
+ }
+
+ public interface ISocket : IDisposable
+ {
+ SocketState State { get; }
+
+ Task ConnectAsync(Uri uri, CancellationToken token);
+ Task CloseAsync(int? code = null, string? reason = null);
+ Task SendAsync(ReadOnlyMemory payload);
+
+ OnAbortionHandler OnAbortion { get; }
+ OnPacketHandler OnPacket { get; }
+ }
+}
diff --git a/src/Discord.Net/Socket/Providers/DefaultSocket.cs b/src/Discord.Net/Socket/Providers/DefaultSocket.cs
new file mode 100644
index 000000000..c4501d2a5
--- /dev/null
+++ b/src/Discord.Net/Socket/Providers/DefaultSocket.cs
@@ -0,0 +1,194 @@
+using System;
+using System.Net.WebSockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Discord.Net.Socket.Providers
+{
+ public static class DefaultSocketFactory
+ {
+ public static ISocket Create(OnAbortionHandler onAbortion, OnPacketHandler onPacket)
+ {
+ return new DefaultSocket(onAbortion, onPacket);
+ }
+ }
+
+ internal class DefaultSocket : ISocket
+ {
+ public SocketState State { get; private set; }
+ public OnAbortionHandler OnAbortion { get; }
+ public OnPacketHandler OnPacket { get; }
+
+ private ClientWebSocket _socket;
+ private Task? _receiveTask;
+
+ private CancellationTokenSource _cancelTokenSource;
+ private SemaphoreSlim _sendLock;
+ private SemaphoreSlim _stateLock;
+
+ public DefaultSocket(OnAbortionHandler onAbortion, OnPacketHandler onPacket)
+ {
+ _socket = new ClientWebSocket();
+
+ _cancelTokenSource = new CancellationTokenSource();
+ _sendLock = new SemaphoreSlim(1);
+ _stateLock = new SemaphoreSlim(1);
+
+ OnAbortion = onAbortion;
+ OnPacket = onPacket;
+ }
+
+ public async Task ConnectAsync(Uri uri, CancellationToken connectCancelToken)
+ {
+ if (State == SocketState.Open
+ || State == SocketState.Opening
+ || State == SocketState.AcquiringOpenLock
+ || State == SocketState.Aborted)
+ {
+ // todo: evaluate how to handle a (redundant?) state operation
+ return;
+ }
+
+ CancellationTokenSource openLock; // create a linked token in case the caller wants to cancel an opening connection
+ try
+ {
+ openLock = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, connectCancelToken);
+ }
+ catch (ObjectDisposedException e)
+ {
+ // Failed to link openLock, an expired cancellation token was passed
+ State = SocketState.Aborted;
+ OnAbortion(e);
+ return;
+ }
+
+ State = SocketState.AcquiringOpenLock;
+ try
+ {
+ await _stateLock.WaitAsync(openLock.Token).ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ // Failed to acquire openLock
+ State = SocketState.Aborted;
+ OnAbortion(e);
+ }
+ State = SocketState.Opening;
+
+ try
+ {
+ await _socket.ConnectAsync(uri, _cancelTokenSource.Token).ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ // Failed to open socket connection
+ State = SocketState.Aborted;
+ OnAbortion(e);
+ return;
+ }
+ State = SocketState.Open;
+
+ // TODO: this should not be expected to fail
+ _stateLock.Release();
+ openLock.Dispose();
+ }
+ public async Task CloseAsync(int? code, string? reason)
+ {
+ if (State == SocketState.Closed
+ || State == SocketState.Closing
+ || State == SocketState.AcquiringClosingLock
+ || State == SocketState.Aborted)
+ {
+ // todo: evaluate how to handle a (redundant?) state operation; see OpenAsync
+ return;
+ }
+
+ State = SocketState.AcquiringClosingLock;
+ try
+ {
+ await _stateLock.WaitAsync();
+ }
+ catch (Exception e)
+ {
+ State = SocketState.Aborted;
+ OnAbortion(e);
+ return;
+ }
+ State = SocketState.Closing;
+
+ // I think it is acceptable to use CancellationToken.None here, as no parallel operation should need to cancel the socket closure
+ await _socket.CloseAsync((WebSocketCloseStatus)(code ?? 1005),
+ reason ?? string.Empty,
+ CancellationToken.None
+ ).ConfigureAwait(false);
+
+ // Wait until after .NET has been told to close the socket to cancel any pending sends/receives
+ //
+ // Presumably, sends/receives should have failed gracefully by this point, instead of aborting the underlying socket
+ try
+ {
+ _cancelTokenSource.Cancel();
+ await (_receiveTask ?? Task.CompletedTask);
+ }
+ catch
+ {
+ // just log for now
+ }
+
+ State = SocketState.Closed;
+ }
+ public async Task ReceiveAsync()
+ {
+ while (State == SocketState.Open && !_cancelTokenSource.IsCancellationRequested)
+ {
+ try
+ {
+ Memory buffer = new Memory();
+ var res = await _socket.ReceiveAsync(buffer, _cancelTokenSource.Token).ConfigureAwait(false);
+ // todo: handle memory renting and ongoing messages
+ // todo: parse and OnPacket
+ }
+ catch (Exception err)
+ {
+ // log error
+ if (_socket.State != WebSocketState.Open) // detrimental error
+ {
+ State = SocketState.Aborted;
+ OnAbortion(err);
+ return;
+ }
+ }
+ }
+ }
+ public async Task SendAsync(ReadOnlyMemory data)
+ {
+ if (State != SocketState.Open)
+ {
+ // raise error?
+ return;
+ }
+
+ await _sendLock.WaitAsync().ConfigureAwait(false);
+ try
+ {
+ // TODO: compression? who needs it
+ await _socket.SendAsync(data, WebSocketMessageType.Text, true, _cancelTokenSource.Token).ConfigureAwait(false);
+ }
+ finally
+ {
+ _sendLock.Release();
+ }
+ }
+
+ public void Dispose()
+ {
+ if (State != SocketState.Closed)
+ {
+ // log error? can this still proceed...
+ }
+ _socket.Dispose();
+ _cancelTokenSource.Dispose();
+ _stateLock.Dispose();
+ }
+ }
+}