From 9f03b4dc0d73c93c594cab81de19074c573aadd5 Mon Sep 17 00:00:00 2001 From: Matt Lyons Date: Tue, 13 Jun 2023 11:47:44 -0500 Subject: [PATCH 1/5] Add support for creating data providers in C# and apply a few Resharper suggestions --- c-sharp/JsonUtils/MessageConverter.cs | 3 +- .../MessageHandlerRequestByRequestType.cs | 4 +- c-sharp/MessageHandlers/ResponseToRequest.cs | 28 ++++--- c-sharp/MessageTransports/PapiClient.cs | 49 +++++++----- c-sharp/Messages/MessageResponse.cs | 30 +++++--- c-sharp/NetworkObjects/DataProvider.cs | 75 +++++++++++++++++++ c-sharp/NetworkObjects/DataUpdateEvent.cs | 16 ++++ c-sharp/NetworkObjects/NetworkObject.cs | 37 +++++++++ c-sharp/NetworkObjects/TimeDataProvider.cs | 39 ++++++++++ c-sharp/ParanextDataProvider.sln.DotSettings | 7 ++ c-sharp/Program.cs | 12 +-- .../lib/hello-world/hello-world.web-view.tsx | 7 ++ 12 files changed, 261 insertions(+), 46 deletions(-) create mode 100644 c-sharp/NetworkObjects/DataProvider.cs create mode 100644 c-sharp/NetworkObjects/DataUpdateEvent.cs create mode 100644 c-sharp/NetworkObjects/NetworkObject.cs create mode 100644 c-sharp/NetworkObjects/TimeDataProvider.cs create mode 100644 c-sharp/ParanextDataProvider.sln.DotSettings diff --git a/c-sharp/JsonUtils/MessageConverter.cs b/c-sharp/JsonUtils/MessageConverter.cs index cf2185fce4..fdc718685d 100644 --- a/c-sharp/JsonUtils/MessageConverter.cs +++ b/c-sharp/JsonUtils/MessageConverter.cs @@ -44,7 +44,8 @@ static MessageConverter() foreach (var evt in GetObjectsOfClosestSubtypes()) { - s_eventTypeMap.Add(evt.EventType, evt.GetType()); + if (evt.EventType != Enum.Null) + s_eventTypeMap.Add(evt.EventType, evt.GetType()); } } diff --git a/c-sharp/MessageHandlers/MessageHandlerRequestByRequestType.cs b/c-sharp/MessageHandlers/MessageHandlerRequestByRequestType.cs index 32372d5043..7a34f44e37 100644 --- a/c-sharp/MessageHandlers/MessageHandlerRequestByRequestType.cs +++ b/c-sharp/MessageHandlers/MessageHandlerRequestByRequestType.cs @@ -39,7 +39,7 @@ public IEnumerable HandleMessage(Message message) var response = handler(request.Contents); if (response.Success) { - yield return new MessageResponse( + yield return MessageResponse.Succeeded( request.RequestType, request.RequestId, request.SenderId, @@ -48,7 +48,7 @@ public IEnumerable HandleMessage(Message message) } else { - yield return new MessageResponse( + yield return MessageResponse.Failed( request.RequestType, request.RequestId, request.SenderId, diff --git a/c-sharp/MessageHandlers/ResponseToRequest.cs b/c-sharp/MessageHandlers/ResponseToRequest.cs index 9ecfeeab0b..6f244cfd0e 100644 --- a/c-sharp/MessageHandlers/ResponseToRequest.cs +++ b/c-sharp/MessageHandlers/ResponseToRequest.cs @@ -3,24 +3,34 @@ namespace Paranext.DataProvider.MessageHandlers; /// /// Internally represents a response we generated to an incoming request /// -public sealed record class ResponseToRequest +public sealed record ResponseToRequest { + private ResponseToRequest(bool success, dynamic? details) + { + Success = success; + if (success) + Contents = details; + else + ErrorMessage = details; + } + /// - /// Response when there was an error - no contents + /// Response when successful /// - public ResponseToRequest(string errorMessage) + public static ResponseToRequest Succeeded(dynamic? contents = null) { - Success = false; - ErrorMessage = errorMessage; + // If the contents are sent as null it is assumed to be a failed response regardless of the value of "Success". + // Replace null with an empty list to avoid this confusing behavior. + contents ??= new List(); + return new ResponseToRequest(true, contents); } /// - /// Response when successful + /// Response when there was an error /// - public ResponseToRequest(dynamic? contents) + public static ResponseToRequest Failed(string errorMessage) { - Success = true; - Contents = contents; + return new ResponseToRequest(false, errorMessage); } public bool Success { get; } diff --git a/c-sharp/MessageTransports/PapiClient.cs b/c-sharp/MessageTransports/PapiClient.cs index 11d4e78755..aa219c0b42 100644 --- a/c-sharp/MessageTransports/PapiClient.cs +++ b/c-sharp/MessageTransports/PapiClient.cs @@ -20,7 +20,7 @@ internal sealed class PapiClient : IDisposable private const int DISCONNECT_TIMEOUT = 2000; private const int RECEIVE_BUFFER_LENGTH = 2048; private const int MAX_OUTGOING_MESSAGES = 10; - private static readonly Encoding s_utf8WithoutBOM = new UTF8Encoding(); + private static readonly Encoding s_utf8WithoutBom = new UTF8Encoding(); private static readonly Uri s_connectionUri = new("ws://localhost:8876"); private static readonly JsonSerializerOptions s_serializationOptions; @@ -79,6 +79,7 @@ public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool isDisposing)' method Dispose(isDisposing: true); + // ReSharper disable once GCSuppressFinalizeForTypeWithoutDestructor GC.SuppressFinalize(this); } @@ -140,7 +141,7 @@ public async Task ConnectAsync() if (!_clientInitializationComplete.Wait(CONNECT_TIMEOUT)) { - Console.Error.WriteLine("PapiClient did not connect"); + await Console.Error.WriteLineAsync("PapiClient did not connect"); await DisconnectAsync(); return false; } @@ -162,9 +163,13 @@ public async Task DisconnectAsync() SignalToBeginGracefulDisconnect(); if (_incomingMessageThread.IsAlive && !_incomingMessageThread.Join(DISCONNECT_TIMEOUT)) - Console.Error.WriteLine("Incoming message thread did not shut down properly"); + await Console.Error.WriteLineAsync( + "Incoming message thread did not shut down properly" + ); if (_outgoingMessageThread.IsAlive && !_outgoingMessageThread.Join(DISCONNECT_TIMEOUT)) - Console.Error.WriteLine("Outgoing message thread did not shut down properly"); + await Console.Error.WriteLineAsync( + "Outgoing message thread did not shut down properly" + ); if (Connected) { @@ -194,12 +199,12 @@ public void BlockUntilMessageHandlingComplete() /// /// The request type to register /// Method that is called when a request of the specified type is received from the server - /// Number of milliseconds to wait for the registration response to be received + /// Number of milliseconds to wait for the registration response to be received /// True if the registration was successful public bool RegisterRequestHandler( Enum requestType, Func requestHandler, - int responseTimeoutInMS = 1000 + int responseTimeoutInMs = 1000 ) { ObjectDisposedException.ThrowIf(_isDisposed, this); @@ -216,7 +221,7 @@ public bool RegisterRequestHandler( _messageHandlersForMyRequests[requestMessage.RequestId] = new MessageHandlerResponse( requestMessage, - (bool success, dynamic? data) => + (bool success, dynamic? _) => { if (!success) { @@ -235,12 +240,13 @@ public bool RegisterRequestHandler( registrationSucceeded = true; } + // ReSharper disable once AccessToDisposedClosure registrationComplete.Set(); } ); QueueOutgoingMessage(requestMessage); - if (!registrationComplete.Wait(responseTimeoutInMS)) + if (!registrationComplete.Wait(responseTimeoutInMs)) { Console.Error.WriteLine( $"No response came back when registering request type \"{requestType}\"" @@ -284,7 +290,7 @@ public void UnregisterEventHandler( } /// - /// Send an event message to the server/>. + /// Send an event message to the server. /// /// Event message to send public void SendEvent(MessageEvent message) @@ -346,7 +352,9 @@ private async void HandleOutgoingMessages() } catch (Exception ex) { - Console.Error.WriteLine($"Exception while sending outgoing messages: {ex}"); + await Console.Error.WriteLineAsync( + $"Exception while sending outgoing messages: {ex}" + ); } } while (!_cancellationToken.IsCancellationRequested && Connected); @@ -359,11 +367,13 @@ private async Task SendOutgoingMessageAsync(Message message) { message.SenderId = _clientId; string jsonData = JsonSerializer.Serialize(message, s_serializationOptions); + /* Helpful for debugging Console.WriteLine( "Sending message over websocket: {0}", StringUtils.LimitLength(jsonData, 180) ); - byte[] data = s_utf8WithoutBOM.GetBytes(jsonData); + */ + byte[] data = s_utf8WithoutBom.GetBytes(jsonData); await _webSocket.SendAsync(data, WebSocketMessageType.Text, true, _cancellationToken); } @@ -387,10 +397,13 @@ private async void HandleIncomingMessages() { Message? message = await ReceiveIncomingMessageAsync(); // Handle each message asynchronously so we can keep receiving more messages - _ = Task.Run(() => - { - HandleIncomingMessage(message); - }); + _ = Task.Run( + () => + { + HandleIncomingMessage(message); + }, + _cancellationToken + ); } catch (OperationCanceledException) { @@ -398,7 +411,7 @@ private async void HandleIncomingMessages() } catch (Exception ex) { - Console.Error.WriteLine($"Exception while handling messages: {ex}"); + await Console.Error.WriteLineAsync($"Exception while handling messages: {ex}"); } } while (!_cancellationToken.IsCancellationRequested && Connected); @@ -431,11 +444,13 @@ private async void HandleIncomingMessages() } } while (!result.EndOfMessage); - string jsonData = s_utf8WithoutBOM.GetString(message.GetBuffer(), 0, (int)message.Position); + string jsonData = s_utf8WithoutBom.GetString(message.GetBuffer(), 0, (int)message.Position); + /* Helpful for debugging Console.WriteLine( "Received message over websocket: {0}", StringUtils.LimitLength(jsonData, 180) ); + */ return JsonSerializer.Deserialize(jsonData, s_serializationOptions); } diff --git a/c-sharp/Messages/MessageResponse.cs b/c-sharp/Messages/MessageResponse.cs index 80047d2e16..c813c68c0f 100644 --- a/c-sharp/Messages/MessageResponse.cs +++ b/c-sharp/Messages/MessageResponse.cs @@ -15,35 +15,41 @@ private MessageResponse() { } /// /// Response when there was an error - no contents /// - public MessageResponse( + public static MessageResponse Failed( Enum requestType, int requestId, int requesterId, string errorMessage ) { - RequestType = requestType; - RequestId = requestId; - RequesterId = requesterId; - Success = false; - ErrorMessage = errorMessage; + return new MessageResponse + { + RequestType = requestType, + RequestId = requestId, + RequesterId = requesterId, + Success = false, + ErrorMessage = errorMessage + }; } /// /// Response when successful /// - public MessageResponse( + public static MessageResponse Succeeded( Enum requestType, int requestId, int requesterId, dynamic? contents ) { - RequestType = requestType; - RequestId = requestId; - RequesterId = requesterId; - Success = true; - Contents = contents; + return new MessageResponse + { + RequestType = requestType, + RequestId = requestId, + RequesterId = requesterId, + Success = true, + Contents = contents + }; } public sealed override Enum Type => MessageType.Response; diff --git a/c-sharp/NetworkObjects/DataProvider.cs b/c-sharp/NetworkObjects/DataProvider.cs new file mode 100644 index 0000000000..943c21e7bc --- /dev/null +++ b/c-sharp/NetworkObjects/DataProvider.cs @@ -0,0 +1,75 @@ +using Paranext.DataProvider.MessageHandlers; +using Paranext.DataProvider.Messages; +using Paranext.DataProvider.MessageTransports; +using PtxUtils; +using System.Text.Json; + +namespace Paranext.DataProvider.NetworkObjects +{ + internal abstract class DataProvider : NetworkObject + { + protected DataProvider(string name, PapiClient papiClient) + : base(papiClient) + { + DataProviderName = name + "-data"; + } + + protected string DataProviderName { get; } + + public void RegisterDataProvider() + { + RegisterNetworkObject(DataProviderName, FunctionHandler); + StartDataProvider(); + } + + // An array of strings serialized as JSON will be sent here. + // The first item in the array is the name of the function to call. + // All remaining items are arguments to pass to the function. + // Data providers must provide "get" and "set" functions. + private ResponseToRequest FunctionHandler(dynamic? request) + { + string[] arguments = JsonSerializer.Deserialize(request); + if (arguments.Length == 0) + return ResponseToRequest.Failed( + $"No function name provided when calling data provider {DataProviderName}" + ); + + string functionName = arguments[0].ToUpperInvariant(); + string[] parameters = arguments.Skip(1).ToArray(); + return functionName switch + { + "GET" => HandleGetRequest(parameters), + "SET" => HandleSetRequest(parameters), + _ => ResponseToRequest.Failed($"Unexpected function call: {functionName}"), + }; + } + + /// + /// Notify all processes on the network that this data provider has new data + /// + protected void ReportDataUpdate() + { + var dataUpdateEventType = new Enum($"{DataProviderName}:onDidUpdate"); + PapiClient.SendEvent(new DataUpdateEvent(dataUpdateEventType, true)); + } + + /// + /// Once a data provider has started, it should send out update events whenever its data changes. + /// + protected abstract void StartDataProvider(); + + /// + /// Read a copy of the requested data + /// + /// The first value in the array is meant to scope what kind of data was requested + /// ResponseToRequest value that either contains the requested data or an error message + protected abstract ResponseToRequest HandleGetRequest(string[] arguments); + + /// + /// Write data to the provided scope + /// + /// The first value in the array is meant to scope what kind of data was provided + /// ResponseToRequest value that either notes success or an error message describing the failure + protected abstract ResponseToRequest HandleSetRequest(string[] arguments); + } +} diff --git a/c-sharp/NetworkObjects/DataUpdateEvent.cs b/c-sharp/NetworkObjects/DataUpdateEvent.cs new file mode 100644 index 0000000000..5b17a9b09d --- /dev/null +++ b/c-sharp/NetworkObjects/DataUpdateEvent.cs @@ -0,0 +1,16 @@ +using Paranext.DataProvider.Messages; +using PtxUtils; + +namespace Paranext.DataProvider.NetworkObjects +{ + internal sealed class DataUpdateEvent : MessageEventGeneric + { + // A parameterless constructor is required for serialization to work, but we never need to deserialize this particular event. So just use a null event type. + // Because the event types are dynamic based on data provider names, we can't create every possible event type ahead of time. + public DataUpdateEvent() + : base(Enum.Null) { } + + public DataUpdateEvent(Enum eventType, bool eventContents) + : base(eventType, eventContents) { } + } +} diff --git a/c-sharp/NetworkObjects/NetworkObject.cs b/c-sharp/NetworkObjects/NetworkObject.cs new file mode 100644 index 0000000000..969ed78215 --- /dev/null +++ b/c-sharp/NetworkObjects/NetworkObject.cs @@ -0,0 +1,37 @@ +using Paranext.DataProvider.MessageHandlers; +using Paranext.DataProvider.Messages; +using Paranext.DataProvider.MessageTransports; +using PtxUtils; + +namespace Paranext.DataProvider.NetworkObjects +{ + internal abstract class NetworkObject + { + protected NetworkObject(PapiClient papiClient) + { + PapiClient = papiClient; + } + + protected PapiClient PapiClient { get; } + + protected void RegisterNetworkObject( + string networkObjectName, + Func requestHandler + ) + { + var getReqType = new Enum($"object:{networkObjectName}.get"); + var functionReqType = new Enum($"object:{networkObjectName}.function"); + + if (!PapiClient.RegisterRequestHandler(getReqType, HandleGet)) + throw new Exception($"Could not register GET for {networkObjectName}"); + + if (!PapiClient.RegisterRequestHandler(functionReqType, requestHandler)) + throw new Exception($"Could not register FUNCTION for {networkObjectName}"); + } + + private static ResponseToRequest HandleGet(dynamic getRequest) + { + return ResponseToRequest.Succeeded(); + } + } +} diff --git a/c-sharp/NetworkObjects/TimeDataProvider.cs b/c-sharp/NetworkObjects/TimeDataProvider.cs new file mode 100644 index 0000000000..c9629a8d0d --- /dev/null +++ b/c-sharp/NetworkObjects/TimeDataProvider.cs @@ -0,0 +1,39 @@ +using System.Timers; +using Paranext.DataProvider.MessageHandlers; +using Paranext.DataProvider.MessageTransports; +using SIL.Extensions; + +namespace Paranext.DataProvider.NetworkObjects +{ + internal class TimeDataProvider : DataProvider + { + private readonly System.Timers.Timer _timer = new System.Timers.Timer( + TimeSpan.FromSeconds(1) + ); + + public TimeDataProvider(PapiClient papiClient) + : base("current-time", papiClient) { } + + private void TimerFired(object? state, ElapsedEventArgs args) + { + ReportDataUpdate(); + } + + protected override void StartDataProvider() + { + _timer.Elapsed += TimerFired; + _timer.AutoReset = true; + _timer.Enabled = true; + } + + protected override ResponseToRequest HandleGetRequest(string[] arguments) + { + return ResponseToRequest.Succeeded(DateTime.Now.ToISO8601TimeFormatWithUTCString()); + } + + protected override ResponseToRequest HandleSetRequest(string[] arguments) + { + return ResponseToRequest.Failed("Setting the time is not allowed"); + } + } +} diff --git a/c-sharp/ParanextDataProvider.sln.DotSettings b/c-sharp/ParanextDataProvider.sln.DotSettings new file mode 100644 index 0000000000..f15df9e32e --- /dev/null +++ b/c-sharp/ParanextDataProvider.sln.DotSettings @@ -0,0 +1,7 @@ + + True + True + True + True + True + True \ No newline at end of file diff --git a/c-sharp/Program.cs b/c-sharp/Program.cs index 82e31fe396..e7d9f9d5fb 100644 --- a/c-sharp/Program.cs +++ b/c-sharp/Program.cs @@ -2,6 +2,7 @@ using Paranext.DataProvider.MessageHandlers; using Paranext.DataProvider.Messages; using Paranext.DataProvider.MessageTransports; +using Paranext.DataProvider.NetworkObjects; using PtxUtils; namespace Paranext.DataProvider; @@ -27,6 +28,9 @@ public static async Task Main() return; } + var tdp = new TimeDataProvider(papi); + tdp.RegisterDataProvider(); + Console.WriteLine("Paranext data provider ready!"); papi.BlockUntilMessageHandlingComplete(); } @@ -43,16 +47,14 @@ public static async Task Main() private static ResponseToRequest RequestAddOne(dynamic val) { if (val is not JsonElement element || element.GetArrayLength() != 1) - return new ResponseToRequest("Unexpected data in request: " + val); + return ResponseToRequest.Failed("Unexpected data in request: " + val); - int? intVal = ErrorUtils.IgnoreErrors( + int intVal = ErrorUtils.IgnoreErrors( "Trying to parse data from server", () => element[0].GetInt32() ); - if (intVal == null) - return new ResponseToRequest("Unexpected data in request: " + val); - return new ResponseToRequest(intVal + 1); + return ResponseToRequest.Succeeded(intVal + 1); } #endregion diff --git a/extensions/lib/hello-world/hello-world.web-view.tsx b/extensions/lib/hello-world/hello-world.web-view.tsx index 6a0b43424d..8066c720b8 100644 --- a/extensions/lib/hello-world/hello-world.web-view.tsx +++ b/extensions/lib/hello-world/hello-world.web-view.tsx @@ -68,6 +68,12 @@ globalThis.webViewComponent = function HelloWorld() { 'Loading Psalm 1...', ); + const [currentTime] = useData( + 'current-time', + '*', + 'Loading current time', + ); + return (
@@ -100,6 +106,7 @@ globalThis.webViewComponent = function HelloWorld() {
{latestVerseText}
+
{currentTime}
setName(e.target.value)} /> From cb5bed6ddce5aabfc10522dbe33e5648f00e88ba Mon Sep 17 00:00:00 2001 From: Matt Lyons Date: Wed, 14 Jun 2023 10:46:16 -0500 Subject: [PATCH 2/5] Add comments and simplify some of the new code --- c-sharp/NetworkObjects/DataProvider.cs | 28 +++++++++++++++++++--- c-sharp/NetworkObjects/DataUpdateEvent.cs | 16 ------------- c-sharp/NetworkObjects/NetworkObject.cs | 7 ++++++ c-sharp/NetworkObjects/TimeDataProvider.cs | 19 +++++++-------- c-sharp/Properties/launchSettings.json | 11 +++++++++ 5 files changed, 52 insertions(+), 29 deletions(-) delete mode 100644 c-sharp/NetworkObjects/DataUpdateEvent.cs create mode 100644 c-sharp/Properties/launchSettings.json diff --git a/c-sharp/NetworkObjects/DataProvider.cs b/c-sharp/NetworkObjects/DataProvider.cs index 943c21e7bc..7ed9a298e1 100644 --- a/c-sharp/NetworkObjects/DataProvider.cs +++ b/c-sharp/NetworkObjects/DataProvider.cs @@ -8,14 +8,37 @@ namespace Paranext.DataProvider.NetworkObjects { internal abstract class DataProvider : NetworkObject { + // This is an internal class because nothing else should be instantiating it directly + private class MessageEventDataUpdated : MessageEventGeneric + { + // A parameterless constructor is required for serialization to work + // ReSharper disable once UnusedMember.Local + public MessageEventDataUpdated() + : base(Enum.Null) { } + + public MessageEventDataUpdated(Enum eventType) + : base(eventType, true) { } + } + + private readonly MessageEventDataUpdated _dataUpdatedEvent; + protected DataProvider(string name, PapiClient papiClient) : base(papiClient) { + // "-data" is the prefix used by PAPI for data provider names DataProviderName = name + "-data"; + + // "onDidUpdate" is the event name used by PAPI for data providers to notify consumers of updates + var eventType = new Enum($"{DataProviderName}:onDidUpdate"); + + _dataUpdatedEvent = new MessageEventDataUpdated(eventType); } protected string DataProviderName { get; } + /// + /// Register this data provider on the network so that other services can use it + /// public void RegisterDataProvider() { RegisterNetworkObject(DataProviderName, FunctionHandler); @@ -47,10 +70,9 @@ private ResponseToRequest FunctionHandler(dynamic? request) /// /// Notify all processes on the network that this data provider has new data /// - protected void ReportDataUpdate() + protected void SendDataUpdateEvent() { - var dataUpdateEventType = new Enum($"{DataProviderName}:onDidUpdate"); - PapiClient.SendEvent(new DataUpdateEvent(dataUpdateEventType, true)); + PapiClient.SendEvent(_dataUpdatedEvent); } /// diff --git a/c-sharp/NetworkObjects/DataUpdateEvent.cs b/c-sharp/NetworkObjects/DataUpdateEvent.cs deleted file mode 100644 index 5b17a9b09d..0000000000 --- a/c-sharp/NetworkObjects/DataUpdateEvent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Paranext.DataProvider.Messages; -using PtxUtils; - -namespace Paranext.DataProvider.NetworkObjects -{ - internal sealed class DataUpdateEvent : MessageEventGeneric - { - // A parameterless constructor is required for serialization to work, but we never need to deserialize this particular event. So just use a null event type. - // Because the event types are dynamic based on data provider names, we can't create every possible event type ahead of time. - public DataUpdateEvent() - : base(Enum.Null) { } - - public DataUpdateEvent(Enum eventType, bool eventContents) - : base(eventType, eventContents) { } - } -} diff --git a/c-sharp/NetworkObjects/NetworkObject.cs b/c-sharp/NetworkObjects/NetworkObject.cs index 969ed78215..26d73f8c4f 100644 --- a/c-sharp/NetworkObjects/NetworkObject.cs +++ b/c-sharp/NetworkObjects/NetworkObject.cs @@ -14,11 +14,18 @@ protected NetworkObject(PapiClient papiClient) protected PapiClient PapiClient { get; } + /// + /// Notify PAPI services we have a new network object they can use + /// + /// Services access this network object using this name + /// Function that will handle calls from services to this network object + /// Throws if the network object could not be registered properly protected void RegisterNetworkObject( string networkObjectName, Func requestHandler ) { + // PAPI requires network objects to expose "get" and "function" requests var getReqType = new Enum($"object:{networkObjectName}.get"); var functionReqType = new Enum($"object:{networkObjectName}.function"); diff --git a/c-sharp/NetworkObjects/TimeDataProvider.cs b/c-sharp/NetworkObjects/TimeDataProvider.cs index c9629a8d0d..44113bfb6f 100644 --- a/c-sharp/NetworkObjects/TimeDataProvider.cs +++ b/c-sharp/NetworkObjects/TimeDataProvider.cs @@ -1,27 +1,26 @@ -using System.Timers; using Paranext.DataProvider.MessageHandlers; using Paranext.DataProvider.MessageTransports; using SIL.Extensions; namespace Paranext.DataProvider.NetworkObjects { + /// + /// This is a sample data provider for demonstration purposes + /// internal class TimeDataProvider : DataProvider { - private readonly System.Timers.Timer _timer = new System.Timers.Timer( - TimeSpan.FromSeconds(1) - ); + // Fire an event that says our "time data" updated once per second + private readonly System.Timers.Timer _timer = new(TimeSpan.FromSeconds(1)); public TimeDataProvider(PapiClient papiClient) : base("current-time", papiClient) { } - private void TimerFired(object? state, ElapsedEventArgs args) - { - ReportDataUpdate(); - } - protected override void StartDataProvider() { - _timer.Elapsed += TimerFired; + _timer.Elapsed += (_, _) => + { + SendDataUpdateEvent(); + }; _timer.AutoReset = true; _timer.Enabled = true; } diff --git a/c-sharp/Properties/launchSettings.json b/c-sharp/Properties/launchSettings.json new file mode 100644 index 0000000000..24eb035021 --- /dev/null +++ b/c-sharp/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "ParanextDataProvider": { + "commandName": "Project" + }, + "WSL": { + "commandName": "WSL2", + "distributionName": "" + } + } +} \ No newline at end of file From adc1d80840dabf5d82dfd74a814580e6ce00d07d Mon Sep 17 00:00:00 2001 From: Matt Lyons Date: Wed, 14 Jun 2023 14:51:34 -0500 Subject: [PATCH 3/5] Updates required to work with the data provider changes --- c-sharp/NetworkObjects/DataProvider.cs | 51 +++++++++---------- c-sharp/NetworkObjects/TimeDataProvider.cs | 17 ++++--- c-sharp/ParanextDataProvider.sln.DotSettings | 4 +- .../lib/hello-world/hello-world.web-view.tsx | 17 ++++--- 4 files changed, 47 insertions(+), 42 deletions(-) diff --git a/c-sharp/NetworkObjects/DataProvider.cs b/c-sharp/NetworkObjects/DataProvider.cs index 7ed9a298e1..aa642948ae 100644 --- a/c-sharp/NetworkObjects/DataProvider.cs +++ b/c-sharp/NetworkObjects/DataProvider.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using Paranext.DataProvider.MessageHandlers; using Paranext.DataProvider.Messages; using Paranext.DataProvider.MessageTransports; @@ -9,18 +10,22 @@ namespace Paranext.DataProvider.NetworkObjects internal abstract class DataProvider : NetworkObject { // This is an internal class because nothing else should be instantiating it directly - private class MessageEventDataUpdated : MessageEventGeneric + private class MessageEventDataUpdated : MessageEventGeneric { // A parameterless constructor is required for serialization to work // ReSharper disable once UnusedMember.Local public MessageEventDataUpdated() : base(Enum.Null) { } - public MessageEventDataUpdated(Enum eventType) - : base(eventType, true) { } + public MessageEventDataUpdated(Enum eventType, string dataScope) + : base(eventType, dataScope) { } } - private readonly MessageEventDataUpdated _dataUpdatedEvent; + private readonly Enum _eventType; + private readonly ConcurrentDictionary< + string, + MessageEventDataUpdated + > _updateEventsByScope = new(); protected DataProvider(string name, PapiClient papiClient) : base(papiClient) @@ -29,9 +34,7 @@ protected DataProvider(string name, PapiClient papiClient) DataProviderName = name + "-data"; // "onDidUpdate" is the event name used by PAPI for data providers to notify consumers of updates - var eventType = new Enum($"{DataProviderName}:onDidUpdate"); - - _dataUpdatedEvent = new MessageEventDataUpdated(eventType); + _eventType = new Enum($"{DataProviderName}:onDidUpdate"); } protected string DataProviderName { get; } @@ -57,22 +60,22 @@ private ResponseToRequest FunctionHandler(dynamic? request) $"No function name provided when calling data provider {DataProviderName}" ); - string functionName = arguments[0].ToUpperInvariant(); + string functionName = arguments[0]; string[] parameters = arguments.Skip(1).ToArray(); - return functionName switch - { - "GET" => HandleGetRequest(parameters), - "SET" => HandleSetRequest(parameters), - _ => ResponseToRequest.Failed($"Unexpected function call: {functionName}"), - }; + return HandleRequest(functionName, parameters); } /// /// Notify all processes on the network that this data provider has new data /// - protected void SendDataUpdateEvent() + /// Indicator of what data changed in the provider + protected void SendDataUpdateEvent(string dataScope) { - PapiClient.SendEvent(_dataUpdatedEvent); + var dataUpdateEventMessage = _updateEventsByScope.GetOrAdd( + dataScope, + (scope) => new MessageEventDataUpdated(_eventType, scope) + ); + PapiClient.SendEvent(dataUpdateEventMessage); } /// @@ -81,17 +84,11 @@ protected void SendDataUpdateEvent() protected abstract void StartDataProvider(); /// - /// Read a copy of the requested data - /// - /// The first value in the array is meant to scope what kind of data was requested - /// ResponseToRequest value that either contains the requested data or an error message - protected abstract ResponseToRequest HandleGetRequest(string[] arguments); - - /// - /// Write data to the provided scope + /// Handle a request from a service using this data provider /// - /// The first value in the array is meant to scope what kind of data was provided - /// ResponseToRequest value that either notes success or an error message describing the failure - protected abstract ResponseToRequest HandleSetRequest(string[] arguments); + /// This would typically be "getXYZ" or "setXYZ", where "XYZ" is a type of data handled by this provider + /// Optional arguments provided by the requester for the function indicated + /// ResponseToRequest value that either contains a response for the function or an error message + protected abstract ResponseToRequest HandleRequest(string functionName, string[] arguments); } } diff --git a/c-sharp/NetworkObjects/TimeDataProvider.cs b/c-sharp/NetworkObjects/TimeDataProvider.cs index 44113bfb6f..c20da8723d 100644 --- a/c-sharp/NetworkObjects/TimeDataProvider.cs +++ b/c-sharp/NetworkObjects/TimeDataProvider.cs @@ -19,20 +19,21 @@ protected override void StartDataProvider() { _timer.Elapsed += (_, _) => { - SendDataUpdateEvent(); + SendDataUpdateEvent("*"); }; _timer.AutoReset = true; _timer.Enabled = true; } - protected override ResponseToRequest HandleGetRequest(string[] arguments) + protected override ResponseToRequest HandleRequest(string functionName, string[] arguments) { - return ResponseToRequest.Succeeded(DateTime.Now.ToISO8601TimeFormatWithUTCString()); - } - - protected override ResponseToRequest HandleSetRequest(string[] arguments) - { - return ResponseToRequest.Failed("Setting the time is not allowed"); + return functionName switch + { + "getTime" + => ResponseToRequest.Succeeded(DateTime.Now.ToISO8601TimeFormatWithUTCString()), + "setTime" => ResponseToRequest.Failed("Cannot set the time"), + _ => ResponseToRequest.Failed($"Unexpected function: {functionName}") + }; } } } diff --git a/c-sharp/ParanextDataProvider.sln.DotSettings b/c-sharp/ParanextDataProvider.sln.DotSettings index f15df9e32e..c8bd68a43f 100644 --- a/c-sharp/ParanextDataProvider.sln.DotSettings +++ b/c-sharp/ParanextDataProvider.sln.DotSettings @@ -1,7 +1,9 @@  + True True True True True + True True - True \ No newline at end of file + True diff --git a/extensions/lib/hello-world/hello-world.web-view.tsx b/extensions/lib/hello-world/hello-world.web-view.tsx index 8066c720b8..dbfdd1d6dd 100644 --- a/extensions/lib/hello-world/hello-world.web-view.tsx +++ b/extensions/lib/hello-world/hello-world.web-view.tsx @@ -11,6 +11,7 @@ import { import { useCallback, useContext, useState } from 'react'; import { QuickVerseDataTypes } from '@extensions/quick-verse/quick-verse'; import { PeopleDataProvider, PeopleDataTypes } from '@extensions/hello-someone/hello-someone'; +import type { DataProviderDataType } from 'shared/models/data-provider.model'; const { react: { @@ -50,6 +51,16 @@ globalThis.webViewComponent = function HelloWorld() { 'Loading latest Scripture text...', ); + type TimeDataType = { + TimeData: DataProviderDataType; + }; + + const [currentTime] = useData.Time( + 'current-time', + '*', + 'Loading current time', + ); + const [name, setName] = useState('Bill'); const peopleDataProvider = useDataProvider('hello-someone.people'); @@ -68,12 +79,6 @@ globalThis.webViewComponent = function HelloWorld() { 'Loading Psalm 1...', ); - const [currentTime] = useData( - 'current-time', - '*', - 'Loading current time', - ); - return (
From 14a0732adde068a6d9fab35cc353a343d0b62fa3 Mon Sep 17 00:00:00 2001 From: Matt Lyons Date: Thu, 15 Jun 2023 08:22:19 -0500 Subject: [PATCH 4/5] Fix a typo --- c-sharp/NetworkObjects/DataProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/c-sharp/NetworkObjects/DataProvider.cs b/c-sharp/NetworkObjects/DataProvider.cs index aa642948ae..cf84969dfa 100644 --- a/c-sharp/NetworkObjects/DataProvider.cs +++ b/c-sharp/NetworkObjects/DataProvider.cs @@ -30,7 +30,7 @@ private readonly ConcurrentDictionary< protected DataProvider(string name, PapiClient papiClient) : base(papiClient) { - // "-data" is the prefix used by PAPI for data provider names + // "-data" is the suffix used by PAPI for data provider names DataProviderName = name + "-data"; // "onDidUpdate" is the event name used by PAPI for data providers to notify consumers of updates From 1e74998ac6823596c92356cc7e35297f9d9aed35 Mon Sep 17 00:00:00 2001 From: Matt Lyons Date: Thu, 15 Jun 2023 14:49:58 -0500 Subject: [PATCH 5/5] PR feedback --- .gitignore | 3 +++ c-sharp/Properties/launchSettings.json | 11 ----------- 2 files changed, 3 insertions(+), 11 deletions(-) delete mode 100644 c-sharp/Properties/launchSettings.json diff --git a/.gitignore b/.gitignore index 742fb64f9b..d5e87be21a 100644 --- a/.gitignore +++ b/.gitignore @@ -35,5 +35,8 @@ npm-debug.log.* # Extra VS Code workspaces *.code-workspace +# Extra Visual Studio files +launchSettings.json + # Test development user appdata files dev-appdata/ diff --git a/c-sharp/Properties/launchSettings.json b/c-sharp/Properties/launchSettings.json deleted file mode 100644 index 24eb035021..0000000000 --- a/c-sharp/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "ParanextDataProvider": { - "commandName": "Project" - }, - "WSL": { - "commandName": "WSL2", - "distributionName": "" - } - } -} \ No newline at end of file