181 lines
4.5 KiB
Go
181 lines
4.5 KiB
Go
// 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
|
|
}
|