From e0d63abdcf273ae5b235f8c498858424ba97608f Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Thu, 19 Jun 2025 01:13:41 +0000 Subject: [PATCH 01/24] Add ParseLiveQuery and dependencies --- .../Infrastructure/CustomServiceHub.cs | 5 + .../Execution/IWebSocketClient.cs | 49 +++ .../Infrastructure/IMutableServiceHub.cs | 2 + .../Infrastructure/IServiceHub.cs | 5 +- .../LiveQueries/IParseLiveQueryController.cs | 18 ++ .../Execution/TextWebSocketClient.cs | 144 +++++++++ .../LateInitializedMutableServiceHub.cs | 15 + Parse/Infrastructure/MutableServiceHub.cs | 3 + .../Infrastructure/OrchestrationServiceHub.cs | 4 + Parse/Infrastructure/ServiceHub.cs | 6 + Parse/Platform/LiveQueries/ParseLiveQuery.cs | 160 +++++++++ .../LiveQueries/ParseLiveQueryClient.cs | 18 ++ .../LiveQueries/ParseLiveQueryController.cs | 304 ++++++++++++++++++ Parse/Platform/ParseClient.cs | 61 ++++ 14 files changed, 793 insertions(+), 1 deletion(-) create mode 100644 Parse/Abstractions/Infrastructure/Execution/IWebSocketClient.cs create mode 100644 Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs create mode 100644 Parse/Infrastructure/Execution/TextWebSocketClient.cs create mode 100644 Parse/Platform/LiveQueries/ParseLiveQuery.cs create mode 100644 Parse/Platform/LiveQueries/ParseLiveQueryClient.cs create mode 100644 Parse/Platform/LiveQueries/ParseLiveQueryController.cs diff --git a/Parse/Abstractions/Infrastructure/CustomServiceHub.cs b/Parse/Abstractions/Infrastructure/CustomServiceHub.cs index 554c91bb..59e6e5b2 100644 --- a/Parse/Abstractions/Infrastructure/CustomServiceHub.cs +++ b/Parse/Abstractions/Infrastructure/CustomServiceHub.cs @@ -5,6 +5,7 @@ using Parse.Abstractions.Platform.Configuration; using Parse.Abstractions.Platform.Files; using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.LiveQueries; using Parse.Abstractions.Platform.Objects; using Parse.Abstractions.Platform.Push; using Parse.Abstractions.Platform.Queries; @@ -41,6 +42,8 @@ public abstract class CustomServiceHub : ICustomServiceHub public virtual IParseQueryController QueryController => Services.QueryController; + public virtual IParseLiveQueryController LiveQueryController => Services.LiveQueryController; + public virtual IParseSessionController SessionController => Services.SessionController; public virtual IParseUserController UserController => Services.UserController; @@ -59,6 +62,8 @@ public abstract class CustomServiceHub : ICustomServiceHub public virtual IServerConnectionData ServerConnectionData => Services.ServerConnectionData; + public virtual IServerConnectionData LiveQueryServerConnectionData => Services.ServerConnectionData; + public virtual IParseDataDecoder Decoder => Services.Decoder; public virtual IParseInstallationDataFinalizer InstallationDataFinalizer => Services.InstallationDataFinalizer; diff --git a/Parse/Abstractions/Infrastructure/Execution/IWebSocketClient.cs b/Parse/Abstractions/Infrastructure/Execution/IWebSocketClient.cs new file mode 100644 index 00000000..933cb56c --- /dev/null +++ b/Parse/Abstractions/Infrastructure/Execution/IWebSocketClient.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Parse.Abstractions.Infrastructure.Execution; + +/// +/// Represents an interface for a WebSocket client to handle WebSocket connections and communications. +/// +public interface IWebSocketClient +{ + /// + /// An event that is triggered when a message is received via the WebSocket connection. + /// + /// + /// The event handler receives the message as a string parameter. This can be used to process incoming + /// WebSocket messages, such as notifications, commands, or data updates. + /// + public event EventHandler MessageReceived; + + /// + /// Establishes a WebSocket connection to the specified server URI. + /// + /// The URI of the WebSocket server to connect to. + /// + /// A token to observe cancellation requests. The operation will stop if the token is canceled. + /// + /// A task that represents the asynchronous operation of opening the WebSocket connection. + public Task OpenAsync(string serverUri, CancellationToken cancellationToken = default); + + /// + /// Closes the active WebSocket connection asynchronously. + /// + /// + /// A token to observe cancellation requests. The operation will stop if the token is canceled. + /// + /// A task that represents the asynchronous operation of closing the WebSocket connection. + public Task CloseAsync(CancellationToken cancellationToken = default); + + /// + /// Sends a message over the established WebSocket connection asynchronously. + /// + /// The message to send through the WebSocket connection. + /// + /// A token to observe cancellation requests. The operation will stop if the token is canceled. + /// + /// A task that represents the asynchronous operation of sending the message. + public Task SendAsync(string message, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs b/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs index 99bd78b9..6d0371a9 100644 --- a/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs +++ b/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs @@ -7,6 +7,7 @@ using Parse.Abstractions.Platform.Configuration; using Parse.Abstractions.Platform.Files; using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.LiveQueries; using Parse.Abstractions.Platform.Objects; using Parse.Abstractions.Platform.Push; using Parse.Abstractions.Platform.Queries; @@ -36,6 +37,7 @@ public interface IMutableServiceHub : IServiceHub IParseFileController FileController { set; } IParseObjectController ObjectController { set; } IParseQueryController QueryController { set; } + IParseLiveQueryController LiveQueryController { set; } IParseSessionController SessionController { set; } IParseUserController UserController { set; } IParseCurrentUserController CurrentUserController { set; } diff --git a/Parse/Abstractions/Infrastructure/IServiceHub.cs b/Parse/Abstractions/Infrastructure/IServiceHub.cs index 9614bab3..00307096 100644 --- a/Parse/Abstractions/Infrastructure/IServiceHub.cs +++ b/Parse/Abstractions/Infrastructure/IServiceHub.cs @@ -7,6 +7,7 @@ using Parse.Abstractions.Platform.Configuration; using Parse.Abstractions.Platform.Files; using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.LiveQueries; using Parse.Abstractions.Platform.Objects; using Parse.Abstractions.Platform.Push; using Parse.Abstractions.Platform.Queries; @@ -23,9 +24,10 @@ namespace Parse.Abstractions.Infrastructure; public interface IServiceHub { /// - /// The current server connection data that the the Parse SDK has been initialized with. + /// The current server connection data that the Parse SDK has been initialized with. /// IServerConnectionData ServerConnectionData { get; } + IServerConnectionData LiveQueryServerConnectionData { get; } IMetadataController MetadataController { get; } IServiceHubCloner Cloner { get; } @@ -44,6 +46,7 @@ public interface IServiceHub IParseFileController FileController { get; } IParseObjectController ObjectController { get; } IParseQueryController QueryController { get; } + IParseLiveQueryController LiveQueryController { get; } IParseSessionController SessionController { get; } IParseUserController UserController { get; } IParseCurrentUserController CurrentUserController { get; } diff --git a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs new file mode 100644 index 00000000..8e78ffc4 --- /dev/null +++ b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs @@ -0,0 +1,18 @@ +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Platform.Objects; + +namespace Parse.Abstractions.Platform.LiveQueries; + +public interface IParseLiveQueryController +{ + Task ConnectAsync(CancellationToken cancellationToken = default); + + Task SubscribeAsync(ParseLiveQuery liveQuery, CancellationToken cancellationToken = default) where T : ParseObject; + + Task UpdateSubscriptionAsync(ParseLiveQuery liveQuery, int requestId, CancellationToken cancellationToken = default) where T : ParseObject; + + Task UnsubscribeAsync(int requestId, CancellationToken cancellationToken = default); + + Task CloseAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/Parse/Infrastructure/Execution/TextWebSocketClient.cs b/Parse/Infrastructure/Execution/TextWebSocketClient.cs new file mode 100644 index 00000000..f2688747 --- /dev/null +++ b/Parse/Infrastructure/Execution/TextWebSocketClient.cs @@ -0,0 +1,144 @@ +using System; +using System.Diagnostics; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure.Execution; + +namespace Parse.Infrastructure.Execution; + +/// +/// Represents a WebSocket client that allows connecting to a WebSocket server, sending messages, and receiving messages. +/// Implements the IWebSocketClient interface for WebSocket operations. +/// +class TextWebSocketClient : IWebSocketClient +{ + /// + /// A private instance of the ClientWebSocket class used to manage the WebSocket connection. + /// This variable is responsible for handling the low-level WebSocket communication, including + /// connecting, sending, and receiving data from the WebSocket server. It is initialized + /// when establishing a connection and is used internally for operations such as sending messages + /// and listening for incoming data. + /// + private ClientWebSocket _webSocket; + + /// + /// A private instance of the Task class representing the background operation + /// responsible for continuously listening for incoming WebSocket messages. + /// This task is used to manage the asynchronous listening process, ensuring that + /// messages are received from the WebSocket server without blocking the main thread. + /// It is initialized when the listening process starts and monitored to prevent + /// multiple concurrent listeners from being created. + /// + private Task _listeningTask; + + /// + /// An event triggered whenever a message is received from the WebSocket server. + /// This event is used to notify subscribers with the content of the received message, + /// represented as a string. Handlers for this event can process or respond to the message + /// based on the application's requirements. + /// + public event EventHandler MessageReceived; + + /// + /// Opens a WebSocket connection to the specified server URI and starts listening for messages. + /// If the connection is already open or in a connecting state, this method does nothing. + /// + /// The URI of the WebSocket server to connect to. + /// A cancellation token that can be used to cancel the connect operation. + /// + /// A task representing the asynchronous operation of connecting to the WebSocket server. + /// + public async Task OpenAsync(string serverUri, CancellationToken cancellationToken = default) + { + _webSocket ??= new ClientWebSocket(); + + if (_webSocket.State != WebSocketState.Open && _webSocket.State != WebSocketState.Connecting) + { + await _webSocket.ConnectAsync(new Uri(serverUri), cancellationToken); + StartListening(cancellationToken); + } + } + + /// + /// Closes the WebSocket connection gracefully with a normal closure status. + /// Ensures that the WebSocket connection is properly terminated and resources are released. + /// + /// A cancellation token that can be used to cancel the close operation. + /// + /// A task representing the asynchronous operation of closing the WebSocket connection. + /// + public async Task CloseAsync(CancellationToken cancellationToken = default) + => await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, String.Empty, cancellationToken); + + private async Task ListenForMessages(CancellationToken cancellationToken) + { + byte[] buffer = new byte[1024 * 4]; + + try + { + while (!cancellationToken.IsCancellationRequested && + _webSocket.State == WebSocketState.Open) + { + WebSocketReceiveResult result = await _webSocket.ReceiveAsync( + new ArraySegment(buffer), + cancellationToken); + + if (result.MessageType == WebSocketMessageType.Close) + { + await CloseAsync(cancellationToken); + break; + } + + string message = Encoding.UTF8.GetString(buffer, 0, result.Count); + MessageReceived?.Invoke(this, message); + } + } + catch (OperationCanceledException ex) + { + // Normal cancellation, no need to handle + Debug.WriteLine($"ClientWebsocket connection was closed: {ex.Message}"); + } + } + + + /// + /// Starts listening for incoming messages from the WebSocket connection. This method ensures that only one listener task is running at a time. + /// + /// A cancellation token to signal the listener task to stop. + private void StartListening(CancellationToken cancellationToken) + { + // Make sure we don't start multiple listeners + if (_listeningTask is { IsCompleted: false }) + { + return; + } + + // Start the listener task + _listeningTask = Task.Run(async () => + { + if (cancellationToken.IsCancellationRequested) + { + cancellationToken.ThrowIfCancellationRequested(); + } + + await ListenForMessages(cancellationToken); + }, cancellationToken); + } + + /// + /// Sends a text message to the connected WebSocket server asynchronously. + /// The message is encoded in UTF-8 format before being sent. + /// + /// The message to be sent to the WebSocket server. + /// A cancellation token that can be used to cancel the send operation. + /// + /// A task representing the asynchronous operation of sending the message to the WebSocket server. + /// + public async Task SendAsync(string message, CancellationToken cancellationToken = default) + { + if (_webSocket is not null && _webSocket.State == WebSocketState.Open) + await _webSocket.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, cancellationToken); + } +} \ No newline at end of file diff --git a/Parse/Infrastructure/LateInitializedMutableServiceHub.cs b/Parse/Infrastructure/LateInitializedMutableServiceHub.cs index b5c671f4..321aafb0 100644 --- a/Parse/Infrastructure/LateInitializedMutableServiceHub.cs +++ b/Parse/Infrastructure/LateInitializedMutableServiceHub.cs @@ -12,6 +12,7 @@ using Parse.Abstractions.Platform.Sessions; using Parse.Abstractions.Platform.Users; using Parse.Abstractions.Platform.Analytics; +using Parse.Abstractions.Platform.LiveQueries; using Parse.Infrastructure.Execution; using Parse.Platform.Objects; using Parse.Platform.Installations; @@ -25,6 +26,7 @@ using Parse.Platform.Push; using Parse.Infrastructure.Data; using Parse.Infrastructure.Utilities; +using Parse.Platform.LiveQueries; namespace Parse.Infrastructure; @@ -46,6 +48,12 @@ public IWebClient WebClient set => LateInitializer.SetValue(value); } + public IWebSocketClient WebSocketClient + { + get => LateInitializer.GetValue(() => new TextWebSocketClient { }); + set => LateInitializer.SetValue(value); + } + public ICacheController CacheController { get => LateInitializer.GetValue(() => new CacheController { }); @@ -100,6 +108,12 @@ public IParseQueryController QueryController set => LateInitializer.SetValue(value); } + public IParseLiveQueryController LiveQueryController + { + get => LateInitializer.GetValue(() => new ParseLiveQueryController(WebSocketClient)); + set => LateInitializer.SetValue(value); + } + public IParseSessionController SessionController { get => LateInitializer.GetValue(() => new ParseSessionController(CommandRunner, Decoder)); @@ -161,4 +175,5 @@ public IParseInstallationDataFinalizer InstallationDataFinalizer } public IServerConnectionData ServerConnectionData { get; set; } + public IServerConnectionData LiveQueryServerConnectionData { get; set; } } diff --git a/Parse/Infrastructure/MutableServiceHub.cs b/Parse/Infrastructure/MutableServiceHub.cs index 3cf50a0d..f7af0ce8 100644 --- a/Parse/Infrastructure/MutableServiceHub.cs +++ b/Parse/Infrastructure/MutableServiceHub.cs @@ -7,6 +7,7 @@ using Parse.Abstractions.Platform.Configuration; using Parse.Abstractions.Platform.Files; using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.LiveQueries; using Parse.Abstractions.Platform.Objects; using Parse.Abstractions.Platform.Push; using Parse.Abstractions.Platform.Queries; @@ -34,6 +35,7 @@ namespace Parse.Infrastructure; public class MutableServiceHub : IMutableServiceHub { public IServerConnectionData ServerConnectionData { get; set; } + public IServerConnectionData LiveQueryServerConnectionData { get; set; } public IMetadataController MetadataController { get; set; } public IServiceHubCloner Cloner { get; set; } @@ -52,6 +54,7 @@ public class MutableServiceHub : IMutableServiceHub public IParseFileController FileController { get; set; } public IParseObjectController ObjectController { get; set; } public IParseQueryController QueryController { get; set; } + public IParseLiveQueryController LiveQueryController { get; set; } public IParseSessionController SessionController { get; set; } public IParseUserController UserController { get; set; } public IParseCurrentUserController CurrentUserController { get; set; } diff --git a/Parse/Infrastructure/OrchestrationServiceHub.cs b/Parse/Infrastructure/OrchestrationServiceHub.cs index d8079425..1db30c59 100644 --- a/Parse/Infrastructure/OrchestrationServiceHub.cs +++ b/Parse/Infrastructure/OrchestrationServiceHub.cs @@ -6,6 +6,7 @@ using Parse.Abstractions.Platform.Configuration; using Parse.Abstractions.Platform.Files; using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.LiveQueries; using Parse.Abstractions.Platform.Objects; using Parse.Abstractions.Platform.Push; using Parse.Abstractions.Platform.Queries; @@ -44,6 +45,8 @@ public class OrchestrationServiceHub : IServiceHub public IParseQueryController QueryController => Custom.QueryController ?? Default.QueryController; + public IParseLiveQueryController LiveQueryController => Custom.LiveQueryController ?? Default.LiveQueryController; + public IParseSessionController SessionController => Custom.SessionController ?? Default.SessionController; public IParseUserController UserController => Custom.UserController ?? Default.UserController; @@ -61,6 +64,7 @@ public class OrchestrationServiceHub : IServiceHub public IParseCurrentInstallationController CurrentInstallationController => Custom.CurrentInstallationController ?? Default.CurrentInstallationController; public IServerConnectionData ServerConnectionData => Custom.ServerConnectionData ?? Default.ServerConnectionData; + public IServerConnectionData LiveQueryServerConnectionData => Custom.LiveQueryServerConnectionData ?? Default.LiveQueryServerConnectionData; public IParseDataDecoder Decoder => Custom.Decoder ?? Default.Decoder; diff --git a/Parse/Infrastructure/ServiceHub.cs b/Parse/Infrastructure/ServiceHub.cs index dbff4b24..c76a16ee 100644 --- a/Parse/Infrastructure/ServiceHub.cs +++ b/Parse/Infrastructure/ServiceHub.cs @@ -7,6 +7,7 @@ using Parse.Abstractions.Platform.Configuration; using Parse.Abstractions.Platform.Files; using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.LiveQueries; using Parse.Abstractions.Platform.Objects; using Parse.Abstractions.Platform.Push; using Parse.Abstractions.Platform.Queries; @@ -20,6 +21,7 @@ using Parse.Platform.Configuration; using Parse.Platform.Files; using Parse.Platform.Installations; +using Parse.Platform.LiveQueries; using Parse.Platform.Objects; using Parse.Platform.Push; using Parse.Platform.Queries; @@ -37,6 +39,7 @@ public class ServiceHub : IServiceHub LateInitializer LateInitializer { get; } = new LateInitializer { }; public IServerConnectionData ServerConnectionData { get; set; } + public IServerConnectionData LiveQueryServerConnectionData { get; set; } public IMetadataController MetadataController => LateInitializer.GetValue(() => new MetadataController { HostManifestData = HostManifestData.Inferred, EnvironmentData = EnvironmentData.Inferred }); public IServiceHubCloner Cloner => LateInitializer.GetValue(() => new { } as object as IServiceHubCloner); @@ -50,11 +53,14 @@ public class ServiceHub : IServiceHub public IParseInstallationController InstallationController => LateInitializer.GetValue(() => new ParseInstallationController(CacheController)); public IParseCommandRunner CommandRunner => LateInitializer.GetValue(() => new ParseCommandRunner(WebClient, InstallationController, MetadataController, ServerConnectionData, new Lazy(() => UserController))); + public IWebSocketClient WebSocketClient => LateInitializer.GetValue(() => new TextWebSocketClient { }); + public IParseCloudCodeController CloudCodeController => LateInitializer.GetValue(() => new ParseCloudCodeController(CommandRunner, Decoder)); public IParseConfigurationController ConfigurationController => LateInitializer.GetValue(() => new ParseConfigurationController(CommandRunner, CacheController, Decoder)); public IParseFileController FileController => LateInitializer.GetValue(() => new ParseFileController(CommandRunner)); public IParseObjectController ObjectController => LateInitializer.GetValue(() => new ParseObjectController(CommandRunner, Decoder, ServerConnectionData)); public IParseQueryController QueryController => LateInitializer.GetValue(() => new ParseQueryController(CommandRunner, Decoder)); + public IParseLiveQueryController LiveQueryController => LateInitializer.GetValue(() => new ParseLiveQueryController(WebSocketClient)); public IParseSessionController SessionController => LateInitializer.GetValue(() => new ParseSessionController(CommandRunner, Decoder)); public IParseUserController UserController => LateInitializer.GetValue(() => new ParseUserController(CommandRunner, Decoder)); public IParseCurrentUserController CurrentUserController => LateInitializer.GetValue(() => new ParseCurrentUserController(CacheController, ClassController, Decoder)); diff --git a/Parse/Platform/LiveQueries/ParseLiveQuery.cs b/Parse/Platform/LiveQueries/ParseLiveQuery.cs new file mode 100644 index 00000000..7cf0d23e --- /dev/null +++ b/Parse/Platform/LiveQueries/ParseLiveQuery.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Net.WebSockets; +using System.Numerics; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualBasic.CompilerServices; +using Parse.Abstractions.Infrastructure; +using Parse.Infrastructure.Data; +using Parse.Infrastructure.Utilities; + +namespace Parse; + +/// +/// The ParseLiveQuery class allows subscribing to a Query. +/// +/// +public class ParseLiveQuery where T : ParseObject +{ + + /// + /// Serialized clauses. + /// + Dictionary Filters { get; } + + /// + /// Serialized key selections. + /// + ReadOnlyCollection KeySelections { get; } + + /// + /// Serialized keys watched. + /// + ReadOnlyCollection KeyWatch { get; } + + internal string ClassName { get; } + + internal IServiceHub Services { get; } + + private int RequestId = 0; + + public ParseLiveQuery(IServiceHub serviceHub, string className, IDictionary filters, IEnumerable selectedKeys = null, IEnumerable watchedKeys = null) + { + if (filters.Count == 0) + { + // Throw error + } + + Services = serviceHub; + ClassName = className; + + Filters = new Dictionary(filters); + if (selectedKeys is not null) + { + KeySelections = new ReadOnlyCollection(selectedKeys.ToList()); + } + + if (watchedKeys is not null) + { + KeyWatch = new ReadOnlyCollection(watchedKeys.ToList()); + } + } + + /// + /// Private constructor for composition of queries. A source query is required, + /// but the remaining values can be null if they aren't changed in this + /// composition. + /// + internal ParseLiveQuery(ParseLiveQuery source, IEnumerable watchedKeys = null) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + Services = source.Services; + ClassName = source.ClassName; + Filters = source.Filters; + KeySelections = source.KeySelections; + + if (watchedKeys is { }) + { + KeyWatch = new ReadOnlyCollection(MergeKeys(watchedKeys).ToList()); + } + } + + HashSet MergeKeys(IEnumerable selectedKeys) => new((KeySelections ?? Enumerable.Empty()).Concat(selectedKeys)); + + /// + /// Add the provided key to the watched fields of returned ParseObjects. + /// If this is called multiple times, then all the keys specified in each of + /// the calls will be watched. + /// + /// The key that should be watched. + /// A new query with the additional constraint. + public ParseLiveQuery Watch(string watch) => new(this, new List { watch }); + + internal IDictionary BuildParameters(bool includeClassName = false) + { + Dictionary result = new Dictionary(); + if (Filters != null) + result["where"] = PointerOrLocalIdEncoder.Instance.Encode(Filters, Services); + if (KeySelections != null) + result["keys"] = String.Join(",", KeySelections.ToArray()); + if (KeyWatch != null) + result["watch"] = String.Join(",", KeyWatch.ToArray()); + if (includeClassName) + result["className"] = ClassName; + return result; + } + + /// + /// Establishes a connection to the Parse Live Query server using the ClientWebSocket instance. + /// Prepares and sends a connection message containing required identifiers such as application ID, client key, and session token. + /// + /// A Task representing the asynchronous operation, returning true if the connection attempt is initialized successfully, false otherwise. + public async Task ConnectAsync() + { + await Services.LiveQueryController.ConnectAsync(CancellationToken.None); + } + + /// + /// Subscribes to the live query, allowing the client to receive real-time updates + /// for the query's results. This establishes a subscription with the Live Query service. + /// + /// + /// A task representing the asynchronous subscription operation. Upon completion + /// of the task, the subscription is successfully registered. + /// + public async Task SubscribeAsync() + { + RequestId = await Services.LiveQueryController.SubscribeAsync(this, CancellationToken.None); + } + + /// + /// Unsubscribes from the live query, stopping the client from receiving further updates related to the subscription. + /// + /// A task representing the asynchronous operation of unsubscribing from the live query. + public async Task UnsubscribeAsync() + { + if (RequestId > 0) + await Services.LiveQueryController.UnsubscribeAsync(RequestId, CancellationToken.None); + } + + /// + /// Closes the connection to the live query server asynchronously. + /// + /// + /// A task representing the asynchronous operation of closing the live query connection. + /// + public async Task CloseAsync() + { + await Services.LiveQueryController.CloseAsync(CancellationToken.None); + } + +} \ No newline at end of file diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryClient.cs b/Parse/Platform/LiveQueries/ParseLiveQueryClient.cs new file mode 100644 index 00000000..a34ca072 --- /dev/null +++ b/Parse/Platform/LiveQueries/ParseLiveQueryClient.cs @@ -0,0 +1,18 @@ +using System.Net.WebSockets; +using System.Threading; + +namespace Parse; + +public class ParseLiveQueryClient +{ + private ClientWebSocket clientWebSocket; + + async void connect() + { + if (clientWebSocket is not null) + { + await clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); + } + clientWebSocket = new ClientWebSocket(); + } +} \ No newline at end of file diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs new file mode 100644 index 00000000..1ffc88e2 --- /dev/null +++ b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs @@ -0,0 +1,304 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Platform.LiveQueries; +using Parse.Infrastructure.Utilities; + +namespace Parse.Platform.LiveQueries; + +public class ParseLiveQueryController : IParseLiveQueryController +{ + IWebSocketClient WebSocketClient { get; } + + private int LastRequestId { get; set; } = 0; + + public int TimeOut { get; set; } = 5000; + + // public event EventHandler Connected; + // public event EventHandler Subscribed; + // public event EventHandler Unsubscribed; + // public event EventHandler SubscribtionUpdated; + public event EventHandler> Error; + public event EventHandler> Create; + public event EventHandler> Enter; + public event EventHandler> Update; + public event EventHandler> Leave; + public event EventHandler> Delete; + + public enum ParseLiveQueryState + { + /// + /// Represents the state where the live query connection is closed. + /// This indicates that any active connection to the live query server + /// has been terminated, and no data updates are being received. + /// + Closed, + Connecting, + Connected + } + + public ParseLiveQueryState State { get; private set; } + public ArrayList SubscriptionIds { get; } + + CancellationTokenSource ConnectionSignal { get; set; } + private IDictionary SubscriptionSignals { get; } = new Dictionary { }; + private IDictionary UnsubscriptionSignals { get; } = new Dictionary { }; + private IDictionary SubscriptionUpdateSignals { get; } = new Dictionary { }; + + public ParseLiveQueryController(IWebSocketClient webSocketClient) + { + WebSocketClient = webSocketClient; + SubscriptionIds = new ArrayList(); + State = ParseLiveQueryState.Closed; + } + + /// + /// Processes an incoming message by determining its operation type and triggering + /// the corresponding events or handling the operation accordingly. + /// + /// + /// A dictionary representing the message received, where the key-value pairs + /// contain the details of the message including the operation type ("op") and + /// any associated data. + /// + private void ProcessMessage(IDictionary message) + { + switch (message["op"]) + { + case "connected": + State = ParseLiveQueryState.Connected; + ConnectionSignal?.Cancel(); + // Connected?.Invoke(this, EventArgs.Empty); + break; + + case "subscribed": + int requestId = Convert.ToInt32(message["requestId"]); + SubscriptionIds.Add(requestId); + if (SubscriptionSignals.TryGetValue(requestId, out CancellationTokenSource subscriptionSignal)) + { + subscriptionSignal?.Cancel(); + } + // Subscribed?.Invoke(this, requestId); + break; + + case "unsubscribed": + requestId = Convert.ToInt32(message["requestId"]); + SubscriptionIds.Remove(requestId); + if (UnsubscriptionSignals.TryGetValue(requestId, out CancellationTokenSource unsubscriptionSignal)) + { + unsubscriptionSignal?.Cancel(); + } + // Unsubscribed?.Invoke(this, requestId); + break; + + case "error": + if ((bool)message["reconnect"]) + { + OpenAsync(); + } + string errorMessage = message["error"] as string; + Error?.Invoke(this, message); + break; + + case "create": + Create?.Invoke(this, message); + break; + + case "enter": + Enter?.Invoke(this, message); + break; + + case "update": + Update?.Invoke(this, message); + break; + + case "leave": + Leave?.Invoke(this, message); + break; + + case "delete": + Delete?.Invoke(this, message); + break; + + default: + Debug.WriteLine($"Unknown operation: {message["op"]}"); + break; + } + } + + private IDictionary AppendSessionToken(IDictionary message) + { + return message.Concat(new Dictionary { + { "sessionToken", ParseClient.Instance.Services.GetCurrentSessionToken() } + }).ToDictionary(); + } + + /// + /// Sends a message to the server over a WebSocket connection. + /// This method processes the message data and ensures it's transmitted asynchronously. + /// + /// + /// A dictionary containing the message data to be sent. + /// + /// + /// A token to observe while waiting for the task to complete, allowing the operation to be canceled. + /// + /// + /// A task that represents the asynchronous operation of sending the message. + /// + private async Task SendMessage(IDictionary message, CancellationToken cancellationToken) + { + await WebSocketClient.SendAsync(JsonUtilities.Encode(AppendSessionToken(message)), cancellationToken); + } + + /// + /// Opens a WebSocket connection and initiates listening for updates. + /// This method establishes the connection to the Live Query server and transitions the state to Open. + /// + /// + /// A token to observe while waiting for the task to complete. It allows canceling the connection process. + /// + /// + /// A task that represents the asynchronous operation of opening the WebSocket connection. + /// + private async Task OpenAsync(CancellationToken cancellationToken = default) + { + if (ParseClient.Instance.Services == null) + { + throw new InvalidOperationException("ParseClient.Services must be initialized before connecting to the LiveQuery server."); + } + + if (ParseClient.Instance.Services.LiveQueryServerConnectionData == null) + { + throw new InvalidOperationException("ParseClient.Services.LiveQueryServerConnectionData must be initialized before connecting to the LiveQuery server."); + } + + await WebSocketClient.OpenAsync(ParseClient.Instance.Services.LiveQueryServerConnectionData.ServerURI, cancellationToken); + } + + /// + /// Handles a message received event from the WebSocket client by parsing the incoming message + /// and processing it as a dictionary of key-value pairs. + /// + /// + /// The source of the event, typically the WebSocket client that triggered the message received event. + /// + /// + /// The raw string message received from the WebSocket client, representing JSON data + /// that can be parsed and processed. + /// + void WebSocketClientOnMessageReceived(object sender, string e) + => ProcessMessage(JsonUtilities.Parse(e) as IDictionary); + + /// + /// Establishes a live query connection using the specified user credentials. + /// This method initializes the connection and sends the required configuration message. + /// + /// + /// The authenticated Parse user initiating the live query connection. + /// + /// + /// A token to monitor for cancellation requests during the connection process. + /// + /// + /// A task that represents the asynchronous operation of connecting to the live query service. + /// + public async Task ConnectAsync(CancellationToken cancellationToken = default) + { + if (State == ParseLiveQueryState.Closed) + { + State = ParseLiveQueryState.Connecting; + await OpenAsync(cancellationToken); + WebSocketClient.MessageReceived += WebSocketClientOnMessageReceived; + Dictionary message = new Dictionary + { + { "op", "connect" }, + { "applicationId", ParseClient.Instance.Services.LiveQueryServerConnectionData.ApplicationID }, + { "clientKey", ParseClient.Instance.Services.LiveQueryServerConnectionData.Key } + }; + await SendMessage(message, cancellationToken); + ConnectionSignal = new CancellationTokenSource(); + bool signalReceived = ConnectionSignal.Token.WaitHandle.WaitOne(TimeOut); + State = ParseLiveQueryState.Connected; + ConnectionSignal.Dispose(); + if (!signalReceived) + { + throw new TimeoutException(); + } + } + } + + public async Task SubscribeAsync(ParseLiveQuery liveQuery, CancellationToken cancellationToken = default) where T : ParseObject + { + Dictionary message = new Dictionary + { + { "op", "subscribe" }, + { "requestId", ++LastRequestId }, + { "query", liveQuery.BuildParameters(true) } + }; + await SendMessage(message, cancellationToken); + CancellationTokenSource completionSignal = new CancellationTokenSource(); + SubscriptionSignals.Add(LastRequestId, completionSignal); + bool signalReceived = completionSignal.Token.WaitHandle.WaitOne(TimeOut); + SubscriptionSignals.Remove(LastRequestId); + completionSignal.Dispose(); + if (signalReceived) + { + return LastRequestId; + } + throw new TimeoutException(); + } + + public async Task UpdateSubscriptionAsync(ParseLiveQuery liveQuery, int requestId, CancellationToken cancellationToken = default) where T : ParseObject + { + Dictionary message = new Dictionary + { + { "op", "update" }, + { "requestId", requestId }, + { "query", liveQuery.BuildParameters(true) } + }; + await SendMessage(message, cancellationToken); + } + + public async Task UnsubscribeAsync(int requestId, CancellationToken cancellationToken = default) + { + Dictionary message = new Dictionary + { + { "op", "unsubscribe" }, + { "requestId", requestId } + }; + await SendMessage(message, cancellationToken); + CancellationTokenSource completionSignal = new CancellationTokenSource(); + UnsubscriptionSignals.Add(requestId, completionSignal); + bool signalReceived = completionSignal.Token.WaitHandle.WaitOne(TimeOut); + UnsubscriptionSignals.Remove(requestId); + completionSignal.Dispose(); + if (!signalReceived) + { + throw new TimeoutException(); + } + } + + /// + /// Closes the WebSocket connection asynchronously and updates the state to reflect that the connection has been closed. + /// + /// + /// A token that can be used to propagate notification that the operation should be canceled. + /// + /// + /// A task that represents the asynchronous operation of closing the WebSocket connection. + /// + public async Task CloseAsync(CancellationToken cancellationToken = default) + { + if (SubscriptionIds.Count == 0) + { + await WebSocketClient.CloseAsync(cancellationToken); + State = ParseLiveQueryState.Closed; + } + } +} \ No newline at end of file diff --git a/Parse/Platform/ParseClient.cs b/Parse/Platform/ParseClient.cs index 4e3b4a8b..55e9211c 100644 --- a/Parse/Platform/ParseClient.cs +++ b/Parse/Platform/ParseClient.cs @@ -97,6 +97,67 @@ public ParseClient(IServerConnectionData configuration, IServiceHub serviceHub = Services.ClassController.AddIntrinsic(); } + /// + /// Creates a new and authenticates it as belonging to your application. This class is a hub for interacting with the SDK. The recommended way to use this class on client applications is to instantiate it, then call on it in your application entry point. This allows you to access . + /// + /// The configuration to initialize Parse with. + /// The configuration to initialize the Parse live query client with. + /// A service hub to override internal services and thereby make the Parse SDK operate in a custom manner. + /// A set of implementation instances to tweak the behaviour of the SDK. + public ParseClient(IServerConnectionData configuration, IServerConnectionData liveQueryConfiguration, IServiceHub serviceHub = default, params IServiceHubMutator[] configurators) + { + Services = serviceHub is { } + ? new OrchestrationServiceHub { Custom = serviceHub, Default = new ServiceHub { ServerConnectionData = GenerateServerConnectionData(), LiveQueryServerConnectionData = GenerateLiveQueryServerConnectionData() } } + : new ServiceHub { ServerConnectionData = GenerateServerConnectionData(), LiveQueryServerConnectionData = GenerateLiveQueryServerConnectionData() } as IServiceHub; + + IServerConnectionData GenerateServerConnectionData() => configuration switch + { + null => throw new ArgumentNullException(nameof(configuration)), + ServerConnectionData { Test: true, ServerURI: { } } data => data, + ServerConnectionData { Test: true } data => new ServerConnectionData + { + ApplicationID = data.ApplicationID, + Headers = data.Headers, + MasterKey = data.MasterKey, + Test = data.Test, + Key = data.Key, + ServerURI = "https://api.parse.com/1/" + }, + { ServerURI: "https://api.parse.com/1/" } => throw new InvalidOperationException("Since the official parse server has shut down, you must specify a URI that points to a hosted instance."), + { ApplicationID: { }, ServerURI: { }, Key: { } } data => data, + _ => throw new InvalidOperationException("The IServerConnectionData implementation instance provided to the ParseClient constructor must be populated with the information needed to connect to a Parse server instance.") + }; + + IServerConnectionData GenerateLiveQueryServerConnectionData() => liveQueryConfiguration switch + { + null => throw new ArgumentNullException(nameof(configuration)), + ServerConnectionData { Test: true, ServerURI: { } } data => data, + ServerConnectionData { Test: true } data => new ServerConnectionData + { + ApplicationID = data.ApplicationID, + Headers = data.Headers, + MasterKey = data.MasterKey, + Test = data.Test, + Key = data.Key, + ServerURI = "wss://api.parse.com/1/" + }, + { ServerURI: "wss://api.parse.com/1/" } => throw new InvalidOperationException("Since the official parse server has shut down, you must specify a URI that points to a hosted instance."), + { ApplicationID: { }, ServerURI: { }, Key: { } } data => data, + _ => throw new InvalidOperationException("The IServerConnectionData implementation instance provided to the ParseClient constructor must be populated with the information needed to connect to a Parse server instance.") + }; + + if (configurators is { Length: int length } && length > 0) + { + Services = serviceHub switch + { + IMutableServiceHub { } mutableServiceHub => BuildHub((Hub: mutableServiceHub, mutableServiceHub.ServerConnectionData = serviceHub.ServerConnectionData ?? Services.ServerConnectionData).Hub, Services, configurators), + { } => BuildHub(default, Services, configurators) + }; + } + + Services.ClassController.AddIntrinsic(); + } + /// /// Initializes a instance using the set on the 's implementation instance. /// From d7c8c71eee4e3ec910e054abda362c565eb53cd7 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Fri, 20 Jun 2025 00:01:05 +0000 Subject: [PATCH 02/24] Added ParseLiveQuerySubscription and refactored accordingly --- .../LiveQueries/IParseLiveQueryController.cs | 7 +- .../IParseLiveQuerySubscription.cs | 70 +++++ Parse/Platform/LiveQueries/ParseLiveQuery.cs | 58 +--- .../LiveQueries/ParseLiveQueryController.cs | 247 ++++++++++++------ .../LiveQueries/ParseLiveQuerySubscription.cs | 132 ++++++++++ Parse/Utilities/ObjectServiceExtensions.cs | 21 ++ 6 files changed, 405 insertions(+), 130 deletions(-) create mode 100644 Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs create mode 100644 Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs diff --git a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs index 8e78ffc4..ef09ab44 100644 --- a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs +++ b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs @@ -1,14 +1,17 @@ using System.Threading; using System.Threading.Tasks; -using Parse.Abstractions.Platform.Objects; namespace Parse.Abstractions.Platform.LiveQueries; +/// +/// Defines an interface for managing LiveQuery connections, subscriptions, and updates +/// in a Parse Server environment. +/// public interface IParseLiveQueryController { Task ConnectAsync(CancellationToken cancellationToken = default); - Task SubscribeAsync(ParseLiveQuery liveQuery, CancellationToken cancellationToken = default) where T : ParseObject; + Task SubscribeAsync(ParseLiveQuery liveQuery, CancellationToken cancellationToken = default) where T : ParseObject; Task UpdateSubscriptionAsync(ParseLiveQuery liveQuery, int requestId, CancellationToken cancellationToken = default) where T : ParseObject; diff --git a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs new file mode 100644 index 00000000..9553b22e --- /dev/null +++ b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Parse.Abstractions.Platform.LiveQueries; + +/// +/// Represents a live query subscription that is used with Parse's Live Query service. +/// It allows real-time monitoring and event handling for object changes that match +/// a specified query. +/// +public interface IParseLiveQuerySubscription +{ + /// + /// Represents the Create event for a live query subscription. + /// This event is triggered when a new object matching the subscription's query is created. + /// + public event EventHandler> Create; + + /// + /// Represents the Enter event for a live query subscription. + /// This event is triggered when an object that did not previously match the query (and was thus not part of the subscription) + /// starts matching the query, typically due to an update. + /// + public event EventHandler> Enter; + + /// + /// Represents the Update event for a live query subscription. + /// This event is triggered when an existing object matching the subscription's query is updated. + /// + public event EventHandler> Update; + + /// + /// Represents the Leave event for a live query subscription. + /// This event is triggered when an object that previously matched the subscription's query + /// no longer matches the criteria and is removed. + /// + public event EventHandler> Leave; + + /// + /// Represents the Delete event for a live query subscription. + /// This event is triggered when an object matching the subscription's query is deleted. + /// + public event EventHandler> Delete; + + /// + /// Updates the current live query subscription with new query parameters, + /// effectively modifying the subscription to reflect the provided live query. + /// This allows adjustments to the filter or watched keys without unsubscribing + /// and re-subscribing. + /// + /// The type of the ParseObject associated with the subscription. + /// The updated live query containing new parameters that + /// will replace the existing ones for this subscription. + /// A token to monitor for cancellation requests. If triggered, + /// the update process will be halted. + /// A task that represents the asynchronous operation of updating + /// the subscription with the new query parameters. + Task UpdateAsync(ParseLiveQuery liveQuery, CancellationToken cancellationToken = default) where T : ParseObject; + + /// + /// Cancels the current live query subscription by unsubscribing from the Parse Live Query server. + /// This ensures that the client will no longer receive real-time updates or notifications + /// associated with this subscription. + /// + /// A token to monitor for cancellation requests. If triggered, the cancellation process will halt. + /// A task that represents the asynchronous operation of canceling the subscription. + Task CancelAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/Parse/Platform/LiveQueries/ParseLiveQuery.cs b/Parse/Platform/LiveQueries/ParseLiveQuery.cs index 7cf0d23e..4a9fbbad 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQuery.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQuery.cs @@ -1,17 +1,12 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics; using System.Linq; -using System.Net.WebSockets; -using System.Numerics; -using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualBasic.CompilerServices; using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.LiveQueries; using Parse.Infrastructure.Data; -using Parse.Infrastructure.Utilities; namespace Parse; @@ -35,7 +30,7 @@ public class ParseLiveQuery where T : ParseObject /// /// Serialized keys watched. /// - ReadOnlyCollection KeyWatch { get; } + ReadOnlyCollection KeyWatchers { get; } internal string ClassName { get; } @@ -61,7 +56,7 @@ public ParseLiveQuery(IServiceHub serviceHub, string className, IDictionary(watchedKeys.ToList()); + KeyWatchers = new ReadOnlyCollection(watchedKeys.ToList()); } } @@ -70,7 +65,7 @@ public ParseLiveQuery(IServiceHub serviceHub, string className, IDictionary - internal ParseLiveQuery(ParseLiveQuery source, IEnumerable watchedKeys = null) + internal ParseLiveQuery(ParseLiveQuery source, IEnumerable watchedKeys = null, Func> onCreate = null) { if (source == null) { @@ -81,14 +76,15 @@ internal ParseLiveQuery(ParseLiveQuery source, IEnumerable watchedKey ClassName = source.ClassName; Filters = source.Filters; KeySelections = source.KeySelections; + KeyWatchers = source.KeyWatchers; if (watchedKeys is { }) { - KeyWatch = new ReadOnlyCollection(MergeKeys(watchedKeys).ToList()); + KeyWatchers = new ReadOnlyCollection(MergeWatchers(watchedKeys).ToList()); } } - HashSet MergeKeys(IEnumerable selectedKeys) => new((KeySelections ?? Enumerable.Empty()).Concat(selectedKeys)); + HashSet MergeWatchers(IEnumerable keys) => new((KeyWatchers ?? Enumerable.Empty()).Concat(keys)); /// /// Add the provided key to the watched fields of returned ParseObjects. @@ -106,23 +102,13 @@ internal IDictionary BuildParameters(bool includeClassName = fal result["where"] = PointerOrLocalIdEncoder.Instance.Encode(Filters, Services); if (KeySelections != null) result["keys"] = String.Join(",", KeySelections.ToArray()); - if (KeyWatch != null) - result["watch"] = String.Join(",", KeyWatch.ToArray()); + if (KeyWatchers != null) + result["watch"] = String.Join(",", KeyWatchers.ToArray()); if (includeClassName) result["className"] = ClassName; return result; } - /// - /// Establishes a connection to the Parse Live Query server using the ClientWebSocket instance. - /// Prepares and sends a connection message containing required identifiers such as application ID, client key, and session token. - /// - /// A Task representing the asynchronous operation, returning true if the connection attempt is initialized successfully, false otherwise. - public async Task ConnectAsync() - { - await Services.LiveQueryController.ConnectAsync(CancellationToken.None); - } - /// /// Subscribes to the live query, allowing the client to receive real-time updates /// for the query's results. This establishes a subscription with the Live Query service. @@ -131,30 +117,8 @@ public async Task ConnectAsync() /// A task representing the asynchronous subscription operation. Upon completion /// of the task, the subscription is successfully registered. /// - public async Task SubscribeAsync() - { - RequestId = await Services.LiveQueryController.SubscribeAsync(this, CancellationToken.None); - } - - /// - /// Unsubscribes from the live query, stopping the client from receiving further updates related to the subscription. - /// - /// A task representing the asynchronous operation of unsubscribing from the live query. - public async Task UnsubscribeAsync() - { - if (RequestId > 0) - await Services.LiveQueryController.UnsubscribeAsync(RequestId, CancellationToken.None); - } - - /// - /// Closes the connection to the live query server asynchronously. - /// - /// - /// A task representing the asynchronous operation of closing the live query connection. - /// - public async Task CloseAsync() + public async Task SubscribeAsync() { - await Services.LiveQueryController.CloseAsync(CancellationToken.None); + return await Services.LiveQueryController.SubscribeAsync(this, CancellationToken.None); } - } \ No newline at end of file diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs index 1ffc88e2..c2acc907 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs @@ -11,24 +11,41 @@ namespace Parse.Platform.LiveQueries; +/// +/// The ParseLiveQueryController is responsible for managing live query subscriptions, maintaining a connection +/// to the Parse LiveQuery server, and handling real-time updates from the server. +/// public class ParseLiveQueryController : IParseLiveQueryController { - IWebSocketClient WebSocketClient { get; } + private IWebSocketClient WebSocketClient { get; } private int LastRequestId { get; set; } = 0; + /// + /// Gets or sets the timeout duration, in milliseconds, used by the ParseLiveQueryController + /// for various operations, such as establishing a connection or completing a subscription. + /// + /// + /// This property determines the maximum amount of time the controller will wait for an operation + /// to complete before throwing a . It is used in operations such as: + /// - Connecting to the LiveQuery server. + /// - Subscribing to a query. + /// - Unsubscribing from a query. + /// Ensure that the value is configured appropriately to avoid premature timeout errors in network-dependent processes. + /// public int TimeOut { get; set; } = 5000; - // public event EventHandler Connected; - // public event EventHandler Subscribed; - // public event EventHandler Unsubscribed; - // public event EventHandler SubscribtionUpdated; + /// + /// Event triggered when an error occurs during Parse Live Query operations. + /// + /// + /// This event provides detailed information about the encountered error through the event arguments, + /// which consist of a dictionary containing key-value pairs describing the error context and specifics. + /// It can be used to log, handle, or analyze the errors that arise during subscription, connection, + /// or message processing operations. Common scenarios triggering this event include protocol issues, + /// connectivity problems, or invalid message formats. + /// public event EventHandler> Error; - public event EventHandler> Create; - public event EventHandler> Enter; - public event EventHandler> Update; - public event EventHandler> Leave; - public event EventHandler> Delete; public enum ParseLiveQueryState { @@ -42,14 +59,27 @@ public enum ParseLiveQueryState Connected } + /// + /// Gets the current state of the ParseLiveQueryController. This property indicates + /// whether the controller is in a Closed, Connecting, or Connected state. + /// + /// + /// - `Closed`: Indicates that the controller is not connected. + /// - `Connecting`: Indicates that a connection attempt is in progress. + /// - `Connected`: Indicates that the controller is actively connected. + /// This property is updated based on the controller's connection lifecycle events, + /// such as when a connection is established or closed, or when an error occurs. + /// public ParseLiveQueryState State { get; private set; } - public ArrayList SubscriptionIds { get; } + ArrayList SubscriptionIds { get; } CancellationTokenSource ConnectionSignal { get; set; } private IDictionary SubscriptionSignals { get; } = new Dictionary { }; private IDictionary UnsubscriptionSignals { get; } = new Dictionary { }; private IDictionary SubscriptionUpdateSignals { get; } = new Dictionary { }; + private IDictionary Subscriptions { get; set; } = new Dictionary { }; + public ParseLiveQueryController(IWebSocketClient webSocketClient) { WebSocketClient = webSocketClient; @@ -57,17 +87,9 @@ public ParseLiveQueryController(IWebSocketClient webSocketClient) State = ParseLiveQueryState.Closed; } - /// - /// Processes an incoming message by determining its operation type and triggering - /// the corresponding events or handling the operation accordingly. - /// - /// - /// A dictionary representing the message received, where the key-value pairs - /// contain the details of the message including the operation type ("op") and - /// any associated data. - /// private void ProcessMessage(IDictionary message) { + int requestId; switch (message["op"]) { case "connected": @@ -77,7 +99,7 @@ private void ProcessMessage(IDictionary message) break; case "subscribed": - int requestId = Convert.ToInt32(message["requestId"]); + requestId = Convert.ToInt32(message["requestId"]); SubscriptionIds.Add(requestId); if (SubscriptionSignals.TryGetValue(requestId, out CancellationTokenSource subscriptionSignal)) { @@ -86,6 +108,8 @@ private void ProcessMessage(IDictionary message) // Subscribed?.Invoke(this, requestId); break; + // TODO subscription update case + case "unsubscribed": requestId = Convert.ToInt32(message["requestId"]); SubscriptionIds.Remove(requestId); @@ -106,23 +130,43 @@ private void ProcessMessage(IDictionary message) break; case "create": - Create?.Invoke(this, message); + requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out ParseLiveQuerySubscription subscription)) + { + subscription.OnCreate(message); + } break; case "enter": - Enter?.Invoke(this, message); + requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out subscription)) + { + subscription.OnEnter(message); + } break; case "update": - Update?.Invoke(this, message); + requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out subscription)) + { + subscription.OnUpdate(message); + } break; case "leave": - Leave?.Invoke(this, message); + requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out subscription)) + { + subscription.OnLeave(message); + } break; case "delete": - Delete?.Invoke(this, message); + requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out subscription)) + { + subscription.OnDelete(message); + } break; default: @@ -138,34 +182,11 @@ private IDictionary AppendSessionToken(IDictionary - /// Sends a message to the server over a WebSocket connection. - /// This method processes the message data and ensures it's transmitted asynchronously. - /// - /// - /// A dictionary containing the message data to be sent. - /// - /// - /// A token to observe while waiting for the task to complete, allowing the operation to be canceled. - /// - /// - /// A task that represents the asynchronous operation of sending the message. - /// private async Task SendMessage(IDictionary message, CancellationToken cancellationToken) { await WebSocketClient.SendAsync(JsonUtilities.Encode(AppendSessionToken(message)), cancellationToken); } - /// - /// Opens a WebSocket connection and initiates listening for updates. - /// This method establishes the connection to the Live Query server and transitions the state to Open. - /// - /// - /// A token to observe while waiting for the task to complete. It allows canceling the connection process. - /// - /// - /// A task that represents the asynchronous operation of opening the WebSocket connection. - /// private async Task OpenAsync(CancellationToken cancellationToken = default) { if (ParseClient.Instance.Services == null) @@ -181,32 +202,21 @@ private async Task OpenAsync(CancellationToken cancellationToken = default) await WebSocketClient.OpenAsync(ParseClient.Instance.Services.LiveQueryServerConnectionData.ServerURI, cancellationToken); } - /// - /// Handles a message received event from the WebSocket client by parsing the incoming message - /// and processing it as a dictionary of key-value pairs. - /// - /// - /// The source of the event, typically the WebSocket client that triggered the message received event. - /// - /// - /// The raw string message received from the WebSocket client, representing JSON data - /// that can be parsed and processed. - /// - void WebSocketClientOnMessageReceived(object sender, string e) + private void WebSocketClientOnMessageReceived(object sender, string e) => ProcessMessage(JsonUtilities.Parse(e) as IDictionary); /// - /// Establishes a live query connection using the specified user credentials. - /// This method initializes the connection and sends the required configuration message. + /// Establishes a connection to the live query server asynchronously. This method initiates the connection process, + /// manages connection states, and handles any timeout scenarios if the connection cannot be established within the specified duration. /// - /// - /// The authenticated Parse user initiating the live query connection. - /// /// - /// A token to monitor for cancellation requests during the connection process. + /// A cancellation token that can be used to cancel the connection process. If the token is triggered, + /// the connection process will be terminated. /// /// - /// A task that represents the asynchronous operation of connecting to the live query service. + /// A task that represents the asynchronous connection operation. If the connection is successful, + /// the task will complete when the connection is established. In the event of a timeout or error, + /// it will throw the appropriate exception. /// public async Task ConnectAsync(CancellationToken cancellationToken = default) { @@ -231,29 +241,88 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) throw new TimeoutException(); } } + else if (State == ParseLiveQueryState.Connecting) + { + if (ConnectionSignal is not null) + { + if (!ConnectionSignal.Token.WaitHandle.WaitOne(TimeOut)) + { + throw new TimeoutException(); + } + } + } } - public async Task SubscribeAsync(ParseLiveQuery liveQuery, CancellationToken cancellationToken = default) where T : ParseObject + /// + /// Subscribes to a live query, enabling real-time updates for the specified query object. + /// This method sends a subscription request to the live query server and manages the lifecycle of the subscription. + /// + /// + /// The type of the ParseObject associated with the live query. + /// + /// + /// The live query instance to subscribe to. It contains details about the query and its parameters. + /// + /// + /// A token to monitor for cancellation requests. It allows the operation to be canceled if requested. + /// + /// + /// An object representing the active subscription for the specified query, enabling interaction with the subscribed events and updates. + /// + /// + /// Thrown when attempting to subscribe while the live query connection is in a closed state. + /// + /// + /// Thrown when the subscription request times out before receiving confirmation from the server. + /// + public async Task SubscribeAsync(ParseLiveQuery liveQuery, CancellationToken cancellationToken = default) where T : ParseObject { + if (State == ParseLiveQueryState.Closed) + { + throw new InvalidOperationException("Cannot subscribe to a live query when the connection is closed."); + } + + int requestId = ++LastRequestId; Dictionary message = new Dictionary { { "op", "subscribe" }, - { "requestId", ++LastRequestId }, + { "requestId", requestId }, { "query", liveQuery.BuildParameters(true) } }; await SendMessage(message, cancellationToken); CancellationTokenSource completionSignal = new CancellationTokenSource(); - SubscriptionSignals.Add(LastRequestId, completionSignal); + SubscriptionSignals.Add(requestId, completionSignal); bool signalReceived = completionSignal.Token.WaitHandle.WaitOne(TimeOut); - SubscriptionSignals.Remove(LastRequestId); + SubscriptionSignals.Remove(requestId); completionSignal.Dispose(); if (signalReceived) { - return LastRequestId; + ParseLiveQuerySubscription subscription = new ParseLiveQuerySubscription(liveQuery.Services, requestId); + Subscriptions.Add(requestId, subscription); + return subscription; } throw new TimeoutException(); } + /// + /// Updates an active subscription by sending an "update" operation to the live query server. + /// This method modifies the parameters of an existing subscription for a specific query. + /// + /// + /// The live query object that holds the query parameters to be updated. + /// + /// + /// The unique identifier of the subscription to update. + /// + /// + /// A token to monitor for cancellation requests, allowing the operation to be cancelled before completion. + /// + /// + /// The type of the ParseObject that the query targets. + /// + /// + /// A task that represents the asynchronous operation of updating the subscription. + /// public async Task UpdateSubscriptionAsync(ParseLiveQuery liveQuery, int requestId, CancellationToken cancellationToken = default) where T : ParseObject { Dictionary message = new Dictionary @@ -265,6 +334,21 @@ public async Task UpdateSubscriptionAsync(ParseLiveQuery liveQuery, int re await SendMessage(message, cancellationToken); } + /// + /// Unsubscribes from a live query subscription associated with the given request identifier. + /// + /// + /// The unique identifier of the subscription to unsubscribe from. + /// + /// + /// A cancellation token that can be used to cancel the unsubscription operation before completion. + /// + /// + /// A task that represents the asynchronous unsubscription operation. + /// + /// + /// Thrown if the unsubscription process does not complete within the specified timeout period. + /// public async Task UnsubscribeAsync(int requestId, CancellationToken cancellationToken = default) { Dictionary message = new Dictionary @@ -285,20 +369,21 @@ public async Task UnsubscribeAsync(int requestId, CancellationToken cancellation } /// - /// Closes the WebSocket connection asynchronously and updates the state to reflect that the connection has been closed. + /// Closes the live query connection, resets the state to closed, and clears all active subscriptions and signals. /// /// - /// A token that can be used to propagate notification that the operation should be canceled. + /// A token to monitor for cancellation requests while closing the connection. /// /// - /// A task that represents the asynchronous operation of closing the WebSocket connection. + /// A task that represents the asynchronous operation for closing the live query connection. /// public async Task CloseAsync(CancellationToken cancellationToken = default) { - if (SubscriptionIds.Count == 0) - { - await WebSocketClient.CloseAsync(cancellationToken); - State = ParseLiveQueryState.Closed; - } + await WebSocketClient.CloseAsync(cancellationToken); + State = ParseLiveQueryState.Closed; + SubscriptionSignals.Clear(); + UnsubscriptionSignals.Clear(); + SubscriptionUpdateSignals.Clear(); + Subscriptions.Clear(); } } \ No newline at end of file diff --git a/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs b/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs new file mode 100644 index 00000000..db311058 --- /dev/null +++ b/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.LiveQueries; + +namespace Parse.Platform.LiveQueries; + +/// +/// Represents a subscription to updates for a LiveQuery in a Parse Server. Provides hooks for handling +/// various events such as creation, update, deletion, entering, and leaving of objects that match the query. +/// +public class ParseLiveQuerySubscription : IParseLiveQuerySubscription +{ + + internal IServiceHub Services { get; } + + private int RequestId { get; set; } + + /// + /// Represents the Create event for a live query subscription. + /// This event is triggered when a new object matching the subscription's query is created. + /// + public event EventHandler> Create; + + /// + /// Represents the Enter event for a live query subscription. + /// This event is triggered when an object that did not previously match the query (and was thus not part of the subscription) + /// starts matching the query, typically due to an update. + /// + public event EventHandler> Enter; + + /// + /// Represents the Update event for a live query subscription. + /// This event is triggered when an existing object matching the subscription's query is updated. + /// + public event EventHandler> Update; + + /// + /// Represents the Leave event for a live query subscription. + /// This event is triggered when an object that previously matched the subscription's query + /// no longer matches the criteria and is removed. + /// + public event EventHandler> Leave; + + /// + /// Represents the Delete event for a live query subscription. + /// This event is triggered when an object matching the subscription's query is deleted. + /// + public event EventHandler> Delete; + + /// + /// Represents a subscription to a live query, allowing the client to receive real-time event notifications + /// from the Parse Live Query server for a specified query. This class is responsible for handling events + /// such as object creation, updates, deletions, and entering or leaving a query's result set. + /// + public ParseLiveQuerySubscription(IServiceHub serviceHub, int requestId) + { + Services = serviceHub; + RequestId = requestId; + } + + /// + /// Updates the current live query subscription with new query parameters, + /// effectively modifying the subscription to reflect the provided live query. + /// This allows adjustments to the filter or watched keys without unsubscribing + /// and re-subscribing. + /// + /// The type of the ParseObject associated with the subscription. + /// The updated live query containing new parameters that + /// will replace the existing ones for this subscription. + /// A token to monitor for cancellation requests. If triggered, + /// the update process will be halted. + /// A task that represents the asynchronous operation of updating + /// the subscription with the new query parameters. + public async Task UpdateAsync(ParseLiveQuery liveQuery, CancellationToken cancellationToken = default) where T : ParseObject + { + await Services.LiveQueryController.UpdateSubscriptionAsync(liveQuery, RequestId, CancellationToken.None); + } + + /// + /// Cancels the current live query subscription by unsubscribing from the Parse Live Query server. + /// This ensures that the client will no longer receive real-time updates or notifications + /// associated with this subscription. + /// + /// A token to monitor for cancellation requests. If triggered, the cancellation process will halt. + /// A task that represents the asynchronous operation of canceling the subscription. + public async Task CancelAsync(CancellationToken cancellationToken = default) + { + await Services.LiveQueryController.UnsubscribeAsync(RequestId, CancellationToken.None); + } + + /// + /// Handles invocation of the Create event for the live query subscription, signaling that a new object + /// has been created and matches the query criteria. This method triggers the associated Create event + /// to notify any subscribed listeners about the creation event. + /// + /// A dictionary containing the data associated with the created object, typically including + /// information such as object attributes and metadata. + public void OnCreate(IDictionary data) => Create?.Invoke(this, data); + + /// + /// Triggers the Enter event, indicating that an object has entered the result set of the live query. + /// This generally occurs when a Parse Object that did not previously match the query conditions now does. + /// + /// A dictionary containing the details of the object that triggered the event. + public void OnEnter(IDictionary data) => Enter?.Invoke(this, data); + + /// + /// Handles the event triggered when an object in the subscribed live query is updated. This method + /// invokes the corresponding handler with the provided update data. + /// + /// A dictionary containing the data associated with the update event. + /// The data typically includes updated fields and their new values. + public void OnUpdate(IDictionary data) => Update?.Invoke(this, data); + + /// + /// Triggers the Leave event when an object leaves the query's result set. + /// This method notifies all registered event handlers, providing the relevant data associated + /// with the event. + /// + /// A dictionary that contains information about the object leaving the query's result set. + public void OnLeave(IDictionary data) => Leave?.Invoke(this, data); + + /// + /// Handles the deletion event triggered by the Parse Live Query server. This method is invoked when an object + /// that matches the current query result set is deleted, notifying all subscribers of this event. + /// + /// A dictionary containing information about the deleted object and any additional context provided by the server. + public void OnDelete(IDictionary data) => Delete?.Invoke(this, data); +} \ No newline at end of file diff --git a/Parse/Utilities/ObjectServiceExtensions.cs b/Parse/Utilities/ObjectServiceExtensions.cs index 4f7a8320..ebe49ba3 100644 --- a/Parse/Utilities/ObjectServiceExtensions.cs +++ b/Parse/Utilities/ObjectServiceExtensions.cs @@ -287,6 +287,27 @@ public static ParseQuery GetQuery(this IServiceHub serviceHub, stri return new ParseQuery(serviceHub, className); } + /// + /// Establishes a connection to the Live Query server using the provided instance. + /// This method ensures that the Live Query controller initiates and maintains a persistent connection. + /// + /// The service hub instance containing the Live Query controller to manage the connection. + /// A task that represents the asynchronous operation of connecting to the Live Query server. + public static async Task ConnectLiveQueryServerAsync(this IServiceHub serviceHub) + { + await serviceHub.LiveQueryController.ConnectAsync(); + } + + /// + /// Disconnects from the live query server by closing the connection established through the LiveQueryController. + /// + /// The instance managing the service resources. + /// A task representing the asynchronous operation of disconnecting from the live query server. + public static async Task DisconnectLiveQueryServerAsync(this IServiceHub serviceHub) + { + await serviceHub.LiveQueryController.CloseAsync(); + } + /// /// Saves each object in the provided list. /// From 0932e350f3795c1d71ad91f14d6475dbf8d4aa41 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Sat, 21 Jun 2025 21:15:14 +0000 Subject: [PATCH 03/24] Added EventArgs --- Parse.sln | 6 + .../IParseLiveQuerySubscription.cs | 11 +- Parse/Platform/LiveQueries/ParseLiveQuery.cs | 18 +-- .../LiveQueries/ParseLiveQueryController.cs | 150 +++++++++++++----- .../ParseLiveQueryErrorEventArgs.cs | 10 ++ .../LiveQueries/ParseLiveQueryEventArgs.cs | 14 ++ .../LiveQueries/ParseLiveQuerySubscription.cs | 21 ++- Parse/Platform/Queries/ParseQuery.cs | 6 + 8 files changed, 165 insertions(+), 71 deletions(-) create mode 100644 Parse/Platform/LiveQueries/ParseLiveQueryErrorEventArgs.cs create mode 100644 Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs diff --git a/Parse.sln b/Parse.sln index 82e6de70..104934b1 100644 --- a/Parse.sln +++ b/Parse.sln @@ -17,6 +17,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parse.Tests", "Parse.Tests\Parse.Tests.csproj", "{FEB46D0F-384C-4F27-9E0E-F4A636768C90}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParseApp", "ParseApp\ParseApp.csproj", "{71EF1783-2BAA-4119-A666-B7DCA3FD3085}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,6 +33,10 @@ Global {FEB46D0F-384C-4F27-9E0E-F4A636768C90}.Debug|Any CPU.Build.0 = Debug|Any CPU {FEB46D0F-384C-4F27-9E0E-F4A636768C90}.Release|Any CPU.ActiveCfg = Release|Any CPU {FEB46D0F-384C-4F27-9E0E-F4A636768C90}.Release|Any CPU.Build.0 = Release|Any CPU + {71EF1783-2BAA-4119-A666-B7DCA3FD3085}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71EF1783-2BAA-4119-A666-B7DCA3FD3085}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71EF1783-2BAA-4119-A666-B7DCA3FD3085}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71EF1783-2BAA-4119-A666-B7DCA3FD3085}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs index 9553b22e..f57f57c3 100644 --- a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs +++ b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Parse.Platform.LiveQueries; namespace Parse.Abstractions.Platform.LiveQueries; @@ -16,33 +17,33 @@ public interface IParseLiveQuerySubscription /// Represents the Create event for a live query subscription. /// This event is triggered when a new object matching the subscription's query is created. /// - public event EventHandler> Create; + public event EventHandler Create; /// /// Represents the Enter event for a live query subscription. /// This event is triggered when an object that did not previously match the query (and was thus not part of the subscription) /// starts matching the query, typically due to an update. /// - public event EventHandler> Enter; + public event EventHandler Enter; /// /// Represents the Update event for a live query subscription. /// This event is triggered when an existing object matching the subscription's query is updated. /// - public event EventHandler> Update; + public event EventHandler Update; /// /// Represents the Leave event for a live query subscription. /// This event is triggered when an object that previously matched the subscription's query /// no longer matches the criteria and is removed. /// - public event EventHandler> Leave; + public event EventHandler Leave; /// /// Represents the Delete event for a live query subscription. /// This event is triggered when an object matching the subscription's query is deleted. /// - public event EventHandler> Delete; + public event EventHandler Delete; /// /// Updates the current live query subscription with new query parameters, diff --git a/Parse/Platform/LiveQueries/ParseLiveQuery.cs b/Parse/Platform/LiveQueries/ParseLiveQuery.cs index 4a9fbbad..1787ea9b 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQuery.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQuery.cs @@ -7,6 +7,7 @@ using Parse.Abstractions.Infrastructure; using Parse.Abstractions.Platform.LiveQueries; using Parse.Infrastructure.Data; +using Parse.Infrastructure.Utilities; namespace Parse; @@ -20,7 +21,7 @@ public class ParseLiveQuery where T : ParseObject /// /// Serialized clauses. /// - Dictionary Filters { get; } + string Filters { get; } /// /// Serialized key selections. @@ -38,17 +39,12 @@ public class ParseLiveQuery where T : ParseObject private int RequestId = 0; - public ParseLiveQuery(IServiceHub serviceHub, string className, IDictionary filters, IEnumerable selectedKeys = null, IEnumerable watchedKeys = null) + public ParseLiveQuery(IServiceHub serviceHub, string className, object filters, IEnumerable selectedKeys = null, IEnumerable watchedKeys = null) { - if (filters.Count == 0) - { - // Throw error - } - Services = serviceHub; ClassName = className; + Filters = JsonUtilities.Encode(filters); - Filters = new Dictionary(filters); if (selectedKeys is not null) { KeySelections = new ReadOnlyCollection(selectedKeys.ToList()); @@ -95,11 +91,11 @@ internal ParseLiveQuery(ParseLiveQuery source, IEnumerable watchedKey /// A new query with the additional constraint. public ParseLiveQuery Watch(string watch) => new(this, new List { watch }); - internal IDictionary BuildParameters(bool includeClassName = false) + internal IDictionary BuildParameters(bool includeClassName = false) { - Dictionary result = new Dictionary(); + Dictionary result = new Dictionary(); if (Filters != null) - result["where"] = PointerOrLocalIdEncoder.Instance.Encode(Filters, Services); + result["where"] = Filters; if (KeySelections != null) result["keys"] = String.Join(",", KeySelections.ToArray()); if (KeyWatchers != null) diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs index c2acc907..c1b677b0 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -21,13 +20,15 @@ public class ParseLiveQueryController : IParseLiveQueryController private int LastRequestId { get; set; } = 0; + private string ClientId { get; set; } + /// /// Gets or sets the timeout duration, in milliseconds, used by the ParseLiveQueryController /// for various operations, such as establishing a connection or completing a subscription. /// /// /// This property determines the maximum amount of time the controller will wait for an operation - /// to complete before throwing a . It is used in operations such as: + /// to complete before throwing a . It is used in operations such as /// - Connecting to the LiveQuery server. /// - Subscribing to a query. /// - Unsubscribing from a query. @@ -36,17 +37,23 @@ public class ParseLiveQueryController : IParseLiveQueryController public int TimeOut { get; set; } = 5000; /// - /// Event triggered when an error occurs during Parse Live Query operations. + /// Event triggered when an error occurs during the operation of the ParseLiveQueryController. /// /// - /// This event provides detailed information about the encountered error through the event arguments, - /// which consist of a dictionary containing key-value pairs describing the error context and specifics. - /// It can be used to log, handle, or analyze the errors that arise during subscription, connection, - /// or message processing operations. Common scenarios triggering this event include protocol issues, - /// connectivity problems, or invalid message formats. + /// This event provides details about a live query operation failure, such as specific error messages, + /// error codes, and whether automatic reconnection is recommended. + /// It is raised in scenarios like: + /// - Receiving an error response from the LiveQuery server. + /// - Issues with subscriptions, unsubscriptions, or query updates. + /// Subscribers to this event can use the provided to + /// understand the error and implement appropriate handling mechanisms. /// - public event EventHandler> Error; + public event EventHandler Error; + /// + /// Represents the state of a connection to the Parse LiveQuery server, indicating whether the connection is closed, + /// in the process of connecting, or fully established. + /// public enum ParseLiveQueryState { /// @@ -71,7 +78,8 @@ public enum ParseLiveQueryState /// such as when a connection is established or closed, or when an error occurs. /// public ParseLiveQueryState State { get; private set; } - ArrayList SubscriptionIds { get; } + + HashSet SubscriptionIds { get; } = new HashSet { }; CancellationTokenSource ConnectionSignal { get; set; } private IDictionary SubscriptionSignals { get; } = new Dictionary { }; @@ -83,89 +91,143 @@ public enum ParseLiveQueryState public ParseLiveQueryController(IWebSocketClient webSocketClient) { WebSocketClient = webSocketClient; - SubscriptionIds = new ArrayList(); State = ParseLiveQueryState.Closed; } private void ProcessMessage(IDictionary message) { int requestId; + string clientId; + ParseLiveQuerySubscription subscription; switch (message["op"]) { case "connected": State = ParseLiveQueryState.Connected; + ClientId = message["clientId"] as string; ConnectionSignal?.Cancel(); - // Connected?.Invoke(this, EventArgs.Empty); break; - case "subscribed": - requestId = Convert.ToInt32(message["requestId"]); - SubscriptionIds.Add(requestId); - if (SubscriptionSignals.TryGetValue(requestId, out CancellationTokenSource subscriptionSignal)) + case "subscribed": // Response from subscription and subscription update + clientId = message["clientId"] as string; + if (clientId == ClientId) { - subscriptionSignal?.Cancel(); + requestId = Convert.ToInt32(message["requestId"]); + SubscriptionIds.Add(requestId); + if (SubscriptionSignals.TryGetValue(requestId, out CancellationTokenSource subscriptionSignal)) + { + subscriptionSignal?.Cancel(); + } } - // Subscribed?.Invoke(this, requestId); break; - // TODO subscription update case - case "unsubscribed": - requestId = Convert.ToInt32(message["requestId"]); - SubscriptionIds.Remove(requestId); - if (UnsubscriptionSignals.TryGetValue(requestId, out CancellationTokenSource unsubscriptionSignal)) + clientId = message["clientId"] as string; + if (clientId == ClientId) { - unsubscriptionSignal?.Cancel(); + requestId = Convert.ToInt32(message["requestId"]); + SubscriptionIds.Remove(requestId); + if (UnsubscriptionSignals.TryGetValue(requestId, out CancellationTokenSource unsubscriptionSignal)) + { + unsubscriptionSignal?.Cancel(); + } } - // Unsubscribed?.Invoke(this, requestId); break; case "error": if ((bool)message["reconnect"]) { - OpenAsync(); + ConnectAsync(); } - string errorMessage = message["error"] as string; - Error?.Invoke(this, message); + + ParseLiveQueryErrorEventArgs errorArgs = new ParseLiveQueryErrorEventArgs + { + Error = message["error"] as string, + Code = Convert.ToInt32(message["code"]), + Reconnected = (bool)message["reconnect"] + }; + Error?.Invoke(this, errorArgs); break; case "create": - requestId = Convert.ToInt32(message["requestId"]); - if (Subscriptions.TryGetValue(requestId, out ParseLiveQuerySubscription subscription)) + clientId = message["clientId"] as string; + if (clientId == ClientId) { - subscription.OnCreate(message); + requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out subscription)) + { + ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs() + { + Object = message["object"] + }; + subscription.OnCreate(args); + } } break; case "enter": - requestId = Convert.ToInt32(message["requestId"]); - if (Subscriptions.TryGetValue(requestId, out subscription)) + clientId = message["clientId"] as string; + if (clientId == ClientId) { - subscription.OnEnter(message); + requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out subscription)) + { + ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs() + { + Object = message["object"], + Original = message["original"] + }; + subscription.OnEnter(args); + } } break; case "update": - requestId = Convert.ToInt32(message["requestId"]); - if (Subscriptions.TryGetValue(requestId, out subscription)) + clientId = message["clientId"] as string; + if (clientId == ClientId) { - subscription.OnUpdate(message); + requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out subscription)) + { + ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs() + { + Object = message["object"], + Original = message["original"] + }; + subscription.OnUpdate(args); + } } break; case "leave": - requestId = Convert.ToInt32(message["requestId"]); - if (Subscriptions.TryGetValue(requestId, out subscription)) + clientId = message["clientId"] as string; + if (clientId == ClientId) { - subscription.OnLeave(message); + requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out subscription)) + { + ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs() + { + Object = message["object"], + Original = message["original"] + }; + subscription.OnLeave(args); + } } break; case "delete": - requestId = Convert.ToInt32(message["requestId"]); - if (Subscriptions.TryGetValue(requestId, out subscription)) + clientId = message["clientId"] as string; + if (clientId == ClientId) { - subscription.OnDelete(message); + requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out subscription)) + { + ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs() + { + Object = message["object"], + }; + subscription.OnDelete(args); + } } break; @@ -369,7 +431,7 @@ public async Task UnsubscribeAsync(int requestId, CancellationToken cancellation } /// - /// Closes the live query connection, resets the state to closed, and clears all active subscriptions and signals. + /// Closes the live query connection, resets the state to close, and clears all active subscriptions and signals. /// /// /// A token to monitor for cancellation requests while closing the connection. diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryErrorEventArgs.cs b/Parse/Platform/LiveQueries/ParseLiveQueryErrorEventArgs.cs new file mode 100644 index 00000000..b6876179 --- /dev/null +++ b/Parse/Platform/LiveQueries/ParseLiveQueryErrorEventArgs.cs @@ -0,0 +1,10 @@ +using System; + +namespace Parse.Platform.LiveQueries; + +public class ParseLiveQueryErrorEventArgs : EventArgs +{ + public string Error { get; set; } + public int Code { get; set; } + public bool Reconnected { get; set; } +} \ No newline at end of file diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs b/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs new file mode 100644 index 00000000..d55645ab --- /dev/null +++ b/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Parse.Platform.LiveQueries; + +/// +/// Provides event arguments for events triggered by Parse's Live Query service. +/// This class encapsulates details about a particular event, such as the operation type, +/// client ID, request ID, and the associated Parse object data. +/// +public class ParseLiveQueryEventArgs : EventArgs +{ + public object Object { get; set; } + public object Original { get; set; } +} diff --git a/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs b/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs index db311058..9e797c3b 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs @@ -13,7 +13,6 @@ namespace Parse.Platform.LiveQueries; /// public class ParseLiveQuerySubscription : IParseLiveQuerySubscription { - internal IServiceHub Services { get; } private int RequestId { get; set; } @@ -22,33 +21,33 @@ public class ParseLiveQuerySubscription : IParseLiveQuerySubscription /// Represents the Create event for a live query subscription. /// This event is triggered when a new object matching the subscription's query is created. /// - public event EventHandler> Create; + public event EventHandler Create; /// /// Represents the Enter event for a live query subscription. /// This event is triggered when an object that did not previously match the query (and was thus not part of the subscription) /// starts matching the query, typically due to an update. /// - public event EventHandler> Enter; + public event EventHandler Enter; /// /// Represents the Update event for a live query subscription. /// This event is triggered when an existing object matching the subscription's query is updated. /// - public event EventHandler> Update; + public event EventHandler Update; /// /// Represents the Leave event for a live query subscription. /// This event is triggered when an object that previously matched the subscription's query /// no longer matches the criteria and is removed. /// - public event EventHandler> Leave; + public event EventHandler Leave; /// /// Represents the Delete event for a live query subscription. /// This event is triggered when an object matching the subscription's query is deleted. /// - public event EventHandler> Delete; + public event EventHandler Delete; /// /// Represents a subscription to a live query, allowing the client to receive real-time event notifications @@ -98,14 +97,14 @@ public async Task CancelAsync(CancellationToken cancellationToken = default) /// /// A dictionary containing the data associated with the created object, typically including /// information such as object attributes and metadata. - public void OnCreate(IDictionary data) => Create?.Invoke(this, data); + public void OnCreate(ParseLiveQueryEventArgs data) => Create?.Invoke(this, data); /// /// Triggers the Enter event, indicating that an object has entered the result set of the live query. /// This generally occurs when a Parse Object that did not previously match the query conditions now does. /// /// A dictionary containing the details of the object that triggered the event. - public void OnEnter(IDictionary data) => Enter?.Invoke(this, data); + public void OnEnter(ParseLiveQueryEventArgs data) => Enter?.Invoke(this, data); /// /// Handles the event triggered when an object in the subscribed live query is updated. This method @@ -113,7 +112,7 @@ public async Task CancelAsync(CancellationToken cancellationToken = default) /// /// A dictionary containing the data associated with the update event. /// The data typically includes updated fields and their new values. - public void OnUpdate(IDictionary data) => Update?.Invoke(this, data); + public void OnUpdate(ParseLiveQueryEventArgs data) => Update?.Invoke(this, data); /// /// Triggers the Leave event when an object leaves the query's result set. @@ -121,12 +120,12 @@ public async Task CancelAsync(CancellationToken cancellationToken = default) /// with the event. /// /// A dictionary that contains information about the object leaving the query's result set. - public void OnLeave(IDictionary data) => Leave?.Invoke(this, data); + public void OnLeave(ParseLiveQueryEventArgs data) => Leave?.Invoke(this, data); /// /// Handles the deletion event triggered by the Parse Live Query server. This method is invoked when an object /// that matches the current query result set is deleted, notifying all subscribers of this event. /// /// A dictionary containing information about the deleted object and any additional context provided by the server. - public void OnDelete(IDictionary data) => Delete?.Invoke(this, data); + public void OnDelete(ParseLiveQueryEventArgs data) => Delete?.Invoke(this, data); } \ No newline at end of file diff --git a/Parse/Platform/Queries/ParseQuery.cs b/Parse/Platform/Queries/ParseQuery.cs index 4587c85b..47117557 100644 --- a/Parse/Platform/Queries/ParseQuery.cs +++ b/Parse/Platform/Queries/ParseQuery.cs @@ -911,4 +911,10 @@ public override int GetHashCode() // TODO (richardross): Implement this. return 0; } + + public ParseLiveQuery GetLive() + { + IDictionary paramsDict = BuildParameters(); + return new ParseLiveQuery(Services, ClassName, paramsDict["where"], paramsDict.TryGetValue("keys", out object selectedKey) ? selectedKey as IEnumerable : null); + } } From 8740db29b1f7506006d845632b84efe17ba44014 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Sun, 22 Jun 2025 23:20:05 +0000 Subject: [PATCH 04/24] ParseLiveQueryController initialization --- .../Infrastructure/CustomServiceHub.cs | 4 +- .../Infrastructure/IMutableServiceHub.cs | 2 + .../Infrastructure/IServiceHub.cs | 1 + .../Execution/TextWebSocketClient.cs | 7 +++ .../LateInitializedMutableServiceHub.cs | 12 ++-- Parse/Infrastructure/MutableServiceHub.cs | 7 ++- .../Infrastructure/OrchestrationServiceHub.cs | 2 + Parse/Infrastructure/ServiceHub.cs | 1 - .../LiveQueries/ParseLiveQueryController.cs | 62 ++++++++----------- .../ParseLiveQueryErrorEventArgs.cs | 47 +++++++++++++- .../LiveQueries/ParseLiveQueryEventArgs.cs | 29 ++++++++- Parse/Platform/ParseClient.cs | 2 +- .../Platform/Queries/ParseQueryController.cs | 1 + 13 files changed, 126 insertions(+), 51 deletions(-) diff --git a/Parse/Abstractions/Infrastructure/CustomServiceHub.cs b/Parse/Abstractions/Infrastructure/CustomServiceHub.cs index 59e6e5b2..6c29d7d9 100644 --- a/Parse/Abstractions/Infrastructure/CustomServiceHub.cs +++ b/Parse/Abstractions/Infrastructure/CustomServiceHub.cs @@ -32,6 +32,8 @@ public abstract class CustomServiceHub : ICustomServiceHub public virtual IParseCommandRunner CommandRunner => Services.CommandRunner; + public virtual IWebSocketClient WebSocketClient => Services.WebSocketClient; + public virtual IParseCloudCodeController CloudCodeController => Services.CloudCodeController; public virtual IParseConfigurationController ConfigurationController => Services.ConfigurationController; @@ -62,7 +64,7 @@ public abstract class CustomServiceHub : ICustomServiceHub public virtual IServerConnectionData ServerConnectionData => Services.ServerConnectionData; - public virtual IServerConnectionData LiveQueryServerConnectionData => Services.ServerConnectionData; + public virtual IServerConnectionData LiveQueryServerConnectionData => Services.LiveQueryServerConnectionData; public virtual IParseDataDecoder Decoder => Services.Decoder; diff --git a/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs b/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs index 6d0371a9..7f5d5043 100644 --- a/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs +++ b/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs @@ -19,6 +19,7 @@ namespace Parse.Abstractions.Infrastructure; public interface IMutableServiceHub : IServiceHub { IServerConnectionData ServerConnectionData { set; } + IServerConnectionData LiveQueryServerConnectionData { set; } IMetadataController MetadataController { set; } IServiceHubCloner Cloner { set; } @@ -31,6 +32,7 @@ public interface IMutableServiceHub : IServiceHub IParseInstallationController InstallationController { set; } IParseCommandRunner CommandRunner { set; } + IWebSocketClient WebSocketClient { set; } IParseCloudCodeController CloudCodeController { set; } IParseConfigurationController ConfigurationController { set; } diff --git a/Parse/Abstractions/Infrastructure/IServiceHub.cs b/Parse/Abstractions/Infrastructure/IServiceHub.cs index 00307096..09ac8403 100644 --- a/Parse/Abstractions/Infrastructure/IServiceHub.cs +++ b/Parse/Abstractions/Infrastructure/IServiceHub.cs @@ -40,6 +40,7 @@ public interface IServiceHub IParseInstallationController InstallationController { get; } IParseCommandRunner CommandRunner { get; } + IWebSocketClient WebSocketClient { get; } IParseCloudCodeController CloudCodeController { get; } IParseConfigurationController ConfigurationController { get; } diff --git a/Parse/Infrastructure/Execution/TextWebSocketClient.cs b/Parse/Infrastructure/Execution/TextWebSocketClient.cs index f2688747..ad747d06 100644 --- a/Parse/Infrastructure/Execution/TextWebSocketClient.cs +++ b/Parse/Infrastructure/Execution/TextWebSocketClient.cs @@ -54,9 +54,12 @@ public async Task OpenAsync(string serverUri, CancellationToken cancellationToke { _webSocket ??= new ClientWebSocket(); + Debug.WriteLine($"Status: {_webSocket.State.ToString()}"); if (_webSocket.State != WebSocketState.Open && _webSocket.State != WebSocketState.Connecting) { + Debug.WriteLine($"Connecting to: {serverUri}"); await _webSocket.ConnectAsync(new Uri(serverUri), cancellationToken); + Debug.WriteLine($"Status: {_webSocket.State.ToString()}"); StartListening(cancellationToken); } } @@ -138,7 +141,11 @@ private void StartListening(CancellationToken cancellationToken) /// public async Task SendAsync(string message, CancellationToken cancellationToken = default) { + Debug.WriteLine($"Sending: {message}"); if (_webSocket is not null && _webSocket.State == WebSocketState.Open) + { await _webSocket.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, cancellationToken); + Console.WriteLine("Sent"); + } } } \ No newline at end of file diff --git a/Parse/Infrastructure/LateInitializedMutableServiceHub.cs b/Parse/Infrastructure/LateInitializedMutableServiceHub.cs index 321aafb0..4354e0eb 100644 --- a/Parse/Infrastructure/LateInitializedMutableServiceHub.cs +++ b/Parse/Infrastructure/LateInitializedMutableServiceHub.cs @@ -48,12 +48,6 @@ public IWebClient WebClient set => LateInitializer.SetValue(value); } - public IWebSocketClient WebSocketClient - { - get => LateInitializer.GetValue(() => new TextWebSocketClient { }); - set => LateInitializer.SetValue(value); - } - public ICacheController CacheController { get => LateInitializer.GetValue(() => new CacheController { }); @@ -78,6 +72,12 @@ public IParseCommandRunner CommandRunner set => LateInitializer.SetValue(value); } + public IWebSocketClient WebSocketClient + { + get => LateInitializer.GetValue(() => new TextWebSocketClient { }); + set => LateInitializer.SetValue(value); + } + public IParseCloudCodeController CloudCodeController { get => LateInitializer.GetValue(() => new ParseCloudCodeController(CommandRunner, Decoder)); diff --git a/Parse/Infrastructure/MutableServiceHub.cs b/Parse/Infrastructure/MutableServiceHub.cs index f7af0ce8..06dc222a 100644 --- a/Parse/Infrastructure/MutableServiceHub.cs +++ b/Parse/Infrastructure/MutableServiceHub.cs @@ -20,6 +20,7 @@ using Parse.Platform.Configuration; using Parse.Platform.Files; using Parse.Platform.Installations; +using Parse.Platform.LiveQueries; using Parse.Platform.Objects; using Parse.Platform.Push; using Parse.Platform.Queries; @@ -48,6 +49,7 @@ public class MutableServiceHub : IMutableServiceHub public IParseInstallationController InstallationController { get; set; } public IParseCommandRunner CommandRunner { get; set; } + public IWebSocketClient WebSocketClient { get; set; } public IParseCloudCodeController CloudCodeController { get; set; } public IParseConfigurationController ConfigurationController { get; set; } @@ -68,9 +70,10 @@ public class MutableServiceHub : IMutableServiceHub public IParseCurrentInstallationController CurrentInstallationController { get; set; } public IParseInstallationDataFinalizer InstallationDataFinalizer { get; set; } - public MutableServiceHub SetDefaults(IServerConnectionData connectionData = default) + public MutableServiceHub SetDefaults(IServerConnectionData connectionData = default, IServerConnectionData liveQueryConnectionData = default) { ServerConnectionData ??= connectionData; + LiveQueryServerConnectionData ??= liveQueryConnectionData; MetadataController ??= new MetadataController { EnvironmentData = EnvironmentData.Inferred, @@ -87,12 +90,14 @@ public MutableServiceHub SetDefaults(IServerConnectionData connectionData = defa InstallationController ??= new ParseInstallationController(CacheController); CommandRunner ??= new ParseCommandRunner(WebClient, InstallationController, MetadataController, ServerConnectionData, new Lazy(() => UserController)); + WebSocketClient ??= new TextWebSocketClient { }; CloudCodeController ??= new ParseCloudCodeController(CommandRunner, Decoder); ConfigurationController ??= new ParseConfigurationController(CommandRunner, CacheController, Decoder); FileController ??= new ParseFileController(CommandRunner); ObjectController ??= new ParseObjectController(CommandRunner, Decoder, ServerConnectionData); QueryController ??= new ParseQueryController(CommandRunner, Decoder); + LiveQueryController ??= new ParseLiveQueryController(WebSocketClient); SessionController ??= new ParseSessionController(CommandRunner, Decoder); UserController ??= new ParseUserController(CommandRunner, Decoder); CurrentUserController ??= new ParseCurrentUserController(CacheController, ClassController, Decoder); diff --git a/Parse/Infrastructure/OrchestrationServiceHub.cs b/Parse/Infrastructure/OrchestrationServiceHub.cs index 1db30c59..8e750074 100644 --- a/Parse/Infrastructure/OrchestrationServiceHub.cs +++ b/Parse/Infrastructure/OrchestrationServiceHub.cs @@ -34,6 +34,7 @@ public class OrchestrationServiceHub : IServiceHub public IParseInstallationController InstallationController => Custom.InstallationController ?? Default.InstallationController; public IParseCommandRunner CommandRunner => Custom.CommandRunner ?? Default.CommandRunner; + public IWebSocketClient WebSocketClient => Custom.WebSocketClient ?? Default.WebSocketClient; public IParseCloudCodeController CloudCodeController => Custom.CloudCodeController ?? Default.CloudCodeController; @@ -64,6 +65,7 @@ public class OrchestrationServiceHub : IServiceHub public IParseCurrentInstallationController CurrentInstallationController => Custom.CurrentInstallationController ?? Default.CurrentInstallationController; public IServerConnectionData ServerConnectionData => Custom.ServerConnectionData ?? Default.ServerConnectionData; + public IServerConnectionData LiveQueryServerConnectionData => Custom.LiveQueryServerConnectionData ?? Default.LiveQueryServerConnectionData; public IParseDataDecoder Decoder => Custom.Decoder ?? Default.Decoder; diff --git a/Parse/Infrastructure/ServiceHub.cs b/Parse/Infrastructure/ServiceHub.cs index c76a16ee..b9004a0e 100644 --- a/Parse/Infrastructure/ServiceHub.cs +++ b/Parse/Infrastructure/ServiceHub.cs @@ -52,7 +52,6 @@ public class ServiceHub : IServiceHub public IParseInstallationController InstallationController => LateInitializer.GetValue(() => new ParseInstallationController(CacheController)); public IParseCommandRunner CommandRunner => LateInitializer.GetValue(() => new ParseCommandRunner(WebClient, InstallationController, MetadataController, ServerConnectionData, new Lazy(() => UserController))); - public IWebSocketClient WebSocketClient => LateInitializer.GetValue(() => new TextWebSocketClient { }); public IParseCloudCodeController CloudCodeController => LateInitializer.GetValue(() => new ParseCloudCodeController(CommandRunner, Decoder)); diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs index c1b677b0..62d83f76 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs @@ -88,8 +88,17 @@ public enum ParseLiveQueryState private IDictionary Subscriptions { get; set; } = new Dictionary { }; + /// + /// Initializes a new instance of the class. + /// + /// + /// The implementation to use for the live query connection. + /// + /// + /// This constructor is used to initialize a new instance of the class public ParseLiveQueryController(IWebSocketClient webSocketClient) { + Debug.WriteLine("ParseLiveQueryController initialized."); WebSocketClient = webSocketClient; State = ParseLiveQueryState.Closed; } @@ -99,7 +108,7 @@ private void ProcessMessage(IDictionary message) int requestId; string clientId; ParseLiveQuerySubscription subscription; - switch (message["op"]) + switch (message["op"] as string) { case "connected": State = ParseLiveQueryState.Connected; @@ -134,17 +143,10 @@ private void ProcessMessage(IDictionary message) break; case "error": - if ((bool)message["reconnect"]) - { - ConnectAsync(); - } - - ParseLiveQueryErrorEventArgs errorArgs = new ParseLiveQueryErrorEventArgs - { - Error = message["error"] as string, - Code = Convert.ToInt32(message["code"]), - Reconnected = (bool)message["reconnect"] - }; + ParseLiveQueryErrorEventArgs errorArgs = new ParseLiveQueryErrorEventArgs( + Convert.ToInt32(message["code"]), + message["error"] as string, + (bool) message["reconnect"]); Error?.Invoke(this, errorArgs); break; @@ -155,10 +157,7 @@ private void ProcessMessage(IDictionary message) requestId = Convert.ToInt32(message["requestId"]); if (Subscriptions.TryGetValue(requestId, out subscription)) { - ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs() - { - Object = message["object"] - }; + ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs(message["object"]); subscription.OnCreate(args); } } @@ -171,11 +170,9 @@ private void ProcessMessage(IDictionary message) requestId = Convert.ToInt32(message["requestId"]); if (Subscriptions.TryGetValue(requestId, out subscription)) { - ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs() - { - Object = message["object"], - Original = message["original"] - }; + ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs( + message["object"], + message["original"]); subscription.OnEnter(args); } } @@ -188,11 +185,9 @@ private void ProcessMessage(IDictionary message) requestId = Convert.ToInt32(message["requestId"]); if (Subscriptions.TryGetValue(requestId, out subscription)) { - ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs() - { - Object = message["object"], - Original = message["original"] - }; + ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs( + message["object"], + message["original"]); subscription.OnUpdate(args); } } @@ -205,11 +200,9 @@ private void ProcessMessage(IDictionary message) requestId = Convert.ToInt32(message["requestId"]); if (Subscriptions.TryGetValue(requestId, out subscription)) { - ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs() - { - Object = message["object"], - Original = message["original"] - }; + ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs( + message["object"], + message["original"]); subscription.OnLeave(args); } } @@ -222,10 +215,7 @@ private void ProcessMessage(IDictionary message) requestId = Convert.ToInt32(message["requestId"]); if (Subscriptions.TryGetValue(requestId, out subscription)) { - ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs() - { - Object = message["object"], - }; + ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs(message["object"]); subscription.OnDelete(args); } } @@ -291,7 +281,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) { { "op", "connect" }, { "applicationId", ParseClient.Instance.Services.LiveQueryServerConnectionData.ApplicationID }, - { "clientKey", ParseClient.Instance.Services.LiveQueryServerConnectionData.Key } + { "windowsKey", ParseClient.Instance.Services.LiveQueryServerConnectionData.Key } }; await SendMessage(message, cancellationToken); ConnectionSignal = new CancellationTokenSource(); diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryErrorEventArgs.cs b/Parse/Platform/LiveQueries/ParseLiveQueryErrorEventArgs.cs index b6876179..da511b13 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryErrorEventArgs.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryErrorEventArgs.cs @@ -2,9 +2,50 @@ namespace Parse.Platform.LiveQueries; +/// +/// Represents the arguments for an error event that occurs during a live query in the Parse platform. +/// public class ParseLiveQueryErrorEventArgs : EventArgs { - public string Error { get; set; } - public int Code { get; set; } - public bool Reconnected { get; set; } + /// + /// Represents the arguments for an error event that occurs during a live query in the Parse platform. + /// + internal ParseLiveQueryErrorEventArgs(int code, string error, bool reconnect) + { + Error = error; + Code = code; + Reconnect = reconnect; + } + + /// + /// Gets or sets the error message associated with a live query operation. + /// + /// + /// The property contains a description of the error that occurred during + /// a live query operation. It can provide detailed information about the nature of the issue, + /// which can be helpful for debugging or logging purposes. + /// + public string Error { get; private set; } + + /// + /// Gets or sets the error code associated with a live query operation. + /// + /// + /// The property contains a numerical identifier that represents + /// the type or category of the error that occurred during a live query operation. + /// This is used alongside the error message to provide detailed diagnostics or logging. + /// + public int Code { get; private set; } + + /// + /// Gets or sets a value indicating whether the client should attempt to reconnect + /// after an error occurs during a live query operation. + /// + /// + /// The property specifies whether a reconnection to the + /// live query server is recommended or required following certain error events. + /// This can be used to determine the client's behavior in maintaining a continuous + /// connection with the server. + /// + public bool Reconnect { get; private set; } } \ No newline at end of file diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs b/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs index d55645ab..3d12afae 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs @@ -9,6 +9,31 @@ namespace Parse.Platform.LiveQueries; /// public class ParseLiveQueryEventArgs : EventArgs { - public object Object { get; set; } - public object Original { get; set; } + /// + /// Event arguments for events triggered by Parse's Live Query service. + /// + /// + /// This class handles the encapsulation of event-related details for Live Query operations, + /// such as the current Parse object state and the original state before updates. + /// + internal ParseLiveQueryEventArgs(object current, object original = null) + { + Object = current; + Original = original; + } + + /// + /// Gets the associated object involved in the live query event. + /// This property provides the current state of the Parse object + /// that triggered the event. The object may vary depending on the live query event + /// type, such as create, update, enter, or leave. + /// + public object Object { get; private set; } + + /// + /// Gets the original state of the Parse object before the live query event occurred. + /// This property holds the state of the object as it was prior to the event that + /// triggered the live query event, such as an update or leave operation. + /// + public object Original { get; private set; } } diff --git a/Parse/Platform/ParseClient.cs b/Parse/Platform/ParseClient.cs index 55e9211c..058500f9 100644 --- a/Parse/Platform/ParseClient.cs +++ b/Parse/Platform/ParseClient.cs @@ -150,7 +150,7 @@ public ParseClient(IServerConnectionData configuration, IServerConnectionData li { Services = serviceHub switch { - IMutableServiceHub { } mutableServiceHub => BuildHub((Hub: mutableServiceHub, mutableServiceHub.ServerConnectionData = serviceHub.ServerConnectionData ?? Services.ServerConnectionData).Hub, Services, configurators), + IMutableServiceHub { } mutableServiceHub => BuildHub((Hub: mutableServiceHub, mutableServiceHub.ServerConnectionData = serviceHub.ServerConnectionData ?? Services.ServerConnectionData, mutableServiceHub.LiveQueryServerConnectionData = serviceHub.LiveQueryServerConnectionData ?? Services.LiveQueryServerConnectionData).Hub, Services, configurators), { } => BuildHub(default, Services, configurators) }; } diff --git a/Parse/Platform/Queries/ParseQueryController.cs b/Parse/Platform/Queries/ParseQueryController.cs index 1793fbd0..a928806e 100644 --- a/Parse/Platform/Queries/ParseQueryController.cs +++ b/Parse/Platform/Queries/ParseQueryController.cs @@ -24,6 +24,7 @@ internal class ParseQueryController : IParseQueryController public ParseQueryController(IParseCommandRunner commandRunner, IParseDataDecoder decoder) { + Debug.WriteLine("ParseQueryController initialized"); CommandRunner = commandRunner; Decoder = decoder; } From 542e3cb487a3c531355458d5d4f425a37387ee93 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Mon, 23 Jun 2025 09:20:58 +0000 Subject: [PATCH 05/24] Subscription bug fixes --- .../Execution/TextWebSocketClient.cs | 5 ---- Parse/Platform/LiveQueries/ParseLiveQuery.cs | 20 ++++++------- .../LiveQueries/ParseLiveQueryController.cs | 28 ++++++++++--------- Parse/Platform/Queries/ParseQuery.cs | 2 +- .../Platform/Queries/ParseQueryController.cs | 1 - 5 files changed, 25 insertions(+), 31 deletions(-) diff --git a/Parse/Infrastructure/Execution/TextWebSocketClient.cs b/Parse/Infrastructure/Execution/TextWebSocketClient.cs index ad747d06..1f979c7e 100644 --- a/Parse/Infrastructure/Execution/TextWebSocketClient.cs +++ b/Parse/Infrastructure/Execution/TextWebSocketClient.cs @@ -54,12 +54,9 @@ public async Task OpenAsync(string serverUri, CancellationToken cancellationToke { _webSocket ??= new ClientWebSocket(); - Debug.WriteLine($"Status: {_webSocket.State.ToString()}"); if (_webSocket.State != WebSocketState.Open && _webSocket.State != WebSocketState.Connecting) { - Debug.WriteLine($"Connecting to: {serverUri}"); await _webSocket.ConnectAsync(new Uri(serverUri), cancellationToken); - Debug.WriteLine($"Status: {_webSocket.State.ToString()}"); StartListening(cancellationToken); } } @@ -141,11 +138,9 @@ private void StartListening(CancellationToken cancellationToken) /// public async Task SendAsync(string message, CancellationToken cancellationToken = default) { - Debug.WriteLine($"Sending: {message}"); if (_webSocket is not null && _webSocket.State == WebSocketState.Open) { await _webSocket.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, cancellationToken); - Console.WriteLine("Sent"); } } } \ No newline at end of file diff --git a/Parse/Platform/LiveQueries/ParseLiveQuery.cs b/Parse/Platform/LiveQueries/ParseLiveQuery.cs index 1787ea9b..e357cac3 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQuery.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQuery.cs @@ -21,7 +21,7 @@ public class ParseLiveQuery where T : ParseObject /// /// Serialized clauses. /// - string Filters { get; } + IDictionary Filters { get; } /// /// Serialized key selections. @@ -39,11 +39,11 @@ public class ParseLiveQuery where T : ParseObject private int RequestId = 0; - public ParseLiveQuery(IServiceHub serviceHub, string className, object filters, IEnumerable selectedKeys = null, IEnumerable watchedKeys = null) + public ParseLiveQuery(IServiceHub serviceHub, string className, IDictionary filters, IEnumerable selectedKeys = null, IEnumerable watchedKeys = null) { Services = serviceHub; ClassName = className; - Filters = JsonUtilities.Encode(filters); + Filters = filters; if (selectedKeys is not null) { @@ -91,17 +91,15 @@ internal ParseLiveQuery(ParseLiveQuery source, IEnumerable watchedKey /// A new query with the additional constraint. public ParseLiveQuery Watch(string watch) => new(this, new List { watch }); - internal IDictionary BuildParameters(bool includeClassName = false) + internal IDictionary BuildParameters() { - Dictionary result = new Dictionary(); - if (Filters != null) - result["where"] = Filters; + Dictionary result = new Dictionary(); + result["className"] = ClassName; + result["where"] = Filters; if (KeySelections != null) - result["keys"] = String.Join(",", KeySelections.ToArray()); + result["keys"] = KeySelections.ToArray(); if (KeyWatchers != null) - result["watch"] = String.Join(",", KeyWatchers.ToArray()); - if (includeClassName) - result["className"] = ClassName; + result["watch"] = KeyWatchers.ToArray(); return result; } diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs index 62d83f76..d99ec6b9 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs @@ -98,7 +98,6 @@ public enum ParseLiveQueryState /// This constructor is used to initialize a new instance of the class public ParseLiveQueryController(IWebSocketClient webSocketClient) { - Debug.WriteLine("ParseLiveQueryController initialized."); WebSocketClient = webSocketClient; State = ParseLiveQueryState.Closed; } @@ -227,16 +226,19 @@ private void ProcessMessage(IDictionary message) } } - private IDictionary AppendSessionToken(IDictionary message) + private async Task> AppendSessionToken(IDictionary message) { - return message.Concat(new Dictionary { - { "sessionToken", ParseClient.Instance.Services.GetCurrentSessionToken() } - }).ToDictionary(); + string sessionToken = await ParseClient.Instance.Services.GetCurrentSessionToken(); + return sessionToken is null + ? message + : message.Concat(new Dictionary { + { "sessionToken", await ParseClient.Instance.Services.GetCurrentSessionToken() } + }).ToDictionary(); } private async Task SendMessage(IDictionary message, CancellationToken cancellationToken) { - await WebSocketClient.SendAsync(JsonUtilities.Encode(AppendSessionToken(message)), cancellationToken); + await WebSocketClient.SendAsync(JsonUtilities.Encode(message), cancellationToken); } private async Task OpenAsync(CancellationToken cancellationToken = default) @@ -283,15 +285,15 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) { "applicationId", ParseClient.Instance.Services.LiveQueryServerConnectionData.ApplicationID }, { "windowsKey", ParseClient.Instance.Services.LiveQueryServerConnectionData.Key } }; - await SendMessage(message, cancellationToken); + await SendMessage(await AppendSessionToken(message), cancellationToken); ConnectionSignal = new CancellationTokenSource(); bool signalReceived = ConnectionSignal.Token.WaitHandle.WaitOne(TimeOut); - State = ParseLiveQueryState.Connected; - ConnectionSignal.Dispose(); if (!signalReceived) { throw new TimeoutException(); } + State = ParseLiveQueryState.Connected; + ConnectionSignal.Dispose(); } else if (State == ParseLiveQueryState.Connecting) { @@ -339,9 +341,9 @@ public async Task SubscribeAsync(ParseLiveQuery< { { "op", "subscribe" }, { "requestId", requestId }, - { "query", liveQuery.BuildParameters(true) } + { "query", liveQuery.BuildParameters() } }; - await SendMessage(message, cancellationToken); + await SendMessage(await AppendSessionToken(message), cancellationToken); CancellationTokenSource completionSignal = new CancellationTokenSource(); SubscriptionSignals.Add(requestId, completionSignal); bool signalReceived = completionSignal.Token.WaitHandle.WaitOne(TimeOut); @@ -381,9 +383,9 @@ public async Task UpdateSubscriptionAsync(ParseLiveQuery liveQuery, int re { { "op", "update" }, { "requestId", requestId }, - { "query", liveQuery.BuildParameters(true) } + { "query", liveQuery.BuildParameters() } }; - await SendMessage(message, cancellationToken); + await SendMessage(await AppendSessionToken(message), cancellationToken); } /// diff --git a/Parse/Platform/Queries/ParseQuery.cs b/Parse/Platform/Queries/ParseQuery.cs index 47117557..3c010a2b 100644 --- a/Parse/Platform/Queries/ParseQuery.cs +++ b/Parse/Platform/Queries/ParseQuery.cs @@ -915,6 +915,6 @@ public override int GetHashCode() public ParseLiveQuery GetLive() { IDictionary paramsDict = BuildParameters(); - return new ParseLiveQuery(Services, ClassName, paramsDict["where"], paramsDict.TryGetValue("keys", out object selectedKey) ? selectedKey as IEnumerable : null); + return new ParseLiveQuery(Services, ClassName, paramsDict["where"] as IDictionary, KeySelections); } } diff --git a/Parse/Platform/Queries/ParseQueryController.cs b/Parse/Platform/Queries/ParseQueryController.cs index a928806e..1793fbd0 100644 --- a/Parse/Platform/Queries/ParseQueryController.cs +++ b/Parse/Platform/Queries/ParseQueryController.cs @@ -24,7 +24,6 @@ internal class ParseQueryController : IParseQueryController public ParseQueryController(IParseCommandRunner commandRunner, IParseDataDecoder decoder) { - Debug.WriteLine("ParseQueryController initialized"); CommandRunner = commandRunner; Decoder = decoder; } From 40044a24441842f9f44726f1d05fe595c33fab5a Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Mon, 23 Jun 2025 16:38:46 +0000 Subject: [PATCH 06/24] Updated event argument types --- .../IParseLiveQuerySubscription.cs | 8 +- .../LateInitializedMutableServiceHub.cs | 2 +- Parse/Infrastructure/MutableServiceHub.cs | 2 +- Parse/Infrastructure/ServiceHub.cs | 2 +- .../LiveQueries/ParseLiveQueryController.cs | 69 ++++++++++------ .../LiveQueries/ParseLiveQueryEventArgs.cs | 29 ++++--- .../LiveQueries/ParseLiveQuerySubscription.cs | 82 ++++++++++++------- 7 files changed, 120 insertions(+), 74 deletions(-) diff --git a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs index f57f57c3..ac31e042 100644 --- a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs +++ b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs @@ -1,7 +1,7 @@ using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Parse.Abstractions.Platform.Objects; using Parse.Platform.LiveQueries; namespace Parse.Abstractions.Platform.LiveQueries; @@ -68,4 +68,10 @@ public interface IParseLiveQuerySubscription /// A token to monitor for cancellation requests. If triggered, the cancellation process will halt. /// A task that represents the asynchronous operation of canceling the subscription. Task CancelAsync(CancellationToken cancellationToken = default); + + internal void OnCreate(IObjectState objectState); + internal void OnEnter(IObjectState objectState, IObjectState originalState); + internal void OnUpdate(IObjectState objectState, IObjectState originalState); + internal void OnLeave(IObjectState objectState, IObjectState originalState); + internal void OnDelete(IObjectState objectState); } \ No newline at end of file diff --git a/Parse/Infrastructure/LateInitializedMutableServiceHub.cs b/Parse/Infrastructure/LateInitializedMutableServiceHub.cs index 4354e0eb..1c789112 100644 --- a/Parse/Infrastructure/LateInitializedMutableServiceHub.cs +++ b/Parse/Infrastructure/LateInitializedMutableServiceHub.cs @@ -110,7 +110,7 @@ public IParseQueryController QueryController public IParseLiveQueryController LiveQueryController { - get => LateInitializer.GetValue(() => new ParseLiveQueryController(WebSocketClient)); + get => LateInitializer.GetValue(() => new ParseLiveQueryController(WebSocketClient, Decoder)); set => LateInitializer.SetValue(value); } diff --git a/Parse/Infrastructure/MutableServiceHub.cs b/Parse/Infrastructure/MutableServiceHub.cs index 06dc222a..09c12f1e 100644 --- a/Parse/Infrastructure/MutableServiceHub.cs +++ b/Parse/Infrastructure/MutableServiceHub.cs @@ -97,7 +97,7 @@ public MutableServiceHub SetDefaults(IServerConnectionData connectionData = defa FileController ??= new ParseFileController(CommandRunner); ObjectController ??= new ParseObjectController(CommandRunner, Decoder, ServerConnectionData); QueryController ??= new ParseQueryController(CommandRunner, Decoder); - LiveQueryController ??= new ParseLiveQueryController(WebSocketClient); + LiveQueryController ??= new ParseLiveQueryController(WebSocketClient, Decoder); SessionController ??= new ParseSessionController(CommandRunner, Decoder); UserController ??= new ParseUserController(CommandRunner, Decoder); CurrentUserController ??= new ParseCurrentUserController(CacheController, ClassController, Decoder); diff --git a/Parse/Infrastructure/ServiceHub.cs b/Parse/Infrastructure/ServiceHub.cs index b9004a0e..b97c1bb2 100644 --- a/Parse/Infrastructure/ServiceHub.cs +++ b/Parse/Infrastructure/ServiceHub.cs @@ -59,7 +59,7 @@ public class ServiceHub : IServiceHub public IParseFileController FileController => LateInitializer.GetValue(() => new ParseFileController(CommandRunner)); public IParseObjectController ObjectController => LateInitializer.GetValue(() => new ParseObjectController(CommandRunner, Decoder, ServerConnectionData)); public IParseQueryController QueryController => LateInitializer.GetValue(() => new ParseQueryController(CommandRunner, Decoder)); - public IParseLiveQueryController LiveQueryController => LateInitializer.GetValue(() => new ParseLiveQueryController(WebSocketClient)); + public IParseLiveQueryController LiveQueryController => LateInitializer.GetValue(() => new ParseLiveQueryController(WebSocketClient, Decoder)); public IParseSessionController SessionController => LateInitializer.GetValue(() => new ParseSessionController(CommandRunner, Decoder)); public IParseUserController UserController => LateInitializer.GetValue(() => new ParseUserController(CommandRunner, Decoder)); public IParseCurrentUserController CurrentUserController => LateInitializer.GetValue(() => new ParseCurrentUserController(CacheController, ClassController, Decoder)); diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs index d99ec6b9..9eed0c4a 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs @@ -4,8 +4,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure.Data; using Parse.Abstractions.Infrastructure.Execution; using Parse.Abstractions.Platform.LiveQueries; +using Parse.Infrastructure.Data; using Parse.Infrastructure.Utilities; namespace Parse.Platform.LiveQueries; @@ -16,6 +18,7 @@ namespace Parse.Platform.LiveQueries; /// public class ParseLiveQueryController : IParseLiveQueryController { + private IParseDataDecoder Decoder { get; } private IWebSocketClient WebSocketClient { get; } private int LastRequestId { get; set; } = 0; @@ -62,7 +65,19 @@ public enum ParseLiveQueryState /// has been terminated, and no data updates are being received. /// Closed, + + /// + /// Represents the state where the live query connection is in the process of being established. + /// This indicates that the client is actively attempting to connect to the live query server, + /// but the connection has not yet been fully established. + /// Connecting, + + /// + /// Represents the state where the live query connection has been successfully established. + /// This state indicates that the client is actively connected to the Parse LiveQuery server + /// and is receiving real-time data updates. + /// Connected } @@ -79,14 +94,10 @@ public enum ParseLiveQueryState /// public ParseLiveQueryState State { get; private set; } - HashSet SubscriptionIds { get; } = new HashSet { }; - CancellationTokenSource ConnectionSignal { get; set; } private IDictionary SubscriptionSignals { get; } = new Dictionary { }; private IDictionary UnsubscriptionSignals { get; } = new Dictionary { }; - private IDictionary SubscriptionUpdateSignals { get; } = new Dictionary { }; - - private IDictionary Subscriptions { get; set; } = new Dictionary { }; + private IDictionary Subscriptions { get; set; } = new Dictionary { }; /// /// Initializes a new instance of the class. @@ -94,19 +105,22 @@ public enum ParseLiveQueryState /// /// The implementation to use for the live query connection. /// + /// /// /// This constructor is used to initialize a new instance of the class - public ParseLiveQueryController(IWebSocketClient webSocketClient) + /// + public ParseLiveQueryController(IWebSocketClient webSocketClient, IParseDataDecoder decoder) { WebSocketClient = webSocketClient; State = ParseLiveQueryState.Closed; + Decoder = decoder; } private void ProcessMessage(IDictionary message) { int requestId; string clientId; - ParseLiveQuerySubscription subscription; + IParseLiveQuerySubscription subscription; switch (message["op"] as string) { case "connected": @@ -120,7 +134,6 @@ private void ProcessMessage(IDictionary message) if (clientId == ClientId) { requestId = Convert.ToInt32(message["requestId"]); - SubscriptionIds.Add(requestId); if (SubscriptionSignals.TryGetValue(requestId, out CancellationTokenSource subscriptionSignal)) { subscriptionSignal?.Cancel(); @@ -133,7 +146,6 @@ private void ProcessMessage(IDictionary message) if (clientId == ClientId) { requestId = Convert.ToInt32(message["requestId"]); - SubscriptionIds.Remove(requestId); if (UnsubscriptionSignals.TryGetValue(requestId, out CancellationTokenSource unsubscriptionSignal)) { unsubscriptionSignal?.Cancel(); @@ -156,8 +168,7 @@ private void ProcessMessage(IDictionary message) requestId = Convert.ToInt32(message["requestId"]); if (Subscriptions.TryGetValue(requestId, out subscription)) { - ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs(message["object"]); - subscription.OnCreate(args); + subscription.OnCreate(ParseObjectCoder.Instance.Decode(message["object"] as IDictionary, Decoder, ParseClient.Instance.Services)); } } break; @@ -169,10 +180,9 @@ private void ProcessMessage(IDictionary message) requestId = Convert.ToInt32(message["requestId"]); if (Subscriptions.TryGetValue(requestId, out subscription)) { - ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs( - message["object"], - message["original"]); - subscription.OnEnter(args); + subscription.OnEnter( + ParseObjectCoder.Instance.Decode(message["object"] as IDictionary, Decoder, ParseClient.Instance.Services), + ParseObjectCoder.Instance.Decode(message["original"] as IDictionary, Decoder, ParseClient.Instance.Services)); } } break; @@ -184,10 +194,9 @@ private void ProcessMessage(IDictionary message) requestId = Convert.ToInt32(message["requestId"]); if (Subscriptions.TryGetValue(requestId, out subscription)) { - ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs( - message["object"], - message["original"]); - subscription.OnUpdate(args); + subscription.OnUpdate( + ParseObjectCoder.Instance.Decode(message["object"] as IDictionary, Decoder, ParseClient.Instance.Services), + ParseObjectCoder.Instance.Decode(message["original"] as IDictionary, Decoder, ParseClient.Instance.Services)); } } break; @@ -199,10 +208,9 @@ private void ProcessMessage(IDictionary message) requestId = Convert.ToInt32(message["requestId"]); if (Subscriptions.TryGetValue(requestId, out subscription)) { - ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs( - message["object"], - message["original"]); - subscription.OnLeave(args); + subscription.OnLeave( + ParseObjectCoder.Instance.Decode(message["object"] as IDictionary, Decoder, ParseClient.Instance.Services), + ParseObjectCoder.Instance.Decode(message["original"] as IDictionary, Decoder, ParseClient.Instance.Services)); } } break; @@ -214,8 +222,7 @@ private void ProcessMessage(IDictionary message) requestId = Convert.ToInt32(message["requestId"]); if (Subscriptions.TryGetValue(requestId, out subscription)) { - ParseLiveQueryEventArgs args = new ParseLiveQueryEventArgs(message["object"]); - subscription.OnDelete(args); + subscription.OnDelete(ParseObjectCoder.Instance.Decode(message["object"] as IDictionary, Decoder, ParseClient.Instance.Services)); } } break; @@ -351,7 +358,7 @@ public async Task SubscribeAsync(ParseLiveQuery< completionSignal.Dispose(); if (signalReceived) { - ParseLiveQuerySubscription subscription = new ParseLiveQuerySubscription(liveQuery.Services, requestId); + ParseLiveQuerySubscription subscription = new ParseLiveQuerySubscription(liveQuery.Services, liveQuery.ClassName, requestId); Subscriptions.Add(requestId, subscription); return subscription; } @@ -386,6 +393,15 @@ public async Task UpdateSubscriptionAsync(ParseLiveQuery liveQuery, int re { "query", liveQuery.BuildParameters() } }; await SendMessage(await AppendSessionToken(message), cancellationToken); + CancellationTokenSource completionSignal = new CancellationTokenSource(); + SubscriptionSignals.Add(requestId, completionSignal); + bool signalReceived = completionSignal.Token.WaitHandle.WaitOne(TimeOut); + SubscriptionSignals.Remove(requestId); + completionSignal.Dispose(); + if (!signalReceived) + { + throw new TimeoutException(); + } } /// @@ -437,7 +453,6 @@ public async Task CloseAsync(CancellationToken cancellationToken = default) State = ParseLiveQueryState.Closed; SubscriptionSignals.Clear(); UnsubscriptionSignals.Clear(); - SubscriptionUpdateSignals.Clear(); Subscriptions.Clear(); } } \ No newline at end of file diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs b/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs index 3d12afae..59877ec3 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs @@ -10,30 +10,29 @@ namespace Parse.Platform.LiveQueries; public class ParseLiveQueryEventArgs : EventArgs { /// - /// Event arguments for events triggered by Parse's Live Query service. + /// Represents the event arguments provided to Live Query event handlers in the Parse platform. + /// This class provides information about the current and original state of the Parse object + /// involved in the Live Query operation. /// - /// - /// This class handles the encapsulation of event-related details for Live Query operations, - /// such as the current Parse object state and the original state before updates. - /// - internal ParseLiveQueryEventArgs(object current, object original = null) + internal ParseLiveQueryEventArgs(ParseObject current, ParseObject original = null) { Object = current; Original = original; } /// - /// Gets the associated object involved in the live query event. - /// This property provides the current state of the Parse object - /// that triggered the event. The object may vary depending on the live query event - /// type, such as create, update, enter, or leave. + /// Gets the current state of the Parse object associated with the live query event. + /// This property provides the details of the Parse object as it existed at the time + /// the event was triggered, reflecting any changes made during operations such as + /// an update or creation. /// - public object Object { get; private set; } + public ParseObject Object { get; private set; } /// - /// Gets the original state of the Parse object before the live query event occurred. - /// This property holds the state of the object as it was prior to the event that - /// triggered the live query event, such as an update or leave operation. + /// Gets the state of the Parse object before the live query event was triggered. + /// This property represents the original data of the Parse object prior to any updates, + /// providing a snapshot of its previous state for comparison purposes during events + /// such as updates or deletes. /// - public object Original { get; private set; } + public ParseObject Original { get; private set; } } diff --git a/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs b/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs index 9e797c3b..25a27563 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs @@ -1,9 +1,9 @@ using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Parse.Abstractions.Infrastructure; using Parse.Abstractions.Platform.LiveQueries; +using Parse.Abstractions.Platform.Objects; namespace Parse.Platform.LiveQueries; @@ -11,9 +11,10 @@ namespace Parse.Platform.LiveQueries; /// Represents a subscription to updates for a LiveQuery in a Parse Server. Provides hooks for handling /// various events such as creation, update, deletion, entering, and leaving of objects that match the query. /// -public class ParseLiveQuerySubscription : IParseLiveQuerySubscription +public class ParseLiveQuerySubscription : IParseLiveQuerySubscription where T : ParseObject { - internal IServiceHub Services { get; } + string ClassName { get; } + IServiceHub Services { get; } private int RequestId { get; set; } @@ -54,9 +55,10 @@ public class ParseLiveQuerySubscription : IParseLiveQuerySubscription /// from the Parse Live Query server for a specified query. This class is responsible for handling events /// such as object creation, updates, deletions, and entering or leaving a query's result set. /// - public ParseLiveQuerySubscription(IServiceHub serviceHub, int requestId) + public ParseLiveQuerySubscription(IServiceHub serviceHub, string className, int requestId) { Services = serviceHub; + ClassName = className; RequestId = requestId; } @@ -91,41 +93,65 @@ public async Task CancelAsync(CancellationToken cancellationToken = default) } /// - /// Handles invocation of the Create event for the live query subscription, signaling that a new object - /// has been created and matches the query criteria. This method triggers the associated Create event - /// to notify any subscribed listeners about the creation event. + /// Handles the creation event for an object that matches the subscription's query. + /// Invokes the Create event with the parsed object details contained within the provided object state. /// - /// A dictionary containing the data associated with the created object, typically including - /// information such as object attributes and metadata. - public void OnCreate(ParseLiveQueryEventArgs data) => Create?.Invoke(this, data); + /// + /// The state of the object that triggered the creation event, containing its data and metadata. + /// + public void OnCreate(IObjectState objectState) + { + Create?.Invoke(this, new ParseLiveQueryEventArgs(Services.GenerateObjectFromState(objectState, ClassName))); + } /// - /// Triggers the Enter event, indicating that an object has entered the result set of the live query. - /// This generally occurs when a Parse Object that did not previously match the query conditions now does. + /// Handles the event when an object enters the result set of a live query subscription. This occurs when an + /// object begins to satisfy the query conditions. /// - /// A dictionary containing the details of the object that triggered the event. - public void OnEnter(ParseLiveQueryEventArgs data) => Enter?.Invoke(this, data); + /// The current state of the object that has entered the query result set. + /// The original state of the object before entering the query result set. + public void OnEnter(IObjectState objectState, IObjectState originalState) + { + Enter?.Invoke(this, new ParseLiveQueryEventArgs( + Services.GenerateObjectFromState(objectState, ClassName), + Services.GenerateObjectFromState(originalState, ClassName))); + } /// - /// Handles the event triggered when an object in the subscribed live query is updated. This method - /// invokes the corresponding handler with the provided update data. + /// Handles the update event for objects subscribed to the Live Query. This method triggers the Update + /// event, providing the updated object and its original state. /// - /// A dictionary containing the data associated with the update event. - /// The data typically includes updated fields and their new values. - public void OnUpdate(ParseLiveQueryEventArgs data) => Update?.Invoke(this, data); + /// The new state of the object after the update. + /// The original state of the object before the update. + public void OnUpdate(IObjectState objectState, IObjectState originalState) + { + Update?.Invoke(this, new ParseLiveQueryEventArgs( + Services.GenerateObjectFromState(objectState, ClassName), + Services.GenerateObjectFromState(originalState, ClassName))); + } /// - /// Triggers the Leave event when an object leaves the query's result set. - /// This method notifies all registered event handlers, providing the relevant data associated - /// with the event. + /// Handles the event when an object leaves the result set of the live query subscription. + /// This method triggers the event to notify that an object has + /// transitioned out of the query's result set. /// - /// A dictionary that contains information about the object leaving the query's result set. - public void OnLeave(ParseLiveQueryEventArgs data) => Leave?.Invoke(this, data); + /// The state of the object that left the result set. + /// The original state of the object before it left the result set. + public void OnLeave(IObjectState objectState, IObjectState originalState) + { + Leave?.Invoke(this, new ParseLiveQueryEventArgs( + Services.GenerateObjectFromState(objectState, ClassName), + Services.GenerateObjectFromState(originalState, ClassName))); + } /// - /// Handles the deletion event triggered by the Parse Live Query server. This method is invoked when an object - /// that matches the current query result set is deleted, notifying all subscribers of this event. + /// Handles the "delete" event for a live query subscription, triggered when an object is removed + /// from the query's result set. This method processes the event by invoking the associated + /// delete event handler, if subscribed, with the relevant object data. /// - /// A dictionary containing information about the deleted object and any additional context provided by the server. - public void OnDelete(ParseLiveQueryEventArgs data) => Delete?.Invoke(this, data); + /// The state information of the object that was deleted. + public void OnDelete(IObjectState objectState) + { + Delete?.Invoke(this, new ParseLiveQueryEventArgs(Services.GenerateObjectFromState(objectState, ClassName))); + } } \ No newline at end of file From 0f737a055244580cb10e84e19f3712c5e6d01377 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Mon, 23 Jun 2025 16:50:25 +0000 Subject: [PATCH 07/24] Added DualParseLiveQueryEventArgs --- .../IParseLiveQuerySubscription.cs | 6 ++--- .../DualParseLiveQueryEventArgs.cs | 27 +++++++++++++++++++ .../LiveQueries/ParseLiveQueryEventArgs.cs | 11 +------- .../LiveQueries/ParseLiveQuerySubscription.cs | 12 ++++----- 4 files changed, 37 insertions(+), 19 deletions(-) create mode 100644 Parse/Platform/LiveQueries/DualParseLiveQueryEventArgs.cs diff --git a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs index ac31e042..bfa4cb99 100644 --- a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs +++ b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs @@ -24,20 +24,20 @@ public interface IParseLiveQuerySubscription /// This event is triggered when an object that did not previously match the query (and was thus not part of the subscription) /// starts matching the query, typically due to an update. /// - public event EventHandler Enter; + public event EventHandler Enter; /// /// Represents the Update event for a live query subscription. /// This event is triggered when an existing object matching the subscription's query is updated. /// - public event EventHandler Update; + public event EventHandler Update; /// /// Represents the Leave event for a live query subscription. /// This event is triggered when an object that previously matched the subscription's query /// no longer matches the criteria and is removed. /// - public event EventHandler Leave; + public event EventHandler Leave; /// /// Represents the Delete event for a live query subscription. diff --git a/Parse/Platform/LiveQueries/DualParseLiveQueryEventArgs.cs b/Parse/Platform/LiveQueries/DualParseLiveQueryEventArgs.cs new file mode 100644 index 00000000..13e90a14 --- /dev/null +++ b/Parse/Platform/LiveQueries/DualParseLiveQueryEventArgs.cs @@ -0,0 +1,27 @@ +namespace Parse.Platform.LiveQueries; + +/// +/// Provides event arguments for events triggered by Parse's Live Query service. +/// This class encapsulates details about a particular event, such as the operation type, +/// client ID, request ID, and the associated Parse object data. +/// +public class DualParseLiveQueryEventArgs : ParseLiveQueryEventArgs +{ + /// + /// Represents the event arguments provided to Live Query event handlers in the Parse platform. + /// This class provides information about the current and original state of the Parse object + /// involved in the Live Query operation. + /// + internal DualParseLiveQueryEventArgs(ParseObject current, ParseObject original) : base(current) + { + Original = original; + } + + /// + /// Gets the state of the Parse object before the live query event was triggered. + /// This property represents the original data of the Parse object prior to any updates, + /// providing a snapshot of its previous state for comparison purposes during events + /// such as updates or deletes. + /// + public ParseObject Original { get; private set; } +} diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs b/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs index 59877ec3..0a6313fa 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs @@ -14,10 +14,9 @@ public class ParseLiveQueryEventArgs : EventArgs /// This class provides information about the current and original state of the Parse object /// involved in the Live Query operation. /// - internal ParseLiveQueryEventArgs(ParseObject current, ParseObject original = null) + internal ParseLiveQueryEventArgs(ParseObject current) { Object = current; - Original = original; } /// @@ -27,12 +26,4 @@ internal ParseLiveQueryEventArgs(ParseObject current, ParseObject original = nul /// an update or creation. /// public ParseObject Object { get; private set; } - - /// - /// Gets the state of the Parse object before the live query event was triggered. - /// This property represents the original data of the Parse object prior to any updates, - /// providing a snapshot of its previous state for comparison purposes during events - /// such as updates or deletes. - /// - public ParseObject Original { get; private set; } } diff --git a/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs b/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs index 25a27563..06d5b414 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs @@ -29,20 +29,20 @@ public class ParseLiveQuerySubscription : IParseLiveQuerySubscription where T /// This event is triggered when an object that did not previously match the query (and was thus not part of the subscription) /// starts matching the query, typically due to an update. /// - public event EventHandler Enter; + public event EventHandler Enter; /// /// Represents the Update event for a live query subscription. /// This event is triggered when an existing object matching the subscription's query is updated. /// - public event EventHandler Update; + public event EventHandler Update; /// /// Represents the Leave event for a live query subscription. /// This event is triggered when an object that previously matched the subscription's query /// no longer matches the criteria and is removed. /// - public event EventHandler Leave; + public event EventHandler Leave; /// /// Represents the Delete event for a live query subscription. @@ -112,7 +112,7 @@ public void OnCreate(IObjectState objectState) /// The original state of the object before entering the query result set. public void OnEnter(IObjectState objectState, IObjectState originalState) { - Enter?.Invoke(this, new ParseLiveQueryEventArgs( + Enter?.Invoke(this, new DualParseLiveQueryEventArgs( Services.GenerateObjectFromState(objectState, ClassName), Services.GenerateObjectFromState(originalState, ClassName))); } @@ -125,7 +125,7 @@ public void OnEnter(IObjectState objectState, IObjectState originalState) /// The original state of the object before the update. public void OnUpdate(IObjectState objectState, IObjectState originalState) { - Update?.Invoke(this, new ParseLiveQueryEventArgs( + Update?.Invoke(this, new DualParseLiveQueryEventArgs( Services.GenerateObjectFromState(objectState, ClassName), Services.GenerateObjectFromState(originalState, ClassName))); } @@ -139,7 +139,7 @@ public void OnUpdate(IObjectState objectState, IObjectState originalState) /// The original state of the object before it left the result set. public void OnLeave(IObjectState objectState, IObjectState originalState) { - Leave?.Invoke(this, new ParseLiveQueryEventArgs( + Leave?.Invoke(this, new DualParseLiveQueryEventArgs( Services.GenerateObjectFromState(objectState, ClassName), Services.GenerateObjectFromState(originalState, ClassName))); } From 98e295a6296826fcaa322725958646ddd305f159 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Mon, 23 Jun 2025 17:24:13 +0000 Subject: [PATCH 08/24] Live query server error management --- .../LiveQueries/IParseLiveQueryController.cs | 94 +++++++++++++++++++ .../LiveQueries/ParseLiveQueryController.cs | 13 ++- Parse/Utilities/ObjectServiceExtensions.cs | 7 +- 3 files changed, 106 insertions(+), 8 deletions(-) diff --git a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs index ef09ab44..0fbae050 100644 --- a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs +++ b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs @@ -1,5 +1,7 @@ +using System; using System.Threading; using System.Threading.Tasks; +using Parse.Platform.LiveQueries; namespace Parse.Abstractions.Platform.LiveQueries; @@ -9,13 +11,105 @@ namespace Parse.Abstractions.Platform.LiveQueries; /// public interface IParseLiveQueryController { + /// + /// Event triggered when an error occurs during the operation of the ParseLiveQueryController. + /// + /// + /// This event provides details about a live query operation failure, such as specific error messages, + /// error codes, and whether automatic reconnection is recommended. + /// It is raised in scenarios like: + /// - Receiving an error response from the LiveQuery server. + /// - Issues with subscriptions, unsubscriptions, or query updates. + /// Subscribers to this event can use the provided to + /// understand the error and implement appropriate handling mechanisms. + /// + public event EventHandler Error; + + /// + /// Establishes a connection to the live query server asynchronously. + /// + /// + /// A cancellation token that can be used to cancel the connection process. If the token is triggered, + /// the connection process will be terminated. + /// + /// + /// A task that represents the asynchronous connection operation. + /// + /// + /// Thrown when the connection request times out before receiving confirmation from the server. + /// Task ConnectAsync(CancellationToken cancellationToken = default); + /// + /// Subscribes to a live query, enabling real-time updates for the specified query object. + /// + /// + /// The type of the ParseObject associated with the live query. + /// + /// + /// The live query instance to subscribe to. It contains details about the query and its parameters. + /// + /// + /// A token to monitor for cancellation requests. It allows the operation to be canceled if requested. + /// + /// + /// An object representing the active subscription for the specified query, enabling interaction with the subscribed events and updates. + /// + /// + /// Thrown when attempting to subscribe while the live query connection is in a closed state. + /// + /// + /// Thrown when the subscription request times out before receiving confirmation from the server. + /// Task SubscribeAsync(ParseLiveQuery liveQuery, CancellationToken cancellationToken = default) where T : ParseObject; + /// + /// Updates an active subscription. This method modifies the parameters of an existing subscription for a specific query. + /// + /// + /// The live query object that holds the query parameters to be updated. + /// + /// + /// The unique identifier of the subscription to update. + /// + /// + /// A token to monitor for cancellation requests, allowing the operation to be cancelled before completion. + /// + /// + /// The type of the ParseObject that the query targets. + /// + /// + /// A task that represents the asynchronous operation of updating the subscription. + /// Task UpdateSubscriptionAsync(ParseLiveQuery liveQuery, int requestId, CancellationToken cancellationToken = default) where T : ParseObject; + /// + /// Unsubscribes from a live query subscription associated with the given request identifier. + /// + /// + /// The unique identifier of the subscription to unsubscribe from. + /// + /// + /// A cancellation token that can be used to cancel the unsubscription operation before completion. + /// + /// + /// A task that represents the asynchronous unsubscription operation. + /// + /// + /// Thrown if the unsubscription process does not complete within the specified timeout period. + /// Task UnsubscribeAsync(int requestId, CancellationToken cancellationToken = default); + /// + /// Closes the live query connection asynchronously. + /// + /// + /// A token to monitor for cancellation requests while closing the live query connection. + /// If the operation is canceled, the task will terminate early. + /// + /// + /// A task that represents the asynchronous operation of closing the live query connection. + /// The task completes when the connection is fully closed and resources are cleaned up. + /// Task CloseAsync(CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs index 9eed0c4a..189c8a6e 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs @@ -267,18 +267,17 @@ private void WebSocketClientOnMessageReceived(object sender, string e) => ProcessMessage(JsonUtilities.Parse(e) as IDictionary); /// - /// Establishes a connection to the live query server asynchronously. This method initiates the connection process, - /// manages connection states, and handles any timeout scenarios if the connection cannot be established within the specified duration. + /// Initiates a connection to the live query server asynchronously. /// /// - /// A cancellation token that can be used to cancel the connection process. If the token is triggered, - /// the connection process will be terminated. + /// A cancellation token to monitor for cancellation requests. /// /// - /// A task that represents the asynchronous connection operation. If the connection is successful, - /// the task will complete when the connection is established. In the event of a timeout or error, - /// it will throw the appropriate exception. + /// A task representing the asynchronous operation. /// + /// + /// Thrown if the connection attempt exceeds the specified timeout period. + /// public async Task ConnectAsync(CancellationToken cancellationToken = default) { if (State == ParseLiveQueryState.Closed) diff --git a/Parse/Utilities/ObjectServiceExtensions.cs b/Parse/Utilities/ObjectServiceExtensions.cs index ebe49ba3..e3fe60fb 100644 --- a/Parse/Utilities/ObjectServiceExtensions.cs +++ b/Parse/Utilities/ObjectServiceExtensions.cs @@ -10,6 +10,7 @@ using Parse.Infrastructure.Utilities; using Parse.Infrastructure.Data; using System.Diagnostics; +using Parse.Platform.LiveQueries; namespace Parse; @@ -293,9 +294,13 @@ public static ParseQuery GetQuery(this IServiceHub serviceHub, stri /// /// The service hub instance containing the Live Query controller to manage the connection. /// A task that represents the asynchronous operation of connecting to the Live Query server. - public static async Task ConnectLiveQueryServerAsync(this IServiceHub serviceHub) + public static async Task ConnectLiveQueryServerAsync(this IServiceHub serviceHub, EventHandler onError = null) { await serviceHub.LiveQueryController.ConnectAsync(); + if (onError is not null) + { + serviceHub.LiveQueryController.Error += onError; + } } /// From c0a6becd1d8c115d730d818f04269916f6460020 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Tue, 24 Jun 2025 17:19:10 +0000 Subject: [PATCH 09/24] Renamed DualParseLiveQueryEventArgs to ParseLiveQueryDualEventArgs --- .../LiveQueries/IParseLiveQueryController.cs | 12 +++++++++ .../IParseLiveQuerySubscription.cs | 6 ++--- ...Args.cs => ParseLiveQueryDualEventArgs.cs} | 19 ++++++------- .../ParseLiveQueryErrorEventArgs.cs | 20 +++++++------- .../LiveQueries/ParseLiveQueryEventArgs.cs | 17 +++++------- .../LiveQueries/ParseLiveQuerySubscription.cs | 12 ++++----- Parse/Utilities/ObjectServiceExtensions.cs | 27 ++++++++++--------- 7 files changed, 61 insertions(+), 52 deletions(-) rename Parse/Platform/LiveQueries/{DualParseLiveQueryEventArgs.cs => ParseLiveQueryDualEventArgs.cs} (82%) diff --git a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs index 0fbae050..26fa5225 100644 --- a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs +++ b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs @@ -11,6 +11,18 @@ namespace Parse.Abstractions.Platform.LiveQueries; /// public interface IParseLiveQueryController { + /// + /// Gets or sets the timeout duration, in milliseconds, for operations performed by the LiveQuery controller. + /// + /// + /// This property determines the maximum time the system will wait for an operation to complete + /// before timing out. It is particularly relevant to tasks such as establishing a connection + /// to the LiveQuery server or awaiting responses for subscription or query update requests. + /// Developers can use this property to configure timeouts based on the expected network + /// and performance conditions of the application environment. + /// + public int TimeOut { get; set; } + /// /// Event triggered when an error occurs during the operation of the ParseLiveQueryController. /// diff --git a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs index bfa4cb99..db98ee9d 100644 --- a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs +++ b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs @@ -24,20 +24,20 @@ public interface IParseLiveQuerySubscription /// This event is triggered when an object that did not previously match the query (and was thus not part of the subscription) /// starts matching the query, typically due to an update. /// - public event EventHandler Enter; + public event EventHandler Enter; /// /// Represents the Update event for a live query subscription. /// This event is triggered when an existing object matching the subscription's query is updated. /// - public event EventHandler Update; + public event EventHandler Update; /// /// Represents the Leave event for a live query subscription. /// This event is triggered when an object that previously matched the subscription's query /// no longer matches the criteria and is removed. /// - public event EventHandler Leave; + public event EventHandler Leave; /// /// Represents the Delete event for a live query subscription. diff --git a/Parse/Platform/LiveQueries/DualParseLiveQueryEventArgs.cs b/Parse/Platform/LiveQueries/ParseLiveQueryDualEventArgs.cs similarity index 82% rename from Parse/Platform/LiveQueries/DualParseLiveQueryEventArgs.cs rename to Parse/Platform/LiveQueries/ParseLiveQueryDualEventArgs.cs index 13e90a14..57d80f13 100644 --- a/Parse/Platform/LiveQueries/DualParseLiveQueryEventArgs.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryDualEventArgs.cs @@ -5,18 +5,8 @@ namespace Parse.Platform.LiveQueries; /// This class encapsulates details about a particular event, such as the operation type, /// client ID, request ID, and the associated Parse object data. /// -public class DualParseLiveQueryEventArgs : ParseLiveQueryEventArgs +public class ParseLiveQueryDualEventArgs : ParseLiveQueryEventArgs { - /// - /// Represents the event arguments provided to Live Query event handlers in the Parse platform. - /// This class provides information about the current and original state of the Parse object - /// involved in the Live Query operation. - /// - internal DualParseLiveQueryEventArgs(ParseObject current, ParseObject original) : base(current) - { - Original = original; - } - /// /// Gets the state of the Parse object before the live query event was triggered. /// This property represents the original data of the Parse object prior to any updates, @@ -24,4 +14,11 @@ internal DualParseLiveQueryEventArgs(ParseObject current, ParseObject original) /// such as updates or deletes. /// public ParseObject Original { get; private set; } + + /// + /// Represents the event arguments provided to Live Query event handlers in the Parse platform. + /// This class provides information about the current and original state of the Parse object + /// involved in the Live Query operation. + /// + internal ParseLiveQueryDualEventArgs(ParseObject current, ParseObject original) : base(current) => Original = original; } diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryErrorEventArgs.cs b/Parse/Platform/LiveQueries/ParseLiveQueryErrorEventArgs.cs index da511b13..cf150692 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryErrorEventArgs.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryErrorEventArgs.cs @@ -7,16 +7,6 @@ namespace Parse.Platform.LiveQueries; /// public class ParseLiveQueryErrorEventArgs : EventArgs { - /// - /// Represents the arguments for an error event that occurs during a live query in the Parse platform. - /// - internal ParseLiveQueryErrorEventArgs(int code, string error, bool reconnect) - { - Error = error; - Code = code; - Reconnect = reconnect; - } - /// /// Gets or sets the error message associated with a live query operation. /// @@ -48,4 +38,14 @@ internal ParseLiveQueryErrorEventArgs(int code, string error, bool reconnect) /// connection with the server. /// public bool Reconnect { get; private set; } + + /// + /// Represents the arguments for an error event that occurs during a live query in the Parse platform. + /// + internal ParseLiveQueryErrorEventArgs(int code, string error, bool reconnect) + { + Error = error; + Code = code; + Reconnect = reconnect; + } } \ No newline at end of file diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs b/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs index 0a6313fa..4925986d 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs @@ -9,16 +9,6 @@ namespace Parse.Platform.LiveQueries; /// public class ParseLiveQueryEventArgs : EventArgs { - /// - /// Represents the event arguments provided to Live Query event handlers in the Parse platform. - /// This class provides information about the current and original state of the Parse object - /// involved in the Live Query operation. - /// - internal ParseLiveQueryEventArgs(ParseObject current) - { - Object = current; - } - /// /// Gets the current state of the Parse object associated with the live query event. /// This property provides the details of the Parse object as it existed at the time @@ -26,4 +16,11 @@ internal ParseLiveQueryEventArgs(ParseObject current) /// an update or creation. /// public ParseObject Object { get; private set; } + + /// + /// Represents the event arguments provided to Live Query event handlers in the Parse platform. + /// This class provides information about the current and original state of the Parse object + /// involved in the Live Query operation. + /// + internal ParseLiveQueryEventArgs(ParseObject current) => Object = current; } diff --git a/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs b/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs index 06d5b414..6081e498 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs @@ -29,20 +29,20 @@ public class ParseLiveQuerySubscription : IParseLiveQuerySubscription where T /// This event is triggered when an object that did not previously match the query (and was thus not part of the subscription) /// starts matching the query, typically due to an update. /// - public event EventHandler Enter; + public event EventHandler Enter; /// /// Represents the Update event for a live query subscription. /// This event is triggered when an existing object matching the subscription's query is updated. /// - public event EventHandler Update; + public event EventHandler Update; /// /// Represents the Leave event for a live query subscription. /// This event is triggered when an object that previously matched the subscription's query /// no longer matches the criteria and is removed. /// - public event EventHandler Leave; + public event EventHandler Leave; /// /// Represents the Delete event for a live query subscription. @@ -112,7 +112,7 @@ public void OnCreate(IObjectState objectState) /// The original state of the object before entering the query result set. public void OnEnter(IObjectState objectState, IObjectState originalState) { - Enter?.Invoke(this, new DualParseLiveQueryEventArgs( + Enter?.Invoke(this, new ParseLiveQueryDualEventArgs( Services.GenerateObjectFromState(objectState, ClassName), Services.GenerateObjectFromState(originalState, ClassName))); } @@ -125,7 +125,7 @@ public void OnEnter(IObjectState objectState, IObjectState originalState) /// The original state of the object before the update. public void OnUpdate(IObjectState objectState, IObjectState originalState) { - Update?.Invoke(this, new DualParseLiveQueryEventArgs( + Update?.Invoke(this, new ParseLiveQueryDualEventArgs( Services.GenerateObjectFromState(objectState, ClassName), Services.GenerateObjectFromState(originalState, ClassName))); } @@ -139,7 +139,7 @@ public void OnUpdate(IObjectState objectState, IObjectState originalState) /// The original state of the object before it left the result set. public void OnLeave(IObjectState objectState, IObjectState originalState) { - Leave?.Invoke(this, new DualParseLiveQueryEventArgs( + Leave?.Invoke(this, new ParseLiveQueryDualEventArgs( Services.GenerateObjectFromState(objectState, ClassName), Services.GenerateObjectFromState(originalState, ClassName))); } diff --git a/Parse/Utilities/ObjectServiceExtensions.cs b/Parse/Utilities/ObjectServiceExtensions.cs index e3fe60fb..428ada3e 100644 --- a/Parse/Utilities/ObjectServiceExtensions.cs +++ b/Parse/Utilities/ObjectServiceExtensions.cs @@ -92,7 +92,7 @@ public static T CreateObject(this IServiceHub serviceHub) where T : ParseObje /// A new ParseObject for the given class name. public static T CreateObject(this IParseObjectClassController classController, IServiceHub serviceHub) where T : ParseObject { - + return (T) classController.Instantiate(classController.GetClassName(typeof(T)), serviceHub); } @@ -289,18 +289,21 @@ public static ParseQuery GetQuery(this IServiceHub serviceHub, stri } /// - /// Establishes a connection to the Live Query server using the provided instance. - /// This method ensures that the Live Query controller initiates and maintains a persistent connection. + /// Connects the Live Query server to the provided instance, enabling real-time updates for subscribed queries. + /// Allows error handling during the connection and sets a custom timeout period for the connection. /// - /// The service hub instance containing the Live Query controller to manage the connection. - /// A task that represents the asynchronous operation of connecting to the Live Query server. - public static async Task ConnectLiveQueryServerAsync(this IServiceHub serviceHub, EventHandler onError = null) + /// The instance through which the Live Query server will be connected. + /// An optional event handler to capture Live Query connection errors. + /// An optional timeout value, in milliseconds, for the Live Query connection. Defaults to 5000 milliseconds. + /// A task representing the asynchronous operation of connecting to the Live Query server. + public static async Task ConnectLiveQueryServerAsync(this IServiceHub serviceHub, EventHandler onError = null, int timeOut = 5000) { await serviceHub.LiveQueryController.ConnectAsync(); if (onError is not null) { serviceHub.LiveQueryController.Error += onError; } + serviceHub.LiveQueryController.TimeOut = timeOut; } /// @@ -363,21 +366,21 @@ internal static T GenerateObjectFromState( { throw new ArgumentNullException(nameof(state), "The state cannot be null."); } - + // Ensure the class name is determined or throw an exception string className = state.ClassName ?? defaultClassName; if (string.IsNullOrEmpty(className)) { - + throw new InvalidOperationException("Both state.ClassName and defaultClassName are null or empty. Unable to determine class name."); } - + // Create the object using the class controller T obj = classController.Instantiate(className, serviceHub) as T; - + if (obj == null) { - + throw new InvalidOperationException($"Failed to instantiate object of type {typeof(T).Name} for class {className}."); } @@ -464,7 +467,7 @@ static void CollectDirtyChildren(this IServiceHub serviceHub, object node, IList { CollectDirtyChildren(serviceHub, node, dirtyChildren, new HashSet(new IdentityEqualityComparer()), new HashSet(new IdentityEqualityComparer())); } - + internal static async Task DeepSaveAsync(this IServiceHub serviceHub, object target, string sessionToken, CancellationToken cancellationToken) { // Collect dirty objects From a267f63b920bc21772f5c104727692cf9edd964f Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Wed, 25 Jun 2025 09:42:13 +0000 Subject: [PATCH 10/24] Code quality --- .../IParseLiveQuerySubscription.cs | 10 +++---- .../Execution/TextWebSocketClient.cs | 27 ++++++++--------- Parse/Platform/LiveQueries/ParseLiveQuery.cs | 27 +++++++---------- .../LiveQueries/ParseLiveQueryClient.cs | 18 ----------- .../LiveQueries/ParseLiveQueryController.cs | 24 +++++++-------- .../LiveQueries/ParseLiveQuerySubscription.cs | 30 +++++-------------- 6 files changed, 47 insertions(+), 89 deletions(-) delete mode 100644 Parse/Platform/LiveQueries/ParseLiveQueryClient.cs diff --git a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs index db98ee9d..638c1991 100644 --- a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs +++ b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs @@ -17,33 +17,33 @@ public interface IParseLiveQuerySubscription /// Represents the Create event for a live query subscription. /// This event is triggered when a new object matching the subscription's query is created. /// - public event EventHandler Create; + event EventHandler Create; /// /// Represents the Enter event for a live query subscription. /// This event is triggered when an object that did not previously match the query (and was thus not part of the subscription) /// starts matching the query, typically due to an update. /// - public event EventHandler Enter; + event EventHandler Enter; /// /// Represents the Update event for a live query subscription. /// This event is triggered when an existing object matching the subscription's query is updated. /// - public event EventHandler Update; + event EventHandler Update; /// /// Represents the Leave event for a live query subscription. /// This event is triggered when an object that previously matched the subscription's query /// no longer matches the criteria and is removed. /// - public event EventHandler Leave; + event EventHandler Leave; /// /// Represents the Delete event for a live query subscription. /// This event is triggered when an object matching the subscription's query is deleted. /// - public event EventHandler Delete; + event EventHandler Delete; /// /// Updates the current live query subscription with new query parameters, diff --git a/Parse/Infrastructure/Execution/TextWebSocketClient.cs b/Parse/Infrastructure/Execution/TextWebSocketClient.cs index 1f979c7e..594d6de0 100644 --- a/Parse/Infrastructure/Execution/TextWebSocketClient.cs +++ b/Parse/Infrastructure/Execution/TextWebSocketClient.cs @@ -21,7 +21,7 @@ class TextWebSocketClient : IWebSocketClient /// when establishing a connection and is used internally for operations such as sending messages /// and listening for incoming data. /// - private ClientWebSocket _webSocket; + private ClientWebSocket webSocket; /// /// A private instance of the Task class representing the background operation @@ -31,7 +31,7 @@ class TextWebSocketClient : IWebSocketClient /// It is initialized when the listening process starts and monitored to prevent /// multiple concurrent listeners from being created. /// - private Task _listeningTask; + private Task listeningTask; /// /// An event triggered whenever a message is received from the WebSocket server. @@ -52,11 +52,11 @@ class TextWebSocketClient : IWebSocketClient /// public async Task OpenAsync(string serverUri, CancellationToken cancellationToken = default) { - _webSocket ??= new ClientWebSocket(); + webSocket ??= new ClientWebSocket(); - if (_webSocket.State != WebSocketState.Open && _webSocket.State != WebSocketState.Connecting) + if (webSocket.State != WebSocketState.Open && webSocket.State != WebSocketState.Connecting) { - await _webSocket.ConnectAsync(new Uri(serverUri), cancellationToken); + await webSocket.ConnectAsync(new Uri(serverUri), cancellationToken); StartListening(cancellationToken); } } @@ -69,8 +69,8 @@ public async Task OpenAsync(string serverUri, CancellationToken cancellationToke /// /// A task representing the asynchronous operation of closing the WebSocket connection. /// - public async Task CloseAsync(CancellationToken cancellationToken = default) - => await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, String.Empty, cancellationToken); + public async Task CloseAsync(CancellationToken cancellationToken = default) => + await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, String.Empty, cancellationToken); private async Task ListenForMessages(CancellationToken cancellationToken) { @@ -79,9 +79,9 @@ private async Task ListenForMessages(CancellationToken cancellationToken) try { while (!cancellationToken.IsCancellationRequested && - _webSocket.State == WebSocketState.Open) + webSocket.State == WebSocketState.Open) { - WebSocketReceiveResult result = await _webSocket.ReceiveAsync( + WebSocketReceiveResult result = await webSocket.ReceiveAsync( new ArraySegment(buffer), cancellationToken); @@ -102,7 +102,6 @@ private async Task ListenForMessages(CancellationToken cancellationToken) } } - /// /// Starts listening for incoming messages from the WebSocket connection. This method ensures that only one listener task is running at a time. /// @@ -110,13 +109,13 @@ private async Task ListenForMessages(CancellationToken cancellationToken) private void StartListening(CancellationToken cancellationToken) { // Make sure we don't start multiple listeners - if (_listeningTask is { IsCompleted: false }) + if (listeningTask is { IsCompleted: false }) { return; } // Start the listener task - _listeningTask = Task.Run(async () => + listeningTask = Task.Run(async () => { if (cancellationToken.IsCancellationRequested) { @@ -138,9 +137,9 @@ private void StartListening(CancellationToken cancellationToken) /// public async Task SendAsync(string message, CancellationToken cancellationToken = default) { - if (_webSocket is not null && _webSocket.State == WebSocketState.Open) + if (webSocket is not null && webSocket.State == WebSocketState.Open) { - await _webSocket.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, cancellationToken); + await webSocket.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, cancellationToken); } } } \ No newline at end of file diff --git a/Parse/Platform/LiveQueries/ParseLiveQuery.cs b/Parse/Platform/LiveQueries/ParseLiveQuery.cs index e357cac3..5a118b8e 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQuery.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQuery.cs @@ -6,18 +6,17 @@ using System.Threading.Tasks; using Parse.Abstractions.Infrastructure; using Parse.Abstractions.Platform.LiveQueries; -using Parse.Infrastructure.Data; -using Parse.Infrastructure.Utilities; namespace Parse; /// -/// The ParseLiveQuery class allows subscribing to a Query. +/// The ParseLiveQuery class provides functionality to create and manage real-time queries on the Parse Server. +/// It allows tracking changes on objects of a specified class that match query constraints, such as filters +/// and watched fields, delivering updates in real-time as changes occur. /// -/// +/// Represents the type of ParseObject that this query operates on. T must inherit from ParseObject. public class ParseLiveQuery where T : ParseObject { - /// /// Serialized clauses. /// @@ -37,8 +36,6 @@ public class ParseLiveQuery where T : ParseObject internal IServiceHub Services { get; } - private int RequestId = 0; - public ParseLiveQuery(IServiceHub serviceHub, string className, IDictionary filters, IEnumerable selectedKeys = null, IEnumerable watchedKeys = null) { Services = serviceHub; @@ -61,7 +58,7 @@ public ParseLiveQuery(IServiceHub serviceHub, string className, IDictionary - internal ParseLiveQuery(ParseLiveQuery source, IEnumerable watchedKeys = null, Func> onCreate = null) + private ParseLiveQuery(ParseLiveQuery source, IEnumerable watchedKeys = null) { if (source == null) { @@ -74,13 +71,13 @@ internal ParseLiveQuery(ParseLiveQuery source, IEnumerable watchedKey KeySelections = source.KeySelections; KeyWatchers = source.KeyWatchers; - if (watchedKeys is { }) + if (watchedKeys is not null) { KeyWatchers = new ReadOnlyCollection(MergeWatchers(watchedKeys).ToList()); } } - HashSet MergeWatchers(IEnumerable keys) => new((KeyWatchers ?? Enumerable.Empty()).Concat(keys)); + private HashSet MergeWatchers(IEnumerable keys) => [..(KeyWatchers ?? Enumerable.Empty()).Concat(keys)]; /// /// Add the provided key to the watched fields of returned ParseObjects. @@ -93,9 +90,7 @@ internal ParseLiveQuery(ParseLiveQuery source, IEnumerable watchedKey internal IDictionary BuildParameters() { - Dictionary result = new Dictionary(); - result["className"] = ClassName; - result["where"] = Filters; + Dictionary result = new Dictionary { ["className"] = ClassName, ["where"] = Filters }; if (KeySelections != null) result["keys"] = KeySelections.ToArray(); if (KeyWatchers != null) @@ -111,8 +106,6 @@ internal IDictionary BuildParameters() /// A task representing the asynchronous subscription operation. Upon completion /// of the task, the subscription is successfully registered. /// - public async Task SubscribeAsync() - { - return await Services.LiveQueryController.SubscribeAsync(this, CancellationToken.None); - } + public async Task SubscribeAsync() => + await Services.LiveQueryController.SubscribeAsync(this, CancellationToken.None); } \ No newline at end of file diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryClient.cs b/Parse/Platform/LiveQueries/ParseLiveQueryClient.cs deleted file mode 100644 index a34ca072..00000000 --- a/Parse/Platform/LiveQueries/ParseLiveQueryClient.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Net.WebSockets; -using System.Threading; - -namespace Parse; - -public class ParseLiveQueryClient -{ - private ClientWebSocket clientWebSocket; - - async void connect() - { - if (clientWebSocket is not null) - { - await clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); - } - clientWebSocket = new ClientWebSocket(); - } -} \ No newline at end of file diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs index 189c8a6e..07cb7518 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs @@ -95,9 +95,9 @@ public enum ParseLiveQueryState public ParseLiveQueryState State { get; private set; } CancellationTokenSource ConnectionSignal { get; set; } - private IDictionary SubscriptionSignals { get; } = new Dictionary { }; - private IDictionary UnsubscriptionSignals { get; } = new Dictionary { }; - private IDictionary Subscriptions { get; set; } = new Dictionary { }; + private Dictionary SubscriptionSignals { get; } = new Dictionary { }; + private Dictionary UnsubscriptionSignals { get; } = new Dictionary { }; + private Dictionary Subscriptions { get; set; } = new Dictionary { }; /// /// Initializes a new instance of the class. @@ -243,10 +243,8 @@ private async Task> AppendSessionToken(IDictionary message, CancellationToken cancellationToken) - { + private async Task SendMessage(IDictionary message, CancellationToken cancellationToken) => await WebSocketClient.SendAsync(JsonUtilities.Encode(message), cancellationToken); - } private async Task OpenAsync(CancellationToken cancellationToken = default) { @@ -263,8 +261,8 @@ private async Task OpenAsync(CancellationToken cancellationToken = default) await WebSocketClient.OpenAsync(ParseClient.Instance.Services.LiveQueryServerConnectionData.ServerURI, cancellationToken); } - private void WebSocketClientOnMessageReceived(object sender, string e) - => ProcessMessage(JsonUtilities.Parse(e) as IDictionary); + private void WebSocketClientOnMessageReceived(object sender, string e) => + ProcessMessage(JsonUtilities.Parse(e) as IDictionary); /// /// Initiates a connection to the live query server asynchronously. @@ -355,13 +353,13 @@ public async Task SubscribeAsync(ParseLiveQuery< bool signalReceived = completionSignal.Token.WaitHandle.WaitOne(TimeOut); SubscriptionSignals.Remove(requestId); completionSignal.Dispose(); - if (signalReceived) + if (!signalReceived) { - ParseLiveQuerySubscription subscription = new ParseLiveQuerySubscription(liveQuery.Services, liveQuery.ClassName, requestId); - Subscriptions.Add(requestId, subscription); - return subscription; + throw new TimeoutException(); } - throw new TimeoutException(); + ParseLiveQuerySubscription subscription = new ParseLiveQuerySubscription(liveQuery.Services, liveQuery.ClassName, requestId); + Subscriptions.Add(requestId, subscription); + return subscription; } /// diff --git a/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs b/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs index 6081e498..16112f57 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs @@ -68,17 +68,15 @@ public ParseLiveQuerySubscription(IServiceHub serviceHub, string className, int /// This allows adjustments to the filter or watched keys without unsubscribing /// and re-subscribing. /// - /// The type of the ParseObject associated with the subscription. + /// The type of the ParseObject associated with the subscription. /// The updated live query containing new parameters that /// will replace the existing ones for this subscription. /// A token to monitor for cancellation requests. If triggered, /// the update process will be halted. /// A task that represents the asynchronous operation of updating /// the subscription with the new query parameters. - public async Task UpdateAsync(ParseLiveQuery liveQuery, CancellationToken cancellationToken = default) where T : ParseObject - { + public async Task UpdateAsync(ParseLiveQuery liveQuery, CancellationToken cancellationToken = default) where T1 : ParseObject => await Services.LiveQueryController.UpdateSubscriptionAsync(liveQuery, RequestId, CancellationToken.None); - } /// /// Cancels the current live query subscription by unsubscribing from the Parse Live Query server. @@ -87,10 +85,8 @@ public async Task UpdateAsync(ParseLiveQuery liveQuery, CancellationToken /// /// A token to monitor for cancellation requests. If triggered, the cancellation process will halt. /// A task that represents the asynchronous operation of canceling the subscription. - public async Task CancelAsync(CancellationToken cancellationToken = default) - { + public async Task CancelAsync(CancellationToken cancellationToken = default) => await Services.LiveQueryController.UnsubscribeAsync(RequestId, CancellationToken.None); - } /// /// Handles the creation event for an object that matches the subscription's query. @@ -99,10 +95,8 @@ public async Task CancelAsync(CancellationToken cancellationToken = default) /// /// The state of the object that triggered the creation event, containing its data and metadata. /// - public void OnCreate(IObjectState objectState) - { + public void OnCreate(IObjectState objectState) => Create?.Invoke(this, new ParseLiveQueryEventArgs(Services.GenerateObjectFromState(objectState, ClassName))); - } /// /// Handles the event when an object enters the result set of a live query subscription. This occurs when an @@ -110,12 +104,10 @@ public void OnCreate(IObjectState objectState) /// /// The current state of the object that has entered the query result set. /// The original state of the object before entering the query result set. - public void OnEnter(IObjectState objectState, IObjectState originalState) - { + public void OnEnter(IObjectState objectState, IObjectState originalState) => Enter?.Invoke(this, new ParseLiveQueryDualEventArgs( Services.GenerateObjectFromState(objectState, ClassName), Services.GenerateObjectFromState(originalState, ClassName))); - } /// /// Handles the update event for objects subscribed to the Live Query. This method triggers the Update @@ -123,12 +115,10 @@ public void OnEnter(IObjectState objectState, IObjectState originalState) /// /// The new state of the object after the update. /// The original state of the object before the update. - public void OnUpdate(IObjectState objectState, IObjectState originalState) - { + public void OnUpdate(IObjectState objectState, IObjectState originalState) => Update?.Invoke(this, new ParseLiveQueryDualEventArgs( Services.GenerateObjectFromState(objectState, ClassName), Services.GenerateObjectFromState(originalState, ClassName))); - } /// /// Handles the event when an object leaves the result set of the live query subscription. @@ -137,12 +127,10 @@ public void OnUpdate(IObjectState objectState, IObjectState originalState) /// /// The state of the object that left the result set. /// The original state of the object before it left the result set. - public void OnLeave(IObjectState objectState, IObjectState originalState) - { + public void OnLeave(IObjectState objectState, IObjectState originalState) => Leave?.Invoke(this, new ParseLiveQueryDualEventArgs( Services.GenerateObjectFromState(objectState, ClassName), Services.GenerateObjectFromState(originalState, ClassName))); - } /// /// Handles the "delete" event for a live query subscription, triggered when an object is removed @@ -150,8 +138,6 @@ public void OnLeave(IObjectState objectState, IObjectState originalState) /// delete event handler, if subscribed, with the relevant object data. /// /// The state information of the object that was deleted. - public void OnDelete(IObjectState objectState) - { + public void OnDelete(IObjectState objectState) => Delete?.Invoke(this, new ParseLiveQueryEventArgs(Services.GenerateObjectFromState(objectState, ClassName))); - } } \ No newline at end of file From 4b83d233cd609974ddc0a221dfca14f0fbf8c4b9 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Wed, 25 Jun 2025 11:53:29 +0000 Subject: [PATCH 11/24] Improve code quality --- Parse/Platform/LiveQueries/ParseLiveQuery.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Parse/Platform/LiveQueries/ParseLiveQuery.cs b/Parse/Platform/LiveQueries/ParseLiveQuery.cs index 5a118b8e..f71093c2 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQuery.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQuery.cs @@ -60,10 +60,7 @@ public ParseLiveQuery(IServiceHub serviceHub, string className, IDictionary private ParseLiveQuery(ParseLiveQuery source, IEnumerable watchedKeys = null) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } + ArgumentNullException.ThrowIfNull(source); Services = source.Services; ClassName = source.ClassName; From 65c73e4c3c39cdf811a4fc564ff1b3261e4977c7 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Wed, 25 Jun 2025 12:22:39 +0000 Subject: [PATCH 12/24] Add null safety for the "where" clause extraction --- Parse/Platform/LiveQueries/ParseLiveQuery.cs | 2 ++ Parse/Platform/Queries/ParseQuery.cs | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Parse/Platform/LiveQueries/ParseLiveQuery.cs b/Parse/Platform/LiveQueries/ParseLiveQuery.cs index f71093c2..00a928a1 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQuery.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQuery.cs @@ -38,6 +38,8 @@ public class ParseLiveQuery where T : ParseObject public ParseLiveQuery(IServiceHub serviceHub, string className, IDictionary filters, IEnumerable selectedKeys = null, IEnumerable watchedKeys = null) { + ArgumentNullException.ThrowIfNull(filters); + Services = serviceHub; ClassName = className; Filters = filters; diff --git a/Parse/Platform/Queries/ParseQuery.cs b/Parse/Platform/Queries/ParseQuery.cs index 3c010a2b..0fa64589 100644 --- a/Parse/Platform/Queries/ParseQuery.cs +++ b/Parse/Platform/Queries/ParseQuery.cs @@ -912,9 +912,15 @@ public override int GetHashCode() return 0; } + /// + /// Converts the current query into a live query that allows real-time updates to be monitored + /// for changes that match the specified query criteria. + /// + /// A ParseLiveQuery object configured to monitor changes for the query. public ParseLiveQuery GetLive() { - IDictionary paramsDict = BuildParameters(); - return new ParseLiveQuery(Services, ClassName, paramsDict["where"] as IDictionary, KeySelections); + ArgumentNullException.ThrowIfNull(Filters); + IDictionary filters = BuildParameters().TryGetValue("where", out object where) ? where as IDictionary : null; + return new ParseLiveQuery(Services, ClassName, filters, KeySelections); } } From 340f6fb9a81f502c7992804aac68c9a4dfd751c8 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Wed, 25 Jun 2025 12:45:42 +0000 Subject: [PATCH 13/24] Improve code quality --- .../Infrastructure/Execution/IWebSocketClient.cs | 3 ++- Parse/Infrastructure/Execution/TextWebSocketClient.cs | 9 +++++---- .../Platform/LiveQueries/ParseLiveQueryDualEventArgs.cs | 5 ----- Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs | 5 ----- Parse/Platform/Queries/ParseQuery.cs | 6 +++--- 5 files changed, 10 insertions(+), 18 deletions(-) diff --git a/Parse/Abstractions/Infrastructure/Execution/IWebSocketClient.cs b/Parse/Abstractions/Infrastructure/Execution/IWebSocketClient.cs index 933cb56c..0ecb9a23 100644 --- a/Parse/Abstractions/Infrastructure/Execution/IWebSocketClient.cs +++ b/Parse/Abstractions/Infrastructure/Execution/IWebSocketClient.cs @@ -45,5 +45,6 @@ public interface IWebSocketClient /// A token to observe cancellation requests. The operation will stop if the token is canceled. /// /// A task that represents the asynchronous operation of sending the message. - public Task SendAsync(string message, CancellationToken cancellationToken); + /// Thrown when trying to send a message on a WebSocket connection that is not in the Open state. + public Task SendAsync(string message, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/Parse/Infrastructure/Execution/TextWebSocketClient.cs b/Parse/Infrastructure/Execution/TextWebSocketClient.cs index 594d6de0..0db5aa5c 100644 --- a/Parse/Infrastructure/Execution/TextWebSocketClient.cs +++ b/Parse/Infrastructure/Execution/TextWebSocketClient.cs @@ -135,11 +135,12 @@ private void StartListening(CancellationToken cancellationToken) /// /// A task representing the asynchronous operation of sending the message to the WebSocket server. /// + /// Thrown when the WebSocket instance is null. + /// Thrown when there is an error during the WebSocket communication. + /// Thrown when trying to send a message on a WebSocket connection that is not in the Open state. public async Task SendAsync(string message, CancellationToken cancellationToken = default) { - if (webSocket is not null && webSocket.State == WebSocketState.Open) - { - await webSocket.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, cancellationToken); - } + ArgumentNullException.ThrowIfNull(webSocket); + await webSocket.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, cancellationToken); } } \ No newline at end of file diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryDualEventArgs.cs b/Parse/Platform/LiveQueries/ParseLiveQueryDualEventArgs.cs index 57d80f13..a3ec2596 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryDualEventArgs.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryDualEventArgs.cs @@ -15,10 +15,5 @@ public class ParseLiveQueryDualEventArgs : ParseLiveQueryEventArgs /// public ParseObject Original { get; private set; } - /// - /// Represents the event arguments provided to Live Query event handlers in the Parse platform. - /// This class provides information about the current and original state of the Parse object - /// involved in the Live Query operation. - /// internal ParseLiveQueryDualEventArgs(ParseObject current, ParseObject original) : base(current) => Original = original; } diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs b/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs index 4925986d..aceb15f1 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs @@ -17,10 +17,5 @@ public class ParseLiveQueryEventArgs : EventArgs /// public ParseObject Object { get; private set; } - /// - /// Represents the event arguments provided to Live Query event handlers in the Parse platform. - /// This class provides information about the current and original state of the Parse object - /// involved in the Live Query operation. - /// internal ParseLiveQueryEventArgs(ParseObject current) => Object = current; } diff --git a/Parse/Platform/Queries/ParseQuery.cs b/Parse/Platform/Queries/ParseQuery.cs index 0fa64589..69fcaf87 100644 --- a/Parse/Platform/Queries/ParseQuery.cs +++ b/Parse/Platform/Queries/ParseQuery.cs @@ -913,10 +913,10 @@ public override int GetHashCode() } /// - /// Converts the current query into a live query that allows real-time updates to be monitored - /// for changes that match the specified query criteria. + /// Creates a live query from this query that can be used to receive real-time updates + /// when objects matching the query are created, updated, or deleted. /// - /// A ParseLiveQuery object configured to monitor changes for the query. + /// A new ParseLiveQuery instace configured with this query's parameters. public ParseLiveQuery GetLive() { ArgumentNullException.ThrowIfNull(Filters); From 7e66bb653a4c48c6bddacdcc455fd29242f000d7 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Wed, 25 Jun 2025 19:18:14 +0000 Subject: [PATCH 14/24] Improvements --- .../Execution/TextWebSocketClient.cs | 30 +- Parse/Platform/LiveQueries/ParseLiveQuery.cs | 6 +- .../LiveQueries/ParseLiveQueryController.cs | 415 +++++++++++------- .../ParseLiveQueryDualEventArgs.cs | 5 +- .../LiveQueries/ParseLiveQueryEventArgs.cs | 2 +- .../LiveQueries/ParseLiveQuerySubscription.cs | 4 +- Parse/Platform/ParseClient.cs | 2 +- 7 files changed, 302 insertions(+), 162 deletions(-) diff --git a/Parse/Infrastructure/Execution/TextWebSocketClient.cs b/Parse/Infrastructure/Execution/TextWebSocketClient.cs index 0db5aa5c..e252919f 100644 --- a/Parse/Infrastructure/Execution/TextWebSocketClient.cs +++ b/Parse/Infrastructure/Execution/TextWebSocketClient.cs @@ -41,6 +41,8 @@ class TextWebSocketClient : IWebSocketClient /// public event EventHandler MessageReceived; + private readonly object connectionLock = new object(); + /// /// Opens a WebSocket connection to the specified server URI and starts listening for messages. /// If the connection is already open or in a connecting state, this method does nothing. @@ -52,7 +54,10 @@ class TextWebSocketClient : IWebSocketClient /// public async Task OpenAsync(string serverUri, CancellationToken cancellationToken = default) { - webSocket ??= new ClientWebSocket(); + lock (connectionLock) + { + webSocket ??= new ClientWebSocket(); + } if (webSocket.State != WebSocketState.Open && webSocket.State != WebSocketState.Connecting) { @@ -92,14 +97,26 @@ private async Task ListenForMessages(CancellationToken cancellationToken) } string message = Encoding.UTF8.GetString(buffer, 0, result.Count); + Debug.WriteLine($"Received message: {message}"); MessageReceived?.Invoke(this, message); } } catch (OperationCanceledException ex) { // Normal cancellation, no need to handle - Debug.WriteLine($"ClientWebsocket connection was closed: {ex.Message}"); + Debug.WriteLine($"Websocket connection was closed: {ex.Message}"); + } + catch (WebSocketException e) + { + // WebSocket error, notify the user + Debug.WriteLine($"Websocket error: {e.Message}"); } + catch (Exception e) + { + // Unexpected error, notify the user + Debug.WriteLine($"Unexpected error in Websocket listener: {e.Message}"); + } + Debug.WriteLine("Websocket ListenForMessage stopped"); } /// @@ -123,7 +140,16 @@ private void StartListening(CancellationToken cancellationToken) } await ListenForMessages(cancellationToken); + Debug.WriteLine("Websocket listeningTask stopped"); }, cancellationToken); + + _ = listeningTask.ContinueWith(task => + { + if (!task.IsFaulted) + return; + Debug.WriteLine($"Websocket listener task faulted: {task.Exception}"); + throw task.Exception; + }, TaskContinuationOptions.OnlyOnFaulted); } /// diff --git a/Parse/Platform/LiveQueries/ParseLiveQuery.cs b/Parse/Platform/LiveQueries/ParseLiveQuery.cs index 00a928a1..14d608f7 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQuery.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQuery.cs @@ -36,7 +36,7 @@ public class ParseLiveQuery where T : ParseObject internal IServiceHub Services { get; } - public ParseLiveQuery(IServiceHub serviceHub, string className, IDictionary filters, IEnumerable selectedKeys = null, IEnumerable watchedKeys = null) + internal ParseLiveQuery(IServiceHub serviceHub, string className, IDictionary filters, IEnumerable selectedKeys = null, IEnumerable watchedKeys = null) { ArgumentNullException.ThrowIfNull(filters); @@ -105,6 +105,6 @@ internal IDictionary BuildParameters() /// A task representing the asynchronous subscription operation. Upon completion /// of the task, the subscription is successfully registered. /// - public async Task SubscribeAsync() => - await Services.LiveQueryController.SubscribeAsync(this, CancellationToken.None); + public async Task SubscribeAsync(CancellationToken cancellationToken = default) => + await Services.LiveQueryController.SubscribeAsync(this, cancellationToken); } \ No newline at end of file diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs index 07cb7518..43cc430f 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -16,15 +17,17 @@ namespace Parse.Platform.LiveQueries; /// The ParseLiveQueryController is responsible for managing live query subscriptions, maintaining a connection /// to the Parse LiveQuery server, and handling real-time updates from the server. /// -public class ParseLiveQueryController : IParseLiveQueryController +public class ParseLiveQueryController : IParseLiveQueryController, IDisposable { private IParseDataDecoder Decoder { get; } private IWebSocketClient WebSocketClient { get; } - private int LastRequestId { get; set; } = 0; + private int LastRequestId; private string ClientId { get; set; } + private bool disposed; + /// /// Gets or sets the timeout duration, in milliseconds, used by the ParseLiveQueryController /// for various operations, such as establishing a connection or completing a subscription. @@ -92,12 +95,14 @@ public enum ParseLiveQueryState /// This property is updated based on the controller's connection lifecycle events, /// such as when a connection is established or closed, or when an error occurs. /// - public ParseLiveQueryState State { get; private set; } + public ParseLiveQueryState State => _state; + private volatile ParseLiveQueryState _state; + + TaskCompletionSource ConnectionSignal { get; set; } + private ConcurrentDictionary SubscriptionSignals { get; } = new ConcurrentDictionary(); + private ConcurrentDictionary UnsubscriptionSignals { get; } = new ConcurrentDictionary(); + private ConcurrentDictionary Subscriptions { get; set; } = new ConcurrentDictionary(); - CancellationTokenSource ConnectionSignal { get; set; } - private Dictionary SubscriptionSignals { get; } = new Dictionary { }; - private Dictionary UnsubscriptionSignals { get; } = new Dictionary { }; - private Dictionary Subscriptions { get; set; } = new Dictionary { }; /// /// Initializes a new instance of the class. @@ -112,134 +117,190 @@ public enum ParseLiveQueryState public ParseLiveQueryController(IWebSocketClient webSocketClient, IParseDataDecoder decoder) { WebSocketClient = webSocketClient; - State = ParseLiveQueryState.Closed; + _state = ParseLiveQueryState.Closed; Decoder = decoder; } private void ProcessMessage(IDictionary message) { - int requestId; - string clientId; - IParseLiveQuerySubscription subscription; - switch (message["op"] as string) + if (!message.TryGetValue("op", out object opValue) || opValue is not string op) + { + Debug.WriteLine("Missing or invalid operation in message"); + return; + } + + switch (op) { case "connected": - State = ParseLiveQueryState.Connected; - ClientId = message["clientId"] as string; - ConnectionSignal?.Cancel(); + ProcessConnectionMessage(message); break; - case "subscribed": // Response from subscription and subscription update - clientId = message["clientId"] as string; - if (clientId == ClientId) - { - requestId = Convert.ToInt32(message["requestId"]); - if (SubscriptionSignals.TryGetValue(requestId, out CancellationTokenSource subscriptionSignal)) - { - subscriptionSignal?.Cancel(); - } - } + ProcessSubscriptionMessage(message); break; - case "unsubscribed": - clientId = message["clientId"] as string; - if (clientId == ClientId) - { - requestId = Convert.ToInt32(message["requestId"]); - if (UnsubscriptionSignals.TryGetValue(requestId, out CancellationTokenSource unsubscriptionSignal)) - { - unsubscriptionSignal?.Cancel(); - } - } + ProcessUnsubscriptionMessage(message); break; - case "error": - ParseLiveQueryErrorEventArgs errorArgs = new ParseLiveQueryErrorEventArgs( - Convert.ToInt32(message["code"]), - message["error"] as string, - (bool) message["reconnect"]); - Error?.Invoke(this, errorArgs); + ProcessErrorMessage(message); break; - case "create": - clientId = message["clientId"] as string; - if (clientId == ClientId) - { - requestId = Convert.ToInt32(message["requestId"]); - if (Subscriptions.TryGetValue(requestId, out subscription)) - { - subscription.OnCreate(ParseObjectCoder.Instance.Decode(message["object"] as IDictionary, Decoder, ParseClient.Instance.Services)); - } - } + ProcessCreateEventMessage(message); break; - case "enter": - clientId = message["clientId"] as string; - if (clientId == ClientId) - { - requestId = Convert.ToInt32(message["requestId"]); - if (Subscriptions.TryGetValue(requestId, out subscription)) - { - subscription.OnEnter( - ParseObjectCoder.Instance.Decode(message["object"] as IDictionary, Decoder, ParseClient.Instance.Services), - ParseObjectCoder.Instance.Decode(message["original"] as IDictionary, Decoder, ParseClient.Instance.Services)); - } - } + ProcessEnterEventMessage(message); break; - case "update": - clientId = message["clientId"] as string; - if (clientId == ClientId) - { - requestId = Convert.ToInt32(message["requestId"]); - if (Subscriptions.TryGetValue(requestId, out subscription)) - { - subscription.OnUpdate( - ParseObjectCoder.Instance.Decode(message["object"] as IDictionary, Decoder, ParseClient.Instance.Services), - ParseObjectCoder.Instance.Decode(message["original"] as IDictionary, Decoder, ParseClient.Instance.Services)); - } - } + ProcessUpdateEventMessage(message); break; - case "leave": - clientId = message["clientId"] as string; - if (clientId == ClientId) - { - requestId = Convert.ToInt32(message["requestId"]); - if (Subscriptions.TryGetValue(requestId, out subscription)) - { - subscription.OnLeave( - ParseObjectCoder.Instance.Decode(message["object"] as IDictionary, Decoder, ParseClient.Instance.Services), - ParseObjectCoder.Instance.Decode(message["original"] as IDictionary, Decoder, ParseClient.Instance.Services)); - } - } + ProcessLeaveEventMessage(message); break; - case "delete": - clientId = message["clientId"] as string; - if (clientId == ClientId) - { - requestId = Convert.ToInt32(message["requestId"]); - if (Subscriptions.TryGetValue(requestId, out subscription)) - { - subscription.OnDelete(ParseObjectCoder.Instance.Decode(message["object"] as IDictionary, Decoder, ParseClient.Instance.Services)); - } - } + ProcessDeleteEventMessage(message); break; - default: Debug.WriteLine($"Unknown operation: {message["op"]}"); break; } } + void ProcessDeleteEventMessage(IDictionary message) + { + string clientId = message["clientId"] as string; + if (clientId != ClientId) + return; + int requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) + { + subscription.OnDelete(ParseObjectCoder.Instance.Decode( + message["object"] as IDictionary, + Decoder, + ParseClient.Instance.Services)); + } + } + + void ProcessLeaveEventMessage(IDictionary message) + { + string clientId = message["clientId"] as string; + if (clientId != ClientId) + return; + int requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) + { + subscription.OnLeave( + ParseObjectCoder.Instance.Decode( + message["object"] as IDictionary, + Decoder, + ParseClient.Instance.Services), + ParseObjectCoder.Instance.Decode( + message["original"] as IDictionary, + Decoder, + ParseClient.Instance.Services)); + } + } + + void ProcessUpdateEventMessage(IDictionary message) + { + string clientId = message["clientId"] as string; + if (clientId != ClientId) + return; + int requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) + { + subscription.OnUpdate( + ParseObjectCoder.Instance.Decode( + message["object"] as IDictionary, + Decoder, + ParseClient.Instance.Services), + ParseObjectCoder.Instance.Decode( + message["original"] as IDictionary, + Decoder, + ParseClient.Instance.Services)); + } + } + + void ProcessEnterEventMessage(IDictionary message) + { + string clientId = message["clientId"] as string; + if (clientId != ClientId) + return; + int requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) + { + subscription.OnEnter( + ParseObjectCoder.Instance.Decode( + message["object"] as IDictionary, + Decoder, + ParseClient.Instance.Services), + ParseObjectCoder.Instance.Decode( + message["original"] as IDictionary, + Decoder, + ParseClient.Instance.Services)); + } + } + + void ProcessCreateEventMessage(IDictionary message) + { + string clientId = message["clientId"] as string; + if (clientId != ClientId) + return; + int requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) + { + subscription.OnCreate(ParseObjectCoder.Instance.Decode( + message["object"] as IDictionary, + Decoder, + ParseClient.Instance.Services)); + } + } + + void ProcessErrorMessage(IDictionary message) + { + ParseLiveQueryErrorEventArgs errorArgs = new ParseLiveQueryErrorEventArgs( + Convert.ToInt32(message["code"]), + message["error"] as string, + (bool) message["reconnect"]); + Error?.Invoke(this, errorArgs); + } + + void ProcessUnsubscriptionMessage(IDictionary message) + { + string clientId = message["clientId"] as string; + if (clientId != ClientId) + return; + int requestId = Convert.ToInt32(message["requestId"]); + if (UnsubscriptionSignals.TryGetValue(requestId, out TaskCompletionSource unsubscriptionSign)) + { + unsubscriptionSign?.TrySetResult(); + } + } + + void ProcessSubscriptionMessage(IDictionary message) + { + string clientId = message["clientId"] as string; + if (clientId != ClientId) + return; + int requestId = Convert.ToInt32(message["requestId"]); + if (SubscriptionSignals.TryGetValue(requestId, out TaskCompletionSource subscriptionSignal)) + { + subscriptionSignal?.TrySetResult(); + } + } + + void ProcessConnectionMessage(IDictionary message) + { + _state = ParseLiveQueryState.Connected; + ClientId = message["clientId"] as string; + ConnectionSignal.TrySetResult(); + } + private async Task> AppendSessionToken(IDictionary message) { string sessionToken = await ParseClient.Instance.Services.GetCurrentSessionToken(); return sessionToken is null ? message : message.Concat(new Dictionary { - { "sessionToken", await ParseClient.Instance.Services.GetCurrentSessionToken() } + { "sessionToken", sessionToken } }).ToDictionary(); } @@ -261,8 +322,18 @@ private async Task OpenAsync(CancellationToken cancellationToken = default) await WebSocketClient.OpenAsync(ParseClient.Instance.Services.LiveQueryServerConnectionData.ServerURI, cancellationToken); } - private void WebSocketClientOnMessageReceived(object sender, string e) => - ProcessMessage(JsonUtilities.Parse(e) as IDictionary); + private void WebSocketClientOnMessageReceived(object sender, string e) + { + object parsed = JsonUtilities.Parse(e); + if (parsed is IDictionary message) + { + ProcessMessage(message); + } + else + { + Debug.WriteLine($"Invalid message format received: {e}"); + } + } /// /// Initiates a connection to the live query server asynchronously. @@ -278,9 +349,9 @@ private void WebSocketClientOnMessageReceived(object sender, string e) => /// public async Task ConnectAsync(CancellationToken cancellationToken = default) { - if (State == ParseLiveQueryState.Closed) + if (_state == ParseLiveQueryState.Closed) { - State = ParseLiveQueryState.Connecting; + _state = ParseLiveQueryState.Connecting; await OpenAsync(cancellationToken); WebSocketClient.MessageReceived += WebSocketClientOnMessageReceived; Dictionary message = new Dictionary @@ -289,26 +360,59 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) { "applicationId", ParseClient.Instance.Services.LiveQueryServerConnectionData.ApplicationID }, { "windowsKey", ParseClient.Instance.Services.LiveQueryServerConnectionData.Key } }; - await SendMessage(await AppendSessionToken(message), cancellationToken); - ConnectionSignal = new CancellationTokenSource(); - bool signalReceived = ConnectionSignal.Token.WaitHandle.WaitOne(TimeOut); - if (!signalReceived) + + ConnectionSignal = new TaskCompletionSource(); + try { - throw new TimeoutException(); + await SendMessage(await AppendSessionToken(message), cancellationToken); + + using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(TimeOut); + + await ConnectionSignal.Task.WaitAsync(cts.Token); + _state = ParseLiveQueryState.Connected; } - State = ParseLiveQueryState.Connected; - ConnectionSignal.Dispose(); - } - else if (State == ParseLiveQueryState.Connecting) - { - if (ConnectionSignal is not null) + catch (OperationCanceledException) + { + throw new TimeoutException("Live query server connection request has reached timeout"); + } + finally { - if (!ConnectionSignal.Token.WaitHandle.WaitOne(TimeOut)) - { - throw new TimeoutException(); - } + ConnectionSignal.Task.Dispose(); + ConnectionSignal = null; } } + else if (_state == ParseLiveQueryState.Connecting && ConnectionSignal is not null) + { + await ConnectionSignal.Task.WaitAsync(cancellationToken); + } + } + + private async Task SendAndWaitForSignalAsync(IDictionary message, + ConcurrentDictionary signalDictionary, + int requestId, + CancellationToken cancellationToken) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + signalDictionary.TryAdd(requestId, tcs); + + try + { + await SendMessage(message, cancellationToken); + + using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(TimeOut); + + await tcs.Task.WaitAsync(cts.Token); + } + catch (OperationCanceledException) + { + throw new TimeoutException($"Operation timeout for request {requestId}"); + } + finally + { + signalDictionary.TryRemove(requestId, out _); + } } /// @@ -335,30 +439,21 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) /// public async Task SubscribeAsync(ParseLiveQuery liveQuery, CancellationToken cancellationToken = default) where T : ParseObject { - if (State == ParseLiveQueryState.Closed) + if (_state == ParseLiveQueryState.Closed) { throw new InvalidOperationException("Cannot subscribe to a live query when the connection is closed."); } - int requestId = ++LastRequestId; + int requestId = Interlocked.Increment(ref LastRequestId); Dictionary message = new Dictionary { { "op", "subscribe" }, { "requestId", requestId }, { "query", liveQuery.BuildParameters() } }; - await SendMessage(await AppendSessionToken(message), cancellationToken); - CancellationTokenSource completionSignal = new CancellationTokenSource(); - SubscriptionSignals.Add(requestId, completionSignal); - bool signalReceived = completionSignal.Token.WaitHandle.WaitOne(TimeOut); - SubscriptionSignals.Remove(requestId); - completionSignal.Dispose(); - if (!signalReceived) - { - throw new TimeoutException(); - } + await SendAndWaitForSignalAsync(await AppendSessionToken(message), SubscriptionSignals, requestId, cancellationToken); ParseLiveQuerySubscription subscription = new ParseLiveQuerySubscription(liveQuery.Services, liveQuery.ClassName, requestId); - Subscriptions.Add(requestId, subscription); + Subscriptions.TryAdd(requestId, subscription); return subscription; } @@ -389,16 +484,7 @@ public async Task UpdateSubscriptionAsync(ParseLiveQuery liveQuery, int re { "requestId", requestId }, { "query", liveQuery.BuildParameters() } }; - await SendMessage(await AppendSessionToken(message), cancellationToken); - CancellationTokenSource completionSignal = new CancellationTokenSource(); - SubscriptionSignals.Add(requestId, completionSignal); - bool signalReceived = completionSignal.Token.WaitHandle.WaitOne(TimeOut); - SubscriptionSignals.Remove(requestId); - completionSignal.Dispose(); - if (!signalReceived) - { - throw new TimeoutException(); - } + await SendAndWaitForSignalAsync(await AppendSessionToken(message), SubscriptionSignals, requestId, cancellationToken); } /// @@ -423,16 +509,8 @@ public async Task UnsubscribeAsync(int requestId, CancellationToken cancellation { "op", "unsubscribe" }, { "requestId", requestId } }; - await SendMessage(message, cancellationToken); - CancellationTokenSource completionSignal = new CancellationTokenSource(); - UnsubscriptionSignals.Add(requestId, completionSignal); - bool signalReceived = completionSignal.Token.WaitHandle.WaitOne(TimeOut); - UnsubscriptionSignals.Remove(requestId); - completionSignal.Dispose(); - if (!signalReceived) - { - throw new TimeoutException(); - } + await SendAndWaitForSignalAsync(message, UnsubscriptionSignals, requestId, cancellationToken); + Subscriptions.TryRemove(requestId, out _); } /// @@ -446,10 +524,43 @@ public async Task UnsubscribeAsync(int requestId, CancellationToken cancellation /// public async Task CloseAsync(CancellationToken cancellationToken = default) { + WebSocketClient.MessageReceived -= WebSocketClientOnMessageReceived; await WebSocketClient.CloseAsync(cancellationToken); - State = ParseLiveQueryState.Closed; + _state = ParseLiveQueryState.Closed; SubscriptionSignals.Clear(); UnsubscriptionSignals.Clear(); Subscriptions.Clear(); } + + /// + /// Releases all resources used by the instance. + /// + /// + /// This method is used to clean up resources, such as closing open connections or unsubscribing from events, + /// and should be called when the instance is no longer needed. After calling this method, the instance + /// cannot be used unless re-initialized. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases the resources used by the instance. + /// + /// + /// This method implements the interface and is used to clean up any managed or unmanaged + /// resources used by the instance. + /// + private void Dispose(bool disposing) + { + if (disposed) + return; + if (disposing) + { + CloseAsync().GetAwaiter().GetResult(); + } + disposed = true; + } } \ No newline at end of file diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryDualEventArgs.cs b/Parse/Platform/LiveQueries/ParseLiveQueryDualEventArgs.cs index a3ec2596..d8ff1347 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryDualEventArgs.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryDualEventArgs.cs @@ -1,3 +1,5 @@ +using System; + namespace Parse.Platform.LiveQueries; /// @@ -15,5 +17,6 @@ public class ParseLiveQueryDualEventArgs : ParseLiveQueryEventArgs /// public ParseObject Original { get; private set; } - internal ParseLiveQueryDualEventArgs(ParseObject current, ParseObject original) : base(current) => Original = original; + internal ParseLiveQueryDualEventArgs(ParseObject current, ParseObject original) : base(current) => + Original = original ?? throw new ArgumentNullException(nameof(original)); } diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs b/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs index aceb15f1..6817e2ac 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs @@ -17,5 +17,5 @@ public class ParseLiveQueryEventArgs : EventArgs /// public ParseObject Object { get; private set; } - internal ParseLiveQueryEventArgs(ParseObject current) => Object = current; + internal ParseLiveQueryEventArgs(ParseObject current) => Object = current ?? throw new ArgumentNullException(nameof(current)); } diff --git a/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs b/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs index 16112f57..dfd46300 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQuerySubscription.cs @@ -76,7 +76,7 @@ public ParseLiveQuerySubscription(IServiceHub serviceHub, string className, int /// A task that represents the asynchronous operation of updating /// the subscription with the new query parameters. public async Task UpdateAsync(ParseLiveQuery liveQuery, CancellationToken cancellationToken = default) where T1 : ParseObject => - await Services.LiveQueryController.UpdateSubscriptionAsync(liveQuery, RequestId, CancellationToken.None); + await Services.LiveQueryController.UpdateSubscriptionAsync(liveQuery, RequestId, cancellationToken); /// /// Cancels the current live query subscription by unsubscribing from the Parse Live Query server. @@ -86,7 +86,7 @@ public async Task UpdateAsync(ParseLiveQuery liveQuery, CancellationToke /// A token to monitor for cancellation requests. If triggered, the cancellation process will halt. /// A task that represents the asynchronous operation of canceling the subscription. public async Task CancelAsync(CancellationToken cancellationToken = default) => - await Services.LiveQueryController.UnsubscribeAsync(RequestId, CancellationToken.None); + await Services.LiveQueryController.UnsubscribeAsync(RequestId, cancellationToken); /// /// Handles the creation event for an object that matches the subscription's query. diff --git a/Parse/Platform/ParseClient.cs b/Parse/Platform/ParseClient.cs index 058500f9..fd818a00 100644 --- a/Parse/Platform/ParseClient.cs +++ b/Parse/Platform/ParseClient.cs @@ -130,7 +130,7 @@ public ParseClient(IServerConnectionData configuration, IServerConnectionData li IServerConnectionData GenerateLiveQueryServerConnectionData() => liveQueryConfiguration switch { - null => throw new ArgumentNullException(nameof(configuration)), + null => throw new ArgumentNullException(nameof(liveQueryConfiguration)), ServerConnectionData { Test: true, ServerURI: { } } data => data, ServerConnectionData { Test: true } data => new ServerConnectionData { From 84f70600e8cfe3bfc51ad53861b839c88580746e Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Wed, 25 Jun 2025 19:33:03 +0000 Subject: [PATCH 15/24] Null checks --- .../Execution/TextWebSocketClient.cs | 26 +++++++++++++++---- Parse/Platform/LiveQueries/ParseLiveQuery.cs | 2 ++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Parse/Infrastructure/Execution/TextWebSocketClient.cs b/Parse/Infrastructure/Execution/TextWebSocketClient.cs index e252919f..288a3c32 100644 --- a/Parse/Infrastructure/Execution/TextWebSocketClient.cs +++ b/Parse/Infrastructure/Execution/TextWebSocketClient.cs @@ -75,7 +75,7 @@ public async Task OpenAsync(string serverUri, CancellationToken cancellationToke /// A task representing the asynchronous operation of closing the WebSocket connection. /// public async Task CloseAsync(CancellationToken cancellationToken = default) => - await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, String.Empty, cancellationToken); + await webSocket?.CloseAsync(WebSocketCloseStatus.NormalClosure, String.Empty, cancellationToken)!; private async Task ListenForMessages(CancellationToken cancellationToken) { @@ -96,9 +96,26 @@ private async Task ListenForMessages(CancellationToken cancellationToken) break; } - string message = Encoding.UTF8.GetString(buffer, 0, result.Count); - Debug.WriteLine($"Received message: {message}"); - MessageReceived?.Invoke(this, message); + if (result.EndOfMessage) + { + string message = Encoding.UTF8.GetString(buffer, 0, result.Count); + MessageReceived?.Invoke(this, message); + } + else + { + // Handle partial messages by accumulating data until EndOfMessage is true + StringBuilder messageBuilder = new StringBuilder(); + messageBuilder.Append(Encoding.UTF8.GetString(buffer, 0, result.Count)); + while (!result.EndOfMessage) + { + result = await webSocket.ReceiveAsync( + new ArraySegment(buffer), + cancellationToken); + messageBuilder.Append(Encoding.UTF8.GetString(buffer, 0, result.Count)); + } + string fullMessage = messageBuilder.ToString(); + MessageReceived?.Invoke(this, fullMessage); + } } } catch (OperationCanceledException ex) @@ -148,7 +165,6 @@ private void StartListening(CancellationToken cancellationToken) if (!task.IsFaulted) return; Debug.WriteLine($"Websocket listener task faulted: {task.Exception}"); - throw task.Exception; }, TaskContinuationOptions.OnlyOnFaulted); } diff --git a/Parse/Platform/LiveQueries/ParseLiveQuery.cs b/Parse/Platform/LiveQueries/ParseLiveQuery.cs index 14d608f7..14785342 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQuery.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQuery.cs @@ -38,6 +38,8 @@ public class ParseLiveQuery where T : ParseObject internal ParseLiveQuery(IServiceHub serviceHub, string className, IDictionary filters, IEnumerable selectedKeys = null, IEnumerable watchedKeys = null) { + ArgumentNullException.ThrowIfNull(serviceHub); + ArgumentException.ThrowIfNullOrWhiteSpace(className); ArgumentNullException.ThrowIfNull(filters); Services = serviceHub; From 834ff8929de07bf34f9f9a9e06183735e132a7d7 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Fri, 27 Jun 2025 23:53:35 +0000 Subject: [PATCH 16/24] Move TimeOut and BufferSize to new LiveQueryServerConnectionData and many improvements --- Parse.sln | 6 --- .../Infrastructure/CustomServiceHub.cs | 2 +- .../ILiveQueryServerConnectionData.cs | 25 ++++++++++ .../Infrastructure/IMutableServiceHub.cs | 2 +- .../Infrastructure/IServiceHub.cs | 2 +- .../LiveQueries/IParseLiveQueryController.cs | 12 ----- .../IParseLiveQuerySubscription.cs | 5 -- .../Execution/TextWebSocketClient.cs | 28 ++++++++--- .../LateInitializedMutableServiceHub.cs | 27 +++++----- .../LiveQueryServerConnectionData.cs | 50 +++++++++++++++++++ Parse/Infrastructure/MutableServiceHub.cs | 9 ++-- .../Infrastructure/OrchestrationServiceHub.cs | 2 +- Parse/Infrastructure/ServiceHub.cs | 7 +-- .../LiveQueries/ParseLiveQueryController.cs | 16 +++--- Parse/Platform/ParseClient.cs | 8 +-- Parse/Utilities/ObjectServiceExtensions.cs | 18 +++---- 16 files changed, 146 insertions(+), 73 deletions(-) create mode 100644 Parse/Abstractions/Infrastructure/ILiveQueryServerConnectionData.cs create mode 100644 Parse/Infrastructure/LiveQueryServerConnectionData.cs diff --git a/Parse.sln b/Parse.sln index 104934b1..82e6de70 100644 --- a/Parse.sln +++ b/Parse.sln @@ -17,8 +17,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parse.Tests", "Parse.Tests\Parse.Tests.csproj", "{FEB46D0F-384C-4F27-9E0E-F4A636768C90}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParseApp", "ParseApp\ParseApp.csproj", "{71EF1783-2BAA-4119-A666-B7DCA3FD3085}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,10 +31,6 @@ Global {FEB46D0F-384C-4F27-9E0E-F4A636768C90}.Debug|Any CPU.Build.0 = Debug|Any CPU {FEB46D0F-384C-4F27-9E0E-F4A636768C90}.Release|Any CPU.ActiveCfg = Release|Any CPU {FEB46D0F-384C-4F27-9E0E-F4A636768C90}.Release|Any CPU.Build.0 = Release|Any CPU - {71EF1783-2BAA-4119-A666-B7DCA3FD3085}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {71EF1783-2BAA-4119-A666-B7DCA3FD3085}.Debug|Any CPU.Build.0 = Debug|Any CPU - {71EF1783-2BAA-4119-A666-B7DCA3FD3085}.Release|Any CPU.ActiveCfg = Release|Any CPU - {71EF1783-2BAA-4119-A666-B7DCA3FD3085}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Parse/Abstractions/Infrastructure/CustomServiceHub.cs b/Parse/Abstractions/Infrastructure/CustomServiceHub.cs index 6c29d7d9..416dc807 100644 --- a/Parse/Abstractions/Infrastructure/CustomServiceHub.cs +++ b/Parse/Abstractions/Infrastructure/CustomServiceHub.cs @@ -64,7 +64,7 @@ public abstract class CustomServiceHub : ICustomServiceHub public virtual IServerConnectionData ServerConnectionData => Services.ServerConnectionData; - public virtual IServerConnectionData LiveQueryServerConnectionData => Services.LiveQueryServerConnectionData; + public virtual ILiveQueryServerConnectionData LiveQueryServerConnectionData => Services.LiveQueryServerConnectionData; public virtual IParseDataDecoder Decoder => Services.Decoder; diff --git a/Parse/Abstractions/Infrastructure/ILiveQueryServerConnectionData.cs b/Parse/Abstractions/Infrastructure/ILiveQueryServerConnectionData.cs new file mode 100644 index 00000000..51390d3b --- /dev/null +++ b/Parse/Abstractions/Infrastructure/ILiveQueryServerConnectionData.cs @@ -0,0 +1,25 @@ +namespace Parse.Abstractions.Infrastructure; + +public interface ILiveQueryServerConnectionData : IServerConnectionData +{ + /// + /// Represents the default timeout duration, in milliseconds. + /// + public const int DefaultTimeOut = 5000; // 5 seconds + + /// + /// The timeout duration, in milliseconds, used for various operations, such as + /// establishing a connection or completing a subscription. + /// + int TimeOut { get; set; } + + /// + /// The default buffer size, in bytes. + /// + public const int DefaultBufferSize = 4096; // 4MB + + /// + /// The buffer size, in bytes, used for the WebSocket operations to handle incoming messages. + /// + int MessageBufferSize { get; set; } +} diff --git a/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs b/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs index 7f5d5043..f5fa14cc 100644 --- a/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs +++ b/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs @@ -19,7 +19,7 @@ namespace Parse.Abstractions.Infrastructure; public interface IMutableServiceHub : IServiceHub { IServerConnectionData ServerConnectionData { set; } - IServerConnectionData LiveQueryServerConnectionData { set; } + ILiveQueryServerConnectionData LiveQueryServerConnectionData { set; } IMetadataController MetadataController { set; } IServiceHubCloner Cloner { set; } diff --git a/Parse/Abstractions/Infrastructure/IServiceHub.cs b/Parse/Abstractions/Infrastructure/IServiceHub.cs index 09ac8403..7bd71aa1 100644 --- a/Parse/Abstractions/Infrastructure/IServiceHub.cs +++ b/Parse/Abstractions/Infrastructure/IServiceHub.cs @@ -27,7 +27,7 @@ public interface IServiceHub /// The current server connection data that the Parse SDK has been initialized with. /// IServerConnectionData ServerConnectionData { get; } - IServerConnectionData LiveQueryServerConnectionData { get; } + ILiveQueryServerConnectionData LiveQueryServerConnectionData { get; } IMetadataController MetadataController { get; } IServiceHubCloner Cloner { get; } diff --git a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs index 26fa5225..0fbae050 100644 --- a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs +++ b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQueryController.cs @@ -11,18 +11,6 @@ namespace Parse.Abstractions.Platform.LiveQueries; /// public interface IParseLiveQueryController { - /// - /// Gets or sets the timeout duration, in milliseconds, for operations performed by the LiveQuery controller. - /// - /// - /// This property determines the maximum time the system will wait for an operation to complete - /// before timing out. It is particularly relevant to tasks such as establishing a connection - /// to the LiveQuery server or awaiting responses for subscription or query update requests. - /// Developers can use this property to configure timeouts based on the expected network - /// and performance conditions of the application environment. - /// - public int TimeOut { get; set; } - /// /// Event triggered when an error occurs during the operation of the ParseLiveQueryController. /// diff --git a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs index 638c1991..dfb65293 100644 --- a/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs +++ b/Parse/Abstractions/Platform/LiveQueries/IParseLiveQuerySubscription.cs @@ -6,11 +6,6 @@ namespace Parse.Abstractions.Platform.LiveQueries; -/// -/// Represents a live query subscription that is used with Parse's Live Query service. -/// It allows real-time monitoring and event handling for object changes that match -/// a specified query. -/// public interface IParseLiveQuerySubscription { /// diff --git a/Parse/Infrastructure/Execution/TextWebSocketClient.cs b/Parse/Infrastructure/Execution/TextWebSocketClient.cs index 288a3c32..6ef176bc 100644 --- a/Parse/Infrastructure/Execution/TextWebSocketClient.cs +++ b/Parse/Infrastructure/Execution/TextWebSocketClient.cs @@ -12,7 +12,7 @@ namespace Parse.Infrastructure.Execution; /// Represents a WebSocket client that allows connecting to a WebSocket server, sending messages, and receiving messages. /// Implements the IWebSocketClient interface for WebSocket operations. /// -class TextWebSocketClient : IWebSocketClient +class TextWebSocketClient(int bufferSize) : IWebSocketClient { /// /// A private instance of the ClientWebSocket class used to manage the WebSocket connection. @@ -43,6 +43,8 @@ class TextWebSocketClient : IWebSocketClient private readonly object connectionLock = new object(); + private int BufferSize { get; } = bufferSize; + /// /// Opens a WebSocket connection to the specified server URI and starts listening for messages. /// If the connection is already open or in a connecting state, this method does nothing. @@ -54,14 +56,19 @@ class TextWebSocketClient : IWebSocketClient /// public async Task OpenAsync(string serverUri, CancellationToken cancellationToken = default) { + ClientWebSocket webSocketToConnect = null; lock (connectionLock) { webSocket ??= new ClientWebSocket(); + if (webSocket.State != WebSocketState.Open && webSocket.State != WebSocketState.Connecting) + { + webSocketToConnect = webSocket; + } } - if (webSocket.State != WebSocketState.Open && webSocket.State != WebSocketState.Connecting) + if (webSocketToConnect is not null) { - await webSocket.ConnectAsync(new Uri(serverUri), cancellationToken); + await webSocketToConnect.ConnectAsync(new Uri(serverUri), cancellationToken); StartListening(cancellationToken); } } @@ -74,12 +81,17 @@ public async Task OpenAsync(string serverUri, CancellationToken cancellationToke /// /// A task representing the asynchronous operation of closing the WebSocket connection. /// - public async Task CloseAsync(CancellationToken cancellationToken = default) => - await webSocket?.CloseAsync(WebSocketCloseStatus.NormalClosure, String.Empty, cancellationToken)!; + public async Task CloseAsync(CancellationToken cancellationToken = default) + { + if (webSocket is not null) + { + await webSocket?.CloseAsync(WebSocketCloseStatus.NormalClosure, String.Empty, cancellationToken)!; + } + } private async Task ListenForMessages(CancellationToken cancellationToken) { - byte[] buffer = new byte[1024 * 4]; + byte[] buffer = new byte[BufferSize]; try { @@ -183,6 +195,10 @@ private void StartListening(CancellationToken cancellationToken) public async Task SendAsync(string message, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(webSocket); + if (webSocket.State != WebSocketState.Open) + { + throw new InvalidOperationException($"WebSocket is not in Open state. Current state: {webSocket.State}"); + } await webSocket.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, cancellationToken); } } \ No newline at end of file diff --git a/Parse/Infrastructure/LateInitializedMutableServiceHub.cs b/Parse/Infrastructure/LateInitializedMutableServiceHub.cs index 1c789112..aaa7c6f3 100644 --- a/Parse/Infrastructure/LateInitializedMutableServiceHub.cs +++ b/Parse/Infrastructure/LateInitializedMutableServiceHub.cs @@ -72,12 +72,6 @@ public IParseCommandRunner CommandRunner set => LateInitializer.SetValue(value); } - public IWebSocketClient WebSocketClient - { - get => LateInitializer.GetValue(() => new TextWebSocketClient { }); - set => LateInitializer.SetValue(value); - } - public IParseCloudCodeController CloudCodeController { get => LateInitializer.GetValue(() => new ParseCloudCodeController(CommandRunner, Decoder)); @@ -108,12 +102,6 @@ public IParseQueryController QueryController set => LateInitializer.SetValue(value); } - public IParseLiveQueryController LiveQueryController - { - get => LateInitializer.GetValue(() => new ParseLiveQueryController(WebSocketClient, Decoder)); - set => LateInitializer.SetValue(value); - } - public IParseSessionController SessionController { get => LateInitializer.GetValue(() => new ParseSessionController(CommandRunner, Decoder)); @@ -174,6 +162,19 @@ public IParseInstallationDataFinalizer InstallationDataFinalizer set => LateInitializer.SetValue(value); } + + public IWebSocketClient WebSocketClient + { + get => LateInitializer.GetValue(() => LiveQueryServerConnectionData is null ? null : new TextWebSocketClient(LiveQueryServerConnectionData.MessageBufferSize)); + set => LateInitializer.SetValue(value); + } + + public IParseLiveQueryController LiveQueryController + { + get => LateInitializer.GetValue(() => LiveQueryServerConnectionData is null ? null : new ParseLiveQueryController(LiveQueryServerConnectionData.TimeOut, WebSocketClient, Decoder)); + set => LateInitializer.SetValue(value); + } + public IServerConnectionData ServerConnectionData { get; set; } - public IServerConnectionData LiveQueryServerConnectionData { get; set; } + public ILiveQueryServerConnectionData LiveQueryServerConnectionData { get; set; } } diff --git a/Parse/Infrastructure/LiveQueryServerConnectionData.cs b/Parse/Infrastructure/LiveQueryServerConnectionData.cs new file mode 100644 index 00000000..9c8dfa52 --- /dev/null +++ b/Parse/Infrastructure/LiveQueryServerConnectionData.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using Parse.Abstractions.Infrastructure; + +namespace Parse.Infrastructure; + +/// +/// Represents the configuration of the Parse Live Query server. +/// +public struct LiveQueryServerConnectionData : ILiveQueryServerConnectionData +{ + public LiveQueryServerConnectionData() { } + + internal bool Test { get; set; } + + /// + /// The timeout duration, in milliseconds, used for various operations, such as + /// establishing a connection or completing a subscription. + /// + public int TimeOut { get; set; } = ILiveQueryServerConnectionData.DefaultTimeOut; + + /// + /// The buffer size, in bytes, used by the WebSocket client for communication operations. + /// + public int MessageBufferSize { get; set; } = ILiveQueryServerConnectionData.DefaultBufferSize; + + /// + /// The App ID of your app. + /// + public string ApplicationID { get; set; } + + /// + /// A URI pointing to the target Parse Server instance hosting the app targeted by . + /// + public string ServerURI { get; set; } + + /// + /// The .NET Key for the Parse app targeted by . + /// + public string Key { get; set; } + + /// + /// The Master Key for the Parse app targeted by . + /// + public string MasterKey { get; set; } + + /// + /// Additional HTTP headers to be sent with network requests from the SDK. + /// + public IDictionary Headers { get; set; } +} diff --git a/Parse/Infrastructure/MutableServiceHub.cs b/Parse/Infrastructure/MutableServiceHub.cs index 09c12f1e..4585f813 100644 --- a/Parse/Infrastructure/MutableServiceHub.cs +++ b/Parse/Infrastructure/MutableServiceHub.cs @@ -36,7 +36,7 @@ namespace Parse.Infrastructure; public class MutableServiceHub : IMutableServiceHub { public IServerConnectionData ServerConnectionData { get; set; } - public IServerConnectionData LiveQueryServerConnectionData { get; set; } + public ILiveQueryServerConnectionData LiveQueryServerConnectionData { get; set; } public IMetadataController MetadataController { get; set; } public IServiceHubCloner Cloner { get; set; } @@ -70,7 +70,7 @@ public class MutableServiceHub : IMutableServiceHub public IParseCurrentInstallationController CurrentInstallationController { get; set; } public IParseInstallationDataFinalizer InstallationDataFinalizer { get; set; } - public MutableServiceHub SetDefaults(IServerConnectionData connectionData = default, IServerConnectionData liveQueryConnectionData = default) + public MutableServiceHub SetDefaults(IServerConnectionData connectionData = default, ILiveQueryServerConnectionData liveQueryConnectionData = default) { ServerConnectionData ??= connectionData; LiveQueryServerConnectionData ??= liveQueryConnectionData; @@ -90,14 +90,12 @@ public MutableServiceHub SetDefaults(IServerConnectionData connectionData = defa InstallationController ??= new ParseInstallationController(CacheController); CommandRunner ??= new ParseCommandRunner(WebClient, InstallationController, MetadataController, ServerConnectionData, new Lazy(() => UserController)); - WebSocketClient ??= new TextWebSocketClient { }; CloudCodeController ??= new ParseCloudCodeController(CommandRunner, Decoder); ConfigurationController ??= new ParseConfigurationController(CommandRunner, CacheController, Decoder); FileController ??= new ParseFileController(CommandRunner); ObjectController ??= new ParseObjectController(CommandRunner, Decoder, ServerConnectionData); QueryController ??= new ParseQueryController(CommandRunner, Decoder); - LiveQueryController ??= new ParseLiveQueryController(WebSocketClient, Decoder); SessionController ??= new ParseSessionController(CommandRunner, Decoder); UserController ??= new ParseUserController(CommandRunner, Decoder); CurrentUserController ??= new ParseCurrentUserController(CacheController, ClassController, Decoder); @@ -111,6 +109,9 @@ public MutableServiceHub SetDefaults(IServerConnectionData connectionData = defa PushChannelsController ??= new ParsePushChannelsController(CurrentInstallationController); InstallationDataFinalizer ??= new ParseInstallationDataFinalizer { }; + WebSocketClient ??= LiveQueryServerConnectionData is null ? null : new TextWebSocketClient(LiveQueryServerConnectionData.MessageBufferSize); + LiveQueryController ??= LiveQueryServerConnectionData is null ? null : new ParseLiveQueryController(LiveQueryServerConnectionData.TimeOut, WebSocketClient, Decoder); + return this; } } diff --git a/Parse/Infrastructure/OrchestrationServiceHub.cs b/Parse/Infrastructure/OrchestrationServiceHub.cs index 8e750074..2517ecdf 100644 --- a/Parse/Infrastructure/OrchestrationServiceHub.cs +++ b/Parse/Infrastructure/OrchestrationServiceHub.cs @@ -66,7 +66,7 @@ public class OrchestrationServiceHub : IServiceHub public IServerConnectionData ServerConnectionData => Custom.ServerConnectionData ?? Default.ServerConnectionData; - public IServerConnectionData LiveQueryServerConnectionData => Custom.LiveQueryServerConnectionData ?? Default.LiveQueryServerConnectionData; + public ILiveQueryServerConnectionData LiveQueryServerConnectionData => Custom.LiveQueryServerConnectionData ?? Default.LiveQueryServerConnectionData; public IParseDataDecoder Decoder => Custom.Decoder ?? Default.Decoder; diff --git a/Parse/Infrastructure/ServiceHub.cs b/Parse/Infrastructure/ServiceHub.cs index b97c1bb2..6beb0591 100644 --- a/Parse/Infrastructure/ServiceHub.cs +++ b/Parse/Infrastructure/ServiceHub.cs @@ -39,7 +39,7 @@ public class ServiceHub : IServiceHub LateInitializer LateInitializer { get; } = new LateInitializer { }; public IServerConnectionData ServerConnectionData { get; set; } - public IServerConnectionData LiveQueryServerConnectionData { get; set; } + public ILiveQueryServerConnectionData LiveQueryServerConnectionData { get; set; } public IMetadataController MetadataController => LateInitializer.GetValue(() => new MetadataController { HostManifestData = HostManifestData.Inferred, EnvironmentData = EnvironmentData.Inferred }); public IServiceHubCloner Cloner => LateInitializer.GetValue(() => new { } as object as IServiceHubCloner); @@ -52,14 +52,12 @@ public class ServiceHub : IServiceHub public IParseInstallationController InstallationController => LateInitializer.GetValue(() => new ParseInstallationController(CacheController)); public IParseCommandRunner CommandRunner => LateInitializer.GetValue(() => new ParseCommandRunner(WebClient, InstallationController, MetadataController, ServerConnectionData, new Lazy(() => UserController))); - public IWebSocketClient WebSocketClient => LateInitializer.GetValue(() => new TextWebSocketClient { }); public IParseCloudCodeController CloudCodeController => LateInitializer.GetValue(() => new ParseCloudCodeController(CommandRunner, Decoder)); public IParseConfigurationController ConfigurationController => LateInitializer.GetValue(() => new ParseConfigurationController(CommandRunner, CacheController, Decoder)); public IParseFileController FileController => LateInitializer.GetValue(() => new ParseFileController(CommandRunner)); public IParseObjectController ObjectController => LateInitializer.GetValue(() => new ParseObjectController(CommandRunner, Decoder, ServerConnectionData)); public IParseQueryController QueryController => LateInitializer.GetValue(() => new ParseQueryController(CommandRunner, Decoder)); - public IParseLiveQueryController LiveQueryController => LateInitializer.GetValue(() => new ParseLiveQueryController(WebSocketClient, Decoder)); public IParseSessionController SessionController => LateInitializer.GetValue(() => new ParseSessionController(CommandRunner, Decoder)); public IParseUserController UserController => LateInitializer.GetValue(() => new ParseUserController(CommandRunner, Decoder)); public IParseCurrentUserController CurrentUserController => LateInitializer.GetValue(() => new ParseCurrentUserController(CacheController, ClassController, Decoder)); @@ -73,6 +71,9 @@ public class ServiceHub : IServiceHub public IParseCurrentInstallationController CurrentInstallationController => LateInitializer.GetValue(() => new ParseCurrentInstallationController(InstallationController, CacheController, InstallationCoder, ClassController)); public IParseInstallationDataFinalizer InstallationDataFinalizer => LateInitializer.GetValue(() => new ParseInstallationDataFinalizer { }); + public IWebSocketClient WebSocketClient => LateInitializer.GetValue(() => LiveQueryServerConnectionData is null ? null : new TextWebSocketClient(LiveQueryServerConnectionData.MessageBufferSize)); + public IParseLiveQueryController LiveQueryController => LateInitializer.GetValue(() => LiveQueryServerConnectionData is null ? null : new ParseLiveQueryController(LiveQueryServerConnectionData.TimeOut, WebSocketClient, Decoder)); + public bool Reset() { return LateInitializer.Used && LateInitializer.Reset(); diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs index 43cc430f..14b8b0c0 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs @@ -40,7 +40,7 @@ public class ParseLiveQueryController : IParseLiveQueryController, IDisposable /// - Unsubscribing from a query. /// Ensure that the value is configured appropriately to avoid premature timeout errors in network-dependent processes. /// - public int TimeOut { get; set; } = 5000; + private int TimeOut { get; } /// /// Event triggered when an error occurs during the operation of the ParseLiveQueryController. @@ -107,6 +107,7 @@ public enum ParseLiveQueryState /// /// Initializes a new instance of the class. /// + /// /// /// The implementation to use for the live query connection. /// @@ -114,8 +115,9 @@ public enum ParseLiveQueryState /// /// This constructor is used to initialize a new instance of the class /// - public ParseLiveQueryController(IWebSocketClient webSocketClient, IParseDataDecoder decoder) + public ParseLiveQueryController(int timeOut, IWebSocketClient webSocketClient, IParseDataDecoder decoder) { + TimeOut = timeOut; WebSocketClient = webSocketClient; _state = ParseLiveQueryState.Closed; Decoder = decoder; @@ -259,7 +261,7 @@ void ProcessErrorMessage(IDictionary message) ParseLiveQueryErrorEventArgs errorArgs = new ParseLiveQueryErrorEventArgs( Convert.ToInt32(message["code"]), message["error"] as string, - (bool) message["reconnect"]); + Convert.ToBoolean(message["reconnect"])); Error?.Invoke(this, errorArgs); } @@ -336,16 +338,16 @@ private void WebSocketClientOnMessageReceived(object sender, string e) } /// - /// Initiates a connection to the live query server asynchronously. + /// Establishes a connection to the live query server asynchronously. /// /// - /// A cancellation token to monitor for cancellation requests. + /// A cancellation token used to propagate notification that the operation should be canceled. /// /// - /// A task representing the asynchronous operation. + /// A task that represents the asynchronous operation. /// /// - /// Thrown if the connection attempt exceeds the specified timeout period. + /// Thrown if the live query server connection request exceeds the defined timeout. /// public async Task ConnectAsync(CancellationToken cancellationToken = default) { diff --git a/Parse/Platform/ParseClient.cs b/Parse/Platform/ParseClient.cs index fd818a00..70aac7d1 100644 --- a/Parse/Platform/ParseClient.cs +++ b/Parse/Platform/ParseClient.cs @@ -104,7 +104,7 @@ public ParseClient(IServerConnectionData configuration, IServiceHub serviceHub = /// The configuration to initialize the Parse live query client with. /// A service hub to override internal services and thereby make the Parse SDK operate in a custom manner. /// A set of implementation instances to tweak the behaviour of the SDK. - public ParseClient(IServerConnectionData configuration, IServerConnectionData liveQueryConfiguration, IServiceHub serviceHub = default, params IServiceHubMutator[] configurators) + public ParseClient(IServerConnectionData configuration, ILiveQueryServerConnectionData liveQueryConfiguration, IServiceHub serviceHub = default, params IServiceHubMutator[] configurators) { Services = serviceHub is { } ? new OrchestrationServiceHub { Custom = serviceHub, Default = new ServiceHub { ServerConnectionData = GenerateServerConnectionData(), LiveQueryServerConnectionData = GenerateLiveQueryServerConnectionData() } } @@ -128,11 +128,11 @@ public ParseClient(IServerConnectionData configuration, IServerConnectionData li _ => throw new InvalidOperationException("The IServerConnectionData implementation instance provided to the ParseClient constructor must be populated with the information needed to connect to a Parse server instance.") }; - IServerConnectionData GenerateLiveQueryServerConnectionData() => liveQueryConfiguration switch + ILiveQueryServerConnectionData GenerateLiveQueryServerConnectionData() => liveQueryConfiguration switch { null => throw new ArgumentNullException(nameof(liveQueryConfiguration)), - ServerConnectionData { Test: true, ServerURI: { } } data => data, - ServerConnectionData { Test: true } data => new ServerConnectionData + LiveQueryServerConnectionData { Test: true, ServerURI: { } } data => data, + LiveQueryServerConnectionData { Test: true } data => new LiveQueryServerConnectionData { ApplicationID = data.ApplicationID, Headers = data.Headers, diff --git a/Parse/Utilities/ObjectServiceExtensions.cs b/Parse/Utilities/ObjectServiceExtensions.cs index 428ada3e..5271edb5 100644 --- a/Parse/Utilities/ObjectServiceExtensions.cs +++ b/Parse/Utilities/ObjectServiceExtensions.cs @@ -10,6 +10,8 @@ using Parse.Infrastructure.Utilities; using Parse.Infrastructure.Data; using System.Diagnostics; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Platform.LiveQueries; using Parse.Platform.LiveQueries; namespace Parse; @@ -289,21 +291,19 @@ public static ParseQuery GetQuery(this IServiceHub serviceHub, stri } /// - /// Connects the Live Query server to the provided instance, enabling real-time updates for subscribed queries. - /// Allows error handling during the connection and sets a custom timeout period for the connection. + /// Establishes a connection to the Live Query Server, enabling real-time updates and operations for subscribed queries. + /// This method configures error handling for the connection. /// - /// The instance through which the Live Query server will be connected. - /// An optional event handler to capture Live Query connection errors. - /// An optional timeout value, in milliseconds, for the Live Query connection. Defaults to 5000 milliseconds. - /// A task representing the asynchronous operation of connecting to the Live Query server. - public static async Task ConnectLiveQueryServerAsync(this IServiceHub serviceHub, EventHandler onError = null, int timeOut = 5000) + /// The current instance managing the Parse services. + /// Optional event handler to manage errors occurring during the live query operations. + /// A task that represents the asynchronous operation of connecting to the Live Query Server. The task completes when the connection is established. + public static async Task ConnectLiveQueryServerAsync(this IServiceHub serviceHub, EventHandler onError = null) { - await serviceHub.LiveQueryController.ConnectAsync(); if (onError is not null) { serviceHub.LiveQueryController.Error += onError; } - serviceHub.LiveQueryController.TimeOut = timeOut; + await serviceHub.LiveQueryController.ConnectAsync(); } /// From bd36b7d77bd9ca089fae2c7d672f33fdb02d8e35 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Sat, 28 Jun 2025 00:10:25 +0000 Subject: [PATCH 17/24] Minor improvements --- .../Infrastructure/ILiveQueryServerConnectionData.cs | 2 +- Parse/Platform/LiveQueries/ParseLiveQueryController.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Parse/Abstractions/Infrastructure/ILiveQueryServerConnectionData.cs b/Parse/Abstractions/Infrastructure/ILiveQueryServerConnectionData.cs index 51390d3b..bb2fa6ba 100644 --- a/Parse/Abstractions/Infrastructure/ILiveQueryServerConnectionData.cs +++ b/Parse/Abstractions/Infrastructure/ILiveQueryServerConnectionData.cs @@ -16,7 +16,7 @@ public interface ILiveQueryServerConnectionData : IServerConnectionData /// /// The default buffer size, in bytes. /// - public const int DefaultBufferSize = 4096; // 4MB + public const int DefaultBufferSize = 4096; // 4KB /// /// The buffer size, in bytes, used for the WebSocket operations to handle incoming messages. diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs index 14b8b0c0..4c0fb685 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs @@ -380,7 +380,6 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) } finally { - ConnectionSignal.Task.Dispose(); ConnectionSignal = null; } } @@ -561,7 +560,7 @@ private void Dispose(bool disposing) return; if (disposing) { - CloseAsync().GetAwaiter().GetResult(); + CloseAsync().ConfigureAwait(false).GetAwaiter().GetResult(); } disposed = true; } From e9c6bcc9b7ffcc707038dbcb3b56ffd16bbcd7c6 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Mon, 30 Jun 2025 16:12:54 +0000 Subject: [PATCH 18/24] Improve message parsing --- .../LiveQueries/ParseLiveQueryController.cs | 70 ++++++++++++------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs index 4c0fb685..019e2d6a 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs @@ -166,12 +166,23 @@ private void ProcessMessage(IDictionary message) } } + private bool ValidateClientMessage(IDictionary message, out int requestId) + { + requestId = 0; + + if (!(message.TryGetValue("clientId", out object clientIdObj) && + clientIdObj is string clientId && clientId == ClientId)) + return false; + + return message.TryGetValue("requestId", out object requestIdObj) && + Int32.TryParse(requestIdObj?.ToString(), out requestId); + } + void ProcessDeleteEventMessage(IDictionary message) { - string clientId = message["clientId"] as string; - if (clientId != ClientId) + if (!ValidateClientMessage(message, out int requestId)) return; - int requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) { subscription.OnDelete(ParseObjectCoder.Instance.Decode( @@ -183,10 +194,9 @@ void ProcessDeleteEventMessage(IDictionary message) void ProcessLeaveEventMessage(IDictionary message) { - string clientId = message["clientId"] as string; - if (clientId != ClientId) + if (!ValidateClientMessage(message, out int requestId)) return; - int requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) { subscription.OnLeave( @@ -203,10 +213,9 @@ void ProcessLeaveEventMessage(IDictionary message) void ProcessUpdateEventMessage(IDictionary message) { - string clientId = message["clientId"] as string; - if (clientId != ClientId) + if (!ValidateClientMessage(message, out int requestId)) return; - int requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) { subscription.OnUpdate( @@ -223,10 +232,9 @@ void ProcessUpdateEventMessage(IDictionary message) void ProcessEnterEventMessage(IDictionary message) { - string clientId = message["clientId"] as string; - if (clientId != ClientId) + if (!ValidateClientMessage(message, out int requestId)) return; - int requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) { subscription.OnEnter( @@ -243,10 +251,9 @@ void ProcessEnterEventMessage(IDictionary message) void ProcessCreateEventMessage(IDictionary message) { - string clientId = message["clientId"] as string; - if (clientId != ClientId) + if (!ValidateClientMessage(message, out int requestId)) return; - int requestId = Convert.ToInt32(message["requestId"]); + if (Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) { subscription.OnCreate(ParseObjectCoder.Instance.Decode( @@ -258,19 +265,27 @@ void ProcessCreateEventMessage(IDictionary message) void ProcessErrorMessage(IDictionary message) { - ParseLiveQueryErrorEventArgs errorArgs = new ParseLiveQueryErrorEventArgs( - Convert.ToInt32(message["code"]), - message["error"] as string, - Convert.ToBoolean(message["reconnect"])); + if (!(message.TryGetValue("code", out object codeObj) && + Int32.TryParse(codeObj?.ToString(), out int code))) + return; + + if (!(message.TryGetValue("error", out object errorObj) && + errorObj is string error)) + return; + + if (!(message.TryGetValue("reconnect", out object reconnectObj) && + Boolean.TryParse(reconnectObj?.ToString(), out bool reconnect))) + return; + + ParseLiveQueryErrorEventArgs errorArgs = new ParseLiveQueryErrorEventArgs(code, error, reconnect); Error?.Invoke(this, errorArgs); } void ProcessUnsubscriptionMessage(IDictionary message) { - string clientId = message["clientId"] as string; - if (clientId != ClientId) + if (!ValidateClientMessage(message, out int requestId)) return; - int requestId = Convert.ToInt32(message["requestId"]); + if (UnsubscriptionSignals.TryGetValue(requestId, out TaskCompletionSource unsubscriptionSign)) { unsubscriptionSign?.TrySetResult(); @@ -279,10 +294,9 @@ void ProcessUnsubscriptionMessage(IDictionary message) void ProcessSubscriptionMessage(IDictionary message) { - string clientId = message["clientId"] as string; - if (clientId != ClientId) + if (!ValidateClientMessage(message, out int requestId)) return; - int requestId = Convert.ToInt32(message["requestId"]); + if (SubscriptionSignals.TryGetValue(requestId, out TaskCompletionSource subscriptionSignal)) { subscriptionSignal?.TrySetResult(); @@ -291,8 +305,12 @@ void ProcessSubscriptionMessage(IDictionary message) void ProcessConnectionMessage(IDictionary message) { + if (!(message.TryGetValue("clientId", out object clientIdObj) && + clientIdObj is string clientId)) + return; + + ClientId = clientId; _state = ParseLiveQueryState.Connected; - ClientId = message["clientId"] as string; ConnectionSignal.TrySetResult(); } From 8938a4dd96f7295082cd9ac0050009ce9c86ff19 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Mon, 30 Jun 2025 16:35:22 +0000 Subject: [PATCH 19/24] Improve the retrieval of data objects from a message --- .../LiveQueries/ParseLiveQueryController.cs | 116 ++++++++++-------- 1 file changed, 64 insertions(+), 52 deletions(-) diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs index 019e2d6a..ae9f8ce4 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs @@ -178,18 +178,31 @@ private bool ValidateClientMessage(IDictionary message, out int Int32.TryParse(requestIdObj?.ToString(), out requestId); } + private bool GetDictEntry(IDictionary message, string key, out IDictionary objDict) + { + if (message.TryGetValue(key, out object obj) && + obj is IDictionary dict) + { + objDict = dict; + return true; + } + + objDict = null; + return false; + } + void ProcessDeleteEventMessage(IDictionary message) { if (!ValidateClientMessage(message, out int requestId)) return; - if (Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) - { - subscription.OnDelete(ParseObjectCoder.Instance.Decode( - message["object"] as IDictionary, - Decoder, - ParseClient.Instance.Services)); - } + if (!Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) + return; + + if (!GetDictEntry(message, "object", out IDictionary objectDict)) + return; + + subscription.OnDelete(ParseObjectCoder.Instance.Decode(objectDict, Decoder, ParseClient.Instance.Services)); } void ProcessLeaveEventMessage(IDictionary message) @@ -197,18 +210,18 @@ void ProcessLeaveEventMessage(IDictionary message) if (!ValidateClientMessage(message, out int requestId)) return; - if (Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) - { - subscription.OnLeave( - ParseObjectCoder.Instance.Decode( - message["object"] as IDictionary, - Decoder, - ParseClient.Instance.Services), - ParseObjectCoder.Instance.Decode( - message["original"] as IDictionary, - Decoder, - ParseClient.Instance.Services)); - } + if (!Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) + return; + + if (!GetDictEntry(message, "object", out IDictionary objectDict)) + return; + + if (!GetDictEntry(message, "original", out IDictionary originalDict)) + return; + + subscription.OnLeave( + ParseObjectCoder.Instance.Decode(objectDict, Decoder, ParseClient.Instance.Services), + ParseObjectCoder.Instance.Decode(originalDict, Decoder, ParseClient.Instance.Services)); } void ProcessUpdateEventMessage(IDictionary message) @@ -216,18 +229,18 @@ void ProcessUpdateEventMessage(IDictionary message) if (!ValidateClientMessage(message, out int requestId)) return; - if (Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) - { - subscription.OnUpdate( - ParseObjectCoder.Instance.Decode( - message["object"] as IDictionary, - Decoder, - ParseClient.Instance.Services), - ParseObjectCoder.Instance.Decode( - message["original"] as IDictionary, - Decoder, - ParseClient.Instance.Services)); - } + if (!Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) + return; + + if (!GetDictEntry(message, "object", out IDictionary objectDict)) + return; + + if (!GetDictEntry(message, "original", out IDictionary originalDict)) + return; + + subscription.OnUpdate( + ParseObjectCoder.Instance.Decode(objectDict, Decoder, ParseClient.Instance.Services), + ParseObjectCoder.Instance.Decode(originalDict, Decoder, ParseClient.Instance.Services)); } void ProcessEnterEventMessage(IDictionary message) @@ -235,18 +248,18 @@ void ProcessEnterEventMessage(IDictionary message) if (!ValidateClientMessage(message, out int requestId)) return; - if (Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) - { - subscription.OnEnter( - ParseObjectCoder.Instance.Decode( - message["object"] as IDictionary, - Decoder, - ParseClient.Instance.Services), - ParseObjectCoder.Instance.Decode( - message["original"] as IDictionary, - Decoder, - ParseClient.Instance.Services)); - } + if (!Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) + return; + + if (!GetDictEntry(message, "object", out IDictionary objectDict)) + return; + + if (!GetDictEntry(message, "original", out IDictionary originalDict)) + return; + + subscription.OnEnter( + ParseObjectCoder.Instance.Decode(objectDict, Decoder, ParseClient.Instance.Services), + ParseObjectCoder.Instance.Decode(originalDict, Decoder, ParseClient.Instance.Services)); } void ProcessCreateEventMessage(IDictionary message) @@ -254,13 +267,13 @@ void ProcessCreateEventMessage(IDictionary message) if (!ValidateClientMessage(message, out int requestId)) return; - if (Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) - { - subscription.OnCreate(ParseObjectCoder.Instance.Decode( - message["object"] as IDictionary, - Decoder, - ParseClient.Instance.Services)); - } + if (!Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) + return; + + if (!GetDictEntry(message, "object", out IDictionary objectDict)) + return; + + subscription.OnCreate(ParseObjectCoder.Instance.Decode(objectDict, Decoder, ParseClient.Instance.Services)); } void ProcessErrorMessage(IDictionary message) @@ -277,8 +290,7 @@ void ProcessErrorMessage(IDictionary message) Boolean.TryParse(reconnectObj?.ToString(), out bool reconnect))) return; - ParseLiveQueryErrorEventArgs errorArgs = new ParseLiveQueryErrorEventArgs(code, error, reconnect); - Error?.Invoke(this, errorArgs); + Error?.Invoke(this, new ParseLiveQueryErrorEventArgs(code, error, reconnect)); } void ProcessUnsubscriptionMessage(IDictionary message) From b65e230a04aa1c37824af3ca78b83fab9c5d5109 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Mon, 30 Jun 2025 16:41:20 +0000 Subject: [PATCH 20/24] Null safety and small changes --- .../LiveQueries/ParseLiveQueryController.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs index ae9f8ce4..8abc4cee 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs @@ -196,10 +196,10 @@ void ProcessDeleteEventMessage(IDictionary message) if (!ValidateClientMessage(message, out int requestId)) return; - if (!Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) + if (!GetDictEntry(message, "object", out IDictionary objectDict)) return; - if (!GetDictEntry(message, "object", out IDictionary objectDict)) + if (!Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) return; subscription.OnDelete(ParseObjectCoder.Instance.Decode(objectDict, Decoder, ParseClient.Instance.Services)); @@ -210,15 +210,15 @@ void ProcessLeaveEventMessage(IDictionary message) if (!ValidateClientMessage(message, out int requestId)) return; - if (!Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) - return; - if (!GetDictEntry(message, "object", out IDictionary objectDict)) return; if (!GetDictEntry(message, "original", out IDictionary originalDict)) return; + if (!Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) + return; + subscription.OnLeave( ParseObjectCoder.Instance.Decode(objectDict, Decoder, ParseClient.Instance.Services), ParseObjectCoder.Instance.Decode(originalDict, Decoder, ParseClient.Instance.Services)); @@ -229,15 +229,15 @@ void ProcessUpdateEventMessage(IDictionary message) if (!ValidateClientMessage(message, out int requestId)) return; - if (!Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) - return; - if (!GetDictEntry(message, "object", out IDictionary objectDict)) return; if (!GetDictEntry(message, "original", out IDictionary originalDict)) return; + if (!Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) + return; + subscription.OnUpdate( ParseObjectCoder.Instance.Decode(objectDict, Decoder, ParseClient.Instance.Services), ParseObjectCoder.Instance.Decode(originalDict, Decoder, ParseClient.Instance.Services)); @@ -248,15 +248,15 @@ void ProcessEnterEventMessage(IDictionary message) if (!ValidateClientMessage(message, out int requestId)) return; - if (!Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) - return; - if (!GetDictEntry(message, "object", out IDictionary objectDict)) return; if (!GetDictEntry(message, "original", out IDictionary originalDict)) return; + if (!Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) + return; + subscription.OnEnter( ParseObjectCoder.Instance.Decode(objectDict, Decoder, ParseClient.Instance.Services), ParseObjectCoder.Instance.Decode(originalDict, Decoder, ParseClient.Instance.Services)); @@ -267,10 +267,10 @@ void ProcessCreateEventMessage(IDictionary message) if (!ValidateClientMessage(message, out int requestId)) return; - if (!Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) + if (!GetDictEntry(message, "object", out IDictionary objectDict)) return; - if (!GetDictEntry(message, "object", out IDictionary objectDict)) + if (!Subscriptions.TryGetValue(requestId, out IParseLiveQuerySubscription subscription)) return; subscription.OnCreate(ParseObjectCoder.Instance.Decode(objectDict, Decoder, ParseClient.Instance.Services)); @@ -323,7 +323,7 @@ void ProcessConnectionMessage(IDictionary message) ClientId = clientId; _state = ParseLiveQueryState.Connected; - ConnectionSignal.TrySetResult(); + ConnectionSignal?.TrySetResult(); } private async Task> AppendSessionToken(IDictionary message) From cc5168cd177d75358e5336d3695d1b1406e2e85e Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Mon, 30 Jun 2025 16:50:54 +0000 Subject: [PATCH 21/24] Improve controller disposal --- .../LiveQueries/ParseLiveQueryController.cs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs index 8abc4cee..5b7abe29 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs @@ -17,7 +17,7 @@ namespace Parse.Platform.LiveQueries; /// The ParseLiveQueryController is responsible for managing live query subscriptions, maintaining a connection /// to the Parse LiveQuery server, and handling real-time updates from the server. /// -public class ParseLiveQueryController : IParseLiveQueryController, IDisposable +public class ParseLiveQueryController : IParseLiveQueryController, IDisposable, IAsyncDisposable { private IParseDataDecoder Decoder { get; } private IWebSocketClient WebSocketClient { get; } @@ -577,6 +577,22 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Asynchronously releases the resources used by the instance. + /// + /// + /// A representing the asynchronous dispose operation. + /// + /// + /// This method is called to perform an asynchronous disposal of the resources held by the current + /// instance. It suppresses finalization of the object to optimize resource cleanup. + /// + public async ValueTask DisposeAsync() + { + await CloseAsync(); + GC.SuppressFinalize(this); + } + /// /// Releases the resources used by the instance. /// @@ -590,7 +606,8 @@ private void Dispose(bool disposing) return; if (disposing) { - CloseAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + // For sync disposal, the best effort cleanup without waiting + _ = Task.Run(async () => await CloseAsync()); } disposed = true; } From e70789e5c7887279d9eece3783481901c70ce7ee Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Mon, 30 Jun 2025 16:57:52 +0000 Subject: [PATCH 22/24] Fix race conditions --- .../LiveQueries/ParseLiveQueryController.cs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs index 5b7abe29..8b53d8f6 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs @@ -117,10 +117,10 @@ public enum ParseLiveQueryState /// public ParseLiveQueryController(int timeOut, IWebSocketClient webSocketClient, IParseDataDecoder decoder) { + WebSocketClient = webSocketClient ?? throw new ArgumentNullException(nameof(webSocketClient)); + Decoder = decoder ?? throw new ArgumentNullException(nameof(decoder)); TimeOut = timeOut; - WebSocketClient = webSocketClient; _state = ParseLiveQueryState.Closed; - Decoder = decoder; } private void ProcessMessage(IDictionary message) @@ -413,9 +413,13 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) ConnectionSignal = null; } } - else if (_state == ParseLiveQueryState.Connecting && ConnectionSignal is not null) + else if (_state == ParseLiveQueryState.Connecting) { - await ConnectionSignal.Task.WaitAsync(cancellationToken); + TaskCompletionSource signal = ConnectionSignal; + if (signal is not null) + { + await signal.Task.WaitAsync(cancellationToken); + } } } @@ -607,7 +611,17 @@ private void Dispose(bool disposing) if (disposing) { // For sync disposal, the best effort cleanup without waiting - _ = Task.Run(async () => await CloseAsync()); + _ = Task.Run(async () => + { + try + { + await CloseAsync(); + } + catch (Exception ex) + { + Debug.WriteLine($"Error during disposal: {ex}"); + } + }); } disposed = true; } From 2cfef04b13bf2482821352378a5bd237177f79a1 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Mon, 30 Jun 2025 18:30:46 +0000 Subject: [PATCH 23/24] Websocket exception handling --- .../Execution/IWebSocketClient.cs | 23 +++++++++++++- .../Execution/MessageReceivedEventArgs.cs | 14 +++++++++ .../Execution/TextWebSocketClient.cs | 20 ++++++++----- .../LiveQueries/ParseLiveQueryController.cs | 30 +++++++++++++++++-- .../ParseLiveQueryDualEventArgs.cs | 2 +- .../ParseLiveQueryErrorEventArgs.cs | 11 ++++--- .../LiveQueries/ParseLiveQueryEventArgs.cs | 2 +- 7 files changed, 85 insertions(+), 17 deletions(-) create mode 100644 Parse/Infrastructure/Execution/MessageReceivedEventArgs.cs diff --git a/Parse/Abstractions/Infrastructure/Execution/IWebSocketClient.cs b/Parse/Abstractions/Infrastructure/Execution/IWebSocketClient.cs index 0ecb9a23..c5343c59 100644 --- a/Parse/Abstractions/Infrastructure/Execution/IWebSocketClient.cs +++ b/Parse/Abstractions/Infrastructure/Execution/IWebSocketClient.cs @@ -1,6 +1,8 @@ using System; +using System.IO; using System.Threading; using System.Threading.Tasks; +using Parse.Infrastructure.Execution; namespace Parse.Abstractions.Infrastructure.Execution; @@ -16,7 +18,26 @@ public interface IWebSocketClient /// The event handler receives the message as a string parameter. This can be used to process incoming /// WebSocket messages, such as notifications, commands, or data updates. /// - public event EventHandler MessageReceived; + public event EventHandler MessageReceived; + + /// + /// An event that is triggered when an error occurs during the WebSocket operation. + /// + /// + /// This event communicates WebSocket-specific errors along with additional details encapsulated in + /// the object. It can be used to handle and log errors during WebSocket + /// communication or connection lifecycle. + /// + public event EventHandler WebsocketError; + + /// + /// An event that is triggered when an unknown or unexpected error occurs during WebSocket communication. + /// + /// + /// This event can be used to handle errors that do not fall under typical WebSocket error events. The event + /// handler receives an parameter containing details about the error. + /// + public event EventHandler UnknownError; /// /// Establishes a WebSocket connection to the specified server URI. diff --git a/Parse/Infrastructure/Execution/MessageReceivedEventArgs.cs b/Parse/Infrastructure/Execution/MessageReceivedEventArgs.cs new file mode 100644 index 00000000..f4342cfc --- /dev/null +++ b/Parse/Infrastructure/Execution/MessageReceivedEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Parse.Infrastructure.Execution; + +/// +/// Provides data for the event that is triggered when a message is received. +/// +public class MessageReceivedEventArgs(string message) : EventArgs +{ + /// + /// Gets the message content that was received. + /// + public string Message { get; } = message; +} \ No newline at end of file diff --git a/Parse/Infrastructure/Execution/TextWebSocketClient.cs b/Parse/Infrastructure/Execution/TextWebSocketClient.cs index 6ef176bc..916e65db 100644 --- a/Parse/Infrastructure/Execution/TextWebSocketClient.cs +++ b/Parse/Infrastructure/Execution/TextWebSocketClient.cs @@ -1,5 +1,7 @@ using System; using System.Diagnostics; +using System.IO; +using System.Net; using System.Net.WebSockets; using System.Text; using System.Threading; @@ -39,7 +41,9 @@ class TextWebSocketClient(int bufferSize) : IWebSocketClient /// represented as a string. Handlers for this event can process or respond to the message /// based on the application's requirements. /// - public event EventHandler MessageReceived; + public event EventHandler MessageReceived; + public event EventHandler WebsocketError; + public event EventHandler UnknownError; private readonly object connectionLock = new object(); @@ -111,7 +115,7 @@ private async Task ListenForMessages(CancellationToken cancellationToken) if (result.EndOfMessage) { string message = Encoding.UTF8.GetString(buffer, 0, result.Count); - MessageReceived?.Invoke(this, message); + MessageReceived?.Invoke(this, new MessageReceivedEventArgs(message)); } else { @@ -126,7 +130,7 @@ private async Task ListenForMessages(CancellationToken cancellationToken) messageBuilder.Append(Encoding.UTF8.GetString(buffer, 0, result.Count)); } string fullMessage = messageBuilder.ToString(); - MessageReceived?.Invoke(this, fullMessage); + MessageReceived?.Invoke(this, new MessageReceivedEventArgs(fullMessage));; } } } @@ -135,15 +139,17 @@ private async Task ListenForMessages(CancellationToken cancellationToken) // Normal cancellation, no need to handle Debug.WriteLine($"Websocket connection was closed: {ex.Message}"); } - catch (WebSocketException e) + catch (WebSocketException ex) { // WebSocket error, notify the user - Debug.WriteLine($"Websocket error: {e.Message}"); + Debug.WriteLine($"Websocket error ({ex.ErrorCode}): {ex.Message}"); + WebsocketError?.Invoke(this, new ErrorEventArgs(ex)); } - catch (Exception e) + catch (Exception ex) { // Unexpected error, notify the user - Debug.WriteLine($"Unexpected error in Websocket listener: {e.Message}"); + Debug.WriteLine($"Unexpected error in Websocket listener: {ex.Message}"); + UnknownError?.Invoke(this, new ErrorEventArgs(ex)); } Debug.WriteLine("Websocket ListenForMessage stopped"); } diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs index 8b53d8f6..cc9e2817 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryController.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryController.cs @@ -2,13 +2,16 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; +using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using Parse.Abstractions.Infrastructure.Data; using Parse.Abstractions.Infrastructure.Execution; using Parse.Abstractions.Platform.LiveQueries; using Parse.Infrastructure.Data; +using Parse.Infrastructure.Execution; using Parse.Infrastructure.Utilities; namespace Parse.Platform.LiveQueries; @@ -133,6 +136,7 @@ private void ProcessMessage(IDictionary message) switch (op) { + // CONNECTION case "connected": ProcessConnectionMessage(message); break; @@ -354,16 +358,16 @@ private async Task OpenAsync(CancellationToken cancellationToken = default) await WebSocketClient.OpenAsync(ParseClient.Instance.Services.LiveQueryServerConnectionData.ServerURI, cancellationToken); } - private void WebSocketClientOnMessageReceived(object sender, string e) + private void WebSocketClientOnMessageReceived(object sender, MessageReceivedEventArgs args) { - object parsed = JsonUtilities.Parse(e); + object parsed = JsonUtilities.Parse(args.Message); if (parsed is IDictionary message) { ProcessMessage(message); } else { - Debug.WriteLine($"Invalid message format received: {e}"); + Debug.WriteLine($"Invalid message format received: {args.Message}"); } } @@ -386,6 +390,8 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) _state = ParseLiveQueryState.Connecting; await OpenAsync(cancellationToken); WebSocketClient.MessageReceived += WebSocketClientOnMessageReceived; + WebSocketClient.WebsocketError += WebSocketClientOnWebsocketError; + WebSocketClient.UnknownError += WebSocketClientOnUnknownError; Dictionary message = new Dictionary { { "op", "connect" }, @@ -423,6 +429,22 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) } } + void WebSocketClientOnWebsocketError(object sender, ErrorEventArgs args) + { + if (args.GetException() is WebSocketException ex) + { + Error?.Invoke(this, new ParseLiveQueryErrorEventArgs(ex.ErrorCode, ex.Message, false, ex)); + } + } + + void WebSocketClientOnUnknownError(object sender, ErrorEventArgs args) + { + if (args.GetException() is { } ex) + { + Error?.Invoke(this, new ParseLiveQueryErrorEventArgs(-1, ex.Message, false, ex)); + } + } + private async Task SendAndWaitForSignalAsync(IDictionary message, ConcurrentDictionary signalDictionary, int requestId, @@ -560,6 +582,8 @@ public async Task UnsubscribeAsync(int requestId, CancellationToken cancellation public async Task CloseAsync(CancellationToken cancellationToken = default) { WebSocketClient.MessageReceived -= WebSocketClientOnMessageReceived; + WebSocketClient.WebsocketError -= WebSocketClientOnWebsocketError; + WebSocketClient.UnknownError -= WebSocketClientOnUnknownError; await WebSocketClient.CloseAsync(cancellationToken); _state = ParseLiveQueryState.Closed; SubscriptionSignals.Clear(); diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryDualEventArgs.cs b/Parse/Platform/LiveQueries/ParseLiveQueryDualEventArgs.cs index d8ff1347..78b820c8 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryDualEventArgs.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryDualEventArgs.cs @@ -15,7 +15,7 @@ public class ParseLiveQueryDualEventArgs : ParseLiveQueryEventArgs /// providing a snapshot of its previous state for comparison purposes during events /// such as updates or deletes. /// - public ParseObject Original { get; private set; } + public ParseObject Original { get; } internal ParseLiveQueryDualEventArgs(ParseObject current, ParseObject original) : base(current) => Original = original ?? throw new ArgumentNullException(nameof(original)); diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryErrorEventArgs.cs b/Parse/Platform/LiveQueries/ParseLiveQueryErrorEventArgs.cs index cf150692..90d225e2 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryErrorEventArgs.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryErrorEventArgs.cs @@ -15,7 +15,7 @@ public class ParseLiveQueryErrorEventArgs : EventArgs /// a live query operation. It can provide detailed information about the nature of the issue, /// which can be helpful for debugging or logging purposes. /// - public string Error { get; private set; } + public string Error { get; } /// /// Gets or sets the error code associated with a live query operation. @@ -25,7 +25,7 @@ public class ParseLiveQueryErrorEventArgs : EventArgs /// the type or category of the error that occurred during a live query operation. /// This is used alongside the error message to provide detailed diagnostics or logging. /// - public int Code { get; private set; } + public int Code { get; } /// /// Gets or sets a value indicating whether the client should attempt to reconnect @@ -37,15 +37,18 @@ public class ParseLiveQueryErrorEventArgs : EventArgs /// This can be used to determine the client's behavior in maintaining a continuous /// connection with the server. /// - public bool Reconnect { get; private set; } + public bool Reconnect { get; } + + public Exception LocalException { get; } /// /// Represents the arguments for an error event that occurs during a live query in the Parse platform. /// - internal ParseLiveQueryErrorEventArgs(int code, string error, bool reconnect) + internal ParseLiveQueryErrorEventArgs(int code, string error, bool reconnect, Exception localException = null) { Error = error; Code = code; Reconnect = reconnect; + LocalException = localException; } } \ No newline at end of file diff --git a/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs b/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs index 6817e2ac..aed7c9f6 100644 --- a/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs +++ b/Parse/Platform/LiveQueries/ParseLiveQueryEventArgs.cs @@ -15,7 +15,7 @@ public class ParseLiveQueryEventArgs : EventArgs /// the event was triggered, reflecting any changes made during operations such as /// an update or creation. /// - public ParseObject Object { get; private set; } + public ParseObject Object { get; } internal ParseLiveQueryEventArgs(ParseObject current) => Object = current ?? throw new ArgumentNullException(nameof(current)); } From 97313bf2e06c14b57078079374c024e530836eb8 Mon Sep 17 00:00:00 2001 From: Sylvain Kamdem Date: Mon, 30 Jun 2025 18:36:48 +0000 Subject: [PATCH 24/24] Small clean up --- Parse/Infrastructure/Execution/TextWebSocketClient.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Parse/Infrastructure/Execution/TextWebSocketClient.cs b/Parse/Infrastructure/Execution/TextWebSocketClient.cs index 916e65db..0b780f36 100644 --- a/Parse/Infrastructure/Execution/TextWebSocketClient.cs +++ b/Parse/Infrastructure/Execution/TextWebSocketClient.cs @@ -1,7 +1,6 @@ using System; using System.Diagnostics; using System.IO; -using System.Net; using System.Net.WebSockets; using System.Text; using System.Threading; @@ -130,7 +129,7 @@ private async Task ListenForMessages(CancellationToken cancellationToken) messageBuilder.Append(Encoding.UTF8.GetString(buffer, 0, result.Count)); } string fullMessage = messageBuilder.ToString(); - MessageReceived?.Invoke(this, new MessageReceivedEventArgs(fullMessage));; + MessageReceived?.Invoke(this, new MessageReceivedEventArgs(fullMessage)); } } }