From 311355aca4575a6fa157fcaa673e7551dd8ac608 Mon Sep 17 00:00:00 2001 From: Chris Davoren Date: Fri, 12 Jan 2024 13:14:16 +1000 Subject: [PATCH] refactor: multiple refactor: TimeOnly JSON converter added - regularizes record types refactor: Strongly typed SignalR hub - removes client method name strings chore: Redundant file cleanup --- TrafficLightsSignalR/Hubs/ILightHub.cs | 8 +++ TrafficLightsSignalR/Hubs/LightHub.cs | 15 ++++- .../ITrafficLightStateService.cs | 2 +- TrafficLightsSignalR/Pages/Privacy.cshtml | 8 --- TrafficLightsSignalR/Pages/Privacy.cshtml.cs | 20 ------- .../Pages/Shared/_Layout.cshtml | 2 +- TrafficLightsSignalR/Schema/Period.cs | 13 ++--- TrafficLightsSignalR/Schema/State.cs | 16 +++--- .../Schema/TimeOnlyConverter.cs | 28 ++++++++++ .../TrafficLightStateService.cs | 27 ++++----- .../TrafficLightsHostedService.cs | 55 ++++++++++--------- TrafficLightsSignalR/TrafficLightsState.cs | 15 ----- TrafficLightsSignalR/wwwroot/js/lights.js | 26 +++------ 13 files changed, 111 insertions(+), 124 deletions(-) create mode 100644 TrafficLightsSignalR/Hubs/ILightHub.cs delete mode 100644 TrafficLightsSignalR/Pages/Privacy.cshtml delete mode 100644 TrafficLightsSignalR/Pages/Privacy.cshtml.cs create mode 100644 TrafficLightsSignalR/Schema/TimeOnlyConverter.cs diff --git a/TrafficLightsSignalR/Hubs/ILightHub.cs b/TrafficLightsSignalR/Hubs/ILightHub.cs new file mode 100644 index 0000000..04918b7 --- /dev/null +++ b/TrafficLightsSignalR/Hubs/ILightHub.cs @@ -0,0 +1,8 @@ +namespace TrafficLightsSignalR.Hubs +{ + public interface ILightHub + { + Task ReceiveStateUpdate(TrafficLightState state); + Task ReceiveServerState(TrafficLightState? state); + } +} diff --git a/TrafficLightsSignalR/Hubs/LightHub.cs b/TrafficLightsSignalR/Hubs/LightHub.cs index ae9cd04..b386261 100644 --- a/TrafficLightsSignalR/Hubs/LightHub.cs +++ b/TrafficLightsSignalR/Hubs/LightHub.cs @@ -4,13 +4,22 @@ using TrafficLightsSignalR.Schema; namespace TrafficLightsSignalR.Hubs { - public class LightHub(ITrafficLightStateService stateService) : Hub + public class LightHub : Hub { - private readonly ITrafficLightStateService _stateService = stateService; + private readonly ITrafficLightStateService _stateService; + private readonly ILogger _logger; + + public LightHub(ITrafficLightStateService stateService, ILogger logger) + { + _stateService = stateService; + _logger = logger; + } public async Task GetServerState(string connectionId) { - await Clients.Client(connectionId).SendAsync("ReceiveServerState", _stateService.GetCurrentTrafficLightState()); + var currentState = _stateService.GetCurrentTrafficLightState(); + _logger.LogInformation($"Request for server state received, current state null?: ${currentState is null}"); + await Clients.Client(connectionId).ReceiveServerState(currentState); } } } diff --git a/TrafficLightsSignalR/ITrafficLightStateService.cs b/TrafficLightsSignalR/ITrafficLightStateService.cs index 7451db1..2be8456 100644 --- a/TrafficLightsSignalR/ITrafficLightStateService.cs +++ b/TrafficLightsSignalR/ITrafficLightStateService.cs @@ -3,6 +3,6 @@ public interface ITrafficLightStateService { public void SetCurrentTrafficLightState(TrafficLightState currentTrafficLightState); - public TrafficLightState GetCurrentTrafficLightState(); + public TrafficLightState? GetCurrentTrafficLightState(); } } diff --git a/TrafficLightsSignalR/Pages/Privacy.cshtml b/TrafficLightsSignalR/Pages/Privacy.cshtml deleted file mode 100644 index 46ba966..0000000 --- a/TrafficLightsSignalR/Pages/Privacy.cshtml +++ /dev/null @@ -1,8 +0,0 @@ -@page -@model PrivacyModel -@{ - ViewData["Title"] = "Privacy Policy"; -} -

@ViewData["Title"]

- -

Use this page to detail your site's privacy policy.

diff --git a/TrafficLightsSignalR/Pages/Privacy.cshtml.cs b/TrafficLightsSignalR/Pages/Privacy.cshtml.cs deleted file mode 100644 index 4c95919..0000000 --- a/TrafficLightsSignalR/Pages/Privacy.cshtml.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace TrafficLightsSignalR.Pages -{ - public class PrivacyModel : PageModel - { - private readonly ILogger _logger; - - public PrivacyModel(ILogger logger) - { - _logger = logger; - } - - public void OnGet() - { - } - } - -} diff --git a/TrafficLightsSignalR/Pages/Shared/_Layout.cshtml b/TrafficLightsSignalR/Pages/Shared/_Layout.cshtml index 5b2b57e..b8b4fbc 100644 --- a/TrafficLightsSignalR/Pages/Shared/_Layout.cshtml +++ b/TrafficLightsSignalR/Pages/Shared/_Layout.cshtml @@ -38,7 +38,7 @@
- © 2024 - TrafficLightsSignalR - Privacy + © 2024 - TrafficLightsSignalR
diff --git a/TrafficLightsSignalR/Schema/Period.cs b/TrafficLightsSignalR/Schema/Period.cs index 0bf1a87..bdeff6d 100644 --- a/TrafficLightsSignalR/Schema/Period.cs +++ b/TrafficLightsSignalR/Schema/Period.cs @@ -3,12 +3,9 @@ using System.Text.Json.Serialization; namespace TrafficLightsSignalR.Schema { - public class Period - { - public string Name { get; set; } = ""; - public string VerboseName { get; set; } = ""; - public string TimeStart { get; set; } = ""; - public TimeOnly TimeStartParsed; - public List States { get; set; } = []; - } + public record Period( + string Name, + string VerboseName, + TimeOnly TimeStart, + List States); } diff --git a/TrafficLightsSignalR/Schema/State.cs b/TrafficLightsSignalR/Schema/State.cs index ce02908..edd6d32 100644 --- a/TrafficLightsSignalR/Schema/State.cs +++ b/TrafficLightsSignalR/Schema/State.cs @@ -1,12 +1,10 @@  namespace TrafficLightsSignalR.Schema { - public record State - { - public int Duration { get; set; } = -1; - public string North { get; set; } = ""; - public string South { get; set; } = ""; - public string East { get; set; } = ""; - public string West { get; set; } = ""; - public string NorthRight { get; set; } = ""; - } + public record State( + int Duration, + string North, + string South, + string East, + string West, + string NorthRight); } diff --git a/TrafficLightsSignalR/Schema/TimeOnlyConverter.cs b/TrafficLightsSignalR/Schema/TimeOnlyConverter.cs new file mode 100644 index 0000000..80004ad --- /dev/null +++ b/TrafficLightsSignalR/Schema/TimeOnlyConverter.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; +using System.Text.Json; + +namespace TrafficLightsSignalR.Schema +{ + public class TimeOnlyConverter : JsonConverter + { + private const string TimeFormat = "HH:mm"; + + public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string timeStr = reader.GetString() ?? throw new JsonException("Invalid time format - null string received."); + + if (TimeOnly.TryParseExact(timeStr, TimeFormat, null, System.Globalization.DateTimeStyles.None, out TimeOnly time)) + { + return time; + } + + throw new JsonException("Invalid time format. Expected format: HH:mm"); + } + + public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString(TimeFormat)); + } + } + +} diff --git a/TrafficLightsSignalR/TrafficLightStateService.cs b/TrafficLightsSignalR/TrafficLightStateService.cs index 5bfdd50..fb3b768 100644 --- a/TrafficLightsSignalR/TrafficLightStateService.cs +++ b/TrafficLightsSignalR/TrafficLightStateService.cs @@ -1,32 +1,29 @@ -namespace TrafficLightsSignalR +using Microsoft.AspNetCore.SignalR; +using TrafficLightsSignalR.Hubs; +using TrafficLightsSignalR.Schema; + +namespace TrafficLightsSignalR { public class TrafficLightStateService : ITrafficLightStateService { - // string objects are immutable - protected TrafficLightState _state; + private readonly IHubContext _hubContext; + protected TrafficLightState? _state; - public TrafficLightStateService() + public TrafficLightStateService(IHubContext hubContext) { - _state = new TrafficLightState( - new Schema.Period(), - new Schema.State(), - -1, - 0.0, - -1, - "", - 0.0, - -1, - -1); + _hubContext = hubContext; } public void SetCurrentTrafficLightState(TrafficLightState currentTrafficLightState) { _state = currentTrafficLightState; + _hubContext.Clients.All.ReceiveStateUpdate(_state); } - public TrafficLightState GetCurrentTrafficLightState() + public TrafficLightState? GetCurrentTrafficLightState() { return _state; } } } + diff --git a/TrafficLightsSignalR/TrafficLightsHostedService.cs b/TrafficLightsSignalR/TrafficLightsHostedService.cs index aad36ad..492b169 100644 --- a/TrafficLightsSignalR/TrafficLightsHostedService.cs +++ b/TrafficLightsSignalR/TrafficLightsHostedService.cs @@ -6,7 +6,7 @@ using TrafficLightsSignalR.Schema; namespace TrafficLightsSignalR { - public class TrafficLightsHostedService(IHubContext hubContext, ITrafficLightStateService stateService, IConfiguration configuration, ILogger logger) : BackgroundService + public class TrafficLightsHostedService(ITrafficLightStateService stateService, IConfiguration configuration, ILogger logger) : BackgroundService { // Simulation constants const long TICKS_PER_SECOND = 10000000; @@ -15,7 +15,6 @@ namespace TrafficLightsSignalR // Interconnects private readonly ILogger _logger = logger; - private readonly IHubContext _hubContext = hubContext; private readonly ITrafficLightStateService _stateService = stateService; private readonly IConfiguration _configuration = configuration; @@ -29,7 +28,26 @@ namespace TrafficLightsSignalR private void UpdateState(TrafficLightState state) { _stateService.SetCurrentTrafficLightState(state); - _ = _hubContext.Clients.All.SendAsync("ReceiveStateUpdate", state); + } + + private TrafficLightState CreateStateRecord(Period currentPeriod, State currentState, int currentStateIndex, long nextChangeoverTicks, DateTime startSimulationDateTime, DateTime currentSimulationDateTime) + { + return new TrafficLightState( + currentPeriod, + currentState, + currentStateIndex, + // Seconds until next changeover + (nextChangeoverTicks - DateTime.UtcNow.Ticks) / (double)TICKS_PER_SECOND, + // Next changeover timestamp in UNIX time, millisecond granularity + (nextChangeoverTicks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000), // Division by 1000 -> needs to be in ms + // Current simulation time string, formatted for display + TimeOnly.FromDateTime(currentSimulationDateTime).ToString(), + // Current time factor + _timeFactor, + // Current simulated timestamp in UNIX time, millisecond granularity + (currentSimulationDateTime.Ticks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000), // Division by 1000 -> needs to be in ms + // Start timestamp (simulation time) in UNIX time, millisecond granularity + (startSimulationDateTime.Ticks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000)); // Division by 1000 -> needs to be in ms } [MemberNotNull(nameof(_periodData))] @@ -46,22 +64,19 @@ namespace TrafficLightsSignalR // Load and deserialised JSON data from file var periodDataFilename = _configuration.GetValue("PeriodConfigurationFile") ?? throw new InvalidOperationException("Unable to find period data filename (PeriodConfigurationFile) in configuration -> please check application settings"); var periodDataStream = File.OpenRead(periodDataFilename); - _periodData = JsonSerializer.Deserialize>(periodDataStream); // may throw JsonException + + var serializerOptions = new JsonSerializerOptions { Converters = { new TimeOnlyConverter() } }; + _periodData = JsonSerializer.Deserialize>(periodDataStream, serializerOptions); // may throw JsonException if (_periodData is null) { throw new InvalidOperationException($"JSON deserialisation returned no data available in given configuration file: {periodDataFilename}"); } + periodDataStream.Close(); // Parse file for convenience foreach (Period period in _periodData) { - timeParseSuccess = TimeOnly.TryParse(period.TimeStart, out period.TimeStartParsed); - if (!timeParseSuccess) - { - throw new InvalidOperationException($"Unable to parse time start string in period data: ${period.TimeStart}"); - } - - _logger.LogInformation($"Period found with name: '{period.Name}', timestart: '{period.TimeStart}', parsed timestart: {period.TimeStartParsed}, parse success: {timeParseSuccess}"); + _logger.LogInformation($"Period loaded - name: '{period.Name}', timestart: '{period.TimeStart}', parsed timestart: {period.TimeStart}, parse success: {timeParseSuccess}"); foreach (State state in period.States) { @@ -82,7 +97,7 @@ namespace TrafficLightsSignalR { return index; } - if (testTime >= _periodData[index].TimeStartParsed && testTime < _periodData[index + 1].TimeStartParsed) + if (testTime >= _periodData[index].TimeStart && testTime < _periodData[index + 1].TimeStart) { return index; } @@ -110,10 +125,10 @@ namespace TrafficLightsSignalR _logger.LogInformation($"Next changeover second(s): {TimeSpan.FromTicks(nextChangeoverTicks - startTicks)}"); _logger.LogInformation($"Start simulation DT: {startSimulationDateTime}"); + UpdateState(CreateStateRecord(_periodData[currentPeriodIndex], _periodData[currentPeriodIndex].States[currentStateIndex], currentStateIndex, nextChangeoverTicks, startSimulationDateTime, startSimulationDateTime)); + while (!cancellationToken.IsCancellationRequested) { - // _logger.LogInformation("Working behind the scenes..."); - // _stateService.SetCurrentTrafficLightState(string.Format("Thoughts about life, the universe, and everything! [ {0} ]", DateTime.Now)); now = DateTime.UtcNow; long nowTicks = now.Ticks; @@ -139,17 +154,7 @@ namespace TrafficLightsSignalR nextChangeoverTicks = nowTicks + (long)((_periodData[currentPeriodIndex].States[currentStateIndex].Duration * TICKS_PER_SECOND) / _timeFactor); _logger.LogInformation($" Next changeover in {_periodData[currentPeriodIndex].States[currentStateIndex].Duration} simulation second(s)"); - var currentState = new TrafficLightState( - _periodData[currentPeriodIndex], - _periodData[currentPeriodIndex].States[currentStateIndex], - currentStateIndex, - (nextChangeoverTicks - DateTime.UtcNow.Ticks) / (double)TICKS_PER_SECOND, - (nextChangeoverTicks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000), // Division by 1000 -> needs to be in ms - TimeOnly.FromDateTime(currentSimulationDateTime).ToString(), - _timeFactor, - (currentSimulationDateTime.Ticks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000), // Division by 1000 -> needs to be in ms - (startSimulationDateTime.Ticks - EPOCH_TICKS) / (TICKS_PER_SECOND / 1000)); // Division by 1000 -> needs to be in ms - UpdateState(currentState); + UpdateState(CreateStateRecord(_periodData[currentPeriodIndex], _periodData[currentPeriodIndex].States[currentStateIndex], currentStateIndex, nextChangeoverTicks, startSimulationDateTime, currentSimulationDateTime)); } await Task.Delay((int)((1000 / UPDATES_PER_SIMULATED_SECOND) / _timeFactor), cancellationToken); diff --git a/TrafficLightsSignalR/TrafficLightsState.cs b/TrafficLightsSignalR/TrafficLightsState.cs index e6b7167..f52a01f 100644 --- a/TrafficLightsSignalR/TrafficLightsState.cs +++ b/TrafficLightsSignalR/TrafficLightsState.cs @@ -3,21 +3,6 @@ using TrafficLightsSignalR.Schema; namespace TrafficLightsSignalR { - /* - public record TrafficLightsState - { - public Period CurrentPeriod { get; init; } - public State CurrentState { get; init; } - public int CurrentStateIndex { get; init; } - public double SecondsUntilChangeover { get; init; } - public long NextChangeoverTimestampMs { get; init; } - public string CurrentSimulationTime { get; init; } // UTC - public double TimeFactor { get; init; } - public long CurrentSimulationTimestampMs { get; init; } // UNIX epoch timestamp (UTC in milliseconds) - public long StartSimulationTimestampMs { get; init; } // UNIX epoch timestamp (UTC in milliseconds) - - } - */ public record TrafficLightState( Period CurrentPeriod, State CurrentState, diff --git a/TrafficLightsSignalR/wwwroot/js/lights.js b/TrafficLightsSignalR/wwwroot/js/lights.js index 2ac90ca..7b189fc 100644 --- a/TrafficLightsSignalR/wwwroot/js/lights.js +++ b/TrafficLightsSignalR/wwwroot/js/lights.js @@ -8,16 +8,21 @@ var nextUpdateTime = null; var connection = new signalR.HubConnectionBuilder().withUrl("/lightHub").build(); connection.on("ReceiveStateUpdate", function (stateData) { - console.log("Update pushed."); + console.log("Received pushed update."); updateView(stateData); }); connection.on("ReceiveServerState", function (stateData) { - console.log("Recevied requested server state...") + console.log("Recevied requested server state..."); + if (stateData == null) { + console.log("State not yet initialized, not updating."); + return; + } updateView(stateData); }); connection.start().then(function () { + console.log("Connection started, requesting server state..."); connection.invoke("GetServerState", connection.connectionId).catch(function (err) { return console.error(err.toString()); }); @@ -25,23 +30,6 @@ connection.start().then(function () { return console.error(err.toString()); }); -document.getElementById("sendButton").addEventListener("click", function (event) { - var user = document.getElementById("userInput").value; - var message = document.getElementById("messageInput").value; - connection.invoke("SendMessage", user, message).catch(function (err) { - return console.error(err.toString()); - }); - event.preventDefault(); -}); - -document.getElementById("updateButton").addEventListener("click", function (event) { - var connectionId = connection.connectionId; - connection.invoke("GetServerState", connectionId).catch(function (err) { - return console.error(err.toString()); - }); - event.preventDefault(); -}); - function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)) }