From 120f251bd9cb81a6f4bd16592a675b6ede5873f1 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Fri, 3 Oct 2025 08:36:43 -0700 Subject: [PATCH 1/5] Adding Abort to C# Websocket --- sdks/csharp/src/SpacetimeDBClient.cs | 4 ++++ sdks/csharp/src/WebSocket.cs | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/sdks/csharp/src/SpacetimeDBClient.cs b/sdks/csharp/src/SpacetimeDBClient.cs index 1dac02ccf60..8c9c89d5608 100644 --- a/sdks/csharp/src/SpacetimeDBClient.cs +++ b/sdks/csharp/src/SpacetimeDBClient.cs @@ -475,6 +475,10 @@ public void Disconnect() { webSocket.Close(); } + else + { + webSocket.Abort(); // forceful during connecting + } _parseCancellationTokenSource.Cancel(); } diff --git a/sdks/csharp/src/WebSocket.cs b/sdks/csharp/src/WebSocket.cs index b9b2e3506ab..4ea2681d9d7 100644 --- a/sdks/csharp/src/WebSocket.cs +++ b/sdks/csharp/src/WebSocket.cs @@ -381,6 +381,31 @@ public Task Close(WebSocketCloseStatus code = WebSocketCloseStatus.NormalClosure return Task.CompletedTask; } + /// + /// Forcefully abort the WebSocket connection. This terminates any in-flight connect/receive/send + /// and ensures the server-side socket is torn down promptly. Prefer Close() for graceful shutdowns. + /// + public void Abort() + { +#if UNITY_WEBGL && !UNITY_EDITOR + // WebGL plugin does not expose an Abort; fall back to a best-effort close if connected. + if (_isConnected && _webglSocketId >= 0) + { + WebSocket_Close(_webglSocketId, (int)WebSocketCloseStatus.NormalClosure, "Aborting connection."); + _isConnected = false; + } +#else + try + { + Ws?.Abort(); + } + catch + { + // Intentionally swallow; Abort is best-effort. + } +#endif + } + private Task? senderTask; private readonly ConcurrentQueue messageSendQueue = new(); From c8dd353ca97f718f60c1b11b1e19bd2d9a177d21 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Fri, 3 Oct 2025 11:33:01 -0700 Subject: [PATCH 2/5] Added check only call Abort if Disconnect is called while Connecting --- sdks/csharp/src/SpacetimeDBClient.cs | 9 ++++++++- sdks/csharp/src/WebSocket.cs | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/sdks/csharp/src/SpacetimeDBClient.cs b/sdks/csharp/src/SpacetimeDBClient.cs index 8c9c89d5608..71fa86d89ee 100644 --- a/sdks/csharp/src/SpacetimeDBClient.cs +++ b/sdks/csharp/src/SpacetimeDBClient.cs @@ -475,10 +475,17 @@ public void Disconnect() { webSocket.Close(); } - else +#if UNITY_WEBGL && !UNITY_EDITOR + else if (webSocket.IsConnecting) + { + webSocket.Abort(); // forceful during connecting + } +#else + else if (webSocket.IsConnecting || webSocket.IsNoneState) { webSocket.Abort(); // forceful during connecting } +#endif _parseCancellationTokenSource.Cancel(); } diff --git a/sdks/csharp/src/WebSocket.cs b/sdks/csharp/src/WebSocket.cs index 4ea2681d9d7..23c294c33ff 100644 --- a/sdks/csharp/src/WebSocket.cs +++ b/sdks/csharp/src/WebSocket.cs @@ -61,8 +61,11 @@ public WebSocket(ConnectOptions options) private bool _isConnected = false; private bool _isConnecting = false; public bool IsConnected => _isConnected; + public bool IsConnecting => _isConnecting; #else public bool IsConnected { get { return Ws != null && Ws.State == WebSocketState.Open; } } + public bool IsConnecting { get { return Ws != null && Ws.State == WebSocketState.Connecting; } } + public bool IsNoneState { get { return Ws != null && Ws.State == WebSocketState.None; } } #endif #if UNITY_WEBGL && !UNITY_EDITOR From 3032236dc3bef2899ee473686c1b320ab138e2e2 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Fri, 3 Oct 2025 17:15:32 -0700 Subject: [PATCH 3/5] Fix issue with WebGL support --- sdks/csharp/src/WebSocket.cs | 61 ++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/sdks/csharp/src/WebSocket.cs b/sdks/csharp/src/WebSocket.cs index 23c294c33ff..b5d45205ab2 100644 --- a/sdks/csharp/src/WebSocket.cs +++ b/sdks/csharp/src/WebSocket.cs @@ -38,6 +38,7 @@ public struct ConnectOptions private readonly ConcurrentQueue dispatchQueue = new(); protected ClientWebSocket Ws = new(); + private CancellationTokenSource? _connectCts; public WebSocket(ConnectOptions options) { @@ -60,6 +61,7 @@ public WebSocket(ConnectOptions options) #if UNITY_WEBGL && !UNITY_EDITOR private bool _isConnected = false; private bool _isConnecting = false; + private bool _cancelConnectRequested = false; public bool IsConnected => _isConnected; public bool IsConnecting => _isConnecting; #else @@ -148,8 +150,9 @@ public async Task Connect(string? auth, string host, string nameOrAddress, Conne { #if UNITY_WEBGL && !UNITY_EDITOR if (_isConnecting || _isConnected) return; - + _isConnecting = true; + _cancelConnectRequested = false; try { var uri = $"{host}/v1/database/{nameOrAddress}/subscribe?connection_id={connectionId}&compression={compression}"; @@ -164,6 +167,11 @@ public async Task Connect(string? auth, string host, string nameOrAddress, Conne dispatchQueue.Enqueue(() => OnConnectError?.Invoke( new Exception("Failed to connect WebSocket"))); } + else if (_cancelConnectRequested) + { + // If cancel was requested before open, proactively close now. + WebSocket_Close(_webglSocketId, (int)WebSocketCloseStatus.NormalClosure, "Canceled during connect."); + } } catch (Exception e) { @@ -183,7 +191,7 @@ public async Task Connect(string? auth, string host, string nameOrAddress, Conne var url = new Uri(uri); Ws.Options.AddSubProtocol(_options.Protocol); - var source = new CancellationTokenSource(10000); + _connectCts = new CancellationTokenSource(10000); if (!string.IsNullOrEmpty(auth)) { Ws.Options.SetRequestHeader("Authorization", $"Bearer {auth}"); @@ -195,7 +203,7 @@ public async Task Connect(string? auth, string host, string nameOrAddress, Conne try { - await Ws.ConnectAsync(url, source.Token); + await Ws.ConnectAsync(url, _connectCts.Token); if (Ws.State == WebSocketState.Open) { if (OnConnect != null) @@ -367,14 +375,36 @@ await Ws.CloseAsync(WebSocketCloseStatus.MessageTooBig, closeMessage, #endif } + /// + /// Cancel an in-flight ConnectAsync. Safe to call if no connect is pending. + /// + public void CancelConnect() + { +#if UNITY_WEBGL && !UNITY_EDITOR + // No CTS on WebGL. Mark cancel intent so that when socket id arrives or open fires, + // we immediately close and avoid reporting a connected state. + _cancelConnectRequested = true; + return; +#else + try { _connectCts?.Cancel(); } catch { /* ignore */ } +#endif + } + public Task Close(WebSocketCloseStatus code = WebSocketCloseStatus.NormalClosure) { #if UNITY_WEBGL && !UNITY_EDITOR - if (_isConnected && _webglSocketId >= 0) + if (_webglSocketId >= 0) { + // If connected or connecting with a valid socket id, request a close. WebSocket_Close(_webglSocketId, (int)code, "Disconnecting normally."); + _cancelConnectRequested = false; // graceful close intent _isConnected = false; } + else if (_isConnecting) + { + // We don't yet have a socket id; remember to cancel once it arrives/opens. + _cancelConnectRequested = true; + } #else if (Ws?.State == WebSocketState.Open) { @@ -391,12 +421,16 @@ public Task Close(WebSocketCloseStatus code = WebSocketCloseStatus.NormalClosure public void Abort() { #if UNITY_WEBGL && !UNITY_EDITOR - // WebGL plugin does not expose an Abort; fall back to a best-effort close if connected. - if (_isConnected && _webglSocketId >= 0) + if (_webglSocketId >= 0) { WebSocket_Close(_webglSocketId, (int)WebSocketCloseStatus.NormalClosure, "Aborting connection."); _isConnected = false; } + else if (_isConnecting) + { + // No socket yet; ensure we close immediately once it opens. + _cancelConnectRequested = true; + } #else try { @@ -475,11 +509,21 @@ public WebSocketState GetState() { return Ws!.State; } + #if UNITY_WEBGL && !UNITY_EDITOR public void HandleWebGLOpen(int socketId) { if (socketId == _webglSocketId) { + if (_cancelConnectRequested) + { + // Immediately close instead of reporting connected. + WebSocket_Close(_webglSocketId, (int)WebSocketCloseStatus.NormalClosure, "Canceled during connect."); + _isConnecting = false; + _isConnected = false; + _cancelConnectRequested = false; + return; + } _isConnected = true; if (OnConnect != null) dispatchQueue.Enqueue(() => OnConnect()); @@ -500,6 +544,9 @@ public void HandleWebGLClose(int socketId, int code, string reason) if (socketId == _webglSocketId && OnClose != null) { _isConnected = false; + _isConnecting = false; + _webglSocketId = -1; + _cancelConnectRequested = false; var ex = code != (int)WebSocketCloseStatus.NormalClosure ? new Exception($"WebSocket closed with code {code}: {reason}") : null; dispatchQueue.Enqueue(() => OnClose?.Invoke(ex)); } @@ -510,6 +557,8 @@ public void HandleWebGLError(int socketId) UnityEngine.Debug.Log($"HandleWebGLError: {socketId}"); if (socketId == _webglSocketId && OnConnectError != null) { + _isConnecting = false; + _webglSocketId = -1; dispatchQueue.Enqueue(() => OnConnectError(new Exception($"Socket {socketId} error."))); } } From 58c46d8144e0a6198f6a83731149bbdbf5644193 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Tue, 28 Oct 2025 09:53:39 -0700 Subject: [PATCH 4/5] Consolidated webSocket.Abort statement paths --- sdks/csharp/src/SpacetimeDBClient.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/sdks/csharp/src/SpacetimeDBClient.cs b/sdks/csharp/src/SpacetimeDBClient.cs index 71fa86d89ee..58f9ddf24d9 100644 --- a/sdks/csharp/src/SpacetimeDBClient.cs +++ b/sdks/csharp/src/SpacetimeDBClient.cs @@ -476,16 +476,13 @@ public void Disconnect() webSocket.Close(); } #if UNITY_WEBGL && !UNITY_EDITOR - else if (webSocket.IsConnecting) - { - webSocket.Abort(); // forceful during connecting - } + else if (webSocket.IsConnecting) #else else if (webSocket.IsConnecting || webSocket.IsNoneState) +#endif { webSocket.Abort(); // forceful during connecting } -#endif _parseCancellationTokenSource.Cancel(); } From 69c86e0c2cfd226ed9da16e5d414b49c12222723 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Tue, 25 Nov 2025 10:13:34 -0800 Subject: [PATCH 5/5] Removes unnecessary return Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com> Signed-off-by: rekhoff --- sdks/csharp/src/WebSocket.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/sdks/csharp/src/WebSocket.cs b/sdks/csharp/src/WebSocket.cs index b5d45205ab2..b6de5d36e6e 100644 --- a/sdks/csharp/src/WebSocket.cs +++ b/sdks/csharp/src/WebSocket.cs @@ -384,7 +384,6 @@ public void CancelConnect() // No CTS on WebGL. Mark cancel intent so that when socket id arrives or open fires, // we immediately close and avoid reporting a connected state. _cancelConnectRequested = true; - return; #else try { _connectCts?.Cancel(); } catch { /* ignore */ } #endif