rama creada apartir de master en j

This commit is contained in:
jrojas
2026-06-26 10:29:23 +02:00
parent 319fd3dfb0
commit c1517fda87
2810 changed files with 1927392 additions and 25392 deletions
@@ -8,6 +8,12 @@ using MongoDB.Bson;
namespace adas_core.Application.Subscriptions;
/// <summary>
/// Provides an implementation of <see cref="ISubscriberGroupedService"/> that coordinates subscription handling for grouped observations, integrating caching, client messaging, and logging capabilities.
/// </summary>
/// <remarks>
/// This service depends on an optional cache, a grouped observation service, a logger, and a lazily-initialized client message service to support its subscriber-related operations.
/// </remarks>
public class SubscriberGroupedService(
ICacheService? cacheService,
IGroupedObservationService groupedObservationService,
@@ -38,6 +44,10 @@ public class SubscriberGroupedService(
}
}
/// <summary>
/// Returns a thread-safe snapshot of the grouped subscribers as a new list. The internal collection is locked during the copy to ensure consistency and prevent concurrent modification.
/// </summary>
/// <returns>A new <see cref="List{WsSubscriberGrouped}"/> containing a copy of the grouped subscribers.</returns>
public List<WsSubscriberGrouped> GetGrouped()
{
lock (_subscriberGrouped)
@@ -46,6 +56,10 @@ public class SubscriberGroupedService(
}
}
/// <summary>
/// Removes all grouped observations associated with the specified patient identifier from the subscriber collection in a thread-safe manner.
/// </summary>
/// <param name="patientId">The string representation of the patient identifier used to locate and remove the matching grouped observations.</param>
public void RemoveGroupedObsByPatientId(string patientId)
{
lock (_subscriberGrouped)
@@ -81,6 +95,11 @@ public class SubscriberGroupedService(
}
}
/// <summary>
/// Removes the specified WebSocket subscriber ID from all subscriber groups associated with the given patient ID in a thread-safe manner, and checks whether any resulting group is now empty for further cleanup.
/// </summary>
/// <param name="patientId">The patient identifier used to locate the subscriber groups to update.</param>
/// <param name="wsIdToRemove">The WebSocket subscriber ID to remove from the matching subscriber and group collections.</param>
public void RemoveWsSubscriberPatientIdAndWsId(string patientId, string wsIdToRemove)
{
lock (_subscriberGrouped)
@@ -155,6 +174,11 @@ public class SubscriberGroupedService(
}
}
/// <summary>
/// Adds a grouped WebSocket subscriber to the internal collection in a thread-safe manner.
/// Uses locking to ensure that concurrent calls do not corrupt the subscriber list.
/// </summary>
/// <param name="wsSubscriberGrouped">The grouped WebSocket subscriber instance to be added to the collection.</param>
public void AddSubscriberGrouped(WsSubscriberGrouped wsSubscriberGrouped)
{
lock (_subscriberGrouped)
@@ -163,8 +187,16 @@ public class SubscriberGroupedService(
}
}
/// <summary>
/// Registers a client connection for grouped-field observation updates for a given patient. If an existing grouped subscription matches the patient and field, the connection is added to it; otherwise, a new grouped subscription is created with a callback that processes empty observations.
/// </summary>
/// <param name="groupedField">The grouped field criteria used to locate or create the matching subscription.</param>
/// <param name="patientId">The identifier of the patient whose grouped observation is being subscribed to.</param>
/// <param name="timeZoneId">The optional time zone identifier applied to the new subscription when one is created.</param>
/// <param name="connectionId">The unique identifier of the client connection being registered.</param>
/// <param name="lastObsInGroup">The most recent observation in the group, used to initialize a new subscription.</param>
public void CheckOnSubscriptionGroup(GroupedField groupedField, ObjectId patientId, string? timeZoneId,
string connectionId, GroupedObservation lastObsInGroup)
string connectionId, GroupedObservation lastObsInGroup)
{
var wsSubscriberGrouped = GetGrouped().FirstOrDefault(s => s.Compare(groupedField, patientId));
if (wsSubscriberGrouped != null)
@@ -182,7 +214,7 @@ public class SubscriberGroupedService(
AddSubscriberGrouped(
// ReSharper disable once AsyncVoidLambda
new WsSubscriberGrouped(patientId, connectionId, timeZoneId, groupedField, lastObsInGroup,
async delegate(object? sender, string _)
async delegate (object? sender, string _)
{
try
{
@@ -197,6 +229,12 @@ public class SubscriberGroupedService(
}
}
/// <summary>
/// Updates the last grouped observation in the subscriber group identified by the specified hash code.
/// Performs a thread-safe lookup; if no matching subscriber group is found, the operation is silently skipped.
/// </summary>
/// <param name="wsgHashCode">The hash code used to identify the target subscriber group whose last grouped observation should be updated.</param>
/// <param name="newGroupedObservation">The new <see cref="GroupedObservation"/> to set as the last grouped observation in the matched group.</param>
public void UpdateLastGroupedObsInGroup(string wsgHashCode, GroupedObservation newGroupedObservation)
{
lock (_subscriberGrouped)
@@ -205,6 +243,10 @@ public class SubscriberGroupedService(
}
}
/// <summary>
/// Creates the next empty grouped observation for the supplied WebSocket subscriber group and distributes it asynchronously to every subscriber in the group, applying the subscriber-specific group value resolved from the group mapping.
/// </summary>
/// <param name="ws">The grouped WebSocket subscriber context whose next empty observation is generated and whose subscribers will receive the notification.</param>
private async Task AddEmptyObs(WsSubscriberGrouped ws)
{
var gobs = await groupedObservationService.CreateNextEmptyObs(ws);
@@ -13,6 +13,9 @@ namespace adas_core.Application.Subscriptions;
/**
* This class is used to group the subscribers by the same group
*/
/// <summary>
/// Represents a grouped WebSocket subscriber that aggregates and manages multiple related subscription registrations.
/// </summary>
public class WsSubscriberGrouped
{
private readonly EventHandler<string> _sendEvent;
@@ -57,23 +60,27 @@ public class WsSubscriberGrouped
public Dictionary<string, string> Group { get; set; } = new();
public Timer Timer { get; set; }
/// <summary>
/// Refreshes the cached last observations by matching each known name against the supplied grouped observation, falling back to the most recent observation when no match is found.
/// </summary>
/// <param name="lastGroupedObservationObs">The source grouped observation whose observations are searched to populate the last-observation cache.</param>
public void UpdateLastGo(GroupedObservation lastGroupedObservationObs)
{
LastGroupedObservationObs.Clear();
foreach (var name in Names)
{
var lastObs = lastGroupedObservationObs.Observations.LastOrDefault(c => c.Name == name);
if (lastObs != null)
LastGroupedObservationObs.Clear();
foreach (var name in Names)
{
LastGroupedObservationObs.Add(lastObs);
}
else
{
lastObs = lastGroupedObservationObs.Observations.LastOrDefault();
if (lastObs != null) LastGroupedObservationObs.Add(lastObs);
var lastObs = lastGroupedObservationObs.Observations.LastOrDefault(c => c.Name == name);
if (lastObs != null)
{
LastGroupedObservationObs.Add(lastObs);
}
else
{
lastObs = lastGroupedObservationObs.Observations.LastOrDefault();
if (lastObs != null) LastGroupedObservationObs.Add(lastObs);
}
}
}
}
private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
@@ -124,104 +131,135 @@ public class WsSubscriberGrouped
TimerReestart();
}
/// <summary>
/// Restarts the timer by recalculating its interval through <c>SetUpTimerInterval</c> and then starting it.
/// </summary>
private void TimerReestart()
{
Timer.Interval = SetUpTimerInterval();
Timer.Start();
}
private int SetUpTimerInterval()
{
var currentDateTime = DateTime.Now;
int graceTime;
DateTime nextIntervalTime;
switch (Regularity)
{
case GroupedObservationEnum.Regularity.Second:
nextIntervalTime = new DateTime(
currentDateTime.Year,
currentDateTime.Month,
currentDateTime.Day,
currentDateTime.Hour,
currentDateTime.Minute,
currentDateTime.Second
).AddSeconds(1);
graceTime = 500;
break;
case GroupedObservationEnum.Regularity.Minute:
nextIntervalTime = new DateTime(
currentDateTime.Year,
currentDateTime.Month,
currentDateTime.Day,
currentDateTime.Hour,
currentDateTime.Minute,
0
).AddMinutes(1);
graceTime = 30000;
break;
case GroupedObservationEnum.Regularity.Day:
nextIntervalTime = new DateTime(
currentDateTime.Year,
currentDateTime.Month,
currentDateTime.Day,
0,
0,
0
).AddDays(1);
graceTime = 60000;
break;
default:
nextIntervalTime = new DateTime(
currentDateTime.Year,
currentDateTime.Month,
currentDateTime.Day,
currentDateTime.Hour,
0,
0
).AddHours(1);
graceTime = 30000;
break;
Timer.Interval = SetUpTimerInterval();
Timer.Start();
}
var timeSpanToNextInterval = nextIntervalTime - currentDateTime;
Log.Information(
"Obs {Obs}, currentDateTime: {CurrentDateTime}, nextIntervalTime: {NextIntervalTime}, timeSpanToNextInterval: {TimeSpanToNextInterval}, timeSpanToNextInterval.TotalMilliseconds+graceTime: {Tt}",
Group, currentDateTime, nextIntervalTime, timeSpanToNextInterval,
(int)timeSpanToNextInterval.TotalMilliseconds + graceTime);
return (int)timeSpanToNextInterval.TotalMilliseconds + graceTime;
}
/// <summary>
/// Calculates the timer interval in milliseconds until the next scheduled occurrence based on the configured <see cref="Regularity"/>, supporting Second, Minute, Day, and a default (Hour) case, and adds a per-regularity grace time.
/// </summary>
/// <returns>The number of milliseconds to wait until the next interval, including the grace time.</returns>
private int SetUpTimerInterval()
{
var currentDateTime = DateTime.Now;
int graceTime;
DateTime nextIntervalTime;
switch (Regularity)
{
case GroupedObservationEnum.Regularity.Second:
nextIntervalTime = new DateTime(
currentDateTime.Year,
currentDateTime.Month,
currentDateTime.Day,
currentDateTime.Hour,
currentDateTime.Minute,
currentDateTime.Second
).AddSeconds(1);
graceTime = 500;
break;
case GroupedObservationEnum.Regularity.Minute:
nextIntervalTime = new DateTime(
currentDateTime.Year,
currentDateTime.Month,
currentDateTime.Day,
currentDateTime.Hour,
currentDateTime.Minute,
0
).AddMinutes(1);
graceTime = 30000;
break;
case GroupedObservationEnum.Regularity.Day:
nextIntervalTime = new DateTime(
currentDateTime.Year,
currentDateTime.Month,
currentDateTime.Day,
0,
0,
0
).AddDays(1);
graceTime = 60000;
break;
default:
nextIntervalTime = new DateTime(
currentDateTime.Year,
currentDateTime.Month,
currentDateTime.Day,
currentDateTime.Hour,
0,
0
).AddHours(1);
graceTime = 30000;
break;
}
var timeSpanToNextInterval = nextIntervalTime - currentDateTime;
Log.Information(
"Obs {Obs}, currentDateTime: {CurrentDateTime}, nextIntervalTime: {NextIntervalTime}, timeSpanToNextInterval: {TimeSpanToNextInterval}, timeSpanToNextInterval.TotalMilliseconds+graceTime: {Tt}",
Group, currentDateTime, nextIntervalTime, timeSpanToNextInterval,
(int)timeSpanToNextInterval.TotalMilliseconds + graceTime);
return (int)timeSpanToNextInterval.TotalMilliseconds + graceTime;
}
}
/// <summary>
/// Provides static extension methods to enhance WebSocket subscriber functionality.
/// </summary>
public static class WsSubscriberExtension
{
/// <summary>
/// Determines whether the specified <see cref="GroupedField"/> and patient identifier match the current <see cref="WsSubscriberGrouped"/> instance by comparing regularity, since, start time shift, maximum, result sequence, patient identifier, and names (treating a null <paramref name="r"/>.Name as an empty string).
/// </summary>
/// <param name="source">The current <see cref="WsSubscriberGrouped"/> instance being compared.</param>
/// <param name="r">The <see cref="GroupedField"/> to compare against the source.</param>
/// <param name="patientId">The patient identifier to match against the source patient identifier.</param>
/// <returns><c>true</c> if all compared properties are equal; otherwise, <c>false</c>.</returns>
public static bool Compare(this WsSubscriberGrouped source, GroupedField r, ObjectId patientId)
{
return r.Regularity == source.Regularity &&
r.Since == source.Since &&
CompareStringShiftList(r.StartTimeShift, source.StartTimeShift) &&
r.Max == source.Max &&
r.Result.SequenceEqual(source.Result) &&
patientId.ToString() == source.PatientId.ToString() &&
CompareNamesStringList(r.Names, [r.Name ?? string.Empty], source.Names);
}
{
return r.Regularity == source.Regularity &&
r.Since == source.Since &&
CompareStringShiftList(r.StartTimeShift, source.StartTimeShift) &&
r.Max == source.Max &&
r.Result.SequenceEqual(source.Result) &&
patientId.ToString() == source.PatientId.ToString() &&
CompareNamesStringList(r.Names, [r.Name ?? string.Empty], source.Names);
}
/// <summary>
/// Compares two string shift lists for equality, returning true when both lists are null or when both contain the same elements in the same order, and false when only one of the lists is null.
/// </summary>
/// <param name="shift">The target shift list to compare.</param>
/// <param name="sourceShift">The source shift list to compare against.</param>
/// <returns><c>true</c> if both lists are null or if their sequence of strings is equal; otherwise, <c>false</c>.</returns>
private static bool CompareStringShiftList(List<string>? shift, List<string>? sourceShift)
{
if (shift != null && sourceShift != null) return sourceShift.SequenceEqual(shift);
if (shift == null && sourceShift == null) return true;
return false;
}
{
if (shift != null && sourceShift != null) return sourceShift.SequenceEqual(shift);
if (shift == null && sourceShift == null) return true;
return false;
}
/// <summary>
/// Compares the <paramref name="sourceNames"/> list against two candidate name lists and returns whether either one matches.
/// Falls back to the second list (<paramref name="name"/>) when the first (<paramref name="names"/>) is empty or null, or does not match.
/// </summary>
/// <param name="names">The primary candidate list of names to compare against <paramref name="sourceNames"/>.</param>
/// <param name="name">The fallback candidate list of names to compare against <paramref name="sourceNames"/> when <paramref name="names"/> cannot be used.</param>
/// <param name="sourceNames">The source list of names being compared.</param>
/// <returns><c>true</c> if <paramref name="sourceNames"/> is a sequence match for either <paramref name="names"/> or <paramref name="name"/>; otherwise, <c>false</c>.</returns>
private static bool CompareNamesStringList(List<string> names, List<string> name, List<string> sourceNames)
{
if (!CollectionsUtils.IsEmptyOrNull(names) && sourceNames.SequenceEqual(names)) return true;
return !CollectionsUtils.IsEmptyOrNull(name) && sourceNames.SequenceEqual(name);
}
{
if (!CollectionsUtils.IsEmptyOrNull(names) && sourceNames.SequenceEqual(names)) return true;
return !CollectionsUtils.IsEmptyOrNull(name) && sourceNames.SequenceEqual(name);
}
/// <summary>
/// Check if the incoming observation affect to the group and is new info or is irrelevant
@@ -6,6 +6,9 @@ using MongoDB.Bson;
namespace adas_core.Application.Subscriptions;
/// <summary>
/// Represents a WebSocket subscriber identified by a unique string identifier.
/// </summary>
public class WsSubscriber(string id)
{
public string? UserName;