package deviceview import ( "fmt" "sync" "time" "example.com/mqtt5-learning/internal/contracts" ) type Snapshot struct { DeviceID string MessageID string ReportedAt time.Time ReceivedAt time.Time Temperature float64 TemperatureSet bool } type Status struct { Snapshot Snapshot HeartbeatFresh bool } type Store struct { mu sync.RWMutex latest map[string]Snapshot } func NewStore() *Store { return &Store{latest: make(map[string]Snapshot)} } func (s *Store) RecordHeartbeat(message contracts.Heartbeat, receivedAt time.Time) (bool, error) { reportedAt, err := time.Parse(time.RFC3339, message.OccurredAt) if err != nil { // Decode already validates this timestamp. Keep the store defensive too. return false, fmt.Errorf("parse heartbeat timestamp: %w", err) } snapshot := Snapshot{ DeviceID: message.DeviceID, MessageID: message.MessageID, ReportedAt: reportedAt, ReceivedAt: receivedAt, } if message.Temperature != nil { snapshot.Temperature = *message.Temperature snapshot.TemperatureSet = true } s.mu.Lock() defer s.mu.Unlock() previous, exists := s.latest[message.DeviceID] if exists && !reportedAt.After(previous.ReportedAt) { return false, nil } s.latest[message.DeviceID] = snapshot return true, nil } func (s *Store) GetStatus(deviceID string, now time.Time, freshFor time.Duration) (Status, bool) { s.mu.RLock() snapshot, exists := s.latest[deviceID] s.mu.RUnlock() if !exists { return Status{}, false } fresh := freshFor > 0 && !now.Before(snapshot.ReceivedAt) && now.Sub(snapshot.ReceivedAt) <= freshFor return Status{Snapshot: snapshot, HeartbeatFresh: fresh}, true }