yao/setting/registry.go
Max 934424f9ea feat(load): add initialization and reloading for Setting Registry
- Implemented initialization for the Setting Registry during the Load process.
- Added reload functionality to refresh the Setting Registry as needed.
- Enhanced error handling to capture and report issues during initialization and reloading.
2026-04-28 15:08:22 +08:00

183 lines
4.5 KiB
Go

package setting
import (
"encoding/json"
"fmt"
"sync"
"time"
"github.com/yaoapp/gou/store"
)
// Global is the singleton Setting Registry.
var Global *Registry
// Registry manages namespaced settings with three-level scope cascade.
type Registry struct {
store store.Store
cache store.Store
mu sync.RWMutex
}
// Init initializes the global Registry.
// Must be called after store.Load (so __yao.store and __yao.cache are available).
func Init() error {
s, err := store.Get("__yao.store")
if err != nil {
return fmt.Errorf("setting.Init: %w", err)
}
c, _ := store.Get("__yao.cache")
Global = &Registry{store: s, cache: c}
return nil
}
// Get reads the raw data for a single scope+namespace.
// If one or more dest pointers are provided, the data is also unmarshalled
// into dest[0] (like json.Unmarshal).
func (r *Registry) Get(scope ScopeID, ns string, dest ...interface{}) (map[string]interface{}, error) {
r.mu.RLock()
defer r.mu.RUnlock()
data, err := storeGet(r.store, r.cache, scope, ns)
if err != nil {
return nil, err
}
if len(dest) > 0 && dest[0] != nil {
if err := bindDest(data, dest[0]); err != nil {
return data, fmt.Errorf("setting bind: %w", err)
}
}
return data, nil
}
// GetMerged reads a namespace across all three scopes and returns a shallow-merged
// result: system <- team <- user (later wins).
// If one or more dest pointers are provided, the merged data is also unmarshalled
// into dest[0].
func (r *Registry) GetMerged(userID, teamID, ns string, dest ...interface{}) (map[string]interface{}, error) {
r.mu.RLock()
defer r.mu.RUnlock()
merged := make(map[string]interface{})
if sys, err := storeGet(r.store, r.cache, ScopeID{Scope: ScopeSystem}, ns); err == nil {
shallowMerge(merged, sys)
}
if teamID != "" {
if team, err := storeGet(r.store, r.cache, ScopeID{Scope: ScopeTeam, TeamID: teamID}, ns); err == nil {
shallowMerge(merged, team)
}
}
if userID != "" {
if user, err := storeGet(r.store, r.cache, ScopeID{Scope: ScopeUser, UserID: userID}, ns); err == nil {
shallowMerge(merged, user)
}
}
if len(merged) == 0 {
return nil, fmt.Errorf("setting %s: no data found at any scope", ns)
}
if len(dest) > 0 && dest[0] != nil {
if err := bindDest(merged, dest[0]); err != nil {
return merged, fmt.Errorf("setting bind: %w", err)
}
}
return merged, nil
}
// Set writes (or overwrites) a namespace entry for the given scope.
func (r *Registry) Set(scope ScopeID, ns string, data map[string]interface{}) (*Entry, error) {
r.mu.Lock()
defer r.mu.Unlock()
if ns == "" {
return nil, fmt.Errorf("namespace is required")
}
if err := storeSet(r.store, r.cache, scope, ns, data); err != nil {
return nil, err
}
if err := indexAdd(r.store, r.cache, scope, ns); err != nil {
return nil, err
}
return &Entry{
Namespace: ns,
Scope: scope,
Data: data,
UpdatedAt: time.Now().UTC().Format(time.RFC3339),
}, nil
}
// Delete removes a namespace entry from a given scope.
func (r *Registry) Delete(scope ScopeID, ns string) error {
r.mu.Lock()
defer r.mu.Unlock()
sk := storeKey(scope, ns)
if !r.store.Has(sk) {
return fmt.Errorf("setting %s/%s not found", scopePrefix(scope), ns)
}
if err := storeDel(r.store, r.cache, scope, ns); err != nil {
return err
}
return indexRemove(r.store, r.cache, scope, ns)
}
// ListNamespaces returns all namespace names stored under the given scope.
func (r *Registry) ListNamespaces(scope ScopeID) ([]string, error) {
r.mu.RLock()
defer r.mu.RUnlock()
return indexGet(r.store, r.cache, scope)
}
// Reload clears the cache and re-populates it from the persistent store.
func (r *Registry) Reload() error {
r.mu.Lock()
defer r.mu.Unlock()
if r.cache != nil {
_ = r.cache.Del(keyPrefix + "*")
}
for _, scope := range []ScopeID{
{Scope: ScopeSystem},
} {
keys, err := indexGet(r.store, nil, scope)
if err != nil {
continue
}
ik := indexKey(scope)
raw, ok := r.store.Get(ik)
if ok && r.cache != nil {
r.cache.Set(ik, raw, 0)
}
for _, ns := range keys {
if data, err := storeGet(r.store, nil, scope, ns); err == nil && r.cache != nil {
r.cache.Set(storeKey(scope, ns), data, 0)
}
}
}
return nil
}
// shallowMerge copies all keys from src into dst (overwrites existing keys).
func shallowMerge(dst, src map[string]interface{}) {
for k, v := range src {
dst[k] = v
}
}
// bindDest marshals data to JSON and then unmarshals into the dest pointer.
func bindDest(data map[string]interface{}, dest interface{}) error {
raw, err := json.Marshal(data)
if err != nil {
return err
}
return json.Unmarshal(raw, dest)
}