using System.Collections.Concurrent; using System.Runtime.InteropServices; using SafelyYou.Config; namespace SafelyYou; public readonly record struct DeviceId(string Value); public sealed class KnownDevices { private readonly HashSet _knownDevices; public KnownDevices(HashSet knownDevices) => _knownDevices = knownDevices; public bool Contains(DeviceId deviceId) => _knownDevices.Contains(deviceId); } public readonly record struct HeartBeatRange(DateTimeOffset Start, DateTimeOffset? End); public sealed class DeviceHeartBeatHistory { private readonly HeartBeatHistoryConfig _config; public enum AddResult { Added, OutOfSequence } public DeviceHeartBeatHistory(HeartBeatHistoryConfig config) => _config = config; private readonly PriorityQueue _histories = // lowest priority is first, but this allows the highest date to be dequeued first new PriorityQueue(new InverseDateTimeComparer()); private sealed class InverseDateTimeComparer : IComparer { public int Compare(DateTimeOffset x, DateTimeOffset y) => -(x.CompareTo(y)); } /// Returns Added, needed to be used as an expression private static AddResult Replace(PriorityQueue histories, HeartBeatRange updated) { histories.DequeueEnqueue(updated, updated.Start); return AddResult.Added; } /// Returns Added, needed to be used as an expression private static AddResult EnqueueNew(PriorityQueue histories, DateTimeOffset sentAt) { histories.Enqueue(new HeartBeatRange(sentAt, null), sentAt); return AddResult.Added; } public AddResult Add(DateTimeOffset sentAt) { // check the latest first, assuming things are in order // if it's "in the past" and doesn't make sense, return an error // histories that are adjacent are combined, if there's more than a one minute gap (what about a margin of error?) // then create a new entry. The margin is coming from the config. I've set it to 1 second. if (!_histories.TryPeek(out var last, out _)) return EnqueueNew(_histories, sentAt); // last registered heartbeat is End if set, otherwise Start return (sentAt, last.Start, last.End) switch { var (sent, start, _) when sent < start => AddResult.OutOfSequence, (var sent, _, { } end) when sent < end => AddResult.OutOfSequence, // no End yet: extend if within Window of Start (var sent, var start, null) when sent <= start + _config.Window => Replace(_histories, last with { End = sentAt }), // End set: extend if within Window of End (var sent, _, { } end) when sent <= end + _config.Window => Replace(_histories, last with { End = sentAt }), // gap larger than Window: start a new range _ => EnqueueNew(_histories, sentAt), }; } } public sealed class HeartBeatHistories { private readonly HeartBeatHistoryConfig _config; private readonly ConcurrentDictionary _histories = new(); public HeartBeatHistories(HeartBeatHistoryConfig config) => _config = config; public DeviceHeartBeatHistory.AddResult Add(DeviceId deviceId, DateTimeOffset sentAt) { if (!_histories.TryGetValue(deviceId, out var history)) { history = new(_config); _histories[deviceId] = history; } return history.Add(sentAt); } }