Creado apartir del commit b888de6c92056de294456f581a56e25e734d4ab7 de develop

This commit is contained in:
jrojas
2026-06-26 09:52:35 +02:00
commit 319fd3dfb0
746 changed files with 106610 additions and 0 deletions
@@ -0,0 +1,224 @@
using adas_core.Application.Services.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.GroupedObservations;
using adas_core.Domain.Utils;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
namespace adas_core.Application.Subscriptions;
public class SubscriberGroupedService(
ICacheService? cacheService,
IGroupedObservationService groupedObservationService,
ILogger<SubscriberGroupedService> logger,
Lazy<IClientMessageService> clientMessageService)
: ISubscriberGroupedService
{
private List<WsSubscriberGrouped> _subscriberGrouped = [];
//private static readonly ILogger _logger = Log.ForContext<SubscriberGrouped>();
public List<WsSubscriberGrouped> SubscriberGroupedList
{
get
{
lock (_subscriberGrouped)
{
return _subscriberGrouped;
}
}
set
{
lock (_subscriberGrouped)
{
_subscriberGrouped = value;
}
}
}
public List<WsSubscriberGrouped> GetGrouped()
{
lock (_subscriberGrouped)
{
return _subscriberGrouped.ToList();
}
}
public void RemoveGroupedObsByPatientId(string patientId)
{
lock (_subscriberGrouped)
{
var itemToRemove = _subscriberGrouped.Where(c => c.PatientId.ToString() == patientId).ToList();
itemToRemove.ForEach(item => _subscriberGrouped.Remove(item));
}
}
/// <summary>
/// Check if what clients dont need to get updated with new grouped observations and remove them
/// diference between section and box if location is updated doesn't matter if is section or box
/// BoxSubscribers don't need to get updated otherwise SectionSubscribers may still need to get updated
/// </summary>
/// <param name="patientId">objectId on db for Patient</param>
/// <param name="newLocation"></param>
public void RemoveWsSubscriberByLocation(string patientId, PatientLocation? newLocation)
{
lock (_subscriberGrouped)
{
var itemToRemove = _subscriberGrouped.Where(c => c.PatientId.ToString() == patientId).ToList();
foreach (var wsSubscriberGrouped in itemToRemove)
{
// Make a separate list for wsClients to be removed
var wsClientsToRemove = new List<string>();
foreach (var wsClient in wsSubscriberGrouped.WsSubscriber) wsClientsToRemove.Add(wsClient);
// Remove the wsClients
foreach (var wsClient in wsClientsToRemove) wsSubscriberGrouped.WsSubscriber.Remove(wsClient);
CheckEmptySubscriberGroup(wsSubscriberGrouped);
}
}
}
public void RemoveWsSubscriberPatientIdAndWsId(string patientId, string wsIdToRemove)
{
lock (_subscriberGrouped)
{
var itemToRemove = _subscriberGrouped
.Where(c => c.PatientId.ToString() == patientId && c.WsSubscriber.Contains(wsIdToRemove)).ToList();
foreach (var wsSubscriberGrouped in itemToRemove)
{
// // Make a separate list for wsClients to be removed
// var wsClientsToRemove = new List<string>();
// foreach (var wsClient in wsSubscriberGrouped.WsSubscriber)
// {
// if(wsClient == wsIdToRemove)
// wsClientsToRemove.Add(wsClient);
// }
//
// // Remove the wsClients
// foreach (var wsClient in wsClientsToRemove)
// {
// wsSubscriberGrouped.WsSubscriber.Remove(wsClient);
// wsSubscriberGrouped.Group.Remove(wsClient);
// }
wsSubscriberGrouped.WsSubscriber.Remove(wsIdToRemove);
wsSubscriberGrouped.Group.Remove(wsIdToRemove);
CheckEmptySubscriberGroup(wsSubscriberGrouped);
}
}
}
/// <summary>
/// Check clients using generated grouped observations on disconections
/// removing the wole object in case of 0 or
/// only the group if any other client is using it
/// </summary>
/// <param name="wsSubscriberGrouped">
/// Object with properties to generate new grouped observations, clients using those
/// grouped obsercations and the generated list of grouped observations
/// </param>
public List<string> CheckEmptySubscriberGroup(WsSubscriberGrouped? wsSubscriberGrouped)
{
lock (_subscriberGrouped)
{
var removed = new List<string>();
if (wsSubscriberGrouped == null) return [];
if (wsSubscriberGrouped.WsSubscriber.Count == 0)
{
//_logger.Debug($"Removing wsSubscriberGrouped from list because client subscriber are empty");
wsSubscriberGrouped.Timer.Stop();
wsSubscriberGrouped.Timer.Dispose();
_subscriberGrouped.Remove(wsSubscriberGrouped);
removed.Add(
$"ws:{wsSubscriberGrouped.PatientId}-{string.Join(";", wsSubscriberGrouped.Names)}-{wsSubscriberGrouped.Regularity}-{wsSubscriberGrouped.Group.Count}");
cacheService?.DeleteObjectAsync(wsSubscriberGrouped.HashCode);
}
else
{
foreach (var kvp in wsSubscriberGrouped.Group.ToList())
if (!wsSubscriberGrouped.WsSubscriber.Any(subscriber =>
subscriber.Contains(kvp.Key)))
{
//_logger.Debug($"Removing group {kvp.Key}:{kvp.Value} from list because client subscriber got disconnected but wsSubscriberGrouped still in use with {wsSubscriberGrouped.WsSubscriber.Count()} clients");
wsSubscriberGrouped.Group.Remove(kvp.Key);
wsSubscriberGrouped.WsSubscriber.Remove(kvp.Key);
removed.Add($"group:{kvp.Key}");
}
}
return removed;
}
}
public void AddSubscriberGrouped(WsSubscriberGrouped wsSubscriberGrouped)
{
lock (_subscriberGrouped)
{
_subscriberGrouped.Add(wsSubscriberGrouped);
}
}
public void CheckOnSubscriptionGroup(GroupedField groupedField, ObjectId patientId, string? timeZoneId,
string connectionId, GroupedObservation lastObsInGroup)
{
var wsSubscriberGrouped = GetGrouped().FirstOrDefault(s => s.Compare(groupedField, patientId));
if (wsSubscriberGrouped != null)
{
wsSubscriberGrouped.WsSubscriber.Add(connectionId);
// Verifica si el diccionario contiene la clave y el valor deseado
if (!wsSubscriberGrouped.Group.ContainsKey(connectionId) ||
wsSubscriberGrouped.Group[connectionId] != connectionId)
// Añade el par clave-valor al diccionario
wsSubscriberGrouped.Group[connectionId] = groupedField.Group ?? string.Empty;
}
else
{
AddSubscriberGrouped(
// ReSharper disable once AsyncVoidLambda
new WsSubscriberGrouped(patientId, connectionId, timeZoneId, groupedField, lastObsInGroup,
async delegate(object? sender, string _)
{
try
{
if (sender is WsSubscriberGrouped ws) await AddEmptyObs(ws);
}
catch (Exception ex)
{
logger.LogError(ex, "Ocurrió un error al procesar el evento del suscriptor.");
throw;
}
}));
}
}
public void UpdateLastGroupedObsInGroup(string wsgHashCode, GroupedObservation newGroupedObservation)
{
lock (_subscriberGrouped)
{
_subscriberGrouped.FirstOrDefault(c => c.HashCode == wsgHashCode)?.UpdateLastGo(newGroupedObservation);
}
}
private async Task AddEmptyObs(WsSubscriberGrouped ws)
{
var gobs = await groupedObservationService.CreateNextEmptyObs(ws);
UpdateLastGroupedObsInGroup(ws.HashCode, gobs);
lock (_subscriberGrouped)
{
foreach (var sub in ws.WsSubscriber)
{
gobs.Group = ws.Group.GetValue(sub)!;
logger.LogInformation("Sending empty obs on gruped for: {Sub}, with group: {Group}", sub, gobs.Group);
_ = clientMessageService.Value.SendAsync(sub, OperationType.GroupedObservation, gobs);
}
}
}
}
@@ -0,0 +1,299 @@
using System.Timers;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.GroupedObservations;
using adas_core.Domain.Utils;
using MongoDB.Bson;
using Serilog;
using static adas_core.Domain.Models.GroupedObservation;
using Timer = System.Timers.Timer;
namespace adas_core.Application.Subscriptions;
/**
* This class is used to group the subscribers by the same group
*/
public class WsSubscriberGrouped
{
private readonly EventHandler<string> _sendEvent;
public WsSubscriberGrouped(ObjectId patientId, string wsId, string? timeZoneId,
GroupedField gf, GroupedObservation lastGroupedObservationObs, EventHandler<string> sendEvent)
{
_sendEvent = sendEvent;
if (WsSubscriber.ToList().FirstOrDefault(s => s == wsId) == null) WsSubscriber.Add(wsId);
TimeZoneId = timeZoneId ?? "Romance Standard Time";
PatientId = patientId;
Names = CollectionsUtils.IfEmptyOrNull(gf.Names, [gf.Name ?? string.Empty]);
Max = gf.Max;
Since = gf.Since;
StartTimeShift = gf.StartTimeShift;
Regularity = gf.Regularity;
Result = gf.Result;
Group.Add(wsId, gf.Group ?? string.Empty);
UpdateLastGo(lastGroupedObservationObs);
HashCode = CryptoAdas.CreateMd5GroupedObs(gf, patientId);
Timer = new Timer
{
Interval = SetUpTimerInterval()
};
Timer.Elapsed += Timer_Elapsed;
if (Regularity != GroupedObservationEnum.Regularity.Times) Timer.Start();
}
public List<string> WsSubscriber { get; set; } = [];
public ObjectId PatientId { get; set; }
public List<string> Names { get; set; }
public int Max { get; set; }
public GroupedObservationEnum.Regularity? Regularity { get; set; }
public List<GroupedObservationEnum.Result> Result { get; set; }
public GroupedObservationEnum.Since Since { get; set; }
public List<string> StartTimeShift { get; set; }
public string HashCode { get; set; }
public string TimeZoneId { get; set; }
public List<GroupedObservationObs> LastGroupedObservationObs { get; set; } = [];
public Dictionary<string, string> Group { get; set; } = new();
public Timer Timer { get; set; }
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.Add(lastObs);
}
else
{
lastObs = lastGroupedObservationObs.Observations.LastOrDefault();
if (lastObs != null) LastGroupedObservationObs.Add(lastObs);
}
}
}
private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
var currentDateTime = DateTime.Now;
Timer.Stop();
try
{
bool hasCurrentObs;
switch (Regularity)
{
case GroupedObservationEnum.Regularity.Second:
// Filtra observaciones con el mismo segundo actual
hasCurrentObs = LastGroupedObservationObs.Any(obs => obs.Time.Second == currentDateTime.Second
&& obs.Time.Minute == currentDateTime.Minute
&& obs.Time.Hour == currentDateTime.Hour
&& obs.Time.Date == currentDateTime.Date);
break;
case GroupedObservationEnum.Regularity.Minute:
// Filtra observaciones con el mismo minuto actual
hasCurrentObs = LastGroupedObservationObs.Any(obs => obs.Time.Minute == currentDateTime.Minute
&& obs.Time.Hour == currentDateTime.Hour
&& obs.Time.Date == currentDateTime.Date);
break;
case GroupedObservationEnum.Regularity.Day:
// Filtra observaciones con la misma fecha actual (día completo)
hasCurrentObs = LastGroupedObservationObs.Any(obs => obs.Time.Date == currentDateTime.Date);
break;
default:
// Filtra observaciones con la misma hora actual
hasCurrentObs = LastGroupedObservationObs.Any(obs => obs.Time.Hour == currentDateTime.Hour
&& obs.Time.Date == currentDateTime.Date);
break;
}
// Si no hay al menos una observación que coincide, invoca el evento
if (!hasCurrentObs) _sendEvent(this, HashCode);
}
catch (Exception ex)
{
Log.Warning(
$"Exception on WsSubsciptor for patient: {PatientId} message: {ex.Message} exception: {ex}");
}
TimerReestart();
}
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;
}
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;
}
}
public static class WsSubscriberExtension
{
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);
}
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;
}
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);
}
/// <summary>
/// Check if the incoming observation affect to the group and is new info or is irrelevant
/// </summary>
/// <returns></returns>
public static bool IsNewObservationRelevantForGroup(this WsSubscriberGrouped group, PatientObservation obs)
{
double.TryParse(obs.Value.ToString(), out var pobsValue);
switch (group.Regularity)
{
case GroupedObservationEnum.Regularity.Shift:
case GroupedObservationEnum.Regularity.Hour:
var obsInThisHour = group.LastGroupedObservationObs.Where(c =>
new DateTime(c.Time.Year, c.Time.Month, c.Time.Day, c.Time.Hour, 0, 0) ==
new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour, 0, 0) && !c.IsFilled);
//Obs is in grouped time, don't generate group for observation outside of range
//Change to don't cut is obs time is a future
var obsInTime = DateTime.UtcNow.AddHours(group.Max) > obs.Time &&
DateTime.UtcNow.AddHours(group.Max * -1) < obs.Time;
//future data is relevant because maybe nothing is inserted in the same hour after it and we cant lose it
/* if (DateTime.UtcNow.AddHours(group.Max) > obs.time)
{
}
*/
if (!obsInTime) return false;
var groupedObservationObsEnumerable = obsInThisHour.ToList();
if (!groupedObservationObsEnumerable.Any() && obsInTime) return true;
//If obs in this hour check if is relevant by Result
if (groupedObservationObsEnumerable.Any())
foreach (var result in group.Result)
switch (result)
{
case GroupedObservationEnum.Result.Max:
var value = groupedObservationObsEnumerable.Max(s => s.Max?.Value);
var parsed = double.TryParse(value?.ToString(), out var valueParsed);
//actual value is higher, is relevant
if (parsed && pobsValue > valueParsed) return true;
break;
case GroupedObservationEnum.Result.Min:
var minValue = groupedObservationObsEnumerable.Min(s => s.Min?.Value);
var isParsed = double.TryParse(minValue?.ToString(), out var minValueParsed);
//actual value is higher, is relevant
if (isParsed && pobsValue < minValueParsed) return true;
break;
case GroupedObservationEnum.Result.Last:
var lastObsInTime = groupedObservationObsEnumerable.OrderBy(t => t.Last?.Time).First();
//is the new latest in the hour, relevant
if (lastObsInTime.Time < obs.Time) return true;
break;
case GroupedObservationEnum.Result.LastFilled:
var lastFilledObsInTime = groupedObservationObsEnumerable
.OrderBy(t => t.LastFilled?.Time).First();
//is the new latest in the hour, relevant
if (lastFilledObsInTime.Time < obs.Time) return true;
break;
//always is relevant because SUM everything in the hour
//TODO halfHour should check if any obs already in the halfHour
case GroupedObservationEnum.Result.Sum:
case GroupedObservationEnum.Result.Average:
case GroupedObservationEnum.Result.HalfHour:
return true;
}
return false;
default:
return true;
}
}
}
@@ -0,0 +1,22 @@
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.GroupedObservations;
using adas_core.Domain.Models.Masters;
using MongoDB.Bson;
namespace adas_core.Application.Subscriptions;
public class WsSubscriber(string id)
{
public string? UserName;
public string Id { get; set; } = id;
public ObjectId? DisplayId { get; set; }
public List<PatientLocation> Locations { get; set; } = [];
public List<ObjectId> LocationIds { get; set; } = [];
public string? Box { get; set; }
public string? Section { get; set; }
public string? Version { get; set; }
public List<GroupedField>? GroupedFields { get; set; }
public LocaleEnum? Locale { get; set; }
public SubscriptionEnum.WsType? SubscriptionType { get; set; }
}