272 lines
6.7 KiB
Go
272 lines
6.7 KiB
Go
|
|
// SPDX-License-Identifier: EUPL-1.2
|
||
|
|
|
||
|
|
// Thread-safe named collection primitive for the Core framework.
|
||
|
|
// Registry[T] is the universal brick — all named registries (services,
|
||
|
|
// commands, actions, drives, data) embed this type.
|
||
|
|
//
|
||
|
|
// Usage:
|
||
|
|
//
|
||
|
|
// r := core.NewRegistry[*MyService]()
|
||
|
|
// r.Set("brain", brainSvc)
|
||
|
|
// r.Get("brain") // Result{brainSvc, true}
|
||
|
|
// r.Has("brain") // true
|
||
|
|
// r.Names() // []string{"brain"} (insertion order)
|
||
|
|
// r.Each(func(name string, svc *MyService) { ... })
|
||
|
|
// r.Lock() // fully frozen — no more writes
|
||
|
|
// r.Seal() // no new keys, updates to existing OK
|
||
|
|
//
|
||
|
|
// Three lock modes:
|
||
|
|
//
|
||
|
|
// Open (default) — anything goes
|
||
|
|
// Sealed — no new keys, existing keys CAN be updated
|
||
|
|
// Locked — fully frozen, no writes at all
|
||
|
|
package core
|
||
|
|
|
||
|
|
import (
|
||
|
|
"path/filepath"
|
||
|
|
"sync"
|
||
|
|
)
|
||
|
|
|
||
|
|
// registryMode controls write behaviour.
|
||
|
|
type registryMode int
|
||
|
|
|
||
|
|
const (
|
||
|
|
registryOpen registryMode = iota // anything goes
|
||
|
|
registrySealed // update existing, no new keys
|
||
|
|
registryLocked // fully frozen
|
||
|
|
)
|
||
|
|
|
||
|
|
// Registry is a thread-safe named collection. The universal brick
|
||
|
|
// for all named registries in Core.
|
||
|
|
//
|
||
|
|
// r := core.NewRegistry[*Service]()
|
||
|
|
// r.Set("brain", svc)
|
||
|
|
// if r.Has("brain") { ... }
|
||
|
|
type Registry[T any] struct {
|
||
|
|
items map[string]T
|
||
|
|
disabled map[string]bool
|
||
|
|
order []string // insertion order
|
||
|
|
mu sync.RWMutex
|
||
|
|
mode registryMode
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewRegistry creates an empty registry in Open mode.
|
||
|
|
//
|
||
|
|
// r := core.NewRegistry[*Service]()
|
||
|
|
func NewRegistry[T any]() *Registry[T] {
|
||
|
|
return &Registry[T]{
|
||
|
|
items: make(map[string]T),
|
||
|
|
disabled: make(map[string]bool),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Set registers an item by name. Returns Result{OK: false} if the
|
||
|
|
// registry is locked, or if sealed and the key doesn't already exist.
|
||
|
|
//
|
||
|
|
// r.Set("brain", brainSvc)
|
||
|
|
func (r *Registry[T]) Set(name string, item T) Result {
|
||
|
|
r.mu.Lock()
|
||
|
|
defer r.mu.Unlock()
|
||
|
|
|
||
|
|
switch r.mode {
|
||
|
|
case registryLocked:
|
||
|
|
return Result{E("registry.Set", Concat("registry is locked, cannot set: ", name), nil), false}
|
||
|
|
case registrySealed:
|
||
|
|
if _, exists := r.items[name]; !exists {
|
||
|
|
return Result{E("registry.Set", Concat("registry is sealed, cannot add new key: ", name), nil), false}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if _, exists := r.items[name]; !exists {
|
||
|
|
r.order = append(r.order, name)
|
||
|
|
}
|
||
|
|
r.items[name] = item
|
||
|
|
return Result{OK: true}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get retrieves an item by name.
|
||
|
|
//
|
||
|
|
// res := r.Get("brain")
|
||
|
|
// if res.OK { svc := res.Value.(*Service) }
|
||
|
|
func (r *Registry[T]) Get(name string) Result {
|
||
|
|
r.mu.RLock()
|
||
|
|
defer r.mu.RUnlock()
|
||
|
|
|
||
|
|
item, ok := r.items[name]
|
||
|
|
if !ok {
|
||
|
|
return Result{}
|
||
|
|
}
|
||
|
|
return Result{item, true}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Has returns true if the name exists in the registry.
|
||
|
|
//
|
||
|
|
// if r.Has("brain") { ... }
|
||
|
|
func (r *Registry[T]) Has(name string) bool {
|
||
|
|
r.mu.RLock()
|
||
|
|
defer r.mu.RUnlock()
|
||
|
|
_, ok := r.items[name]
|
||
|
|
return ok
|
||
|
|
}
|
||
|
|
|
||
|
|
// Names returns all registered names in insertion order.
|
||
|
|
//
|
||
|
|
// names := r.Names() // ["brain", "monitor", "process"]
|
||
|
|
func (r *Registry[T]) Names() []string {
|
||
|
|
r.mu.RLock()
|
||
|
|
defer r.mu.RUnlock()
|
||
|
|
|
||
|
|
out := make([]string, len(r.order))
|
||
|
|
copy(out, r.order)
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
// List returns items whose names match the glob pattern.
|
||
|
|
// Uses filepath.Match semantics: "*" matches any sequence, "?" matches one char.
|
||
|
|
//
|
||
|
|
// services := r.List("process.*")
|
||
|
|
func (r *Registry[T]) List(pattern string) []T {
|
||
|
|
r.mu.RLock()
|
||
|
|
defer r.mu.RUnlock()
|
||
|
|
|
||
|
|
var result []T
|
||
|
|
for _, name := range r.order {
|
||
|
|
if matched, _ := filepath.Match(pattern, name); matched {
|
||
|
|
if !r.disabled[name] {
|
||
|
|
result = append(result, r.items[name])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
// Each iterates over all items in insertion order, calling fn for each.
|
||
|
|
// Disabled items are skipped.
|
||
|
|
//
|
||
|
|
// r.Each(func(name string, svc *Service) {
|
||
|
|
// fmt.Println(name, svc)
|
||
|
|
// })
|
||
|
|
func (r *Registry[T]) Each(fn func(string, T)) {
|
||
|
|
r.mu.RLock()
|
||
|
|
defer r.mu.RUnlock()
|
||
|
|
|
||
|
|
for _, name := range r.order {
|
||
|
|
if !r.disabled[name] {
|
||
|
|
fn(name, r.items[name])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Len returns the number of registered items (including disabled).
|
||
|
|
//
|
||
|
|
// count := r.Len()
|
||
|
|
func (r *Registry[T]) Len() int {
|
||
|
|
r.mu.RLock()
|
||
|
|
defer r.mu.RUnlock()
|
||
|
|
return len(r.items)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Delete removes an item. Returns Result{OK: false} if locked or not found.
|
||
|
|
//
|
||
|
|
// r.Delete("old-service")
|
||
|
|
func (r *Registry[T]) Delete(name string) Result {
|
||
|
|
r.mu.Lock()
|
||
|
|
defer r.mu.Unlock()
|
||
|
|
|
||
|
|
if r.mode == registryLocked {
|
||
|
|
return Result{E("registry.Delete", Concat("registry is locked, cannot delete: ", name), nil), false}
|
||
|
|
}
|
||
|
|
if _, exists := r.items[name]; !exists {
|
||
|
|
return Result{E("registry.Delete", Concat("not found: ", name), nil), false}
|
||
|
|
}
|
||
|
|
|
||
|
|
delete(r.items, name)
|
||
|
|
delete(r.disabled, name)
|
||
|
|
// Remove from order slice
|
||
|
|
for i, n := range r.order {
|
||
|
|
if n == name {
|
||
|
|
r.order = append(r.order[:i], r.order[i+1:]...)
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return Result{OK: true}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Disable soft-disables an item. It still exists but Each/List skip it.
|
||
|
|
// Returns Result{OK: false} if not found.
|
||
|
|
//
|
||
|
|
// r.Disable("broken-handler")
|
||
|
|
func (r *Registry[T]) Disable(name string) Result {
|
||
|
|
r.mu.Lock()
|
||
|
|
defer r.mu.Unlock()
|
||
|
|
|
||
|
|
if _, exists := r.items[name]; !exists {
|
||
|
|
return Result{E("registry.Disable", Concat("not found: ", name), nil), false}
|
||
|
|
}
|
||
|
|
r.disabled[name] = true
|
||
|
|
return Result{OK: true}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Enable re-enables a disabled item.
|
||
|
|
//
|
||
|
|
// r.Enable("fixed-handler")
|
||
|
|
func (r *Registry[T]) Enable(name string) Result {
|
||
|
|
r.mu.Lock()
|
||
|
|
defer r.mu.Unlock()
|
||
|
|
|
||
|
|
if _, exists := r.items[name]; !exists {
|
||
|
|
return Result{E("registry.Enable", Concat("not found: ", name), nil), false}
|
||
|
|
}
|
||
|
|
delete(r.disabled, name)
|
||
|
|
return Result{OK: true}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Disabled returns true if the item is soft-disabled.
|
||
|
|
func (r *Registry[T]) Disabled(name string) bool {
|
||
|
|
r.mu.RLock()
|
||
|
|
defer r.mu.RUnlock()
|
||
|
|
return r.disabled[name]
|
||
|
|
}
|
||
|
|
|
||
|
|
// Lock fully freezes the registry. No Set, no Delete.
|
||
|
|
//
|
||
|
|
// r.Lock() // after startup, prevent late registration
|
||
|
|
func (r *Registry[T]) Lock() {
|
||
|
|
r.mu.Lock()
|
||
|
|
defer r.mu.Unlock()
|
||
|
|
r.mode = registryLocked
|
||
|
|
}
|
||
|
|
|
||
|
|
// Locked returns true if the registry is fully frozen.
|
||
|
|
func (r *Registry[T]) Locked() bool {
|
||
|
|
r.mu.RLock()
|
||
|
|
defer r.mu.RUnlock()
|
||
|
|
return r.mode == registryLocked
|
||
|
|
}
|
||
|
|
|
||
|
|
// Seal prevents new keys but allows updates to existing keys.
|
||
|
|
// Use for hot-reload: shape is fixed, implementations can change.
|
||
|
|
//
|
||
|
|
// r.Seal() // no new capabilities, but handlers can be swapped
|
||
|
|
func (r *Registry[T]) Seal() {
|
||
|
|
r.mu.Lock()
|
||
|
|
defer r.mu.Unlock()
|
||
|
|
r.mode = registrySealed
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sealed returns true if the registry is sealed (no new keys).
|
||
|
|
func (r *Registry[T]) Sealed() bool {
|
||
|
|
r.mu.RLock()
|
||
|
|
defer r.mu.RUnlock()
|
||
|
|
return r.mode == registrySealed
|
||
|
|
}
|
||
|
|
|
||
|
|
// Open resets the registry to open mode (default).
|
||
|
|
//
|
||
|
|
// r.Open() // re-enable writes for testing
|
||
|
|
func (r *Registry[T]) Open() {
|
||
|
|
r.mu.Lock()
|
||
|
|
defer r.mu.Unlock()
|
||
|
|
r.mode = registryOpen
|
||
|
|
}
|