package appcontext import ( "context" "encoding/json" "fmt" "strings" "time" "github.com/AFASystems/presence/internal/pkg/model" "github.com/redis/go-redis/v9" "github.com/segmentio/kafka-go" ) // AppState provides centralized access to application state type AppState struct { beacons model.BeaconsList settings model.Settings beaconEvents model.BeaconEventList beaconsLookup map[string]struct{} latestList model.LatestBeaconsList kafkaReadersList model.KafkaReadersList kafkaWritersList model.KafkaWritersList valkeyDB *redis.Client } // NewAppState creates a new application context AppState with default values func NewAppState() *AppState { return &AppState{ beacons: model.BeaconsList{ Beacons: make(map[string]model.Beacon), }, settings: model.Settings{ Settings: model.SettingsVal{ LocationConfidence: 4, LastSeenThreshold: 15, BeaconMetricSize: 30, HASendInterval: 5, HASendChangesOnly: false, RSSIEnforceThreshold: false, RSSIMinThreshold: 100, }, }, beaconEvents: model.BeaconEventList{ Beacons: make(map[string]model.BeaconEvent), }, beaconsLookup: make(map[string]struct{}), latestList: model.LatestBeaconsList{ LatestList: make(map[string]model.Beacon), }, kafkaReadersList: model.KafkaReadersList{ KafkaReaders: make([]*kafka.Reader, 0), }, kafkaWritersList: model.KafkaWritersList{ KafkaWriters: make([]*kafka.Writer, 0), }, } } func (m *AppState) AddValkeyClient(url string) *redis.Client { valkeyDB := redis.NewClient(&redis.Options{ Addr: url, Password: "", }) m.valkeyDB = valkeyDB return valkeyDB } func (m *AppState) CleanValkeyClient() { fmt.Println("shutdown of valkey client starts") if err := m.valkeyDB.Close(); err != nil { fmt.Println("Error in shuting down valkey client") } fmt.Println("Succesfully shutting down valkey client") } func (m *AppState) AddKafkaWriter(kafkaUrl, topic string) *kafka.Writer { kafkaWriter := &kafka.Writer{ Addr: kafka.TCP(kafkaUrl), Topic: topic, Balancer: &kafka.LeastBytes{}, Async: false, RequiredAcks: kafka.RequireAll, BatchSize: 100, BatchTimeout: 10 * time.Millisecond, } m.kafkaWritersList.KafkaWritersLock.Lock() m.kafkaWritersList.KafkaWriters = append(m.kafkaWritersList.KafkaWriters, kafkaWriter) m.kafkaWritersList.KafkaWritersLock.Unlock() return kafkaWriter } func (m *AppState) CleanKafkaWriters() { fmt.Println("shutdown of kafka readers starts") for _, r := range m.kafkaWritersList.KafkaWriters { if err := r.Close(); err != nil { fmt.Printf("Error in closing kafka writer %v", err) } } fmt.Println("Kafka writers graceful shutdown complete") } func (m *AppState) AddKafkaReader(kafkaUrl, topic, groupID string) *kafka.Reader { brokers := strings.Split(kafkaUrl, ",") kafkaReader := kafka.NewReader(kafka.ReaderConfig{ Brokers: brokers, GroupID: groupID, Topic: topic, MinBytes: 1, MaxBytes: 10e6, }) m.kafkaReadersList.KafkaReadersLock.Lock() m.kafkaReadersList.KafkaReaders = append(m.kafkaReadersList.KafkaReaders, kafkaReader) m.kafkaReadersList.KafkaReadersLock.Unlock() return kafkaReader } func (m *AppState) CleanKafkaReaders() { for _, r := range m.kafkaReadersList.KafkaReaders { if err := r.Close(); err != nil { fmt.Printf("Error in closing kafka reader %v", err) } } fmt.Println("Kafka readers graceful shutdown complete") } // GetBeacons returns thread-safe access to beacons list func (m *AppState) GetBeacons() *model.BeaconsList { return &m.beacons } // GetSettings returns thread-safe access to settings func (m *AppState) GetSettings() *model.Settings { return &m.settings } // GetBeaconEvents returns thread-safe access to beacon events func (m *AppState) GetBeaconEvents() *model.BeaconEventList { return &m.beaconEvents } // GetBeaconsLookup returns thread-safe access to beacon lookup map func (m *AppState) GetBeaconsLookup() map[string]struct{} { return m.beaconsLookup } // GetLatestList returns thread-safe access to latest beacons list func (m *AppState) GetLatestList() *model.LatestBeaconsList { return &m.latestList } // AddBeaconToLookup adds a beacon ID to the lookup map func (m *AppState) AddBeaconToLookup(id string) { m.beaconsLookup[id] = struct{}{} } // RemoveBeaconFromLookup removes a beacon ID from the lookup map func (m *AppState) RemoveBeaconFromLookup(id string) { delete(m.beaconsLookup, id) } // BeaconExists checks if a beacon exists in the lookup func (m *AppState) BeaconExists(id string) bool { _, exists := m.beaconsLookup[id] return exists } // GetBeacon returns a beacon by ID (thread-safe) func (m *AppState) GetBeacon(id string) (model.Beacon, bool) { m.beacons.Lock.RLock() defer m.beacons.Lock.RUnlock() beacon, exists := m.beacons.Beacons[id] return beacon, exists } // UpdateBeacon updates a beacon in the list (thread-safe) func (m *AppState) UpdateBeacon(id string, beacon model.Beacon) { m.beacons.Lock.Lock() defer m.beacons.Lock.Unlock() m.beacons.Beacons[id] = beacon } // GetBeaconEvent returns a beacon event by ID (thread-safe) func (m *AppState) GetBeaconEvent(id string) (model.BeaconEvent, bool) { m.beaconEvents.Lock.RLock() defer m.beaconEvents.Lock.RUnlock() event, exists := m.beaconEvents.Beacons[id] return event, exists } // UpdateBeaconEvent updates a beacon event in the list (thread-safe) func (m *AppState) UpdateBeaconEvent(id string, event model.BeaconEvent) { m.beaconEvents.Lock.Lock() defer m.beaconEvents.Lock.Unlock() m.beaconEvents.Beacons[id] = event } // GetLatestBeacon returns the latest beacon by ID (thread-safe) func (m *AppState) GetLatestBeacon(id string) (model.Beacon, bool) { m.latestList.Lock.RLock() defer m.latestList.Lock.RUnlock() beacon, exists := m.latestList.LatestList[id] return beacon, exists } // UpdateLatestBeacon updates the latest beacon in the list (thread-safe) func (m *AppState) UpdateLatestBeacon(id string, beacon model.Beacon) { m.latestList.Lock.Lock() defer m.latestList.Lock.Unlock() m.latestList.LatestList[id] = beacon } // GetAllBeacons returns a copy of all beacons func (m *AppState) GetAllBeacons() map[string]model.Beacon { m.beacons.Lock.RLock() defer m.beacons.Lock.RUnlock() beacons := make(map[string]model.Beacon) for id, beacon := range m.beacons.Beacons { beacons[id] = beacon } return beacons } // GetAllLatestBeacons returns a copy of all latest beacons func (m *AppState) GetAllLatestBeacons() map[string]model.Beacon { m.latestList.Lock.RLock() defer m.latestList.Lock.RUnlock() beacons := make(map[string]model.Beacon) for id, beacon := range m.latestList.LatestList { beacons[id] = beacon } return beacons } // GetBeaconCount returns the number of tracked beacons func (m *AppState) GetBeaconCount() int { m.beacons.Lock.RLock() defer m.beacons.Lock.RUnlock() return len(m.beacons.Beacons) } // GetSettingsValue returns current settings as a value func (m *AppState) GetSettingsValue() model.SettingsVal { m.settings.Lock.RLock() defer m.settings.Lock.RUnlock() return m.settings.Settings } // UpdateSettings updates the system settings (thread-safe) func (m *AppState) UpdateSettings(newSettings model.SettingsVal) { m.settings.Lock.Lock() defer m.settings.Lock.Unlock() m.settings.Settings = newSettings } func (m *AppState) PersistSettings(client *redis.Client, ctx context.Context) { d, err := json.Marshal(m.GetSettingsValue()) if err != nil { fmt.Printf("Error in marshalling settings: %v", err) return } if err := client.Set(ctx, "settings", d, 0).Err(); err != nil { fmt.Printf("Error in persisting settings: %v", err) } }