go/registry.go

272 lines
6.7 KiB
Go
Raw Normal View History

feat: implement RFC plans 1-5 — Registry[T], Action/Task, Process, primitives Plans 1-5 complete for core/go scope. 456 tests, 84.4% coverage, 100% AX-7 naming. Critical bugs (Plan 1): - P4-3+P7-3: ACTION broadcast calls all handlers with panic recovery - P7-2+P7-4: RunE() with defer ServiceShutdown, Run() delegates - P3-1: Startable/Stoppable return Result (breaking, clean) - P9-1: Zero os/exec — App.Find() rewritten with os.Stat+PATH - I3: Embed() removed, I15: New() comment fixed - I9: CommandLifecycle removed → Command.Managed field Registry[T] (Plan 2): - Universal thread-safe named collection with 3 lock modes - All 5 registries migrated: services, commands, drive, data, lock - Insertion order preserved (fixes P4-1) - c.RegistryOf("name") cross-cutting accessor Action/Task system (Plan 3): - Action type with Run()/Exists(), ActionHandler signature - c.Action("name") dual-purpose accessor (register/invoke) - TaskDef with Steps — sequential chain, async dispatch, previous-input piping - Panic recovery on all Action execution - broadcast() internal, ACTION() sugar Process primitive (Plan 4): - c.Process() returns Action sugar — Run/RunIn/RunWithEnv/Start/Kill/Exists - No deps added — delegates to c.Action("process.*") - Permission-by-registration: no handler = no capability Missing primitives (Plan 5): - core.ID() — atomic counter + crypto/rand suffix - ValidateName() / SanitisePath() — reusable validation - Fs.WriteAtomic() — write-to-temp-then-rename - Fs.NewUnrestricted() / Fs.Root() — legitimate sandbox bypass - AX-7: 456/456 tests renamed to TestFile_Function_{Good,Bad,Ugly} Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-25 15:18:25 +00:00
// 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
}