using Microsoft.Extensions.Options; using System.Collections.Concurrent; using System.Text.Json; using YD_TrainingCompetition_Api.Models; using YD_TrainingCompetition_Api.Options; namespace YD_TrainingCompetition_Api.Services; public sealed class DeviceStateStore : IDeviceStateStore { private readonly ConcurrentDictionary _states = new(StringComparer.OrdinalIgnoreCase); private readonly ScoringOptions _options; private readonly ILogger _logger; public DeviceStateStore(IOptions options, ILogger logger) { _options = options.Value; _logger = logger; LoadSnapshot(); } public bool TryUpsert(DeviceTelemetry telemetry, out string? reason) { reason = null; if (string.IsNullOrWhiteSpace(telemetry.DeviceId)) { reason = "deviceId is required"; return false; } if (!_states.ContainsKey(telemetry.DeviceId) && _states.Count >= _options.MaxDevices) { reason = $"max device count({_options.MaxDevices}) reached"; return false; } telemetry.Timestamp = telemetry.Timestamp == default ? DateTimeOffset.UtcNow : telemetry.Timestamp; _states[telemetry.DeviceId] = telemetry; PersistSnapshot(); return true; } public IReadOnlyCollection GetAll() { var now = DateTimeOffset.UtcNow; return _states.Values .OrderBy(x => x.DeviceId, StringComparer.OrdinalIgnoreCase) .Select(x => new DeviceSnapshot { Telemetry = x, LastSeen = x.Timestamp, Online = now - x.Timestamp <= TimeSpan.FromSeconds(_options.OfflineThresholdSeconds) }) .ToArray(); } public DeviceSnapshot? GetByDeviceId(string deviceId) { if (!_states.TryGetValue(deviceId, out var telemetry)) { return null; } var now = DateTimeOffset.UtcNow; return new DeviceSnapshot { Telemetry = telemetry, LastSeen = telemetry.Timestamp, Online = now - telemetry.Timestamp <= TimeSpan.FromSeconds(_options.OfflineThresholdSeconds) }; } private void LoadSnapshot() { try { if (!File.Exists(_options.SnapshotPath)) { return; } var json = File.ReadAllText(_options.SnapshotPath); var data = JsonSerializer.Deserialize>(json); if (data is null) { return; } foreach (var telemetry in data) { if (!string.IsNullOrWhiteSpace(telemetry.DeviceId)) { _states[telemetry.DeviceId] = telemetry; } } _logger.LogInformation("Loaded {Count} device snapshots from disk", _states.Count); } catch (Exception ex) { _logger.LogError(ex, "Failed to load device snapshot"); } } private void PersistSnapshot() { try { var directory = Path.GetDirectoryName(_options.SnapshotPath); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } var json = JsonSerializer.Serialize(_states.Values, new JsonSerializerOptions { WriteIndented = false }); File.WriteAllText(_options.SnapshotPath, json); } catch (Exception ex) { _logger.LogError(ex, "Failed to persist device snapshot"); } } }