Files
silo-server/internal/watchsync/registry.go
40a9de7f26 feat(watchsync): add plugin-backed providers (#475)
* feat(watchsync): add plugin-backed providers

* fix(watchsync): address plugin review findings

* fix(watchsync): harden plugin provider failures

* feat(watchsync): complete plugin provider contract

* fix(watchsync): address provider review feedback

* fix(watchsync): keep device state host-private

* fix(watchsync): build reconciliation index concurrently

* fix(watchsync): preserve empty device state updates

* chore(deps): use released watch-sync SDK

---------

Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>
2026-08-06 10:30:49 -04:00

105 lines
2.4 KiB
Go

package watchsync
import (
"fmt"
"sort"
"sync"
)
const providerSourcePlugin = "plugin"
type sourcedProvider interface {
ProviderSource() string
}
type Registry struct {
mu sync.RWMutex
providers map[string]Provider
}
func NewRegistry() *Registry {
return &Registry{providers: make(map[string]Provider)}
}
func (r *Registry) Register(provider Provider) error {
if provider == nil {
return fmt.Errorf("watchsync provider is nil")
}
key := provider.Key()
if key == "" {
return fmt.Errorf("watchsync provider key is empty")
}
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.providers[key]; exists {
return fmt.Errorf("watchsync provider %q already registered", key)
}
r.providers[key] = provider
return nil
}
// ReplacePluginProviders atomically replaces providers discovered from enabled
// plugin installations while preserving built-in providers. A plugin may not
// shadow a built-in key, and duplicate plugin keys reject the entire reload.
func (r *Registry) ReplacePluginProviders(providers []Provider) error {
if r == nil {
return fmt.Errorf("watchsync registry is nil")
}
r.mu.Lock()
defer r.mu.Unlock()
next := make(map[string]Provider, len(r.providers)+len(providers))
for key, provider := range r.providers {
if sourced, ok := provider.(sourcedProvider); ok && sourced.ProviderSource() == providerSourcePlugin {
continue
}
next[key] = provider
}
for _, provider := range providers {
if provider == nil || provider.Key() == "" {
return fmt.Errorf("watchsync plugin provider and key are required")
}
if _, exists := next[provider.Key()]; exists {
return fmt.Errorf("watchsync plugin provider key %q conflicts with another provider", provider.Key())
}
next[provider.Key()] = provider
}
r.providers = next
return nil
}
func (r *Registry) Get(key string) (Provider, bool) {
if r == nil {
return nil, false
}
r.mu.RLock()
defer r.mu.RUnlock()
provider, ok := r.providers[key]
return provider, ok
}
func (r *Registry) List() []ProviderSummary {
if r == nil {
return nil
}
r.mu.RLock()
defer r.mu.RUnlock()
summaries := make([]ProviderSummary, 0, len(r.providers))
for key, provider := range r.providers {
summaries = append(summaries, ProviderSummary{
Key: key,
DisplayName: provider.DisplayName(),
Capabilities: provider.Capabilities(),
})
}
sort.Slice(summaries, func(i, j int) bool {
return summaries[i].Key < summaries[j].Key
})
return summaries
}