package app import ( "errors" "fmt" "log/slog" "sync" "sync/atomic" "github.com/alex-savin/go-receipt-tracker/system" ) const ( SetOptions byte = iota OnSysInfoTick OnStarted OnStopped OnWorkerConnected OnWorkerDisconnected // OnSysInfoTick ) var ( // ErrInvalidConfigType indicates a different Type of config value was expected to what was received. ErrInvalidConfigType = errors.New("invalid config type provided") ) // Hook provides an interface of handlers for different events which occur // during the lifecycle of the broker. type Hook interface { ID() string Provides(b byte) bool Init(config any) error Stop() error SetOpts(l *slog.Logger) OnSysInfoTick(*system.Info) OnStarted() OnStopped() OnWorkerConnected() OnWorkerDisconnected() // OnSysInfoTick(*system.Info) } // Hooks is a slice of Hook interfaces to be called in sequence. type Hooks struct { Log *slog.Logger // a logger for the hook (from the server) internal atomic.Value // a slice of []Hook wg sync.WaitGroup // a waitgroup for syncing hook shutdown qty int64 // the number of hooks in use sync.Mutex // a mutex for locking when adding hooks } // Len returns the number of hooks added. func (h *Hooks) Len() int64 { return atomic.LoadInt64(&h.qty) } // Provides returns true if any one hook provides any of the requested hook methods. func (h *Hooks) Provides(b ...byte) bool { for _, hook := range h.GetAll() { for _, hb := range b { if hook.Provides(hb) { return true } } } return false } // Add adds and initializes a new hook. func (h *Hooks) Add(hook Hook, config any) error { h.Lock() defer h.Unlock() err := hook.Init(config) if err != nil { return fmt.Errorf("failed initialising %s hook: %w", hook.ID(), err) } i, ok := h.internal.Load().([]Hook) if !ok { i = []Hook{} } i = append(i, hook) h.internal.Store(i) atomic.AddInt64(&h.qty, 1) h.wg.Add(1) return nil } // GetAll returns a slice of all the hooks. func (h *Hooks) GetAll() []Hook { i, ok := h.internal.Load().([]Hook) if !ok { return []Hook{} } return i } // Stop indicates all attached hooks to gracefully end. func (h *Hooks) Stop() { go func() { for _, hook := range h.GetAll() { h.Log.Info("stopping hook", "hook", hook.ID()) if err := hook.Stop(); err != nil { h.Log.Debug("problem stopping hook", "error", err, "hook", hook.ID()) } h.wg.Done() } }() h.wg.Wait() } // OnSysInfoTick is called when the $SYS topic values are published out. func (h *Hooks) OnSysInfoTick(sys *system.Info) { for _, hook := range h.GetAll() { if hook.Provides(OnSysInfoTick) { hook.OnSysInfoTick(sys) } } } // OnStarted is called when the server has successfully started. func (h *Hooks) OnStarted() { for _, hook := range h.GetAll() { if hook.Provides(OnStarted) { hook.OnStarted() } } } // OnStopped is called when the server has successfully stopped. func (h *Hooks) OnStopped() { for _, hook := range h.GetAll() { if hook.Provides(OnStopped) { hook.OnStopped() } } } // OnWorkerConnected is called when the worker has successfully connected. func (h *Hooks) OnWorkerConnected() { for _, hook := range h.GetAll() { if hook.Provides(OnWorkerConnected) { hook.OnWorkerConnected() } } } // OnWorkerDisconnected is called when the worker has disconnected. func (h *Hooks) OnWorkerDisconnected() { for _, hook := range h.GetAll() { if hook.Provides(OnWorkerDisconnected) { hook.OnWorkerDisconnected() } } } // HookBase provides a set of default methods for each hook. It should be embedded in // all hooks. type HookBase struct { Hook Log *slog.Logger } // ID returns the ID of the hook. func (h *HookBase) ID() string { return "base" } // Provides indicates which methods a hook provides. The default is none - this method // should be overridden by the embedding hook. func (h *HookBase) Provides(b byte) bool { return false } // Init performs any pre-start initializations for the hook, such as connecting to databases // or opening files. func (h *HookBase) Init(config any) error { return nil } // SetOpts is called by the server to propagate internal values and generally should // not be called manually. func (h *HookBase) SetOpts(l *slog.Logger) { h.Log = l } // Stop is called to gracefully shut down the hook. func (h *HookBase) Stop() error { return nil } // OnSysInfoTick is called when the server publishes system info. func (h *HookBase) OnSysInfoTick(*system.Info) {} // OnStarted is called when the server starts. func (h *HookBase) OnStarted() {} // OnStopped is called when the server stops. func (h *HookBase) OnStopped() {} // OnWorkerConnected is called when the worker connected. func (h *HookBase) OnWorkerConnected() {} // OnWorkerDisconnected is called when the worker disconnected. func (h *HookBase) OnWorkerDisconnected() {} // // OnSysInfoTick is called when the server publishes system info. // func (h *HookBase) OnSysInfoTick(*system.Info) {}