// BucketRegistry resolves logical bucket names → *Bucket. G0 holds // exactly one bucket ("primary"); G2 (multi-bucket federation, per // Rust ADR-017) extends this to many. Keeping the registry as a // distinct type now means cmd/storaged never has to be refactored // when G2 lands — every handler already routes through Resolve. package storaged import ( "errors" "fmt" "sync" ) var ErrBucketUnknown = errors.New("storaged: unknown bucket") // BucketRegistry is a thread-safe map of logical name → *Bucket. // In G0 only "primary" is registered; the registry exists so that // when G2 multi-bucket lands we add buckets in registry.go and the // HTTP layer is unchanged. type BucketRegistry struct { mu sync.RWMutex buckets map[string]*Bucket } func NewRegistry() *BucketRegistry { return &BucketRegistry{buckets: make(map[string]*Bucket)} } // Register adds a bucket under its logical name. Re-registering the // same name is rejected — G0 services build the registry once at // startup and a duplicate is almost always a config bug. func (r *BucketRegistry) Register(b *Bucket) error { r.mu.Lock() defer r.mu.Unlock() if _, exists := r.buckets[b.name]; exists { return fmt.Errorf("bucket %q already registered", b.name) } r.buckets[b.name] = b return nil } // Resolve returns the bucket for a logical name. G0 callers pass // "primary" since that's the only one registered. func (r *BucketRegistry) Resolve(name string) (*Bucket, error) { r.mu.RLock() defer r.mu.RUnlock() b, ok := r.buckets[name] if !ok { return nil, fmt.Errorf("%w: %q", ErrBucketUnknown, name) } return b, nil } // Names returns the registered logical names (sorted-order not // guaranteed). Useful for /health surface or debug dumps. func (r *BucketRegistry) Names() []string { r.mu.RLock() defer r.mu.RUnlock() out := make([]string, 0, len(r.buckets)) for name := range r.buckets { out = append(out, name) } return out }