diff --git a/internal/component/pool.go b/internal/component/pool.go index 419ed06..3ea1fcc 100644 --- a/internal/component/pool.go +++ b/internal/component/pool.go @@ -1,65 +1,34 @@ package component import ( - "reflect" + "sync" "github.com/FAU-CDI/wisski-distillery/pkg/lazy" ) -// Pool holds a pool of components and provides factilities to create and access them. -// See [Pool.All], [ExportComponents] and [ExportComponent]. -type Pool struct { - all lazy.Lazy[[]Component] // all components +type ComponentPool struct { + pool lazy.Pool[Component, Core] + poolInit sync.Once } -// All initializes or returns all components stored in this pool. -// -// The All function should return an array of calls to [Make] with the provided context. -// Multiple calls to the this method return the same return value. -func (p *Pool) All(All func(context *PoolContext) []Component) []Component { - return p.all.Get(func() []Component { - // create a new context - context := &PoolContext{ - all: All, - cache: make(map[string]Component), +func (pool *ComponentPool) init() { + pool.poolInit.Do(func() { + pool.pool.Init = func(component Component, core Core) Component { + component.getBase().Core = core + return component } - - // and process them all - all := context.all(context) - context.process(all) - return all }) } -// PoolContext is a context used during [Make]. -// It should not be initialized by a user. -type PoolContext struct { - all func(context *PoolContext) []Component // function to return all components +type ComponentPoolContext = *lazy.PoolContext[Component] +type ComponentAllFunc = func(context ComponentPoolContext) []Component - cache map[string]Component // cached components - queue []delayedInit // init queue +func (pool *ComponentPool) All(core Core, init func(context ComponentPoolContext) []Component) []Component { + pool.init() + return pool.pool.All(core, init) } -type delayedInit struct { - meta meta - value reflect.Value -} - -func (di delayedInit) Do(all []Component) { - di.meta.InitComponent(di.value, all) -} - -// process processes the queue in this process -func (p *PoolContext) process(all []Component) { - index := 0 - for len(p.queue) > index { - p.queue[index].Do(all) - index++ - } - p.queue = nil -} - -// Make creates or returns a cached component of the given Context. +// MakeComponent creates or returns a cached component of the given Context. // // Components are initialized by first calling the init function. // Then all component-like fields of fields are filled with their appropriate components. @@ -74,74 +43,24 @@ func (p *PoolContext) process(all []Component) { // Furthermore, the init function may not cause other components to be initialized. // // The init function may be nil, indicating that no additional initialization is required. -func Make[C Component](context *PoolContext, core Core, init func(component C)) C { - // get a description of the type - cd := getMeta[C]() - - // if an instance already exists, return it! - if instance, ok := context.cache[cd.Name]; ok { - return instance.(C) - } - - // make sure that we have an array of components - if context.cache == nil { - context.cache = make(map[string]Component) - } - - // create a fresh (empty) instance - context.cache[cd.Name] = cd.New() - instance := context.cache[cd.Name].(C) - - // do the core and self-initialization - instance.getBase().Core = core - - if init != nil { - init(instance) - } - - if cd.NeedsInitComponent() { - context.queue = append(context.queue, delayedInit{ - meta: cd, - value: reflect.ValueOf(instance), - }) - } - - return instance +func MakeComponent[C Component](context ComponentPoolContext, core Core, init func(component C)) C { + return lazy.Make(context, init) } -// -// PUBLIC FUNCTIONS -// - // ExportComponents exports all components that are a C from the pool. // // All should be the function of the core that initializes all components. // All should only make calls to [InitComponent]. -func ExportComponents[C Component](p *Pool, All func(context *PoolContext) []Component) []C { - components := p.All(All) - - results := make([]C, 0, len(components)) - for _, comp := range components { - if cc, ok := comp.(C); ok { - results = append(results, cc) - } - } - return results +func ExportComponents[C Component](pool *ComponentPool, core Core, All ComponentAllFunc) []C { + pool.init() + return lazy.ExportComponents[Component, Core, C](&pool.pool, core, All) } // ExportComponent exports the first component that is a C from the pool. // // All should be the function of the core that initializes all components. // All should only make calls to [InitComponent]. -func ExportComponent[C Component](p *Pool, All func(context *PoolContext) []Component) C { - components := p.All(All) - - for _, comp := range components { - if cc, ok := comp.(C); ok { - return cc - } - } - - var c C - return c +func ExportComponent[C Component](pool *ComponentPool, core Core, All ComponentAllFunc) C { + pool.init() + return lazy.ExportComponent[Component, Core, C](&pool.pool, core, All) } diff --git a/internal/dis/component.go b/internal/dis/component.go index fc44e33..81cd403 100644 --- a/internal/dis/component.go +++ b/internal/dis/component.go @@ -18,7 +18,7 @@ import ( ) // register returns all components of the distillery -func (dis *Distillery) register(context *component.PoolContext) []component.Component { +func (dis *Distillery) register(context component.ComponentPoolContext) []component.Component { return []component.Component{ ra[*web.Web](dis, context), @@ -58,11 +58,11 @@ func (dis *Distillery) register(context *component.PoolContext) []component.Comp } // r initializes a component from the provided distillery. -func r[C component.Component](dis *Distillery, context *component.PoolContext, init func(component C)) C { - return component.Make(context, dis.Core, init) +func r[C component.Component](dis *Distillery, context component.ComponentPoolContext, init func(component C)) C { + return component.MakeComponent(context, dis.Core, init) } // ra is like r, but does not provided additional initialization -func ra[C component.Component](dis *Distillery, context *component.PoolContext) C { +func ra[C component.Component](dis *Distillery, context component.ComponentPoolContext) C { return r[C](dis, context, nil) } diff --git a/internal/dis/distillery.go b/internal/dis/distillery.go index 6eea1f7..38d2283 100644 --- a/internal/dis/distillery.go +++ b/internal/dis/distillery.go @@ -32,7 +32,7 @@ type Distillery struct { Upstream Upstream // Pool holds all the components in this pool - pool component.Pool + pool component.ComponentPool } // Upstream contains the configuration for accessing remote configuration. @@ -54,6 +54,7 @@ func (dis *Distillery) Context() context.Context { func e[C component.Component](dis *Distillery) C { return component.ExportComponent[C]( &dis.pool, + dis.Core, dis.register, ) } @@ -61,6 +62,7 @@ func e[C component.Component](dis *Distillery) C { func ea[C component.Component](dis *Distillery) []C { return component.ExportComponents[C]( &dis.pool, + dis.Core, dis.register, ) } diff --git a/pkg/errorx/errorx.go b/pkg/errorx/errorx.go index 7c4528f..ae1ab71 100644 --- a/pkg/errorx/errorx.go +++ b/pkg/errorx/errorx.go @@ -1,11 +1,8 @@ package errorx +import "github.com/tkw1536/goprogram/lib/collection" + // First returns the first non-nil error, or nil otherwise. func First(errors ...error) error { - for _, err := range errors { - if err != nil { - return err - } - } - return nil + return collection.First(errors, func(err error) bool { return err != nil }) } diff --git a/pkg/lazy/lazy.go b/pkg/lazy/lazy.go index 4c38be5..bc4406d 100644 --- a/pkg/lazy/lazy.go +++ b/pkg/lazy/lazy.go @@ -4,20 +4,20 @@ import ( "sync" ) -// Lazy is an object that a lazily-initialized value of type T. -// -// A Lazy must not be copied after first use. +// Lazy holds a lazily initialized value of T. +// Lazy non-zero lazy must not be copied after first use. type Lazy[T any] struct { once sync.Once m sync.RWMutex // m protects setting the value of this T - value T + value T // the stored value } // Get returns the value associated with this Lazy. // -// If no other call to Get has started or completed an initialization, initializes the value using the init function. -// Otherwise, it returns the initialized value. +// If no other call to Get has started or completed an initialization, calls init to initialize the value. +// A nil init function indicates to store the zero value of T. +// If an initialization has been previously completed, the previously stored value is returned. // // If init panics, the initization is considered to be completed. // Future calls to Get() do not invoke init, and the zero value of T is returned. @@ -28,13 +28,19 @@ func (lazy *Lazy[T]) Get(init func() T) T { defer lazy.m.RUnlock() lazy.once.Do(func() { - lazy.value = init() + if init != nil { + lazy.value = init() + } }) + return lazy.value } -// Set atomically sets the value of this lazy, preventing future calls to get from invoking init. -// It may be called concurrently with calls to [Get] and [Reset]. +// Set atomically sets the value of this lazy. +// Any previously set value will be overwritten. +// Future calls to [Get] will not invoke init. +// +// It may be called concurrently with calls to [Get]. func (lazy *Lazy[T]) Set(value T) { lazy.m.Lock() defer lazy.m.Unlock() diff --git a/pkg/lazy/lazy_test.go b/pkg/lazy/lazy_test.go new file mode 100644 index 0000000..22e1ab1 --- /dev/null +++ b/pkg/lazy/lazy_test.go @@ -0,0 +1,31 @@ +package lazy + +import "fmt" + +func ExampleLazy() { + + var lazy Lazy[int] + + // the first invocation to lazy will be called and set the value + fmt.Println(lazy.Get(func() int { return 42 })) + + // the second invocation will not call init again, using the first value + fmt.Println(lazy.Get(func() int { return 43 })) + + // Set can be used to set a specific value + lazy.Set(0) + fmt.Println(lazy.Get(func() int { panic("never called") })) + + // Output: 42 + // 42 + // 0 +} + +func ExampleLazy_nil() { + var lazy Lazy[int] + + // passing nil as the initialization function causes the zero value to be set + fmt.Println(lazy.Get(nil)) + + // Output: 0 +} diff --git a/pkg/lazy/pool.go b/pkg/lazy/pool.go new file mode 100644 index 0000000..1734cb7 --- /dev/null +++ b/pkg/lazy/pool.go @@ -0,0 +1,170 @@ +package lazy + +import ( + "reflect" + "sync" +) + +// Pool represents a pool of laziliy initialized and potentially referencing Component instances. +// +// Component must be an interface type, that should be implemented by various pointers to structs. +// Components may reference each other, even circularly. +// +// Each type of struct is considered a singleton an initialized only once. +// +// See [Pool.All], [ExportComponents] and [ExportComponent]. +// +// The zero value is ready to use. +type Pool[Component any, InitParams any] struct { + // Init is called on every component to be initialized. + Init func(Component, InitParams) Component + + all Lazy[[]Component] +} + +// All initializes or returns all components stored in this pool. +// +// The All function should return an array of calls to [Make] with the provided context. +// Multiple calls to the this method return the same return value. +func (p *Pool[Component, InitParams]) All(Params InitParams, All func(context *PoolContext[Component]) []Component) []Component { + return p.all.Get(func() []Component { + // create a new context + context := &PoolContext[Component]{ + all: All, + init: func(c Component) Component { + if p.Init == nil { + return c + } + return p.Init(c, Params) + }, + cache: make(map[string]Component), + } + + // and process them all + all := context.all(context) + context.Process(all) + return all + }) +} + +// PoolContext is a context used during [Make]. +type PoolContext[Component any] struct { + init func(Component) Component // initializes a new component + all func(context *PoolContext[Component]) []Component // initializes all components + + // cache for metas + + // function to return all components + + metaCache sync.Map + cache map[string]Component // cached components + queue []delayedInit[Component] // init queue +} + +type delayedInit[Component any] struct { + meta meta[Component] + value reflect.Value +} + +// Process processes all components in the queue +func (p *PoolContext[Component]) Process(all []Component) { + index := 0 + for len(p.queue) > index { + p.queue[index].Run(all) + index++ + } + p.queue = nil +} + +func (di *delayedInit[Component]) Run(all []Component) { + di.meta.InitComponent(di.value, all) +} + +// Make creates or returns a cached component of the given Context. +// +// Components are initialized by first +// Then all component-like fields of fields are filled with their appropriate components. +// +// A component-like field has one of the following types: +// +// - A pointer to a struct type that implements component +// - A slice type of an interface type that implements component +// +// These fields are initialized in an undefined order during initialization. +// The init function may not rely on these existing. +// Furthermore, the init function may not cause other components to be initialized. +// +// The init function may be nil, indicating that no additional initialization is required. +func Make[Component any, ConcreteComponent any](context *PoolContext[Component], init func(component ConcreteComponent)) ConcreteComponent { + // get a description of the type + cd := getMeta[Component, ConcreteComponent](&context.metaCache) + + // if an instance already exists, return it! + if instance, ok := context.cache[cd.Name]; ok { + return any(instance).(ConcreteComponent) + } + + // create a fresh new instance and store it in the cache + context.cache[cd.Name] = context.init(cd.New()) + instance := any(context.cache[cd.Name]).(ConcreteComponent) + + // call the passed init function + if init != nil { + init(instance) + } + + // and queue it up + if cd.NeedsInitComponent() { + context.queue = append(context.queue, delayedInit[Component]{ + meta: cd, + value: reflect.ValueOf(instance), + }) + } + + return instance +} + +// +// PUBLIC FUNCTIONS +// + +// ExportComponents exports all components that are a ConcreteComponentType from the pool. +// +// All should be the function of the core that initializes all components. +// All should only make calls to [InitComponent]. +func ExportComponents[Component any, InitParams any, ConcreteComponentType any]( + p *Pool[Component, InitParams], + Params InitParams, + All func(context *PoolContext[Component]) []Component, +) []ConcreteComponentType { + components := p.All(Params, All) + + results := make([]ConcreteComponentType, 0, len(components)) + for _, comp := range components { + if cc, ok := any(comp).(ConcreteComponentType); ok { + results = append(results, cc) + } + } + return results +} + +// ExportComponent exports the first component that is a ConcreteComponent from the pool. +// +// All should be the function of the core that initializes all components. +// All should only make calls to [InitComponent]. +func ExportComponent[Component any, InitParams any, ConcreteComponentType any]( + pool *Pool[Component, InitParams], + Params InitParams, + All func(context *PoolContext[Component]) []Component, +) ConcreteComponentType { + components := pool.All(Params, All) + + for _, comp := range components { + if cc, ok := any(comp).(ConcreteComponentType); ok { + return cc + } + } + + var component ConcreteComponentType + return component +} diff --git a/internal/component/pool_meta.go b/pkg/lazy/pool_meta.go similarity index 84% rename from internal/component/pool_meta.go rename to pkg/lazy/pool_meta.go index 6b66193..54539e7 100644 --- a/internal/component/pool_meta.go +++ b/pkg/lazy/pool_meta.go @@ -1,4 +1,4 @@ -package component +package lazy import ( "reflect" @@ -8,19 +8,17 @@ import ( "github.com/tkw1536/goprogram/lib/reflectx" ) -var metaCache sync.Map // Map[reflect.Type]meta - // getMeta gets the component belonging to a component type -func getMeta[C Component]() meta { - tp := reflectx.TypeOf[C]() +func getMeta[Component any, ConcreteComponent any](metaCache *sync.Map) meta[Component] { + tp := reflectx.TypeOf[ConcreteComponent]() // we already have a m => return it if m, ok := metaCache.Load(tp); ok { - return m.(meta) + return m.(meta[Component]) } // create a new m - var m meta + var m meta[Component] m.init(tp) // store it in the cache @@ -29,7 +27,7 @@ func getMeta[C Component]() meta { } // meta stores meta-information about a specific component -type meta struct { +type meta[Component any] struct { Name string // the type name of this component Elem reflect.Type // the element type of the component @@ -37,11 +35,10 @@ type meta struct { IFields map[string]reflect.Type // fields []I where I is an interface that implements component } -// componentType is the type of components -var componentType = reflectx.TypeOf[Component]() +// init initializes this meta +func (m *meta[Component]) init(tp reflect.Type) { + var componentType = reflectx.TypeOf[Component]() -// init initializes this refklecttype -func (m *meta) init(tp reflect.Type) { if tp.Kind() != reflect.Pointer && tp.Elem().Kind() != reflect.Struct { panic("GetMeta: Type (" + tp.String() + ") must be backed by a pointer to slice") } @@ -73,17 +70,17 @@ func (m *meta) init(tp reflect.Type) { } // New creates a new ComponentDescription -func (m meta) New() Component { +func (m meta[Component]) New() Component { return reflect.New(m.Elem).Interface().(Component) } // NeedsInitComponent -func (m meta) NeedsInitComponent() bool { +func (m meta[Component]) NeedsInitComponent() bool { return len(m.CFields) > 0 || len(m.IFields) > 0 } // InitComponent sets up the fields of the given instance of a component. -func (m meta) InitComponent(instance reflect.Value, all []Component) { +func (m meta[Component]) InitComponent(instance reflect.Value, all []Component) { elem := instance.Elem() // assign the component fields