From 98d078130e839f63ce59480f1e5c3d3c2d5d172c Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 24 Mar 2026 17:24:50 +0000 Subject: [PATCH] fix: move HandleIPCEvents discovery to New() post-construction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WithService is now a simple factory call — no reflect, no auto-registration. New() calls discoverHandlers() after all opts run, scanning Config for service instances that implement HandleIPCEvents. This eliminates both double-registration and empty-placeholder issues: - Factories wire their own lifecycle via c.Service() - HandleIPCEvents discovered once, after all services are registered - No tension between factory-registered and auto-discovered paths Co-Authored-By: Virgil --- contract.go | 42 +++++------------------------------------- core.go | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/contract.go b/contract.go index 70db9d7..c181c91 100644 --- a/contract.go +++ b/contract.go @@ -6,7 +6,6 @@ package core import ( "context" - "reflect" ) // Message is the type for IPC broadcasts (fire-and-forget). @@ -113,6 +112,10 @@ func New(opts ...CoreOption) Result { } } + // Post-construction: discover IPC handlers on all registered services. + // Services that implement HandleIPCEvents get auto-wired to the bus. + c.discoverHandlers() + return Result{c, true} } @@ -141,42 +144,7 @@ func WithOptions(opts Options) CoreOption { // core.WithService(display.Register(nil)) func WithService(factory func(*Core) Result) CoreOption { return func(c *Core) Result { - r := factory(c) - if !r.OK { - return r - } - - // If the factory returned a service instance, auto-discover and register. - // Only applies when the factory didn't register the service itself. - if r.Value != nil { - instance := r.Value - typeOf := reflect.TypeOf(instance) - if typeOf.Kind() == reflect.Ptr { - typeOf = typeOf.Elem() - } - pkgPath := typeOf.PkgPath() - parts := Split(pkgPath, "/") - name := Lower(parts[len(parts)-1]) - - if name != "" { - // Only auto-register if the factory didn't already do it - if sr := c.Service(name); !sr.OK { - c.Service(name, Service{}) - - // IPC handler discovery — only on auto-registered services - // to avoid double-registration when the factory already wired handlers - instanceValue := reflect.ValueOf(instance) - handlerMethod := instanceValue.MethodByName("HandleIPCEvents") - if handlerMethod.IsValid() { - if handler, ok := handlerMethod.Interface().(func(*Core, Message) Result); ok { - c.RegisterAction(handler) - } - } - } - } - } - - return Result{OK: true} + return factory(c) } } diff --git a/core.go b/core.go index fb9c5d9..e89c835 100644 --- a/core.go +++ b/core.go @@ -7,6 +7,7 @@ package core import ( "context" + "reflect" "sync" "sync/atomic" ) @@ -80,4 +81,28 @@ func (c *Core) Must(err error, op, msg string) { c.log.Must(err, op, msg) } +// --- Post-Construction --- + +// discoverHandlers scans Config for service instances that implement HandleIPCEvents. +// Called once after all WithService options have run — services are fully registered. +func (c *Core) discoverHandlers() { + if c.config == nil || c.config.ConfigOptions == nil || c.config.Settings == nil { + return + } + c.config.mu.RLock() + defer c.config.mu.RUnlock() + for _, val := range c.config.Settings { + if val == nil { + continue + } + instanceValue := reflect.ValueOf(val) + handlerMethod := instanceValue.MethodByName("HandleIPCEvents") + if handlerMethod.IsValid() { + if handler, ok := handlerMethod.Interface().(func(*Core, Message) Result); ok { + c.RegisterAction(handler) + } + } + } +} + // --- Global Instance ---