From a370fdc2ad7dd1a5df92b8e5f8d056a75d23dac8 Mon Sep 17 00:00:00 2001 From: Lisandro Crespo Date: Mon, 24 Aug 2026 12:02:32 -0500 Subject: [PATCH 1/2] Clean after errors and disconnections --- sdks/csharp/src/SpacetimeDBClient.cs | 28 ++++++++++++++++++---- sdks/csharp/src/Stats.cs | 35 ++++++++++++++++++++++++++++ sdks/csharp/src/WebSocket.cs | 4 ++++ 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/sdks/csharp/src/SpacetimeDBClient.cs b/sdks/csharp/src/SpacetimeDBClient.cs index b5ed336e314..9d1b2bb2086 100644 --- a/sdks/csharp/src/SpacetimeDBClient.cs +++ b/sdks/csharp/src/SpacetimeDBClient.cs @@ -185,6 +185,8 @@ private sealed class PendingReducerCall private void FailPendingOperations(Exception error) { + stats.ClearRequestsAwaitingResponse(); + foreach (var (requestId, _) in waitingOneOffQueries.ToArray()) { if (waitingOneOffQueries.TryRemove(requestId, out var resultSource)) @@ -753,6 +755,8 @@ private void ApplyMessage(ParsedMessage parsed) { Log.Exception(e); } + + subscriptions.Remove(subscriptionError.QuerySetId.Id); } else { @@ -944,15 +948,31 @@ void IDbConnection.InternalCallProcedure( async Task IDbConnection.RemoteQuery(string query) { + if (!webSocket.IsConnected) + { + var error = "Cannot run one-off query, not connected to server!"; + Log.Error(error); + throw new InvalidOperationException(error); + } + var requestId = stats.OneOffRequestTracker.StartTrackingRequest(); var resultSource = new TaskCompletionSource(); waitingOneOffQueries[requestId] = resultSource; - webSocket.Send(new ClientMessage.OneOffQuery(new OneOffQuery + try { - RequestId = requestId, - QueryString = query, - })); + webSocket.Send(new ClientMessage.OneOffQuery(new OneOffQuery + { + RequestId = requestId, + QueryString = query, + })); + } + catch + { + waitingOneOffQueries.TryRemove(requestId, out _); + stats.OneOffRequestTracker.RemoveRequestAwaitingResponse(requestId); + throw; + } var result = await resultSource.Task; diff --git a/sdks/csharp/src/Stats.cs b/sdks/csharp/src/Stats.cs index ecba4d6dcdd..a216767a193 100644 --- a/sdks/csharp/src/Stats.cs +++ b/sdks/csharp/src/Stats.cs @@ -174,6 +174,17 @@ internal bool FinishTrackingRequest(uint requestId, DateTime finished, string? m } } + /// + /// Stop tracking an outstanding request that can no longer receive a response. + /// + internal bool RemoveRequestAwaitingResponse(uint requestId) + { + lock (this) + { + return _requests.Remove(requestId); + } + } + internal void InsertRequest(TimeSpan duration, string metadata) { lock (this) @@ -246,6 +257,17 @@ internal void InsertRequest(DateTime start, string metadata) /// /// public int GetRequestsAwaitingResponse() => _requests.Count; + + /// + /// Clear outstanding tracked requests that can no longer receive responses. + /// + internal void ClearRequestsAwaitingResponse() + { + lock (this) + { + _requests.Clear(); + } + } } public class Stats @@ -314,5 +336,18 @@ public class Stats /// Includes: apply time (on main thread). /// public readonly NetworkRequestTracker ApplyMessageTracker = new(); + + /// + /// Clear all outstanding tracked requests that can no longer receive responses. + /// + internal void ClearRequestsAwaitingResponse() + { + ReducerRequestTracker.ClearRequestsAwaitingResponse(); + ProcedureRequestTracker.ClearRequestsAwaitingResponse(); + SubscriptionRequestTracker.ClearRequestsAwaitingResponse(); + OneOffRequestTracker.ClearRequestsAwaitingResponse(); + ParseMessageQueueTracker.ClearRequestsAwaitingResponse(); + ApplyMessageQueueTracker.ClearRequestsAwaitingResponse(); + } } } diff --git a/sdks/csharp/src/WebSocket.cs b/sdks/csharp/src/WebSocket.cs index 26ce87127ba..8ae335db8f0 100644 --- a/sdks/csharp/src/WebSocket.cs +++ b/sdks/csharp/src/WebSocket.cs @@ -558,6 +558,10 @@ public void HandleWebGLClose(int socketId, int code, string reason) _isConnecting = false; _webglSocketId = -1; _cancelConnectRequested = false; + if (ReferenceEquals(Instance, this)) + { + Instance = null; + } var ex = code != (int)WebSocketCloseStatus.NormalClosure ? new Exception($"WebSocket closed with code {code}: {reason}") : null; dispatchQueue.Enqueue(() => OnClose?.Invoke(ex)); } From eb51f1e88ca6b91bb39a6b55e999538f9e2aa567 Mon Sep 17 00:00:00 2001 From: Lisandro Crespo Date: Mon, 24 Aug 2026 12:05:08 -0500 Subject: [PATCH 2/2] Fix EOL in some files --- sdks/csharp/src/AuthToken.cs | 336 +++++++++++++------------- sdks/csharp/src/ConsoleLogger.cs | 164 ++++++------- sdks/csharp/src/ISpacetimeDBLogger.cs | 112 ++++----- sdks/csharp/src/UnityDebugLogger.cs | 68 +++--- 4 files changed, 340 insertions(+), 340 deletions(-) diff --git a/sdks/csharp/src/AuthToken.cs b/sdks/csharp/src/AuthToken.cs index 748d3beec8a..7fd30fbeaff 100644 --- a/sdks/csharp/src/AuthToken.cs +++ b/sdks/csharp/src/AuthToken.cs @@ -1,168 +1,168 @@ -/* This is an optional helper class to store your auth token in local storage - * - Example: - - AuthToken.Init(".my_app_name"); - - SpacetimeDBClient.instance.onIdentityReceived += (token, identity) => - { - AuthToken.SaveToken(token); - - ... - }; - - SpacetimeDBClient.instance.Connect(AuthToken.Token, "localhost:3000", "basicchat", false); - */ -#if UNITY_5_3_OR_NEWER -using UnityEngine; - -namespace SpacetimeDB -{ - // This is an optional helper class to store your auth token in PlayerPrefs - // Override GetTokenKey() if you want to use a player pref key specific to your game - public static class AuthToken - { - public static string Token => PlayerPrefs.GetString(GetTokenKey()); - - public static void SaveToken(string token) - { - PlayerPrefs.SetString(GetTokenKey(), token); - } - - private static string GetTokenKey() - { - var key = "spacetimedb.identity_token"; -#if UNITY_EDITOR - // Different editors need different keys - key += $" - {Application.dataPath}"; -#endif - return key; - } - } -} -#elif GODOT -using Godot; - -namespace SpacetimeDB -{ - // This is an optional helper class to store your auth token in PlayerPrefs - // You can use keySuffix to save and retrieve different tokens - public static class AuthToken - { - private const string Path = "user://spacetimedb-token.cfg"; - private const string Section = "stdb"; - private static string Key => "identity_token"; - - private static ConfigFile? _config; - private static ConfigFile Config => _config ??= new ConfigFile(); - - public static string GetToken(string? keySuffix = null) - { - var key = GetKey(keySuffix); - if(!Config.HasSectionKey(Section, key)) - { - Config.Load(Path); - } - return Config.GetValue(Section, key, "").As(); - } - public static bool TryGetToken(out string result) => TryGetToken(null, out result); - public static bool TryGetToken(string? keySuffix, out string result) { - result = GetToken(keySuffix); - return !string.IsNullOrWhiteSpace(result); - } - - public static void SaveToken(string token, string? keySuffix = null) - { - Config.SetValue(Section, GetKey(keySuffix), token); - Config.Save(Path); - } - - private static string GetKey(string? suffix) => string.IsNullOrWhiteSpace(suffix) ? Key : $"{Key}_{suffix}"; - } -} -#else -using System; -using System.IO; -using System.Linq; - -namespace SpacetimeDB -{ - public static class AuthToken - { - private static string? settingsPath; - private static string? token; - - private const string PREFIX = "auth_token="; - - /// - /// Initializes the AuthToken class. This must be called before any other methods. - /// - /// The folder to store the config file in. Default is ".spacetime_csharp_sdk". - /// The name of the config file. Default is "settings.ini". - /// The root folder to store the config file in. Default is the user's home directory. - /// - public static void Init(string configFolder = ".spacetime_csharp_sdk", string configFile = "settings.ini", string? configRoot = null) - { - configRoot ??= Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - - if (Environment.GetCommandLineArgs().Any(arg => arg == "--client")) - { - int clientIndex = Array.FindIndex(Environment.GetCommandLineArgs(), arg => arg == "--client"); - var configFileParts = configFile.Split("."); - configFile = $"{configFileParts[0]}_{Environment.GetCommandLineArgs()[clientIndex + 1]}.{configFileParts[1]}"; - } - - settingsPath = Path.Combine(configRoot, configFolder, configFile); - - if (File.Exists(settingsPath)) - { - token = - File.ReadLines(settingsPath) - .FirstOrDefault(line => line.StartsWith(PREFIX)) - ?[PREFIX.Length..]; - } - } - - /// - /// This is the auth token that was saved to local storage. Null if not never saved. - /// When you specify null to the SpacetimeDBClient, SpacetimeDB will generate a new identity for you. - /// - public static string? Token - { - get - { - if (settingsPath == null) - { - throw new Exception("Token not initialized. Call AuthToken.Init() first."); - } - return token; - } - } - - /// - /// Save the auth token to local storage. - /// SpacetimeDBClient provides this token to you in the onIdentityReceived callback. - /// - public static void SaveToken(string token) - { - if (settingsPath == null) - { - throw new Exception("Token not initialized. Call AuthToken.Init() first."); - } - Directory.CreateDirectory(Path.GetDirectoryName(settingsPath)!); - var newAuthLine = PREFIX + token; - var lines = File.Exists(settingsPath) ? File.ReadAllLines(settingsPath).ToList() : new(); - var i = lines.FindIndex(line => line.StartsWith(PREFIX)); - if (i >= 0) - { - lines[i] = newAuthLine; - } - else - { - lines.Add(newAuthLine); - } - File.WriteAllLines(settingsPath, lines); - } - } -} -#endif +/* This is an optional helper class to store your auth token in local storage + * + Example: + + AuthToken.Init(".my_app_name"); + + SpacetimeDBClient.instance.onIdentityReceived += (token, identity) => + { + AuthToken.SaveToken(token); + + ... + }; + + SpacetimeDBClient.instance.Connect(AuthToken.Token, "localhost:3000", "basicchat", false); + */ +#if UNITY_5_3_OR_NEWER +using UnityEngine; + +namespace SpacetimeDB +{ + // This is an optional helper class to store your auth token in PlayerPrefs + // Override GetTokenKey() if you want to use a player pref key specific to your game + public static class AuthToken + { + public static string Token => PlayerPrefs.GetString(GetTokenKey()); + + public static void SaveToken(string token) + { + PlayerPrefs.SetString(GetTokenKey(), token); + } + + private static string GetTokenKey() + { + var key = "spacetimedb.identity_token"; +#if UNITY_EDITOR + // Different editors need different keys + key += $" - {Application.dataPath}"; +#endif + return key; + } + } +} +#elif GODOT +using Godot; + +namespace SpacetimeDB +{ + // This is an optional helper class to store your auth token in PlayerPrefs + // You can use keySuffix to save and retrieve different tokens + public static class AuthToken + { + private const string Path = "user://spacetimedb-token.cfg"; + private const string Section = "stdb"; + private static string Key => "identity_token"; + + private static ConfigFile? _config; + private static ConfigFile Config => _config ??= new ConfigFile(); + + public static string GetToken(string? keySuffix = null) + { + var key = GetKey(keySuffix); + if(!Config.HasSectionKey(Section, key)) + { + Config.Load(Path); + } + return Config.GetValue(Section, key, "").As(); + } + public static bool TryGetToken(out string result) => TryGetToken(null, out result); + public static bool TryGetToken(string? keySuffix, out string result) { + result = GetToken(keySuffix); + return !string.IsNullOrWhiteSpace(result); + } + + public static void SaveToken(string token, string? keySuffix = null) + { + Config.SetValue(Section, GetKey(keySuffix), token); + Config.Save(Path); + } + + private static string GetKey(string? suffix) => string.IsNullOrWhiteSpace(suffix) ? Key : $"{Key}_{suffix}"; + } +} +#else +using System; +using System.IO; +using System.Linq; + +namespace SpacetimeDB +{ + public static class AuthToken + { + private static string? settingsPath; + private static string? token; + + private const string PREFIX = "auth_token="; + + /// + /// Initializes the AuthToken class. This must be called before any other methods. + /// + /// The folder to store the config file in. Default is ".spacetime_csharp_sdk". + /// The name of the config file. Default is "settings.ini". + /// The root folder to store the config file in. Default is the user's home directory. + /// + public static void Init(string configFolder = ".spacetime_csharp_sdk", string configFile = "settings.ini", string? configRoot = null) + { + configRoot ??= Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + + if (Environment.GetCommandLineArgs().Any(arg => arg == "--client")) + { + int clientIndex = Array.FindIndex(Environment.GetCommandLineArgs(), arg => arg == "--client"); + var configFileParts = configFile.Split("."); + configFile = $"{configFileParts[0]}_{Environment.GetCommandLineArgs()[clientIndex + 1]}.{configFileParts[1]}"; + } + + settingsPath = Path.Combine(configRoot, configFolder, configFile); + + if (File.Exists(settingsPath)) + { + token = + File.ReadLines(settingsPath) + .FirstOrDefault(line => line.StartsWith(PREFIX)) + ?[PREFIX.Length..]; + } + } + + /// + /// This is the auth token that was saved to local storage. Null if not never saved. + /// When you specify null to the SpacetimeDBClient, SpacetimeDB will generate a new identity for you. + /// + public static string? Token + { + get + { + if (settingsPath == null) + { + throw new Exception("Token not initialized. Call AuthToken.Init() first."); + } + return token; + } + } + + /// + /// Save the auth token to local storage. + /// SpacetimeDBClient provides this token to you in the onIdentityReceived callback. + /// + public static void SaveToken(string token) + { + if (settingsPath == null) + { + throw new Exception("Token not initialized. Call AuthToken.Init() first."); + } + Directory.CreateDirectory(Path.GetDirectoryName(settingsPath)!); + var newAuthLine = PREFIX + token; + var lines = File.Exists(settingsPath) ? File.ReadAllLines(settingsPath).ToList() : new(); + var i = lines.FindIndex(line => line.StartsWith(PREFIX)); + if (i >= 0) + { + lines[i] = newAuthLine; + } + else + { + lines.Add(newAuthLine); + } + File.WriteAllLines(settingsPath, lines); + } + } +} +#endif diff --git a/sdks/csharp/src/ConsoleLogger.cs b/sdks/csharp/src/ConsoleLogger.cs index 2fda0ab5b2f..dd6ba7f8229 100644 --- a/sdks/csharp/src/ConsoleLogger.cs +++ b/sdks/csharp/src/ConsoleLogger.cs @@ -1,82 +1,82 @@ -using System; - -namespace SpacetimeDB -{ - internal class ConsoleLogger : ISpacetimeDBLogger - { - [Flags] - public enum LogLevel - { - None = 0, - Debug = 1, - Trace = 2, - Info = 4, - Warning = 8, - Error = 16, - Exception = 32, - All = ~0 - } - LogLevel _logLevel; - - public ConsoleLogger(LogLevel logLevel = LogLevel.All) - { - _logLevel = logLevel; - } - - public void Debug(string message) - { - if (_logLevel.HasFlag(LogLevel.Debug)) - { - Console.WriteLine($"[D] {message}"); - } - } - - public void Trace(string message) - { - if (_logLevel.HasFlag(LogLevel.Trace)) - { - Console.WriteLine($"[T] {message}"); - } - } - - public void Info(string message) - { - if (_logLevel.HasFlag(LogLevel.Info)) - { - Console.WriteLine($"[I] {message}"); - } - } - - public void Warn(string message) - { - if (_logLevel.HasFlag(LogLevel.Warning)) - { - Console.WriteLine($"[W] {message}"); - } - } - - public void Error(string message) - { - if (_logLevel.HasFlag(LogLevel.Error)) - { - Console.WriteLine($"[E] {message}"); - } - } - - public void Exception(string message) - { - if (_logLevel.HasFlag(LogLevel.Exception)) - { - Console.WriteLine($"[X] {message}"); - } - } - - public void Exception(Exception exception) - { - if (_logLevel.HasFlag(LogLevel.Exception)) - { - Console.WriteLine($"[X] {exception}"); - } - } - } -} +using System; + +namespace SpacetimeDB +{ + internal class ConsoleLogger : ISpacetimeDBLogger + { + [Flags] + public enum LogLevel + { + None = 0, + Debug = 1, + Trace = 2, + Info = 4, + Warning = 8, + Error = 16, + Exception = 32, + All = ~0 + } + LogLevel _logLevel; + + public ConsoleLogger(LogLevel logLevel = LogLevel.All) + { + _logLevel = logLevel; + } + + public void Debug(string message) + { + if (_logLevel.HasFlag(LogLevel.Debug)) + { + Console.WriteLine($"[D] {message}"); + } + } + + public void Trace(string message) + { + if (_logLevel.HasFlag(LogLevel.Trace)) + { + Console.WriteLine($"[T] {message}"); + } + } + + public void Info(string message) + { + if (_logLevel.HasFlag(LogLevel.Info)) + { + Console.WriteLine($"[I] {message}"); + } + } + + public void Warn(string message) + { + if (_logLevel.HasFlag(LogLevel.Warning)) + { + Console.WriteLine($"[W] {message}"); + } + } + + public void Error(string message) + { + if (_logLevel.HasFlag(LogLevel.Error)) + { + Console.WriteLine($"[E] {message}"); + } + } + + public void Exception(string message) + { + if (_logLevel.HasFlag(LogLevel.Exception)) + { + Console.WriteLine($"[X] {message}"); + } + } + + public void Exception(Exception exception) + { + if (_logLevel.HasFlag(LogLevel.Exception)) + { + Console.WriteLine($"[X] {exception}"); + } + } + } +} diff --git a/sdks/csharp/src/ISpacetimeDBLogger.cs b/sdks/csharp/src/ISpacetimeDBLogger.cs index c91d1b13d9c..07b1c05efdb 100644 --- a/sdks/csharp/src/ISpacetimeDBLogger.cs +++ b/sdks/csharp/src/ISpacetimeDBLogger.cs @@ -1,56 +1,56 @@ -using System; -#if UNITY_5_3_OR_NEWER -using UnityEngine; -#endif - -namespace SpacetimeDB -{ - internal interface ISpacetimeDBLogger - { - void Debug(string message); - void Trace(string message); - void Info(string message); - void Warn(string message); - void Error(string message); - void Exception(string message); - void Exception(Exception e); - } - - public static class Log - { - internal static ISpacetimeDBLogger Current = - -#if UNITY_5_3_OR_NEWER - new UnityDebugLogger(); -#elif GODOT - new GodotDebugLogger(); -#else - new ConsoleLogger(); -#endif - -#if UNITY_5_3_OR_NEWER - /// - /// Resets the static instance to prevent data persistence when Enter Play Mode Options (Disable Domain Reloading) is active. - /// RuntimeInitializeOnLoadMethod is used since it is supported in older versions of Unity. - /// AutoStaticsCleanup and NoAutoStaticsCleanup is only supported in Unity 6+ - /// - /// - /// See the Unity Domain Reloading Manual - /// and the RuntimeInitializeOnLoadMethodAttribute API Docs for details. - /// - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] - private static void ResetStaticFields() - { - Current = new UnityDebugLogger(); - } -#endif - - public static void Debug(string message) => Current.Debug(message); - public static void Trace(string message) => Current.Trace(message); - public static void Info(string message) => Current.Info(message); - public static void Warn(string message) => Current.Warn(message); - public static void Error(string message) => Current.Error(message); - public static void Exception(string message) => Current.Exception(message); - public static void Exception(Exception exception) => Current.Exception(exception); - } -} +using System; +#if UNITY_5_3_OR_NEWER +using UnityEngine; +#endif + +namespace SpacetimeDB +{ + internal interface ISpacetimeDBLogger + { + void Debug(string message); + void Trace(string message); + void Info(string message); + void Warn(string message); + void Error(string message); + void Exception(string message); + void Exception(Exception e); + } + + public static class Log + { + internal static ISpacetimeDBLogger Current = + +#if UNITY_5_3_OR_NEWER + new UnityDebugLogger(); +#elif GODOT + new GodotDebugLogger(); +#else + new ConsoleLogger(); +#endif + +#if UNITY_5_3_OR_NEWER + /// + /// Resets the static instance to prevent data persistence when Enter Play Mode Options (Disable Domain Reloading) is active. + /// RuntimeInitializeOnLoadMethod is used since it is supported in older versions of Unity. + /// AutoStaticsCleanup and NoAutoStaticsCleanup is only supported in Unity 6+ + /// + /// + /// See the Unity Domain Reloading Manual + /// and the RuntimeInitializeOnLoadMethodAttribute API Docs for details. + /// + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void ResetStaticFields() + { + Current = new UnityDebugLogger(); + } +#endif + + public static void Debug(string message) => Current.Debug(message); + public static void Trace(string message) => Current.Trace(message); + public static void Info(string message) => Current.Info(message); + public static void Warn(string message) => Current.Warn(message); + public static void Error(string message) => Current.Error(message); + public static void Exception(string message) => Current.Exception(message); + public static void Exception(Exception exception) => Current.Exception(exception); + } +} diff --git a/sdks/csharp/src/UnityDebugLogger.cs b/sdks/csharp/src/UnityDebugLogger.cs index 94d21aee8a4..8eddb4217dd 100644 --- a/sdks/csharp/src/UnityDebugLogger.cs +++ b/sdks/csharp/src/UnityDebugLogger.cs @@ -1,34 +1,34 @@ -/* SpacetimeDB logging for Unity - * * This class is only used in Unity projects. - * * - */ -#if UNITY_5_3_OR_NEWER -using System; - -namespace SpacetimeDB -{ - internal class UnityDebugLogger : ISpacetimeDBLogger - { - public void Debug(string message) => - UnityEngine.Debug.Log(message); - - public void Trace(string message) => - UnityEngine.Debug.Log(message); - - public void Info(string message) => - UnityEngine.Debug.Log(message); - - public void Warn(string message) => - UnityEngine.Debug.LogWarning(message); - - public void Error(string message) => - UnityEngine.Debug.LogError(message); - - public void Exception(string message) => - UnityEngine.Debug.LogError(message); - - public void Exception(Exception e) => - UnityEngine.Debug.LogException(e); - } -} -#endif +/* SpacetimeDB logging for Unity + * * This class is only used in Unity projects. + * * + */ +#if UNITY_5_3_OR_NEWER +using System; + +namespace SpacetimeDB +{ + internal class UnityDebugLogger : ISpacetimeDBLogger + { + public void Debug(string message) => + UnityEngine.Debug.Log(message); + + public void Trace(string message) => + UnityEngine.Debug.Log(message); + + public void Info(string message) => + UnityEngine.Debug.Log(message); + + public void Warn(string message) => + UnityEngine.Debug.LogWarning(message); + + public void Error(string message) => + UnityEngine.Debug.LogError(message); + + public void Exception(string message) => + UnityEngine.Debug.LogError(message); + + public void Exception(Exception e) => + UnityEngine.Debug.LogException(e); + } +} +#endif