package adapters import ( "context" "fmt" "iop/apps/node/internal/runtime" ) // LifecycleAdapter is an optional interface for adapters that need start/stop lifecycle management. type LifecycleAdapter interface { Start(ctx context.Context) error Stop(ctx context.Context) error } type entry struct { typeName string adapter runtime.Adapter } // Registry holds all registered adapters keyed by instance key. type Registry struct { entries map[string]entry order []string // insertion order for lifecycle and diagnostics } func NewRegistry() *Registry { return &Registry{entries: make(map[string]entry)} } // Register adds an adapter using a.Name() as both instance key and type name. // This preserves backward compatibility for callers that register by type. func (r *Registry) Register(a runtime.Adapter) { r.RegisterKeyed(a.Name(), a.Name(), a) } // RegisterKeyed adds an adapter with an explicit instance key and type name. // If key is empty, typeName is used as the key. Duplicate keys replace the // existing entry but preserve insertion order. func (r *Registry) RegisterKeyed(key, typeName string, a runtime.Adapter) { if key == "" { key = typeName } if _, exists := r.entries[key]; !exists { r.order = append(r.order, key) } r.entries[key] = entry{typeName: typeName, adapter: a} } // Get returns the adapter registered under the exact instance key. func (r *Registry) Get(key string) (runtime.Adapter, bool) { e, ok := r.entries[key] return e.adapter, ok } // Lookup returns an adapter by exact instance key first. If no exact match is // found it falls back to type-name lookup: succeeds when exactly one instance // of that type is registered, and returns an ambiguous error when multiple // instances share the same type name. func (r *Registry) Lookup(name string) (runtime.Adapter, error) { if e, ok := r.entries[name]; ok { return e.adapter, nil } var matchKeys []string for _, k := range r.order { if r.entries[k].typeName == name { matchKeys = append(matchKeys, k) } } switch len(matchKeys) { case 0: return nil, fmt.Errorf("adapter %q not found", name) case 1: return r.entries[matchKeys[0]].adapter, nil default: return nil, fmt.Errorf("adapter %q is ambiguous: matches instance keys %v; use an instance key", name, matchKeys) } } // All returns all registered adapters in registration order. func (r *Registry) All() []runtime.Adapter { out := make([]runtime.Adapter, 0, len(r.order)) for _, key := range r.order { out = append(out, r.entries[key].adapter) } return out } // MustGet returns the adapter or panics — useful during bootstrap validation. func (r *Registry) MustGet(name string) runtime.Adapter { a, err := r.Lookup(name) if err != nil { panic(fmt.Sprintf("adapter %q not registered", name)) } return a } // Start calls Start on all registered LifecycleAdapters in registration order. // On failure, already-started adapters are stopped in reverse order before returning. func (r *Registry) Start(ctx context.Context) error { started := make([]string, 0, len(r.order)) for _, key := range r.order { lc, ok := r.entries[key].adapter.(LifecycleAdapter) if !ok { continue } if err := lc.Start(ctx); err != nil { for i := len(started) - 1; i >= 0; i-- { if startedLC, ok := r.entries[started[i]].adapter.(LifecycleAdapter); ok { _ = startedLC.Stop(context.Background()) } } return fmt.Errorf("adapter %q start: %w", key, err) } started = append(started, key) } return nil } // Stop calls Stop on all registered LifecycleAdapters in reverse registration order. // All adapters are stopped even if some fail; the first error is returned. func (r *Registry) Stop(ctx context.Context) error { var firstErr error for i := len(r.order) - 1; i >= 0; i-- { key := r.order[i] lc, ok := r.entries[key].adapter.(LifecycleAdapter) if !ok { continue } if err := lc.Stop(ctx); err != nil { if firstErr == nil { firstErr = fmt.Errorf("adapter %q stop: %w", key, err) } } } return firstErr }