// Package runtimeconfig keeps the active database-backed integration settings // and notifies long-lived provider clients when they change. package runtimeconfig import ( "bytes" "encoding/json" "strings" "sync" ) type Config struct { Kind string Provider string Enabled bool Values json.RawMessage } type listener struct { kind string provider string callback func(Config) } type Store struct { // This store is intentionally not merged with config.Store. Integration // settings are database-backed and listeners subscribe by provider key, // whereas config.Store owns file snapshots and fsnotify lifecycle. mu sync.RWMutex values map[string]Config listeners map[uint64]listener nextID uint64 } func NewStore() *Store { return &Store{values: make(map[string]Config), listeners: make(map[uint64]listener)} } func configKey(kind, provider string) string { return strings.ToLower(strings.TrimSpace(kind)) + "/" + strings.ToLower(strings.TrimSpace(provider)) } func cloneConfig(config Config) Config { config.Values = append(json.RawMessage(nil), config.Values...) return config } func sameConfig(left, right Config) bool { return left.Kind == right.Kind && left.Provider == right.Provider && left.Enabled == right.Enabled && bytes.Equal(left.Values, right.Values) } func (s *Store) Get(kind, provider string) (Config, bool) { if s == nil { return Config{}, false } s.mu.RLock() config, ok := s.values[configKey(kind, provider)] s.mu.RUnlock() return cloneConfig(config), ok } func (s *Store) Set(config Config) { if s == nil { return } config.Kind = strings.ToLower(strings.TrimSpace(config.Kind)) config.Provider = strings.ToLower(strings.TrimSpace(config.Provider)) config = cloneConfig(config) key := configKey(config.Kind, config.Provider) s.mu.Lock() if s.values == nil { s.values = make(map[string]Config) } if previous, exists := s.values[key]; exists && sameConfig(previous, config) { s.mu.Unlock() return } s.values[key] = config callbacks := s.matchingListenersLocked(config.Kind, config.Provider) s.mu.Unlock() for _, callback := range callbacks { callback(cloneConfig(config)) } } func (s *Store) Delete(kind, provider string) { if s == nil { return } kind = strings.ToLower(strings.TrimSpace(kind)) provider = strings.ToLower(strings.TrimSpace(provider)) s.mu.Lock() key := configKey(kind, provider) if _, exists := s.values[key]; !exists { s.mu.Unlock() return } delete(s.values, key) callbacks := s.matchingListenersLocked(kind, provider) s.mu.Unlock() config := Config{Kind: kind, Provider: provider} for _, callback := range callbacks { callback(config) } } func (s *Store) Replace(configs []Config) { if s == nil { return } next := make(map[string]Config, len(configs)) for _, config := range configs { config.Kind = strings.ToLower(strings.TrimSpace(config.Kind)) config.Provider = strings.ToLower(strings.TrimSpace(config.Provider)) config = cloneConfig(config) next[configKey(config.Kind, config.Provider)] = config } s.mu.Lock() previous := s.values s.values = next listeners := make([]listener, 0, len(s.listeners)) for _, item := range s.listeners { listeners = append(listeners, item) } s.mu.Unlock() changed := make(map[string]Config, len(previous)+len(next)) for key, previousConfig := range previous { nextConfig, exists := next[key] if !exists { changed[key] = Config{Kind: previousConfig.Kind, Provider: previousConfig.Provider} continue } if !sameConfig(previousConfig, nextConfig) { changed[key] = nextConfig } } for key, nextConfig := range next { if _, exists := previous[key]; !exists { changed[key] = nextConfig } } for _, item := range listeners { if config, ok := changed[configKey(item.kind, item.provider)]; ok { item.callback(cloneConfig(config)) } } } func (s *Store) Subscribe(kind, provider string, callback func(Config)) func() { if s == nil || callback == nil { return func() {} } s.mu.Lock() s.nextID++ id := s.nextID if s.listeners == nil { s.listeners = make(map[uint64]listener) } s.listeners[id] = listener{kind: strings.ToLower(strings.TrimSpace(kind)), provider: strings.ToLower(strings.TrimSpace(provider)), callback: callback} s.mu.Unlock() return func() { s.mu.Lock() delete(s.listeners, id) s.mu.Unlock() } } func (s *Store) matchingListenersLocked(kind, provider string) []func(Config) { callbacks := make([]func(Config), 0) for _, item := range s.listeners { if item.kind == kind && item.provider == provider { callbacks = append(callbacks, item.callback) } } return callbacks }