Skip to content

Commit e0d63ab

Browse files
committed
Add ParseLiveQuery and dependencies
1 parent a9c1855 commit e0d63ab

File tree

14 files changed

+793
-1
lines changed

14 files changed

+793
-1
lines changed

Parse/Abstractions/Infrastructure/CustomServiceHub.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Parse.Abstractions.Platform.Configuration;
66
using Parse.Abstractions.Platform.Files;
77
using Parse.Abstractions.Platform.Installations;
8+
using Parse.Abstractions.Platform.LiveQueries;
89
using Parse.Abstractions.Platform.Objects;
910
using Parse.Abstractions.Platform.Push;
1011
using Parse.Abstractions.Platform.Queries;
@@ -41,6 +42,8 @@ public abstract class CustomServiceHub : ICustomServiceHub
4142

4243
public virtual IParseQueryController QueryController => Services.QueryController;
4344

45+
public virtual IParseLiveQueryController LiveQueryController => Services.LiveQueryController;
46+
4447
public virtual IParseSessionController SessionController => Services.SessionController;
4548

4649
public virtual IParseUserController UserController => Services.UserController;
@@ -59,6 +62,8 @@ public abstract class CustomServiceHub : ICustomServiceHub
5962

6063
public virtual IServerConnectionData ServerConnectionData => Services.ServerConnectionData;
6164

65+
public virtual IServerConnectionData LiveQueryServerConnectionData => Services.ServerConnectionData;
66+
6267
public virtual IParseDataDecoder Decoder => Services.Decoder;
6368

6469
public virtual IParseInstallationDataFinalizer InstallationDataFinalizer => Services.InstallationDataFinalizer;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
5+
namespace Parse.Abstractions.Infrastructure.Execution;
6+
7+
/// <summary>
8+
/// Represents an interface for a WebSocket client to handle WebSocket connections and communications.
9+
/// </summary>
10+
public interface IWebSocketClient
11+
{
12+
/// <summary>
13+
/// An event that is triggered when a message is received via the WebSocket connection.
14+
/// </summary>
15+
/// <remarks>
16+
/// The event handler receives the message as a string parameter. This can be used to process incoming
17+
/// WebSocket messages, such as notifications, commands, or data updates.
18+
/// </remarks>
19+
public event EventHandler<string> MessageReceived;
20+
21+
/// <summary>
22+
/// Establishes a WebSocket connection to the specified server URI.
23+
/// </summary>
24+
/// <param name="serverUri">The URI of the WebSocket server to connect to.</param>
25+
/// <param name="cancellationToken">
26+
/// A token to observe cancellation requests. The operation will stop if the token is canceled.
27+
/// </param>
28+
/// <returns>A task that represents the asynchronous operation of opening the WebSocket connection.</returns>
29+
public Task OpenAsync(string serverUri, CancellationToken cancellationToken = default);
30+
31+
/// <summary>
32+
/// Closes the active WebSocket connection asynchronously.
33+
/// </summary>
34+
/// <param name="cancellationToken">
35+
/// A token to observe cancellation requests. The operation will stop if the token is canceled.
36+
/// </param>
37+
/// <returns>A task that represents the asynchronous operation of closing the WebSocket connection.</returns>
38+
public Task CloseAsync(CancellationToken cancellationToken = default);
39+
40+
/// <summary>
41+
/// Sends a message over the established WebSocket connection asynchronously.
42+
/// </summary>
43+
/// <param name="message">The message to send through the WebSocket connection.</param>
44+
/// <param name="cancellationToken">
45+
/// A token to observe cancellation requests. The operation will stop if the token is canceled.
46+
/// </param>
47+
/// <returns>A task that represents the asynchronous operation of sending the message.</returns>
48+
public Task SendAsync(string message, CancellationToken cancellationToken);
49+
}

Parse/Abstractions/Infrastructure/IMutableServiceHub.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Parse.Abstractions.Platform.Configuration;
88
using Parse.Abstractions.Platform.Files;
99
using Parse.Abstractions.Platform.Installations;
10+
using Parse.Abstractions.Platform.LiveQueries;
1011
using Parse.Abstractions.Platform.Objects;
1112
using Parse.Abstractions.Platform.Push;
1213
using Parse.Abstractions.Platform.Queries;
@@ -36,6 +37,7 @@ public interface IMutableServiceHub : IServiceHub
3637
IParseFileController FileController { set; }
3738
IParseObjectController ObjectController { set; }
3839
IParseQueryController QueryController { set; }
40+
IParseLiveQueryController LiveQueryController { set; }
3941
IParseSessionController SessionController { set; }
4042
IParseUserController UserController { set; }
4143
IParseCurrentUserController CurrentUserController { set; }

Parse/Abstractions/Infrastructure/IServiceHub.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Parse.Abstractions.Platform.Configuration;
88
using Parse.Abstractions.Platform.Files;
99
using Parse.Abstractions.Platform.Installations;
10+
using Parse.Abstractions.Platform.LiveQueries;
1011
using Parse.Abstractions.Platform.Objects;
1112
using Parse.Abstractions.Platform.Push;
1213
using Parse.Abstractions.Platform.Queries;
@@ -23,9 +24,10 @@ namespace Parse.Abstractions.Infrastructure;
2324
public interface IServiceHub
2425
{
2526
/// <summary>
26-
/// The current server connection data that the the Parse SDK has been initialized with.
27+
/// The current server connection data that the Parse SDK has been initialized with.
2728
/// </summary>
2829
IServerConnectionData ServerConnectionData { get; }
30+
IServerConnectionData LiveQueryServerConnectionData { get; }
2931
IMetadataController MetadataController { get; }
3032

3133
IServiceHubCloner Cloner { get; }
@@ -44,6 +46,7 @@ public interface IServiceHub
4446
IParseFileController FileController { get; }
4547
IParseObjectController ObjectController { get; }
4648
IParseQueryController QueryController { get; }
49+
IParseLiveQueryController LiveQueryController { get; }
4750
IParseSessionController SessionController { get; }
4851
IParseUserController UserController { get; }
4952
IParseCurrentUserController CurrentUserController { get; }
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System.Threading;
2+
using System.Threading.Tasks;
3+
using Parse.Abstractions.Platform.Objects;
4+
5+
namespace Parse.Abstractions.Platform.LiveQueries;
6+
7+
public interface IParseLiveQueryController
8+
{
9+
Task ConnectAsync(CancellationToken cancellationToken = default);
10+
11+
Task<int> SubscribeAsync<T>(ParseLiveQuery<T> liveQuery, CancellationToken cancellationToken = default) where T : ParseObject;
12+
13+
Task UpdateSubscriptionAsync<T>(ParseLiveQuery<T> liveQuery, int requestId, CancellationToken cancellationToken = default) where T : ParseObject;
14+
15+
Task UnsubscribeAsync(int requestId, CancellationToken cancellationToken = default);
16+
17+
Task CloseAsync(CancellationToken cancellationToken = default);
18+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Net.WebSockets;
4+
using System.Text;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Parse.Abstractions.Infrastructure.Execution;
8+
9+
namespace Parse.Infrastructure.Execution;
10+
11+
/// <summary>
12+
/// Represents a WebSocket client that allows connecting to a WebSocket server, sending messages, and receiving messages.
13+
/// Implements the <c>IWebSocketClient</c> interface for WebSocket operations.
14+
/// </summary>
15+
class TextWebSocketClient : IWebSocketClient
16+
{
17+
/// <summary>
18+
/// A private instance of the ClientWebSocket class used to manage the WebSocket connection.
19+
/// This variable is responsible for handling the low-level WebSocket communication, including
20+
/// connecting, sending, and receiving data from the WebSocket server. It is initialized
21+
/// when establishing a connection and is used internally for operations such as sending messages
22+
/// and listening for incoming data.
23+
/// </summary>
24+
private ClientWebSocket _webSocket;
25+
26+
/// <summary>
27+
/// A private instance of the Task class representing the background operation
28+
/// responsible for continuously listening for incoming WebSocket messages.
29+
/// This task is used to manage the asynchronous listening process, ensuring that
30+
/// messages are received from the WebSocket server without blocking the main thread.
31+
/// It is initialized when the listening process starts and monitored to prevent
32+
/// multiple concurrent listeners from being created.
33+
/// </summary>
34+
private Task _listeningTask;
35+
36+
/// <summary>
37+
/// An event triggered whenever a message is received from the WebSocket server.
38+
/// This event is used to notify subscribers with the content of the received message,
39+
/// represented as a string. Handlers for this event can process or respond to the message
40+
/// based on the application's requirements.
41+
/// </summary>
42+
public event EventHandler<string> MessageReceived;
43+
44+
/// <summary>
45+
/// Opens a WebSocket connection to the specified server URI and starts listening for messages.
46+
/// If the connection is already open or in a connecting state, this method does nothing.
47+
/// </summary>
48+
/// <param name="serverUri">The URI of the WebSocket server to connect to.</param>
49+
/// <param name="cancellationToken">A cancellation token that can be used to cancel the connect operation.</param>
50+
/// <returns>
51+
/// A task representing the asynchronous operation of connecting to the WebSocket server.
52+
/// </returns>
53+
public async Task OpenAsync(string serverUri, CancellationToken cancellationToken = default)
54+
{
55+
_webSocket ??= new ClientWebSocket();
56+
57+
if (_webSocket.State != WebSocketState.Open && _webSocket.State != WebSocketState.Connecting)
58+
{
59+
await _webSocket.ConnectAsync(new Uri(serverUri), cancellationToken);
60+
StartListening(cancellationToken);
61+
}
62+
}
63+
64+
/// <summary>
65+
/// Closes the WebSocket connection gracefully with a normal closure status.
66+
/// Ensures that the WebSocket connection is properly terminated and resources are released.
67+
/// </summary>
68+
/// <param name="cancellationToken">A cancellation token that can be used to cancel the close operation.</param>
69+
/// <returns>
70+
/// A task representing the asynchronous operation of closing the WebSocket connection.
71+
/// </returns>
72+
public async Task CloseAsync(CancellationToken cancellationToken = default)
73+
=> await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, String.Empty, cancellationToken);
74+
75+
private async Task ListenForMessages(CancellationToken cancellationToken)
76+
{
77+
byte[] buffer = new byte[1024 * 4];
78+
79+
try
80+
{
81+
while (!cancellationToken.IsCancellationRequested &&
82+
_webSocket.State == WebSocketState.Open)
83+
{
84+
WebSocketReceiveResult result = await _webSocket.ReceiveAsync(
85+
new ArraySegment<byte>(buffer),
86+
cancellationToken);
87+
88+
if (result.MessageType == WebSocketMessageType.Close)
89+
{
90+
await CloseAsync(cancellationToken);
91+
break;
92+
}
93+
94+
string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
95+
MessageReceived?.Invoke(this, message);
96+
}
97+
}
98+
catch (OperationCanceledException ex)
99+
{
100+
// Normal cancellation, no need to handle
101+
Debug.WriteLine($"ClientWebsocket connection was closed: {ex.Message}");
102+
}
103+
}
104+
105+
106+
/// <summary>
107+
/// Starts listening for incoming messages from the WebSocket connection. This method ensures that only one listener task is running at a time.
108+
/// </summary>
109+
/// <param name="cancellationToken">A cancellation token to signal the listener task to stop.</param>
110+
private void StartListening(CancellationToken cancellationToken)
111+
{
112+
// Make sure we don't start multiple listeners
113+
if (_listeningTask is { IsCompleted: false })
114+
{
115+
return;
116+
}
117+
118+
// Start the listener task
119+
_listeningTask = Task.Run(async () =>
120+
{
121+
if (cancellationToken.IsCancellationRequested)
122+
{
123+
cancellationToken.ThrowIfCancellationRequested();
124+
}
125+
126+
await ListenForMessages(cancellationToken);
127+
}, cancellationToken);
128+
}
129+
130+
/// <summary>
131+
/// Sends a text message to the connected WebSocket server asynchronously.
132+
/// The message is encoded in UTF-8 format before being sent.
133+
/// </summary>
134+
/// <param name="message">The message to be sent to the WebSocket server.</param>
135+
/// <param name="cancellationToken">A cancellation token that can be used to cancel the send operation.</param>
136+
/// <returns>
137+
/// A task representing the asynchronous operation of sending the message to the WebSocket server.
138+
/// </returns>
139+
public async Task SendAsync(string message, CancellationToken cancellationToken = default)
140+
{
141+
if (_webSocket is not null && _webSocket.State == WebSocketState.Open)
142+
await _webSocket.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, cancellationToken);
143+
}
144+
}

Parse/Infrastructure/LateInitializedMutableServiceHub.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Parse.Abstractions.Platform.Sessions;
1313
using Parse.Abstractions.Platform.Users;
1414
using Parse.Abstractions.Platform.Analytics;
15+
using Parse.Abstractions.Platform.LiveQueries;
1516
using Parse.Infrastructure.Execution;
1617
using Parse.Platform.Objects;
1718
using Parse.Platform.Installations;
@@ -25,6 +26,7 @@
2526
using Parse.Platform.Push;
2627
using Parse.Infrastructure.Data;
2728
using Parse.Infrastructure.Utilities;
29+
using Parse.Platform.LiveQueries;
2830

2931
namespace Parse.Infrastructure;
3032

@@ -46,6 +48,12 @@ public IWebClient WebClient
4648
set => LateInitializer.SetValue(value);
4749
}
4850

51+
public IWebSocketClient WebSocketClient
52+
{
53+
get => LateInitializer.GetValue<IWebSocketClient>(() => new TextWebSocketClient { });
54+
set => LateInitializer.SetValue(value);
55+
}
56+
4957
public ICacheController CacheController
5058
{
5159
get => LateInitializer.GetValue<ICacheController>(() => new CacheController { });
@@ -100,6 +108,12 @@ public IParseQueryController QueryController
100108
set => LateInitializer.SetValue(value);
101109
}
102110

111+
public IParseLiveQueryController LiveQueryController
112+
{
113+
get => LateInitializer.GetValue<IParseLiveQueryController>(() => new ParseLiveQueryController(WebSocketClient));
114+
set => LateInitializer.SetValue(value);
115+
}
116+
103117
public IParseSessionController SessionController
104118
{
105119
get => LateInitializer.GetValue<IParseSessionController>(() => new ParseSessionController(CommandRunner, Decoder));
@@ -161,4 +175,5 @@ public IParseInstallationDataFinalizer InstallationDataFinalizer
161175
}
162176

163177
public IServerConnectionData ServerConnectionData { get; set; }
178+
public IServerConnectionData LiveQueryServerConnectionData { get; set; }
164179
}

Parse/Infrastructure/MutableServiceHub.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Parse.Abstractions.Platform.Configuration;
88
using Parse.Abstractions.Platform.Files;
99
using Parse.Abstractions.Platform.Installations;
10+
using Parse.Abstractions.Platform.LiveQueries;
1011
using Parse.Abstractions.Platform.Objects;
1112
using Parse.Abstractions.Platform.Push;
1213
using Parse.Abstractions.Platform.Queries;
@@ -34,6 +35,7 @@ namespace Parse.Infrastructure;
3435
public class MutableServiceHub : IMutableServiceHub
3536
{
3637
public IServerConnectionData ServerConnectionData { get; set; }
38+
public IServerConnectionData LiveQueryServerConnectionData { get; set; }
3739
public IMetadataController MetadataController { get; set; }
3840

3941
public IServiceHubCloner Cloner { get; set; }
@@ -52,6 +54,7 @@ public class MutableServiceHub : IMutableServiceHub
5254
public IParseFileController FileController { get; set; }
5355
public IParseObjectController ObjectController { get; set; }
5456
public IParseQueryController QueryController { get; set; }
57+
public IParseLiveQueryController LiveQueryController { get; set; }
5558
public IParseSessionController SessionController { get; set; }
5659
public IParseUserController UserController { get; set; }
5760
public IParseCurrentUserController CurrentUserController { get; set; }

Parse/Infrastructure/OrchestrationServiceHub.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Parse.Abstractions.Platform.Configuration;
77
using Parse.Abstractions.Platform.Files;
88
using Parse.Abstractions.Platform.Installations;
9+
using Parse.Abstractions.Platform.LiveQueries;
910
using Parse.Abstractions.Platform.Objects;
1011
using Parse.Abstractions.Platform.Push;
1112
using Parse.Abstractions.Platform.Queries;
@@ -44,6 +45,8 @@ public class OrchestrationServiceHub : IServiceHub
4445

4546
public IParseQueryController QueryController => Custom.QueryController ?? Default.QueryController;
4647

48+
public IParseLiveQueryController LiveQueryController => Custom.LiveQueryController ?? Default.LiveQueryController;
49+
4750
public IParseSessionController SessionController => Custom.SessionController ?? Default.SessionController;
4851

4952
public IParseUserController UserController => Custom.UserController ?? Default.UserController;
@@ -61,6 +64,7 @@ public class OrchestrationServiceHub : IServiceHub
6164
public IParseCurrentInstallationController CurrentInstallationController => Custom.CurrentInstallationController ?? Default.CurrentInstallationController;
6265

6366
public IServerConnectionData ServerConnectionData => Custom.ServerConnectionData ?? Default.ServerConnectionData;
67+
public IServerConnectionData LiveQueryServerConnectionData => Custom.LiveQueryServerConnectionData ?? Default.LiveQueryServerConnectionData;
6468

6569
public IParseDataDecoder Decoder => Custom.Decoder ?? Default.Decoder;
6670

0 commit comments

Comments
 (0)