using System.Collections.Concurrent; 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 sealed class DeviceHeartBeatHistory { public enum AddResult { Added, OutOfSequence } public long NumberOfHeartBeats { get; private set; } public DateTimeOffset? FirstHeartBeat { get; private set; } public DateTimeOffset? LastHeartBeat { get; private set; } public AddResult Add(DateTimeOffset sentAt) { // Reject heartbeats that arrive before the latest registered time. // Gaps are fine: uptime can be derived from first, last, and count. if (LastHeartBeat is null) { FirstHeartBeat = sentAt; LastHeartBeat = sentAt; NumberOfHeartBeats = 1; return AddResult.Added; } if (sentAt < LastHeartBeat) return AddResult.OutOfSequence; // note: does not account for rejecting heartbeats that come between the one minute window. LastHeartBeat = sentAt; NumberOfHeartBeats++; return AddResult.Added; } } public sealed class HeartBeatHistories { private readonly ConcurrentDictionary _histories = new(); public DeviceHeartBeatHistory.AddResult Add(DeviceId deviceId, DateTimeOffset sentAt) { if (!_histories.TryGetValue(deviceId, out var history)) { history = new(); _histories[deviceId] = history; } return history.Add(sentAt); } public double Uptime(DeviceId deviceId) { if (!_histories.TryGetValue(deviceId, out var history)) return 0; var heartBeatMinutes = (history.FirstHeartBeat, history.LastHeartBeat) switch { ({} first, {} second ) => (second - first).TotalMinutes, (not null, null) => 1, (null, _) => 0 }; if (heartBeatMinutes == 0) return 0L; var uptime = (history.NumberOfHeartBeats / heartBeatMinutes) * 100L; return uptime; } }